rdapper 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "rdapper",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/jakejarvis/rdapper.git"
9
9
  },
10
+ "bugs": {
11
+ "url": "https://github.com/jakejarvis/rdapper/issues"
12
+ },
10
13
  "author": {
11
14
  "name": "Jake Jarvis",
12
15
  "email": "jake@jarv.is",
@@ -14,6 +17,7 @@
14
17
  },
15
18
  "type": "module",
16
19
  "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
17
21
  "types": "./dist/index.d.ts",
18
22
  "exports": {
19
23
  ".": {
@@ -26,10 +30,12 @@
26
30
  "dist"
27
31
  ],
28
32
  "scripts": {
29
- "clean": "rm -rf dist",
30
- "build": "npm run clean && tsc -p tsconfig.build.json",
31
- "test": "npm run clean && tsc -p tsconfig.json && node --test dist/**/*.test.js",
32
- "lint": "biome check --write",
33
+ "build": "tsdown",
34
+ "dev": "tsdown --watch",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "vitest",
37
+ "test:run": "vitest run",
38
+ "lint": "biome check",
33
39
  "prepublishOnly": "npm run build"
34
40
  },
35
41
  "dependencies": {
@@ -39,7 +45,9 @@
39
45
  "@biomejs/biome": "2.2.4",
40
46
  "@types/node": "24.5.2",
41
47
  "@types/psl": "1.1.3",
42
- "typescript": "5.9.2"
48
+ "tsdown": "0.15.5",
49
+ "typescript": "5.9.2",
50
+ "vitest": "^3.2.4"
43
51
  },
44
52
  "engines": {
45
53
  "node": ">=18.17"
@@ -1,2 +0,0 @@
1
- export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, reason?: string): Promise<T>;
2
- export declare function sleep(ms: number): Promise<void>;
package/dist/lib/async.js DELETED
@@ -1,18 +0,0 @@
1
- export function withTimeout(promise, timeoutMs, reason = "Timeout") {
2
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
3
- return promise;
4
- let timer;
5
- const timeout = new Promise((_, reject) => {
6
- timer = setTimeout(() => reject(new Error(reason)), timeoutMs);
7
- });
8
- return Promise.race([
9
- promise.finally(() => {
10
- if (timer !== undefined)
11
- clearTimeout(timer);
12
- }),
13
- timeout,
14
- ]);
15
- }
16
- export function sleep(ms) {
17
- return new Promise((resolve) => setTimeout(resolve, ms));
18
- }
@@ -1 +0,0 @@
1
- export declare const DEFAULT_TIMEOUT_MS = 15000;
@@ -1 +0,0 @@
1
- export const DEFAULT_TIMEOUT_MS = 15000;
@@ -1 +0,0 @@
1
- export declare function toISO(dateLike: string | number | Date | undefined | null): string | undefined;
package/dist/lib/dates.js DELETED
@@ -1,84 +0,0 @@
1
- // Lightweight date parsing helpers to avoid external dependencies.
2
- // We aim to parse common RDAP and WHOIS date representations and return a UTC ISO string.
3
- export function toISO(dateLike) {
4
- if (dateLike == null)
5
- return undefined;
6
- if (dateLike instanceof Date)
7
- return toIsoFromDate(dateLike);
8
- if (typeof dateLike === "number")
9
- return toIsoFromDate(new Date(dateLike));
10
- const raw = String(dateLike).trim();
11
- if (!raw)
12
- return undefined;
13
- // Try several structured formats seen in WHOIS outputs (treat as UTC when no TZ provided)
14
- const tryFormats = [
15
- // 2023-01-02 03:04:05Z or without Z
16
- /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z)?$/,
17
- // 2023/01/02 03:04:05
18
- /^(\d{4})\/(\d{2})\/(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/,
19
- // 02-Jan-2023
20
- /^(\d{2})-([A-Za-z]{3})-(\d{4})$/,
21
- // Jan 02 2023
22
- /^([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})$/,
23
- ];
24
- for (const re of tryFormats) {
25
- const m = raw.match(re);
26
- if (!m)
27
- continue;
28
- const d = parseWithRegex(m, re);
29
- if (d)
30
- return toIsoFromDate(d);
31
- }
32
- // Fallback to native Date parsing (handles ISO and RFC2822 with TZ)
33
- const native = new Date(raw);
34
- if (!Number.isNaN(native.getTime()))
35
- return toIsoFromDate(native);
36
- return undefined;
37
- }
38
- function toIsoFromDate(d) {
39
- try {
40
- return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), 0))
41
- .toISOString()
42
- .replace(/\.\d{3}Z$/, "Z");
43
- }
44
- catch {
45
- return undefined;
46
- }
47
- }
48
- function parseWithRegex(m, _re) {
49
- const monthMap = {
50
- jan: 0,
51
- feb: 1,
52
- mar: 2,
53
- apr: 3,
54
- may: 4,
55
- jun: 5,
56
- jul: 6,
57
- aug: 7,
58
- sep: 8,
59
- oct: 9,
60
- nov: 10,
61
- dec: 11,
62
- };
63
- try {
64
- // If the matched string contains time components, parse as Y-M-D H:M:S
65
- if (m[0].includes(":")) {
66
- const [_, y, mo, d, hh, mm, ss] = m;
67
- return new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss)));
68
- }
69
- // If the matched string contains hyphens, treat as DD-MMM-YYYY
70
- if (m[0].includes("-")) {
71
- const [_, dd, monStr, yyyy] = m;
72
- const mon = monthMap[monStr.toLowerCase()];
73
- return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
74
- }
75
- // Otherwise treat as MMM DD YYYY
76
- const [_, monStr, dd, yyyy] = m;
77
- const mon = monthMap[monStr.toLowerCase()];
78
- return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
79
- }
80
- catch {
81
- // fall through to undefined
82
- }
83
- return undefined;
84
- }
@@ -1,8 +0,0 @@
1
- export declare function extractTld(domain: string): string;
2
- export declare function getDomainParts(domain: string): {
3
- publicSuffix: string;
4
- tld: string;
5
- };
6
- export declare function isLikelyDomain(input: string): boolean;
7
- export declare function punyToUnicode(domain: string): string;
8
- export declare function isWhoisAvailable(text: string | undefined): boolean;
@@ -1,64 +0,0 @@
1
- import psl from "psl";
2
- export function extractTld(domain) {
3
- const lower = domain.trim().toLowerCase();
4
- try {
5
- const parsed = psl.parse?.(lower);
6
- const suffix = parsed?.tld;
7
- if (suffix) {
8
- const labels = String(suffix).split(".").filter(Boolean);
9
- if (labels.length)
10
- return labels[labels.length - 1];
11
- }
12
- }
13
- catch {
14
- // ignore and fall back
15
- }
16
- const parts = lower.split(".").filter(Boolean);
17
- return parts[parts.length - 1] ?? lower;
18
- }
19
- export function getDomainParts(domain) {
20
- const lower = domain.toLowerCase().trim();
21
- let publicSuffix;
22
- try {
23
- const parsed = psl.parse?.(lower);
24
- publicSuffix = parsed?.tld;
25
- }
26
- catch {
27
- // ignore
28
- }
29
- if (!publicSuffix) {
30
- const parts = lower.split(".").filter(Boolean);
31
- publicSuffix = parts.length ? parts[parts.length - 1] : lower;
32
- }
33
- const labels = publicSuffix.split(".").filter(Boolean);
34
- const tld = labels.length ? labels[labels.length - 1] : publicSuffix;
35
- return { publicSuffix, tld };
36
- }
37
- export function isLikelyDomain(input) {
38
- return /^[a-z0-9.-]+$/i.test(input) && input.includes(".");
39
- }
40
- export function punyToUnicode(domain) {
41
- try {
42
- return domain.normalize("NFC");
43
- }
44
- catch {
45
- return domain;
46
- }
47
- }
48
- // Common WHOIS availability phrases seen across registries/registrars
49
- const WHOIS_AVAILABLE_PATTERNS = [
50
- /\bno match\b/i,
51
- /\bnot found\b/i,
52
- /\bno entries found\b/i,
53
- /\bno data found\b/i,
54
- /\bavailable for registration\b/i,
55
- /\bdomain\s+available\b/i,
56
- /\bdomain status[:\s]+available\b/i,
57
- /\bobject does not exist\b/i,
58
- /\bthe queried object does not exist\b/i,
59
- ];
60
- export function isWhoisAvailable(text) {
61
- if (!text)
62
- return false;
63
- return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text));
64
- }
@@ -1,6 +0,0 @@
1
- export declare function uniq<T>(arr: T[] | undefined | null): T[] | undefined;
2
- export declare function parseKeyValueLines(text: string): Record<string, string[]>;
3
- export declare function parseCsv(value: string | undefined): string[] | undefined;
4
- export declare function asString(value: unknown): string | undefined;
5
- export declare function asStringArray(value: unknown): string[] | undefined;
6
- export declare function asDateLike(value: unknown): string | number | Date | undefined;
package/dist/lib/text.js DELETED
@@ -1,77 +0,0 @@
1
- export function uniq(arr) {
2
- if (!arr)
3
- return undefined;
4
- return Array.from(new Set(arr));
5
- }
6
- export function parseKeyValueLines(text) {
7
- const map = new Map();
8
- const lines = text.split(/\r?\n/);
9
- let lastKey;
10
- for (const rawLine of lines) {
11
- const line = rawLine.replace(/\s+$/, "");
12
- if (!line.trim())
13
- continue;
14
- // Bracketed form: [Key] value (common in .jp and some ccTLDs)
15
- const bracket = line.match(/^\s*\[([^\]]+)\]\s*(.*)$/);
16
- if (bracket) {
17
- const key = bracket[1].trim().toLowerCase();
18
- const value = bracket[2].trim();
19
- const list = map.get(key) ?? [];
20
- if (value)
21
- list.push(value);
22
- map.set(key, list);
23
- lastKey = key;
24
- continue;
25
- }
26
- // Colon form: Key: value
27
- const idx = line.indexOf(":");
28
- if (idx !== -1) {
29
- const key = line.slice(0, idx).trim().toLowerCase();
30
- const value = line.slice(idx + 1).trim();
31
- if (!key) {
32
- lastKey = undefined;
33
- continue;
34
- }
35
- const list = map.get(key) ?? [];
36
- if (value)
37
- list.push(value);
38
- map.set(key, list);
39
- lastKey = key;
40
- continue;
41
- }
42
- // Continuation line: starts with indentation after a key appeared
43
- if (lastKey && /^\s+/.test(line)) {
44
- const value = line.trim();
45
- if (value) {
46
- const list = map.get(lastKey) ?? [];
47
- list.push(value);
48
- map.set(lastKey, list);
49
- }
50
- }
51
- // Otherwise ignore non key-value lines
52
- }
53
- return Object.fromEntries(map);
54
- }
55
- export function parseCsv(value) {
56
- if (!value)
57
- return undefined;
58
- return value
59
- .split(/[,\s]+/)
60
- .map((s) => s.trim())
61
- .filter(Boolean);
62
- }
63
- export function asString(value) {
64
- return typeof value === "string" ? value : undefined;
65
- }
66
- export function asStringArray(value) {
67
- return Array.isArray(value)
68
- ? value.filter((x) => typeof x === "string")
69
- : undefined;
70
- }
71
- export function asDateLike(value) {
72
- if (typeof value === "string" ||
73
- typeof value === "number" ||
74
- value instanceof Date)
75
- return value;
76
- return undefined;
77
- }
@@ -1,6 +0,0 @@
1
- import type { LookupOptions } from "../types.js";
2
- /**
3
- * Resolve RDAP base URLs for a given TLD using IANA's bootstrap registry.
4
- * Returns zero or more base URLs (always suffixed with a trailing slash).
5
- */
6
- export declare function getRdapBaseUrlsForTld(tld: string, options?: LookupOptions): Promise<string[]>;
@@ -1,30 +0,0 @@
1
- import { withTimeout } from "../lib/async.js";
2
- import { DEFAULT_TIMEOUT_MS } from "../lib/constants.js";
3
- /**
4
- * Resolve RDAP base URLs for a given TLD using IANA's bootstrap registry.
5
- * Returns zero or more base URLs (always suffixed with a trailing slash).
6
- */
7
- export async function getRdapBaseUrlsForTld(tld, options) {
8
- const bootstrapUrl = options?.customBootstrapUrl ?? "https://data.iana.org/rdap/dns.json";
9
- const res = await withTimeout(fetch(bootstrapUrl, {
10
- method: "GET",
11
- headers: { accept: "application/json" },
12
- signal: options?.signal,
13
- }), options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, "RDAP bootstrap timeout");
14
- if (!res.ok)
15
- return [];
16
- const data = (await res.json());
17
- const target = tld.toLowerCase();
18
- const bases = [];
19
- for (const svc of data.services) {
20
- const tlds = svc[0];
21
- const urls = svc[1];
22
- if (tlds.map((x) => x.toLowerCase()).includes(target)) {
23
- for (const u of urls) {
24
- const base = u.endsWith("/") ? u : `${u}/`;
25
- bases.push(base);
26
- }
27
- }
28
- }
29
- return Array.from(new Set(bases));
30
- }
@@ -1,9 +0,0 @@
1
- import type { LookupOptions } from "../types.js";
2
- /**
3
- * Fetch RDAP JSON for a domain from a specific RDAP base URL.
4
- * Throws on HTTP >= 400 (includes RDAP error JSON payloads).
5
- */
6
- export declare function fetchRdapDomain(domain: string, baseUrl: string, options?: LookupOptions): Promise<{
7
- url: string;
8
- json: unknown;
9
- }>;
@@ -1,21 +0,0 @@
1
- import { withTimeout } from "../lib/async.js";
2
- import { DEFAULT_TIMEOUT_MS } from "../lib/constants.js";
3
- // Use global fetch (Node 18+). For large JSON we keep it simple.
4
- /**
5
- * Fetch RDAP JSON for a domain from a specific RDAP base URL.
6
- * Throws on HTTP >= 400 (includes RDAP error JSON payloads).
7
- */
8
- export async function fetchRdapDomain(domain, baseUrl, options) {
9
- const url = new URL(`domain/${encodeURIComponent(domain)}`, baseUrl).toString();
10
- const res = await withTimeout(fetch(url, {
11
- method: "GET",
12
- headers: { accept: "application/rdap+json, application/json" },
13
- signal: options?.signal,
14
- }), options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, "RDAP lookup timeout");
15
- if (!res.ok) {
16
- const bodyText = await res.text();
17
- throw new Error(`RDAP ${res.status}: ${bodyText.slice(0, 500)}`);
18
- }
19
- const json = await res.json();
20
- return { url, json };
21
- }
@@ -1,6 +0,0 @@
1
- import type { DomainRecord } from "../types.js";
2
- /**
3
- * Convert RDAP JSON into our normalized DomainRecord.
4
- * This function is defensive: RDAP servers vary in completeness and field naming.
5
- */
6
- export declare function normalizeRdap(inputDomain: string, tld: string, rdap: unknown, rdapServersTried: string[], fetchedAtISO: string, includeRaw?: boolean): DomainRecord;
@@ -1,214 +0,0 @@
1
- import { toISO } from "../lib/dates.js";
2
- import { asDateLike, asString, asStringArray, uniq } from "../lib/text.js";
3
- /**
4
- * Convert RDAP JSON into our normalized DomainRecord.
5
- * This function is defensive: RDAP servers vary in completeness and field naming.
6
- */
7
- export function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, fetchedAtISO, includeRaw = false) {
8
- const doc = (rdap ?? {});
9
- // Safe helpers for optional fields
10
- const _get = (obj, path) => path.reduce((o, k) => o?.[k] ?? undefined, obj);
11
- // Prefer ldhName (punycode) and unicodeName if provided
12
- const ldhName = asString(doc.ldhName) || asString(doc.handle);
13
- const unicodeName = asString(doc.unicodeName);
14
- // Registrar entity can be provided with role "registrar"
15
- const registrar = extractRegistrar(doc.entities);
16
- // Nameservers: normalize host + IPs
17
- const nameservers = Array.isArray(doc.nameservers)
18
- ? doc.nameservers
19
- .map((ns) => {
20
- const host = (asString(ns.ldhName) ??
21
- asString(ns.unicodeName) ??
22
- "").toLowerCase();
23
- const ip = ns.ipAddresses;
24
- const ipv4 = asStringArray(ip?.v4);
25
- const ipv6 = asStringArray(ip?.v6);
26
- const n = { host };
27
- if (ipv4?.length)
28
- n.ipv4 = ipv4;
29
- if (ipv6?.length)
30
- n.ipv6 = ipv6;
31
- return n;
32
- })
33
- .filter((n) => !!n.host)
34
- : undefined;
35
- // Contacts: RDAP entities include roles like registrant, administrative, technical, billing, abuse
36
- const contacts = extractContacts(doc.entities);
37
- // RDAP uses IANA EPP status values. Preserve raw plus a description if any remarks are present.
38
- const statuses = Array.isArray(doc.status)
39
- ? doc.status
40
- .filter((s) => typeof s === "string")
41
- .map((s) => ({ status: s, raw: s }))
42
- : undefined;
43
- // Secure DNS info
44
- const secureDNS = doc.secureDNS;
45
- const dnssec = secureDNS
46
- ? {
47
- enabled: !!secureDNS.delegationSigned,
48
- dsRecords: Array.isArray(secureDNS.dsData)
49
- ? secureDNS.dsData.map((d) => ({
50
- keyTag: d.keyTag,
51
- algorithm: d.algorithm,
52
- digestType: d.digestType,
53
- digest: d.digest,
54
- }))
55
- : undefined,
56
- }
57
- : undefined;
58
- const events = Array.isArray(doc.events)
59
- ? doc.events
60
- : [];
61
- const byAction = (action) => events.find((e) => typeof e?.eventAction === "string" &&
62
- e.eventAction.toLowerCase().includes(action));
63
- const creationDate = toISO(asDateLike(byAction("registration")?.eventDate) ??
64
- asDateLike(doc.registrationDate));
65
- const updatedDate = toISO(asDateLike(byAction("last changed")?.eventDate) ??
66
- asDateLike(doc.lastChangedDate));
67
- const expirationDate = toISO(asDateLike(byAction("expiration")?.eventDate) ??
68
- asDateLike(doc.expirationDate));
69
- const deletionDate = toISO(asDateLike(byAction("deletion")?.eventDate) ?? asDateLike(doc.deletionDate));
70
- // Derive a simple transfer lock flag from statuses
71
- const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
72
- // The RDAP document may include "port43" pointer to authoritative WHOIS
73
- const whoisServer = asString(doc.port43);
74
- const record = {
75
- domain: unicodeName || ldhName || inputDomain,
76
- tld,
77
- isRegistered: true,
78
- isIDN: /(^|\.)xn--/i.test(ldhName || inputDomain),
79
- unicodeName: unicodeName || undefined,
80
- punycodeName: ldhName || undefined,
81
- registry: undefined, // RDAP rarely includes a clean registry operator name
82
- registrar: registrar,
83
- reseller: undefined,
84
- statuses: statuses,
85
- creationDate,
86
- updatedDate,
87
- expirationDate,
88
- deletionDate,
89
- transferLock,
90
- dnssec,
91
- nameservers: nameservers
92
- ? uniq(nameservers.map((n) => ({ ...n, host: n.host.toLowerCase() })))
93
- : undefined,
94
- contacts,
95
- whoisServer,
96
- rdapServers: rdapServersTried,
97
- rawRdap: includeRaw ? rdap : undefined,
98
- rawWhois: undefined,
99
- source: "rdap",
100
- fetchedAt: fetchedAtISO,
101
- warnings: undefined,
102
- };
103
- return record;
104
- }
105
- function extractRegistrar(entities) {
106
- if (!Array.isArray(entities))
107
- return undefined;
108
- for (const ent of entities) {
109
- const roles = Array.isArray(ent?.roles)
110
- ? ent.roles.filter((r) => typeof r === "string")
111
- : [];
112
- if (!roles.some((r) => /registrar/i.test(r)))
113
- continue;
114
- const v = parseVcard(ent?.vcardArray);
115
- const ianaId = Array.isArray(ent?.publicIds)
116
- ? ent.publicIds.find((id) => /iana\s*registrar\s*id/i.test(String(id?.type)))?.identifier
117
- : undefined;
118
- return {
119
- name: v.fn || v.org || asString(ent?.handle) || undefined,
120
- ianaId: asString(ianaId),
121
- url: v.url ?? undefined,
122
- email: v.email ?? undefined,
123
- phone: v.tel ?? undefined,
124
- };
125
- }
126
- return undefined;
127
- }
128
- function extractContacts(entities) {
129
- if (!Array.isArray(entities))
130
- return undefined;
131
- const out = [];
132
- for (const ent of entities) {
133
- const roles = Array.isArray(ent?.roles)
134
- ? ent.roles.filter((r) => typeof r === "string")
135
- : [];
136
- const v = parseVcard(ent?.vcardArray);
137
- const type = roles.find((r) => /registrant|administrative|technical|billing|abuse|reseller/i.test(r));
138
- if (!type)
139
- continue;
140
- const map = {
141
- registrant: "registrant",
142
- administrative: "admin",
143
- technical: "tech",
144
- billing: "billing",
145
- abuse: "abuse",
146
- reseller: "reseller",
147
- };
148
- const roleKey = (map[type.toLowerCase()] ?? "unknown");
149
- out.push({
150
- type: roleKey,
151
- name: v.fn,
152
- organization: v.org,
153
- email: v.email,
154
- phone: v.tel,
155
- fax: v.fax,
156
- street: v.street,
157
- city: v.locality,
158
- state: v.region,
159
- postalCode: v.postcode,
160
- country: v.country,
161
- countryCode: v.countryCode,
162
- });
163
- }
164
- return out.length ? out : undefined;
165
- }
166
- // Parse a minimal subset of vCard 4.0 arrays as used in RDAP "vcardArray" fields
167
- function parseVcard(vcardArray) {
168
- // vcardArray is typically ["vcard", [["version",{} ,"text","4.0"], ["fn",{} ,"text","Example"], ...]]
169
- if (!Array.isArray(vcardArray) ||
170
- vcardArray[0] !== "vcard" ||
171
- !Array.isArray(vcardArray[1]))
172
- return {};
173
- const entries = vcardArray[1];
174
- const out = {};
175
- for (const e of entries) {
176
- const key = e?.[0];
177
- const _valueType = e?.[2];
178
- const value = e?.[3];
179
- if (!key)
180
- continue;
181
- switch (String(key).toLowerCase()) {
182
- case "fn":
183
- out.fn = asString(value);
184
- break;
185
- case "org":
186
- out.org = Array.isArray(value)
187
- ? value.map((x) => String(x)).join(" ")
188
- : asString(value);
189
- break;
190
- case "email":
191
- out.email = asString(value);
192
- break;
193
- case "tel":
194
- out.tel = asString(value);
195
- break;
196
- case "url":
197
- out.url = asString(value);
198
- break;
199
- case "adr": {
200
- // adr value is [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]
201
- if (Array.isArray(value)) {
202
- out.street = value[2] ? String(value[2]).split(/\n|,\s*/) : undefined;
203
- out.locality = asString(value[3]);
204
- out.region = asString(value[4]);
205
- out.postcode = asString(value[5]);
206
- out.country = asString(value[6]);
207
- }
208
- break;
209
- }
210
- }
211
- }
212
- // Best effort country code from country name (often omitted). Leaving undefined unless explicitly provided.
213
- return out;
214
- }