# Stacks Whois

> Use when performing WHOIS lookups in Stacks - domain queries, batch lookups, SOCKS proxy support, TLD server discovery, response parsing, the WhoIsParser class, or the built-in SocksClient. Covers @stacksjs/whois.

- Skill: `stacksjs/stacks-whois` (Agent Skill)
- Install (CLI): `npx skillmds@latest add stacksjs/stacks-whois`
- Raw SKILL.md: https://api.skillmd.com/api/skills/stacksjs/stacks-whois/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- License: MIT
- Author: stacksjs (https://skillmd.com/u/stacksjs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/stacksjs/stacks-whois

---


# Stacks WHOIS

Domain WHOIS lookup with SOCKS proxy support, batch processing, TLD server discovery, and response parsing. Includes a native SOCKS4/SOCKS5 client implementation.

## Key Paths
- Core package: `storage/framework/core/whois/src/`

## Source Files
```
whois/src/
├── index.ts          # whois(), lookup(), batchWhois(), tcpWhois(), WhoIsParser, exports
├── types.ts          # WhoIsResponse, WhoIsOptions, ProxyData, ProxyType
├── constants.ts      # IANA_CHK_URL, PARAMETERS, SERVERS
├── utils.ts          # shallowCopy helper
└── socks.ts          # Native SocksClient implementation (SOCKS4/SOCKS5)
```

## Main Functions

### whois() -- Primary Lookup
```typescript
async function whois(
  domain: string,
  parse?: boolean,          // default: false (but still parses with default fields)
  options?: WhoIsOptions | null,
): Promise<WhoIsResponse>
```

When `parse` is `false` (default), it still calls `WhoIsParser.parseData(rawData, null)` which parses with the default field set. When `parse` is `true` and `options.parseData` is provided, it uses a shallow copy of that object as the parse template.

Flow:
1. Determine TLD from domain (or use `options.tld`)
2. Find WHOIS server: check `SERVERS` map first, then query IANA via `findWhoIsServer()`
3. Get query parameters via `getParameters()` for the server
4. Execute TCP query via `tcpWhois()`
5. Parse response via `WhoIsParser.parseData()`
6. Returns `{ _raw: string, parsedData: any | null }`

### lookup() -- Parsed Lookup Shortcut
```typescript
function lookup(domain: string, options?: WhoIsOptions | null): Promise<WhoIsResponse>
```
Calls `whois(domain, true, options)` -- always parses the response.

### batchWhois() -- Batch Lookups
```typescript
async function batchWhois(
  domains: string[],
  parallel?: boolean,       // default: false
  threads?: number,         // default: 1 (batch size for parallel mode)
  parse?: boolean,          // default: false
  options?: WhoIsOptions | null,
): Promise<WhoIsResponse[]>
```

- Sequential mode (`parallel=false`): processes domains one at a time
- Parallel mode (`parallel=true`): processes in batches of `threads` size using `Promise.all`
- If `threads > domains.length`, clamps to `domains.length`
- Note: In parallel mode, only the last batch's results are returned (`response` is reassigned, not concatenated)

### tcpWhois() -- Raw TCP Query
```typescript
async function tcpWhois(
  domain: string,
  queryOptions: string,     // server-specific prefix (e.g., '-T dn,ace')
  server: string,
  port: number,             // default 43
  encoding: string,         // default 'utf-8'
  proxy: ProxyData | null,
): Promise<string>
```

- Without proxy: creates `Net.Socket`, connects directly to WHOIS server on specified port
- With proxy: uses `SocksClient.createConnection()` to tunnel through SOCKS proxy
- Sends query as `"<queryOptions> <domain>\r\n"` or `"<domain>\r\n"` if no queryOptions
- Uses `TextDecoder` with specified encoding for response

## Server Discovery

### findWhoIsServer() -- Remote IANA Lookup
```typescript
async function findWhoIsServer(tld: string): Promise<string>
```
Fetches `https://www.iana.org/whois?q=<tld>` and extracts server from `whois:\s+(\S+)` regex match. Returns empty string on failure.

### getWhoIsServer() -- Local Lookup
```typescript
function getWhoIsServer(tld: keyof typeof SERVERS): string | undefined
```
Looks up TLD in the built-in `SERVERS` map. Special case: `'com'` always returns `'whois.verisign-grs.com'`.

### getTLD() -- Extract TLD
```typescript
function getTLD(domain: string): keyof typeof SERVERS
```
Splits domain by `.` and checks from longest suffix first against the `SERVERS` map. For example, `sub.example.co.uk` checks `co.uk` first (which exists in SERVERS), not just `uk`. Falls back to the last part if no match found.

### getParameters() -- Server Query Parameters
```typescript
function getParameters(server: string): string | undefined
```
Returns server-specific query format from the `PARAMETERS` map.

## WhoIsParser

```typescript
class WhoIsParser {
  // Parse raw WHOIS text with custom or default field template
  static parseData(rawData: string, outputData: any | null): any

  // Internal iterative parser (private)
  private static iterParse(rawData: string, outputData: any): any
}
```

When `outputData` is `null`, `parseData()` uses this default template:
```typescript
{
  'Domain Name': '',
  'Creation Date': '',
  'Updated Date': '',
  'Registry Expiry Date': '',
  'Domain Status': [],       // array collects multiple values
  'Registrar': '',
}
```

The parser iterates character by character through raw WHOIS text:
- Keys are identified when a line matches a key in `outputData`
- Values follow after `": "` separator
- Array-typed fields (like `Domain Status`) accumulate multiple values
- Parsing stops at `"Record maintained by"` or `">>>"` markers

Custom parse templates: pass an object with the field names you want extracted. Use empty string `''` for single values, use `[]` for fields with multiple values.

```typescript
const result = await whois('example.com', true, {
  parseData: {
    'Domain Name': '',
    'Domain Status': [],     // collects ALL status values
    'Registrar': '',
    'Name Server': [],       // collects ALL name servers
  }
})
```

## Types

### WhoIsResponse
```typescript
interface WhoIsResponse {
  _raw: string              // raw text from WHOIS server
  parsedData: any | null    // parsed key-value pairs (null on error)
}
```

### WhoIsOptions
```typescript
interface WhoIsOptions {
  tld?: string | null              // override TLD detection
  encoding?: string | null         // character encoding (default: 'utf-8')
  proxy?: ProxyData | null         // SOCKS proxy configuration
  server?: string | null           // override WHOIS server
  serverPort?: number | null       // override port (default: 43)
  parseData?: object | null        // custom parse template (keys to extract)
}
```

### ProxyData
```typescript
interface ProxyData {
  ip: string
  port: number
  username?: string | null
  password?: string | null
  type: ProxyType
}
```

### ProxyType
```typescript
enum ProxyType {
  SOCKS4 = 0,
  SOCKS5 = 1,
}
```

Note: `ProxyType.SOCKS4 = 0` and `ProxyType.SOCKS5 = 1` -- these are mapped to SOCKS versions `4` and `5` internally when creating SocksClient options.

## SOCKS Proxy Support

```typescript
const result = await whois('example.com', false, {
  proxy: {
    ip: '127.0.0.1',
    port: 1080,
    type: ProxyType.SOCKS5,
    username: 'user',        // optional
    password: 'pass',        // optional
  }
})
```

## Built-in SocksClient (socks.ts)

Native SOCKS client implementation using `node:net`. Replaces the `socks` npm package.

```typescript
class SocksClient {
  static createConnection(
    options: SocksClientOptions,
    callback: (err: Error | null, info?: SocksClientEstablishedEvent) => void,
  ): void
}

interface SocksClientOptions {
  proxy: SocksProxy
  command: 'connect' | 'bind' | 'associate'
  destination: SocksDestination
}

interface SocksProxy {
  host: string
  port: number
  type: 4 | 5               // SOCKS version
  userId?: string
  password?: string
}

interface SocksDestination {
  host: string
  port: number
}

interface SocksClientEstablishedEvent {
  socket: Net.Socket
  remoteHost?: string
  remotePort?: number
}
```

### SOCKS4 Implementation
- Supports SOCKS4 and SOCKS4a (domain name support via special IP marker `0.0.0.1`)
- Request format: VN(1) + CD(1) + DSTPORT(2) + DSTIP(4) + USERID(var) + NULL(1)
- Success response: status byte `0x5A` (90)

### SOCKS5 Implementation
- Three-step handshake: greeting -> authentication (optional) -> connect request
- Supports no-auth (`0x00`) and username/password auth (`0x02`)
- Supports IPv4 (`0x01`), IPv6 (`0x04`), and domain name (`0x03`) address types
- Detailed error messages for all SOCKS5 status codes (general failure, not allowed, network unreachable, host unreachable, connection refused, TTL expired, command not supported, address type not supported)

## Constants

### IANA_CHK_URL
```typescript
const IANA_CHK_URL = 'https://www.iana.org/whois?q='
```

### PARAMETERS -- Server-Specific Query Formats
```typescript
const PARAMETERS: Record<string, string> = {
  'whois.denic.de': '-T dn,ace',
  'whois.nic.fr': '-V Md5.2',
}
```

### SERVERS -- TLD to WHOIS Server Mapping
Comprehensive mapping of hundreds of TLDs to their WHOIS servers. Examples:
- `'com'` -> `'whois.verisign-grs.com'`
- `'net'` -> `'whois.verisign-grs.com'`
- `'org'` -> `'whois.pir.org'`
- `'io'` -> `'whois.nic.io'`
- `'co.uk'` -> compound TLD support via `getTLD()` longest-match logic
- Country code TLDs, new gTLDs, compound TLDs (e.g., `br.com`, `co.ca`)

## Utility: shallowCopy (utils.ts)

```typescript
function shallowCopy<T>(obj: T): T
```
Recursively copies objects and arrays. Used to clone `parseData` templates before filling them with parsed values, so the original template is not mutated.

## Exports from index.ts

Functions: `whois`, `lookup`, `batchWhois`, `tcpWhois`, `findWhoIsServer`, `getWhoIsServer`, `getTLD`, `getParameters`
Classes: `WhoIsParser`, `SocksClient`
Constants: `IANA_CHK_URL`, `PARAMETERS`, `SERVERS`
Types: `WhoIsResponse`, `WhoIsOptions`, `ProxyData`, `ProxyType`, `SocksClientOptions`

## Dependencies
- `node:net` -- TCP socket connections (direct WHOIS queries and SOCKS client)
- `@stacksjs/logging` -- `log` for debug logging during lookups

## Gotchas
- `whois()` with `parse=false` still parses with default fields -- the `parse` flag controls whether custom `parseData` is used
- `batchWhois()` in parallel mode has a bug: it reassigns `response` each batch instead of concatenating, so only the last batch's results are returned
- `ProxyType` enum values are `0` and `1`, not `4` and `5` -- they are mapped to SOCKS versions internally
- `getWhoIsServer()` returns `undefined` for unknown TLDs, not an empty string
- `findWhoIsServer()` returns empty string `''` on failure (not `undefined`)
- `getTLD()` checks compound TLDs (like `co.uk`) before simple ones via longest-suffix matching
- The parser stops at `"Record maintained by"` or `">>>"` markers in the raw response
- WHOIS servers are rate-limited -- batch queries should use small thread counts
- `shallowCopy()` is a recursive deep copy despite its name -- it clones nested objects and arrays
- TCP connections use `node:net` Socket, not `fetch` -- WHOIS is a raw TCP protocol on port 43
- The SOCKS client uses callback-style API, not promises
- Query options are prepended to the domain in the TCP query (e.g., `"-T dn,ace example.de\r\n"`)

