Commit Graph
105 Commits
Author SHA1 Message Date
jake 1ab34f4561 feat: add country to ContactField, drop placeholder countries, and make finalizeContact idempotent
- Extend `ContactField` union and `REPORTED_FIELDS`/`ADR_INDEX_FIELDS`/`VCARD_PROPERTY_FIELDS` to include `"country"` so RFC 9537 redactions targeting the country field and vCard `adr[6]` are now tracked in `redactedFields`
- Drop placeholder country values (`"N/A"`, `"REDACTED FOR PRIVACY"`, etc.) in `finalizeContact`, but only after confirming the value isn't a real country code or name (e.g. `"NA"` resolves to Namibia and is kept)
- Seed `redactedFields` and `redacted` from the contact's existing values at the start of `finalizeContact` so calling it twice on the same contact produces identical output (idempotent)
- Expand `PLACEHOLDER_VALUE_PATTERNS` to match bare dot sequences (`".."`) and whole-value-only phrases (`"Not Available"`, `"Not Published"`, `"Not Public"`, `"No Data"`) without risk of false-positives on substrings like `"No Data Corp"`
- Export `finalizeContact` and `isPrivacyContact` from the package entry point
2026-09-20 11:04:47 -04:00
jake 0fea5e0a05 0.16.1 v0.16.1 2026-09-20 00:14:13 -04:00
jake 194ce2bfd7 fix: fall back to vCard prePath in redactionFields and consolidate privacy phrase lists
- Extend `redactionFields(name, prePath?)` to parse the vCard property from an RFC 9537 `prePath` expression (e.g. `[?(@[0]=='email')]`) when the human-readable name alone doesn't identify fields; add `ADR_INDEX_FIELDS` to narrow a specific address element (`[3][N]`) to the right `ContactField`
- Pass `r.prePath` through in `extractContacts` so redactions with generic names (e.g. `"Redacted"`) still populate `redactedFields` correctly
- Split `PRIVACY_STRONG_KEYWORDS` source into `REDACTION_PHRASES` (text that stands in for a withheld value) and `PRIVACY_SERVICE_PHRASES` (proxy/shield names); derive `PLACEHOLDER_VALUE_PATTERNS` entries from `REDACTION_PHRASES` with `\b…\b` anchoring so the two lists can't drift apart
- Drop `!!registrant?.redacted` from the `privacyEnabled` check in `normalizeRdap`; `isPrivacyContact` and the RFC 9537 redaction scan already cover the same signal without double-counting
- Expand `isPlaceholderValue` test to assert that bare tokens (`"Unknown"`, `"Na"`) only match the whole value and not substrings like `"Unknown Pleasures Ltd"` or `"Na Health Inc"`
- Update `privacyEnabled` JSDoc to clarify it is a coarse signal (an email-only redaction can set it) and point callers to `redactedFields`/`privacyService` for precision
2026-09-20 00:13:44 -04:00
jake adac9d3349 feat: add redactedFields, privacyService, and ContactField to contact post-processing
- Add `ContactField` union type and `redactedFields?: ContactField[]` to `Contact` so callers can see exactly which fields were dropped as placeholders or known-redacted, rather than just a boolean flag
- Add `privacyService?: boolean` to `Contact`, set by `finalizeContact` when the name/organization text identifies a privacy proxy service (value is kept; `redacted` is still set)
- Expand `finalizeContact` to clean all address/identity fields (not just email/phone/fax), record each dropped field in `redactedFields`, and accept `ContactField[]` as `redactedHint` so RFC 9537 field names can be passed through directly
- Add `redactionFields(text)` to map RFC 9537 redaction names (e.g. `"Registrant Email"`) to the corresponding `ContactField` entries; use it in `extractContacts` to narrow `redactedHint` to only fields that are actually absent
- Add `isPrivacyContact` helper and switch both RDAP and WHOIS normalizers to use it instead of inlining `isPrivacyName` checks
- Expand `PLACEHOLDER_VALUE_PATTERNS` in `privacy.ts` to cover `"Not available from registry"`, `"Not applicable"`, `"Data Protected"`, `"STATUTORY/GDPR Masking"`, `"Select Request Email Form"`, and `"Unknown"` (as an exact word)
- Export `resolveCountry`, `isPlaceholderValue`, and `isPrivacyName` from the package entry point
- Add `contacts.test.ts` covering placeholder-dropping, `redactedFields` population, `privacyService` flag, hint merging, `redactionFields` mapping, and the new placeholder patterns
2026-09-20 00:04:25 -04:00
jake 7ab25ceecc 0.16.0 v0.16.0 2026-09-19 23:24:12 -04:00
jake 992c54ad28 refactor: extract vCard parser into its own module and expand parsed fields
- Move `parseVcard` out of `normalize.ts` into a new `src/rdap/vcard.ts` module; export `readJCard` (jCard → `VCardProp[]`) and `paramList` as reusable primitives
- Parse `KIND` into a typed `VCardKind` union and split structured `ORG` values into a top-level `org` plus an `orgUnits` array for sub-levels
- Parse `TITLE` and `ROLE` properties from the vCard
- Extract the PO box (element 0) and extended address (element 1) from `ADR` into `poBox` and the beginning of `street` respectively, rather than discarding them
- Strip `tel:` URI scheme from `TEL` entries whose `valueType` is `uri` (RFC 6350)
- Add `kind`, `organizationUnits`, `title`, `role`, and `poBox` to the `Contact` type and wire them through `extractContacts` in `normalize.ts`
- Document new `Contact` fields in README
- Add `vcard.test.ts` covering malformed input tolerance, `paramList` normalisation, ORG level splitting, unknown KIND, PO box extraction, and `tel:` URI stripping
2026-09-19 23:20:15 -04:00
jake 3595e5b42c refactor: replace runtime Intl.DisplayNames with a static CLDR-generated country table
- Add `scripts/generate-countries.mjs` to enumerate all current ISO 3166-1 alpha-2 codes via CLDR and emit `src/lib/countries-data.ts`; retired, exceptionally-reserved, and CLDR-only pseudo codes are explicitly excluded, XK (Kosovo) is kept
- Replace the live `Intl.DisplayNames` calls in `countries.ts` with a direct lookup against the generated `COUNTRY_NAMES` record, eliminating variance between Node/ICU versions
- Build the reverse `nameToCode` map from `COUNTRY_NAMES` entries instead of a double-nested loop over the alphabet
- Fix `countryCodeFromName` to fall through to `ALIASES` for two-letter inputs that aren't valid codes (e.g. `"UK"` → `"GB"`)
- Add `myanmar` / `"myanmar burma"` aliases so the CLDR-style name and the legacy WHOIS variant both resolve to `MM`
- Extend `countries.test.ts` to assert pinned names, XK presence, retired/reserved code exclusion, and `UK` alias behaviour
2026-09-19 23:14:43 -04:00
jake 89fa13ffef feat: add contact redacted flag, placeholder detection, and country name/code resolution
- Add `redacted?: boolean` to `Contact`; set when any field was dropped as a placeholder, the name matches a privacy-service heuristic, or an RFC 9537 redaction targets that entity's role
- Add `isPlaceholderValue` to `privacy.ts` to identify boilerplate registrar strings (`"Please query the RDDS…"`, `"redacted"`, `"n/a"`, etc.) used in place of real email/phone/fax values; drop them from the contact during finalization
- Add `countries.ts` backed by `Intl.DisplayNames` (with a curated `ALIASES` table for common alternate names) providing `countryNameFromCode`, `countryCodeFromName`, and `resolveCountry`; a two-letter `country` value is now treated as a code and expanded to a full name
- Introduce `finalizeContact` in `contacts.ts` to centralise all contact post-processing (placeholder stripping, country resolution, `redacted` flag); call it from both RDAP and WHOIS normalizers
- Wire RFC 9537 redaction role-matching into `extractContacts` via a new `redactionTargetsRole` helper so entities targeted by a `redacted` entry automatically receive `redacted: true`
- Add `countries.test.ts` and extend RDAP/WHOIS normalizer tests to cover placeholder stripping, country fill-in, and redaction flag propagation
2026-09-19 23:07:38 -04:00
jake 1d04be5c74 fix: tighten isPrivacyName to avoid false positives on ambiguous privacy words
- Split keyword list into `PRIVACY_STRONG_KEYWORDS` (matched as plain substrings) and two weaker sets (`PRIVACY_WEAK_WORDS` / `PRIVACY_CONTEXT_WORDS`) that only trigger when at least two distinct terms appear together, preventing names like "Private Equity Partners LLC" or "Protection One" from being misclassified
- Remove empty-placeholder values (`-`, `n/a`, `none`, etc.) from the privacy check; they are not privacy notices and should be handled separately by callers
- Add `privacy.test.ts` covering redaction notices, known privacy-service names, ordinary names with ambiguous words, and empty placeholders
- Update README comment to clarify that `privacyEnabled` is set by name heuristics (privacy-service and redaction phrases) or RFC 9537 redactions
2026-09-19 23:04:01 -04:00
jake 386006dc61 feat: add RFC 9537 redaction support, vCard address/fax parsing, and registrar address fields
- Parse the top-level RDAP `redacted` array (RFC 9537) into a new `redactions` field on `DomainRecord`; set `privacyEnabled` when any redaction references "registrant"
- Expose `street`, `city`, `state`, `postalCode`, `country`, and `countryCode` on both `RegistrarInfo` and `Contact`; extract ISO 3166-1 alpha-2 from the vCard `cc` parameter (RFC 8605)
- Split `tel` entries by fax type parameter (`"fax"` / `["work","fax"]`) into separate `tel` and `fax` lists; collect multiple email addresses as an array, collapsing singletons to a plain string
- Prefer an exact `registration` event match over `reregistration` when extracting `creationDate` (RFC 9083)
- Merge `redacted` arrays in `mergeRdapDocs`, deduplicating by JSON identity
- Split `adr` street on `\r?\n` only (not commas) to avoid splitting inline comma-separated addresses
2026-09-19 23:01:30 -04:00
jake e77fc7f4ed 0.15.1 v0.15.1 2026-09-19 12:32:07 -04:00
jake 93f877c357 fix: extend Retry-After parsing to RDAP 503 and fix block pattern across newlines
- Parse `Retry-After` on RDAP `503` responses (previously only `429`), exposing `retryAfterMs` on the attempt and on the top-level result when `rdapOnly` makes it terminal
- Fix the WHOIS `BLOCK_PATTERNS` regex to use `[\s\S]` instead of `.` so a block notice that spans multiple lines (e.g. `"Your IP address\nhas been blocked"`) is still classified as `blocked`
- Update README to document `retryAfterMs` on `LookupResult`, clarify `rate_limited` / `blocked` / `unparseable` semantics, and note referral-host validation behaviour
2026-09-19 12:30:06 -04:00
jake 35959b208b feat: surface retryAfterMs, add blocked error code, and harden private-address guard
- Parse `Retry-After` headers (delay-seconds or HTTP-date) into `retryAfterMs` via a new `parseRetryAfterMs` helper and expose the value on both `LookupAttempt` and `LookupResult` so callers can honour server-requested back-off without parsing the error string
- Propagate `retryAfterMs` through `classifyError`, `traced`, `failure`, and the `rdap_unavailable` path so the value surfaces on the top-level result
- Add `blocked` to `LookupErrorCode` for WHOIS servers that permanently refuse a client (distinct from `rate_limited`, which is a temporary throttle that may succeed on retry)
- Add `blockPrivateAddresses` to `WhoisTransportOptions`; when set, a custom `dns.lookup` shim rejects the connection before it opens if any resolved address is non-public, covering DNS rebinding and hostnames that resolve to private ranges
- Expand `isPrivateIp` / `isSafeWhoisReferralHost` to reject IPv4-mapped (`::ffff:7f00:1`), NAT64 (`64:ff9b::`), 6to4 (`2002:7f00::`), Teredo (`2001:0:`), deprecated site-local (`fec0::`), and bracketed IPv6 literals
2026-09-19 12:25:30 -04:00
jake 208fd6c69f 0.15.0 v0.15.0 2026-09-19 12:17:47 -04:00
jake 3023c5dd1b feat: add SSRF host validation, rate_limited/unparseable error codes, and referral chain warnings
- Add `isSafeWhoisReferralHost` to reject private/loopback/link-local IPs and malformed hostnames before following WHOIS referrals (SSRF guard)
- Detect WHOIS throttle replies in the referral chain: drop the registrar response, keep the registry record, and surface a warning instead of returning bad data
- Add `looksEmptyWhois` guard in the main lookup path: a "registered" record with no parseable fields now fails with `errorCode: "unparseable"` rather than resolving silently
- Map RDAP 429 responses to a structured `rate_limited` error (including `Retry-After` header) so callers can distinguish throttling from generic HTTP errors and the fallback to WHOIS is recorded in `attempts`
- Add `rate_limited` and `unparseable` to `LookupErrorCode`
- Change `collectWhoisReferralChain` to return `{ results, warnings }` instead of a bare array; warnings are merged onto the final `DomainRecord`
- Remove the `followWhoisReferrals` fallback path from `index.ts` (dead code after the chain API stabilised)
2026-09-19 12:14:19 -04:00
jake 61c5897317 0.14.0 v0.14.0 2026-09-19 00:56:46 -04:00
jake 56051dccd0 chore: fix lockfile 2026-09-19 00:56:30 -04:00
jake be8e88592f feat: add ccTLD-specific date formats, noisy-token parsing, and .gg/.je/.br/.ua normalization, closes #20 and #25
- Add `toISOFromTokens` to extract ISO dates from noisy values (e.g. ".ua" `0-UANIC 20111004161638`, `OK-UNTIL 20261004161638`)
- Parse compact `YYYYMMDDHHMMSS` (.ua) and `YYYYMMDD` (.br, including ticket suffix like `20260319 #31066859`) date formats
- Parse ordinal/prose dates used by .gg/.je (`28th December 2018 at 05:54:43.861`)
- Teach `parseKeyValueLines` to collect indented continuation lines for header-style blocks (.gg/.je) without mistaking URLs or times as key separators
- Extract .gg/.je expiry from `Relevant dates:` sentences and split inline registrar URL (`epag (http://www.epag.de)`)
- Map .ua `status: OK-UNTIL <timestamp>` to `expirationDate` when no explicit expiry field is present
- Treat RDAP `pending release` / `release process: waiting` statuses as `isRegistered: false` (previously always `true`)
- Mark WHOIS `.br` `release process: waiting` as available
2026-09-19 00:51:55 -04:00
jake e736045de1 fix: treat WHOIS empty-close as no_data and ETIMEDOUT as structured timeout
- A server that accepts the connection and closes it without sending data now rejects with `errorCode: "no_data"` and `stage: "read"` instead of resolving with an empty string
- OS-level `ETIMEDOUT` errors are mapped to the same `timeout` / `stage` shape as the library's own timer, with `stage` derived from whether the socket had connected yet
- `ECONNRESET` after data arrives is no longer marked `partial: true`; it was a complete reply and the reset just signalled the end of the stream
- `stage` on `LookupAttempt` is now documented as covering all WHOIS failures (not only timeout failures)
2026-09-18 14:47:31 -04:00
jake 69f85ee925 feat: add attempt tracking, deadlineMs, and structured error codes
- Resolve `LookupResult` with an `attempts` array describing every network operation (phase, server, duration, error) so failures recovered by fallback remain visible
- Add `deadlineMs` option for a hard cap on total lookup time, distinct from per-operation `timeoutMs` (lowered default from 15 s to 10 s)
- Add `errorCode` (machine-readable `LookupErrorCode`), `errorPhase`, and `errorServer` to `LookupResult`; `timeout` covers both per-op and deadline timeouts, `aborted` means the caller's signal fired
- WHOIS timeouts now distinguish `connect` vs `read` stage and resolve with partial text (marked `partial: true`) when data arrived before the socket stalled
- Drop Node 18 support; minimum engine is now 20
2026-09-18 14:41:16 -04:00
jake 87e20f4a47 chore: replace Biome with oxlint and oxfmt 2026-09-18 14:21:15 -04:00
jake 5ff837b2bb chore(deps): update dependencies and build configuration 2026-09-18 14:10:09 -04:00
jake a37ab8a6d8 0.13.0 v0.13.0 2026-01-10 13:35:11 -05:00
jake 0f64809bb1 chore: add GitHub Actions workflow for testing across multiple Node.js versions 2026-01-10 13:34:27 -05:00
renovate[bot] 8deef1e145 chore(deps): update dependency tsdown to v0.19.0 2026-01-10 13:30:18 -05:00
renovate[bot] 5c7ee9734e chore(deps): update dependency @biomejs/biome to v2.3.11 2026-01-10 13:30:10 -05:00
renovate[bot] 72821dbfec chore(deps): update dependency @types/node to v25.0.6 2026-01-10 13:28:52 -05:00
renovate[bot] 6884504d88 chore(deps): update dependency node to v24 2026-01-10 13:28:36 -05:00
renovate[bot] efc0277399 chore(deps): update actions/checkout action to v6 2026-01-10 13:28:16 -05:00
Nodar Davituri 2ca7ce68ce fix: treat RDAP 404 responses as "domain not found" instead of failure (#29) 2026-01-10 13:26:45 -05:00
jake 6ef4291d0e 0.12.1 v0.12.1 2025-12-30 12:53:25 -05:00
jake e6b2e94614 fix: update tsdown outputs from .js to .mjs 2025-12-30 12:53:17 -05:00
jake c6da4c1f69 0.12.0 v0.12.0 2025-12-30 12:45:00 -05:00
jake ab1326d99c fix: improve transfer lock status detection 2025-12-30 12:43:55 -05:00
jake 1b73b7dc8d chore: bump deps 2025-12-30 12:41:13 -05:00
jake b72324fd95 0.11.0 v0.11.0 2025-10-30 19:07:47 -04:00
jake 619185b5d6 Add support for custom fetch function and/or IANA bootstrap data (#21) 2025-10-30 19:07:12 -04:00
renovate[bot] acae007a4a Update dependency vitest to v4 2025-10-30 18:39:10 -04:00
renovate[bot] 9bbc52b981 Update dependency tsdown to v0.15.12 2025-10-30 18:19:50 -04:00
renovate[bot] d47009dcd6 Update dependency @biomejs/biome to v2.3.2 2025-10-30 18:19:21 -04:00
jake 8c8dec5a37 0.10.4 v0.10.4 2025-10-27 15:01:07 -04:00
jake 2a92375116 Add support for DD-MM-YYYY date format in toISO function and update tests (fixes #19) 2025-10-27 14:59:14 -04:00
jake fd29899f4e 0.10.3 v0.10.3 2025-10-27 14:44:25 -04:00
jake 9d87ef8ecb Add to list of parser strings to improve WHOIS data normalization 2025-10-27 14:42:43 -04:00
jake 1c6fbad1f7 0.10.2 v0.10.2 2025-10-22 17:45:40 -04:00
jake 87b85f7d3d Add query transformation for non-English WHOIS servers, specifically for whois.jprs.jp (fixes #15) 2025-10-22 17:45:19 -04:00
jake c3b6477094 0.10.1 v0.10.1 2025-10-22 17:38:36 -04:00
jake b8357e38bc Fix .hk, .ee, .il date parsing (fixes #13, #14, #16) 2025-10-22 17:37:37 -04:00
jake f90c481c94 0.10.0 v0.10.0 2025-10-20 15:06:29 -04:00
jake b3adbf8307 Rename lookupDomain() to lookup() (with backwards compatibility) 2025-10-20 15:05:59 -04:00