@zhin.js/plugin-nettools 0.1.2 → 0.1.4
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/CHANGELOG.md +20 -0
- package/commands/dns/[name]/[[type]]/index.js +5 -5
- package/commands/dns/[name]/[[type]]/index.ts +5 -5
- package/commands/domain-price/[suffix]/[[order]]/index.js +5 -5
- package/commands/domain-price/[suffix]/[[order]]/index.ts +5 -5
- package/commands/ping/[host]/index.js +4 -4
- package/commands/ping/[host]/index.ts +4 -4
- package/commands/whois/[domain]/index.js +4 -4
- package/commands/whois/[domain]/index.ts +4 -4
- package/lib/address-policy.d.ts +2 -0
- package/lib/address-policy.js +55 -54
- package/lib/address-policy.js.map +1 -1
- package/lib/cooldown.js +2 -1
- package/lib/cooldown.js.map +1 -1
- package/lib/dns.d.ts +2 -1
- package/lib/dns.js +14 -12
- package/lib/dns.js.map +1 -1
- package/lib/domain-price.d.ts +2 -1
- package/lib/domain-price.js +16 -19
- package/lib/domain-price.js.map +1 -1
- package/lib/errors.d.ts +5 -2
- package/lib/errors.js +8 -4
- package/lib/errors.js.map +1 -1
- package/lib/http.js +12 -11
- package/lib/http.js.map +1 -1
- package/lib/messages.d.ts +329 -0
- package/lib/messages.js +335 -0
- package/lib/messages.js.map +1 -0
- package/lib/ping.d.ts +2 -1
- package/lib/ping.js +13 -11
- package/lib/ping.js.map +1 -1
- package/lib/presentation.d.ts +1 -0
- package/lib/presentation.js +36 -0
- package/lib/presentation.js.map +1 -0
- package/lib/service.d.ts +6 -5
- package/lib/service.js +16 -15
- package/lib/service.js.map +1 -1
- package/lib/target-guard.js +6 -4
- package/lib/target-guard.js.map +1 -1
- package/lib/whois.d.ts +2 -1
- package/lib/whois.js +18 -16
- package/lib/whois.js.map +1 -1
- package/package.json +7 -7
- package/skills/nettools/tools/dns_lookup/index.js +1 -1
- package/skills/nettools/tools/domain_price/index.js +1 -1
- package/skills/nettools/tools/net_ping/index.js +1 -1
- package/skills/nettools/tools/whois_lookup/index.js +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address-policy.js","sourceRoot":"","sources":["../src/address-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Target address policy (SSRF guard).\n *\n * Network tools probe hosts that users type into chat. Even though the probes\n * themselves run on third-party services (check-host.net, public DoH, RDAP),\n * we refuse anything that points at private, loopback, link-local, reserved or\n * cloud-metadata space so the bot can never be used to map internal networks\n * or poke metadata endpoints through a relay.\n *\n * Design:\n * 1. Canonicalise first. The input is fed through the WHATWG URL host parser,\n * the same parser browsers and Node's fetch use. It turns odd IPv4 spellings\n * (`2130706433`, `0x7f.1`, `017700000001`, `127.1`) into dotted quads,\n * normalises IPv6 and converts IDN to punycode. We then classify the\n * canonical form, so \"what we checked\" is exactly \"what we send\".\n * 2. Classify by bytes. IPv4 and IPv6 are compared as raw bytes against\n * prefix tables. Embedded IPv4 (IPv4-mapped, NAT64, 6to4) is unwrapped and\n * classified as IPv4.\n * 3. Allow-list IPv6. Only global unicast (2000::/3) is accepted, minus the\n * documentation / Teredo / special ranges listed below.\n * 4. Names. Single-label names, internal suffixes (`.local`, `.internal`, ...)\n * and well-known metadata hostnames are refused before any lookup.\n * Hostnames are additionally resolved (see target-guard.ts) and refused\n * when any A/AAAA record is blocked.\n */\nimport { isIPv4, isIPv6 } from 'node:net';\nimport { NettoolsError } from './errors.js';\n\nexport type ParsedHost =\n | { readonly kind: 'ipv4'; readonly address: string; readonly bytes: Uint8Array }\n | { readonly kind: 'ipv6'; readonly address: string; readonly bytes: Uint8Array }\n | { readonly kind: 'domain'; readonly name: string };\n\nexport type AddressVerdict =\n | { readonly blocked: false }\n | { readonly blocked: true; readonly reason: string };\n\nexport interface ParseHostOptions {\n /** Allow `_` in labels (needed for DNS names such as `_dmarc.example.com`). */\n readonly allowUnderscore?: boolean;\n /** Minimum number of labels for a domain (default 2, i.e. a full domain). */\n readonly minLabels?: number;\n}\n\nconst ALLOWED: AddressVerdict = Object.freeze({ blocked: false });\n\nfunction blocked(reason: string): AddressVerdict {\n return Object.freeze({ blocked: true, reason });\n}\n\n/* ------------------------------------------------------------------ */\n/* Tables */\n/* ------------------------------------------------------------------ */\n\ninterface PrefixRule {\n readonly prefix: Uint8Array;\n readonly bits: number;\n readonly reason: string;\n}\n\nconst CLOUD_METADATA_REASON = '云服务元数据地址';\n\n/** Exact IPv4 metadata endpoints, checked before the range table for a clearer message. */\nconst CLOUD_METADATA_IPV4 = new Set([\n '169.254.169.254', // AWS / GCP / Azure / Oracle / OpenStack instance metadata\n '169.254.170.2', // AWS ECS task metadata\n '169.254.169.123', // AWS Time Sync\n '100.100.100.200', // Alibaba Cloud metadata\n '168.63.129.16', // Azure WireServer (public range, still host-internal)\n]);\n\n/** Exact IPv6 metadata endpoints. */\nconst CLOUD_METADATA_IPV6 = ['fd00:ec2::254', 'fd00:ec2::23'];\n\nconst BLOCKED_IPV4_RANGES: readonly PrefixRule[] = [\n ipv4Rule('0.0.0.0', 8, '未指定 / “本网络”地址'),\n ipv4Rule('10.0.0.0', 8, '私有地址 (RFC 1918)'),\n ipv4Rule('100.64.0.0', 10, '运营商级 NAT 地址 (CGNAT, RFC 6598)'),\n ipv4Rule('127.0.0.0', 8, '回环地址'),\n ipv4Rule('169.254.0.0', 16, '链路本地地址'),\n ipv4Rule('172.16.0.0', 12, '私有地址 (RFC 1918)'),\n ipv4Rule('192.0.0.0', 24, 'IETF 协议保留地址'),\n ipv4Rule('192.0.2.0', 24, '文档示例地址'),\n ipv4Rule('192.88.99.0', 24, '已废弃的 6to4 中继地址'),\n ipv4Rule('192.168.0.0', 16, '私有地址 (RFC 1918)'),\n ipv4Rule('198.18.0.0', 15, '基准测试保留地址'),\n ipv4Rule('198.51.100.0', 24, '文档示例地址'),\n ipv4Rule('203.0.113.0', 24, '文档示例地址'),\n ipv4Rule('224.0.0.0', 4, '组播地址'),\n ipv4Rule('240.0.0.0', 4, '保留地址(含广播地址)'),\n];\n\n/** Special IPv6 ranges inside 2000::/3 that are still refused. */\nconst BLOCKED_GLOBAL_IPV6_RANGES: readonly PrefixRule[] = [\n ipv6Rule('2001::', 32, 'Teredo 隧道地址'),\n ipv6Rule('2001:2::', 48, '基准测试保留地址'),\n ipv6Rule('2001:10::', 28, 'ORCHID 保留地址'),\n ipv6Rule('2001:db8::', 32, '文档示例地址'),\n ipv6Rule('3fff::', 20, '文档示例地址'),\n];\n\nconst BLOCKED_HOSTNAMES = new Set([\n 'localhost',\n 'metadata',\n 'metadata.google.internal',\n 'metadata.goog',\n 'instance-data',\n 'instance-data.ec2.internal',\n]);\n\n/** Suffixes that only make sense inside a private network or are reserved (RFC 2606 / 6761 / 6762). */\nconst BLOCKED_SUFFIXES = [\n 'localhost',\n 'local',\n 'localdomain',\n 'internal',\n 'intranet',\n 'private',\n 'corp',\n 'home',\n 'lan',\n 'home.arpa',\n 'arpa',\n 'test',\n 'invalid',\n 'example',\n 'onion',\n];\n\nconst MAX_HOSTNAME_LENGTH = 253;\nconst MAX_LABEL_LENGTH = 63;\n\n/* ------------------------------------------------------------------ */\n/* Parsing */\n/* ------------------------------------------------------------------ */\n\n/**\n * Parse and canonicalise one user-supplied host (domain, IPv4 or IPv6).\n * Throws {@link NettoolsError} (`invalid_input`) for anything that is not a\n * bare host: URLs, ports, paths, user-info, zone ids and percent-encoding are\n * all refused rather than \"helpfully\" stripped.\n */\nexport function parseHostInput(input: string, options: ParseHostOptions = {}): ParsedHost {\n const trimmed = input.trim();\n if (trimmed.length === 0) {\n throw invalid('请提供主机名或 IP 地址');\n }\n if (trimmed.length > MAX_HOSTNAME_LENGTH + 2) {\n throw invalid('主机名过长');\n }\n if (/[\\s/\\\\@?#%]/u.test(trimmed)) {\n throw invalid('只接受裸主机名或 IP,不要带协议、端口、路径或特殊字符');\n }\n\n const bracketed = trimmed.startsWith('[') && trimmed.endsWith(']');\n const ipv6Candidate = bracketed ? trimmed.slice(1, -1) : trimmed;\n if (bracketed || trimmed.includes(':')) {\n if (!isIPv6(ipv6Candidate)) {\n throw invalid('不是有效的 IPv6 地址(如需端口请去掉端口)');\n }\n const canonical = canonicalizeWithUrlParser(`[${ipv6Candidate}]`);\n const address = canonical.slice(1, -1);\n return Object.freeze({ kind: 'ipv6', address, bytes: ipv6ToBytes(address) });\n }\n\n const canonical = canonicalizeWithUrlParser(trimmed);\n if (isIPv4(canonical)) {\n return Object.freeze({ kind: 'ipv4', address: canonical, bytes: ipv4ToBytes(canonical) });\n }\n const name = canonical.endsWith('.') ? canonical.slice(0, -1) : canonical;\n validateDomainSyntax(name, options);\n return Object.freeze({ kind: 'domain', name });\n}\n\n/**\n * Run the WHATWG host parser by assigning to `URL#hostname` on a placeholder\n * URL. An invalid host leaves the placeholder untouched, which we detect.\n */\nfunction canonicalizeWithUrlParser(host: string): string {\n const placeholder = 'placeholder.invalid';\n const probe = new URL(`http://${placeholder}/`);\n probe.hostname = host;\n const result = probe.hostname;\n const unchanged = result === placeholder && host.toLowerCase() !== placeholder;\n if (unchanged || result.length === 0) {\n throw invalid('不是有效的主机名或 IP 地址');\n }\n return result;\n}\n\nfunction validateDomainSyntax(name: string, options: ParseHostOptions): void {\n if (name.length === 0 || name.length > MAX_HOSTNAME_LENGTH) {\n throw invalid('主机名长度不合法');\n }\n const labels = name.split('.');\n if (labels.length < (options.minLabels ?? 2)) {\n throw invalid('请提供完整域名(例如 example.com)');\n }\n const labelPattern = options.allowUnderscore\n ? /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/u\n : /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;\n for (const label of labels) {\n if (label.length === 0 || label.length > MAX_LABEL_LENGTH || !labelPattern.test(label)) {\n throw invalid(`域名标签不合法:${label || '(空)'}`);\n }\n }\n const topLevel = labels[labels.length - 1] ?? '';\n if (/^\\d+$/u.test(topLevel)) {\n throw invalid('顶级域不能是纯数字');\n }\n}\n\nfunction invalid(message: string): NettoolsError {\n return new NettoolsError('invalid_input', message);\n}\n\n/* ------------------------------------------------------------------ */\n/* Classification */\n/* ------------------------------------------------------------------ */\n\n/** Classify a parsed host without any network access. Domains are checked by name only. */\nexport function classifyParsedHost(host: ParsedHost): AddressVerdict {\n if (host.kind === 'domain') {\n return classifyHostname(host.name);\n }\n if (host.kind === 'ipv4') {\n return classifyIpv4Bytes(host.bytes);\n }\n return classifyIpv6Bytes(host.bytes);\n}\n\n/** Classify a literal address string (any spelling the URL parser understands). */\nexport function classifyAddress(address: string): AddressVerdict {\n const parsed = parseHostInput(address);\n if (parsed.kind === 'domain') {\n throw invalid(`不是 IP 地址:${address}`);\n }\n return classifyParsedHost(parsed);\n}\n\nexport function classifyHostname(name: string): AddressVerdict {\n const normalized = name.toLowerCase().replace(/\\.$/u, '');\n if (BLOCKED_HOSTNAMES.has(normalized)) {\n return normalized.startsWith('localhost')\n ? blocked('本机主机名')\n : blocked(CLOUD_METADATA_REASON);\n }\n for (const suffix of BLOCKED_SUFFIXES) {\n if (normalized === suffix || normalized.endsWith(`.${suffix}`)) {\n return blocked(`内网或保留域名后缀 .${suffix}`);\n }\n }\n return ALLOWED;\n}\n\nfunction classifyIpv4Bytes(bytes: Uint8Array): AddressVerdict {\n const dotted = Array.from(bytes).join('.');\n if (CLOUD_METADATA_IPV4.has(dotted)) {\n return blocked(CLOUD_METADATA_REASON);\n }\n for (const rule of BLOCKED_IPV4_RANGES) {\n if (matchesPrefix(bytes, rule.prefix, rule.bits)) {\n return blocked(rule.reason);\n }\n }\n return ALLOWED;\n}\n\nconst IPV4_MAPPED_PREFIX = ipv6ToBytes('::ffff:0:0');\nconst NAT64_WELL_KNOWN_PREFIX = ipv6ToBytes('64:ff9b::');\nconst NAT64_LOCAL_PREFIX = ipv6ToBytes('64:ff9b:1::');\nconst SIX_TO_FOUR_PREFIX = ipv6ToBytes('2002::');\nconst GLOBAL_UNICAST_PREFIX = ipv6ToBytes('2000::');\nconst UNIQUE_LOCAL_PREFIX = ipv6ToBytes('fc00::');\nconst LINK_LOCAL_PREFIX = ipv6ToBytes('fe80::');\nconst SITE_LOCAL_PREFIX = ipv6ToBytes('fec0::');\nconst MULTICAST_PREFIX = ipv6ToBytes('ff00::');\nconst CLOUD_METADATA_IPV6_BYTES = CLOUD_METADATA_IPV6.map((address) => ipv6ToBytes(address));\n\nfunction classifyIpv6Bytes(bytes: Uint8Array): AddressVerdict {\n if (bytes.every((byte) => byte === 0)) {\n return blocked('未指定地址 (::)');\n }\n const isLoopback = bytes.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;\n if (isLoopback) {\n return blocked('回环地址 (::1)');\n }\n for (const metadata of CLOUD_METADATA_IPV6_BYTES) {\n if (matchesPrefix(bytes, metadata, 128)) {\n return blocked(CLOUD_METADATA_REASON);\n }\n }\n if (matchesPrefix(bytes, IPV4_MAPPED_PREFIX, 96)) {\n return classifyEmbeddedIpv4(bytes.slice(12, 16), 'IPv4 映射地址');\n }\n if (matchesPrefix(bytes, NAT64_WELL_KNOWN_PREFIX, 96)) {\n return classifyEmbeddedIpv4(bytes.slice(12, 16), 'NAT64 地址');\n }\n if (matchesPrefix(bytes, NAT64_LOCAL_PREFIX, 48)) {\n return blocked('本地 NAT64 地址');\n }\n if (matchesPrefix(bytes, SIX_TO_FOUR_PREFIX, 16)) {\n return classifyEmbeddedIpv4(bytes.slice(2, 6), '6to4 地址');\n }\n if (matchesPrefix(bytes, UNIQUE_LOCAL_PREFIX, 7)) {\n return blocked('唯一本地地址 (ULA, fc00::/7)');\n }\n if (matchesPrefix(bytes, LINK_LOCAL_PREFIX, 10)) {\n return blocked('链路本地地址 (fe80::/10)');\n }\n if (matchesPrefix(bytes, SITE_LOCAL_PREFIX, 10)) {\n return blocked('已废弃的站点本地地址 (fec0::/10)');\n }\n if (matchesPrefix(bytes, MULTICAST_PREFIX, 8)) {\n return blocked('组播地址 (ff00::/8)');\n }\n if (!matchesPrefix(bytes, GLOBAL_UNICAST_PREFIX, 3)) {\n return blocked('非全球单播 IPv6 地址');\n }\n for (const rule of BLOCKED_GLOBAL_IPV6_RANGES) {\n if (matchesPrefix(bytes, rule.prefix, rule.bits)) {\n return blocked(rule.reason);\n }\n }\n return ALLOWED;\n}\n\nfunction classifyEmbeddedIpv4(embedded: Uint8Array, wrapper: string): AddressVerdict {\n const verdict = classifyIpv4Bytes(embedded);\n if (!verdict.blocked) {\n return verdict;\n }\n const dotted = Array.from(embedded).join('.');\n return blocked(`${wrapper}指向 ${dotted}(${verdict.reason})`);\n}\n\n/* ------------------------------------------------------------------ */\n/* Byte helpers */\n/* ------------------------------------------------------------------ */\n\nfunction matchesPrefix(bytes: Uint8Array, prefix: Uint8Array, bits: number): boolean {\n if (bytes.length !== prefix.length) {\n return false;\n }\n const fullBytes = Math.floor(bits / 8);\n for (let index = 0; index < fullBytes; index += 1) {\n if (bytes[index] !== prefix[index]) {\n return false;\n }\n }\n const remainingBits = bits % 8;\n if (remainingBits === 0) {\n return true;\n }\n const mask = (0xff << (8 - remainingBits)) & 0xff;\n return ((bytes[fullBytes] ?? 0) & mask) === ((prefix[fullBytes] ?? 0) & mask);\n}\n\n/** Dotted-quad IPv4 to 4 bytes. Callers must pass a canonical dotted quad. */\nexport function ipv4ToBytes(address: string): Uint8Array {\n const parts = address.split('.').map((part) => Number(part));\n const valid = parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255);\n if (!valid) {\n throw invalid(`不是有效的 IPv4 地址:${address}`);\n }\n return Uint8Array.from(parts);\n}\n\n/** Textual IPv6 (with optional `::` and dotted IPv4 tail) to 16 bytes. */\nexport function ipv6ToBytes(address: string): Uint8Array {\n let text = address.toLowerCase();\n const groups: number[] = [];\n const lastColon = text.lastIndexOf(':');\n const tail = text.slice(lastColon + 1);\n const tailGroups: number[] = [];\n if (tail.includes('.')) {\n const v4 = ipv4ToBytes(tail);\n tailGroups.push(((v4[0] ?? 0) << 8) | (v4[1] ?? 0));\n tailGroups.push(((v4[2] ?? 0) << 8) | (v4[3] ?? 0));\n text = text.slice(0, lastColon + 1) + '0:0';\n }\n const halves = text.split('::');\n if (halves.length > 2) {\n throw invalid(`不是有效的 IPv6 地址:${address}`);\n }\n const head = parseGroups(halves[0] ?? '');\n const rest = halves.length === 2 ? parseGroups(halves[1] ?? '') : [];\n const missing = 8 - head.length - rest.length;\n if (halves.length === 2 ? missing < 1 : missing !== 0) {\n throw invalid(`不是有效的 IPv6 地址:${address}`);\n }\n groups.push(...head);\n for (let index = 0; index < missing && halves.length === 2; index += 1) {\n groups.push(0);\n }\n groups.push(...rest);\n if (tailGroups.length === 2) {\n groups.splice(6, 2, ...tailGroups);\n }\n const bytes = new Uint8Array(16);\n groups.forEach((group, index) => {\n bytes[index * 2] = (group >> 8) & 0xff;\n bytes[index * 2 + 1] = group & 0xff;\n });\n return bytes;\n}\n\nfunction parseGroups(text: string): number[] {\n if (text.length === 0) {\n return [];\n }\n return text.split(':').map((group) => {\n if (!/^[0-9a-f]{1,4}$/u.test(group)) {\n throw invalid(`IPv6 分组不合法:${group}`);\n }\n return Number.parseInt(group, 16);\n });\n}\n\nfunction ipv4Rule(base: string, bits: number, reason: string): PrefixRule {\n return Object.freeze({ prefix: ipv4ToBytes(base), bits, reason });\n}\n\nfunction ipv6Rule(base: string, bits: number, reason: string): PrefixRule {\n return Object.freeze({ prefix: ipv6ToBytes(base), bits, reason });\n}\n\n/** Build the PTR query name for an IP (`in-addr.arpa` / `ip6.arpa`). */\nexport function reverseLookupName(host: ParsedHost): string {\n if (host.kind === 'ipv4') {\n return `${Array.from(host.bytes).reverse().join('.')}.in-addr.arpa`;\n }\n if (host.kind === 'ipv6') {\n const nibbles: string[] = [];\n for (const byte of host.bytes) {\n nibbles.push((byte >> 4).toString(16));\n nibbles.push((byte & 0x0f).toString(16));\n }\n return `${nibbles.reverse().join('.')}.ip6.arpa`;\n }\n throw invalid('反向解析需要 IP 地址');\n}\n\n/** Human label for a parsed host. */\nexport function displayHost(host: ParsedHost): string {\n return host.kind === 'domain' ? host.name : host.address;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"address-policy.js","sourceRoot":"","sources":["../src/address-policy.ts"],"names":[],"mappings":"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","sourcesContent":["import { message, formatMessage, type NettoolsMessage } from './messages.js';\n/**\n * Target address policy (SSRF guard).\n *\n * Network tools probe hosts that users type into chat. Even though the probes\n * themselves run on third-party services (check-host.net, public DoH, RDAP),\n * we refuse anything that points at private, loopback, link-local, reserved or\n * cloud-metadata space so the bot can never be used to map internal networks\n * or poke metadata endpoints through a relay.\n *\n * Design:\n * 1. Canonicalise first. The input is fed through the WHATWG URL host parser,\n * the same parser browsers and Node's fetch use. It turns odd IPv4 spellings\n * (`2130706433`, `0x7f.1`, `017700000001`, `127.1`) into dotted quads,\n * normalises IPv6 and converts IDN to punycode. We then classify the\n * canonical form, so \"what we checked\" is exactly \"what we send\".\n * 2. Classify by bytes. IPv4 and IPv6 are compared as raw bytes against\n * prefix tables. Embedded IPv4 (IPv4-mapped, NAT64, 6to4) is unwrapped and\n * classified as IPv4.\n * 3. Allow-list IPv6. Only global unicast (2000::/3) is accepted, minus the\n * documentation / Teredo / special ranges listed below.\n * 4. Names. Single-label names, internal suffixes (`.local`, `.internal`, ...)\n * and well-known metadata hostnames are refused before any lookup.\n * Hostnames are additionally resolved (see target-guard.ts) and refused\n * when any A/AAAA record is blocked.\n */\nimport { isIPv4, isIPv6 } from 'node:net';\nimport { NettoolsError } from './errors.js';\n\nexport type ParsedHost =\n | { readonly kind: 'ipv4'; readonly address: string; readonly bytes: Uint8Array }\n | { readonly kind: 'ipv6'; readonly address: string; readonly bytes: Uint8Array }\n | { readonly kind: 'domain'; readonly name: string };\n\nexport type AddressVerdict =\n | { readonly blocked: false }\n | { readonly blocked: true; readonly reason: string; readonly detail: NettoolsMessage };\n\nexport interface ParseHostOptions {\n /** Allow `_` in labels (needed for DNS names such as `_dmarc.example.com`). */\n readonly allowUnderscore?: boolean;\n /** Minimum number of labels for a domain (default 2, i.e. a full domain). */\n readonly minLabels?: number;\n}\n\nconst ALLOWED: AddressVerdict = Object.freeze({ blocked: false });\n\nfunction blocked(detail: NettoolsMessage): AddressVerdict {\n return Object.freeze({ blocked: true, reason: formatMessage(detail), detail });\n}\n\n/* ------------------------------------------------------------------ */\n/* Tables */\n/* ------------------------------------------------------------------ */\n\ninterface PrefixRule {\n readonly prefix: Uint8Array;\n readonly bits: number;\n readonly reason: NettoolsMessage;\n}\n\nconst CLOUD_METADATA_REASON = message('cloudMetadata');\n\n/** Exact IPv4 metadata endpoints, checked before the range table for a clearer message. */\nconst CLOUD_METADATA_IPV4 = new Set([\n '169.254.169.254', // AWS / GCP / Azure / Oracle / OpenStack instance metadata\n '169.254.170.2', // AWS ECS task metadata\n '169.254.169.123', // AWS Time Sync\n '100.100.100.200', // Alibaba Cloud metadata\n '168.63.129.16', // Azure WireServer (public range, still host-internal)\n]);\n\n/** Exact IPv6 metadata endpoints. */\nconst CLOUD_METADATA_IPV6 = ['fd00:ec2::254', 'fd00:ec2::23'];\n\nconst BLOCKED_IPV4_RANGES: readonly PrefixRule[] = [\n ipv4Rule('0.0.0.0', 8, message('unspecifiedV4')),\n ipv4Rule('10.0.0.0', 8, message('privateV4')),\n ipv4Rule('100.64.0.0', 10, message('cgnat')),\n ipv4Rule('127.0.0.0', 8, message('loopback')),\n ipv4Rule('169.254.0.0', 16, message('linkLocal')),\n ipv4Rule('172.16.0.0', 12, message('privateV4')),\n ipv4Rule('192.0.0.0', 24, message('ietfReserved')),\n ipv4Rule('192.0.2.0', 24, message('documentation')),\n ipv4Rule('192.88.99.0', 24, message('relay6to4')),\n ipv4Rule('192.168.0.0', 16, message('privateV4')),\n ipv4Rule('198.18.0.0', 15, message('benchmark')),\n ipv4Rule('198.51.100.0', 24, message('documentation')),\n ipv4Rule('203.0.113.0', 24, message('documentation')),\n ipv4Rule('224.0.0.0', 4, message('multicast')),\n ipv4Rule('240.0.0.0', 4, message('reservedBroadcast')),\n];\n\n/** Special IPv6 ranges inside 2000::/3 that are still refused. */\nconst BLOCKED_GLOBAL_IPV6_RANGES: readonly PrefixRule[] = [\n ipv6Rule('2001::', 32, message('teredo')),\n ipv6Rule('2001:2::', 48, message('benchmark')),\n ipv6Rule('2001:10::', 28, message('orchid')),\n ipv6Rule('2001:db8::', 32, message('documentation')),\n ipv6Rule('3fff::', 20, message('documentation')),\n];\n\nconst BLOCKED_HOSTNAMES = new Set([\n 'localhost',\n 'metadata',\n 'metadata.google.internal',\n 'metadata.goog',\n 'instance-data',\n 'instance-data.ec2.internal',\n]);\n\n/** Suffixes that only make sense inside a private network or are reserved (RFC 2606 / 6761 / 6762). */\nconst BLOCKED_SUFFIXES = [\n 'localhost',\n 'local',\n 'localdomain',\n 'internal',\n 'intranet',\n 'private',\n 'corp',\n 'home',\n 'lan',\n 'home.arpa',\n 'arpa',\n 'test',\n 'invalid',\n 'example',\n 'onion',\n];\n\nconst MAX_HOSTNAME_LENGTH = 253;\nconst MAX_LABEL_LENGTH = 63;\n\n/* ------------------------------------------------------------------ */\n/* Parsing */\n/* ------------------------------------------------------------------ */\n\n/**\n * Parse and canonicalise one user-supplied host (domain, IPv4 or IPv6).\n * Throws {@link NettoolsError} (`invalid_input`) for anything that is not a\n * bare host: URLs, ports, paths, user-info, zone ids and percent-encoding are\n * all refused rather than \"helpfully\" stripped.\n */\nexport function parseHostInput(input: string, options: ParseHostOptions = {}): ParsedHost {\n const trimmed = input.trim();\n if (trimmed.length === 0) {\n throw invalid(message('hostRequired'));\n }\n if (trimmed.length > MAX_HOSTNAME_LENGTH + 2) {\n throw invalid(message('hostTooLong'));\n }\n if (/[\\s/\\\\@?#%]/u.test(trimmed)) {\n throw invalid(message('bareHost'));\n }\n\n const bracketed = trimmed.startsWith('[') && trimmed.endsWith(']');\n const ipv6Candidate = bracketed ? trimmed.slice(1, -1) : trimmed;\n if (bracketed || trimmed.includes(':')) {\n if (!isIPv6(ipv6Candidate)) {\n throw invalid(message('ipv6NoPort'));\n }\n const canonical = canonicalizeWithUrlParser(`[${ipv6Candidate}]`);\n const address = canonical.slice(1, -1);\n return Object.freeze({ kind: 'ipv6', address, bytes: ipv6ToBytes(address) });\n }\n\n const canonical = canonicalizeWithUrlParser(trimmed);\n if (isIPv4(canonical)) {\n return Object.freeze({ kind: 'ipv4', address: canonical, bytes: ipv4ToBytes(canonical) });\n }\n const name = canonical.endsWith('.') ? canonical.slice(0, -1) : canonical;\n validateDomainSyntax(name, options);\n return Object.freeze({ kind: 'domain', name });\n}\n\n/**\n * Run the WHATWG host parser by assigning to `URL#hostname` on a placeholder\n * URL. An invalid host leaves the placeholder untouched, which we detect.\n */\nfunction canonicalizeWithUrlParser(host: string): string {\n const placeholder = 'placeholder.invalid';\n const probe = new URL(`http://${placeholder}/`);\n probe.hostname = host;\n const result = probe.hostname;\n const unchanged = result === placeholder && host.toLowerCase() !== placeholder;\n if (unchanged || result.length === 0) {\n throw invalid(message('invalidHost'));\n }\n return result;\n}\n\nfunction validateDomainSyntax(name: string, options: ParseHostOptions): void {\n if (name.length === 0 || name.length > MAX_HOSTNAME_LENGTH) {\n throw invalid(message('hostLength'));\n }\n const labels = name.split('.');\n if (labels.length < (options.minLabels ?? 2)) {\n throw invalid(message('fullDomain'));\n }\n const labelPattern = options.allowUnderscore\n ? /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/u\n : /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;\n for (const label of labels) {\n if (label.length === 0 || label.length > MAX_LABEL_LENGTH || !labelPattern.test(label)) {\n throw invalid(message('invalidLabel', { label: label || message('empty') }));\n }\n }\n const topLevel = labels[labels.length - 1] ?? '';\n if (/^\\d+$/u.test(topLevel)) {\n throw invalid(message('numericTld'));\n }\n}\n\nfunction invalid(detail: NettoolsMessage): NettoolsError {\n return new NettoolsError('invalid_input', detail);\n}\n\n/* ------------------------------------------------------------------ */\n/* Classification */\n/* ------------------------------------------------------------------ */\n\n/** Classify a parsed host without any network access. Domains are checked by name only. */\nexport function classifyParsedHost(host: ParsedHost): AddressVerdict {\n if (host.kind === 'domain') {\n return classifyHostname(host.name);\n }\n if (host.kind === 'ipv4') {\n return classifyIpv4Bytes(host.bytes);\n }\n return classifyIpv6Bytes(host.bytes);\n}\n\n/** Classify a literal address string (any spelling the URL parser understands). */\nexport function classifyAddress(address: string): AddressVerdict {\n const parsed = parseHostInput(address);\n if (parsed.kind === 'domain') {\n throw invalid(message('notIp', { address }));\n }\n return classifyParsedHost(parsed);\n}\n\nexport function classifyHostname(name: string): AddressVerdict {\n const normalized = name.toLowerCase().replace(/\\.$/u, '');\n if (BLOCKED_HOSTNAMES.has(normalized)) {\n return normalized.startsWith('localhost')\n ? blocked(message('localhost'))\n : blocked(CLOUD_METADATA_REASON);\n }\n for (const suffix of BLOCKED_SUFFIXES) {\n if (normalized === suffix || normalized.endsWith(`.${suffix}`)) {\n return blocked(message('reservedSuffix', { suffix }));\n }\n }\n return ALLOWED;\n}\n\nfunction classifyIpv4Bytes(bytes: Uint8Array): AddressVerdict {\n const dotted = Array.from(bytes).join('.');\n if (CLOUD_METADATA_IPV4.has(dotted)) {\n return blocked(CLOUD_METADATA_REASON);\n }\n for (const rule of BLOCKED_IPV4_RANGES) {\n if (matchesPrefix(bytes, rule.prefix, rule.bits)) {\n return blocked(rule.reason);\n }\n }\n return ALLOWED;\n}\n\nconst IPV4_MAPPED_PREFIX = ipv6ToBytes('::ffff:0:0');\nconst NAT64_WELL_KNOWN_PREFIX = ipv6ToBytes('64:ff9b::');\nconst NAT64_LOCAL_PREFIX = ipv6ToBytes('64:ff9b:1::');\nconst SIX_TO_FOUR_PREFIX = ipv6ToBytes('2002::');\nconst GLOBAL_UNICAST_PREFIX = ipv6ToBytes('2000::');\nconst UNIQUE_LOCAL_PREFIX = ipv6ToBytes('fc00::');\nconst LINK_LOCAL_PREFIX = ipv6ToBytes('fe80::');\nconst SITE_LOCAL_PREFIX = ipv6ToBytes('fec0::');\nconst MULTICAST_PREFIX = ipv6ToBytes('ff00::');\nconst CLOUD_METADATA_IPV6_BYTES = CLOUD_METADATA_IPV6.map((address) => ipv6ToBytes(address));\n\nfunction classifyIpv6Bytes(bytes: Uint8Array): AddressVerdict {\n if (bytes.every((byte) => byte === 0)) {\n return blocked(message('unspecifiedV6'));\n }\n const isLoopback = bytes.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;\n if (isLoopback) {\n return blocked(message('loopbackV6'));\n }\n for (const metadata of CLOUD_METADATA_IPV6_BYTES) {\n if (matchesPrefix(bytes, metadata, 128)) {\n return blocked(CLOUD_METADATA_REASON);\n }\n }\n if (matchesPrefix(bytes, IPV4_MAPPED_PREFIX, 96)) {\n return classifyEmbeddedIpv4(bytes.slice(12, 16), message('mappedV4'));\n }\n if (matchesPrefix(bytes, NAT64_WELL_KNOWN_PREFIX, 96)) {\n return classifyEmbeddedIpv4(bytes.slice(12, 16), message('nat64'));\n }\n if (matchesPrefix(bytes, NAT64_LOCAL_PREFIX, 48)) {\n return blocked(message('localNat64'));\n }\n if (matchesPrefix(bytes, SIX_TO_FOUR_PREFIX, 16)) {\n return classifyEmbeddedIpv4(bytes.slice(2, 6), message('sixToFour'));\n }\n if (matchesPrefix(bytes, UNIQUE_LOCAL_PREFIX, 7)) {\n return blocked(message('ula'));\n }\n if (matchesPrefix(bytes, LINK_LOCAL_PREFIX, 10)) {\n return blocked(message('linkLocalV6'));\n }\n if (matchesPrefix(bytes, SITE_LOCAL_PREFIX, 10)) {\n return blocked(message('siteLocalV6'));\n }\n if (matchesPrefix(bytes, MULTICAST_PREFIX, 8)) {\n return blocked(message('multicastV6'));\n }\n if (!matchesPrefix(bytes, GLOBAL_UNICAST_PREFIX, 3)) {\n return blocked(message('notGlobalV6'));\n }\n for (const rule of BLOCKED_GLOBAL_IPV6_RANGES) {\n if (matchesPrefix(bytes, rule.prefix, rule.bits)) {\n return blocked(rule.reason);\n }\n }\n return ALLOWED;\n}\n\nfunction classifyEmbeddedIpv4(embedded: Uint8Array, wrapper: NettoolsMessage): AddressVerdict {\n const verdict = classifyIpv4Bytes(embedded);\n if (!verdict.blocked) {\n return verdict;\n }\n const dotted = Array.from(embedded).join('.');\n return blocked(message('embeddedV4', { wrapper, address: dotted, reason: verdict.detail }));\n}\n\n/* ------------------------------------------------------------------ */\n/* Byte helpers */\n/* ------------------------------------------------------------------ */\n\nfunction matchesPrefix(bytes: Uint8Array, prefix: Uint8Array, bits: number): boolean {\n if (bytes.length !== prefix.length) {\n return false;\n }\n const fullBytes = Math.floor(bits / 8);\n for (let index = 0; index < fullBytes; index += 1) {\n if (bytes[index] !== prefix[index]) {\n return false;\n }\n }\n const remainingBits = bits % 8;\n if (remainingBits === 0) {\n return true;\n }\n const mask = (0xff << (8 - remainingBits)) & 0xff;\n return ((bytes[fullBytes] ?? 0) & mask) === ((prefix[fullBytes] ?? 0) & mask);\n}\n\n/** Dotted-quad IPv4 to 4 bytes. Callers must pass a canonical dotted quad. */\nexport function ipv4ToBytes(address: string): Uint8Array {\n const parts = address.split('.').map((part) => Number(part));\n const valid = parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255);\n if (!valid) {\n throw invalid(message('invalidV4', { address }));\n }\n return Uint8Array.from(parts);\n}\n\n/** Textual IPv6 (with optional `::` and dotted IPv4 tail) to 16 bytes. */\nexport function ipv6ToBytes(address: string): Uint8Array {\n let text = address.toLowerCase();\n const groups: number[] = [];\n const lastColon = text.lastIndexOf(':');\n const tail = text.slice(lastColon + 1);\n const tailGroups: number[] = [];\n if (tail.includes('.')) {\n const v4 = ipv4ToBytes(tail);\n tailGroups.push(((v4[0] ?? 0) << 8) | (v4[1] ?? 0));\n tailGroups.push(((v4[2] ?? 0) << 8) | (v4[3] ?? 0));\n text = text.slice(0, lastColon + 1) + '0:0';\n }\n const halves = text.split('::');\n if (halves.length > 2) {\n throw invalid(message('invalidV6', { address }));\n }\n const head = parseGroups(halves[0] ?? '');\n const rest = halves.length === 2 ? parseGroups(halves[1] ?? '') : [];\n const missing = 8 - head.length - rest.length;\n if (halves.length === 2 ? missing < 1 : missing !== 0) {\n throw invalid(message('invalidV6', { address }));\n }\n groups.push(...head);\n for (let index = 0; index < missing && halves.length === 2; index += 1) {\n groups.push(0);\n }\n groups.push(...rest);\n if (tailGroups.length === 2) {\n groups.splice(6, 2, ...tailGroups);\n }\n const bytes = new Uint8Array(16);\n groups.forEach((group, index) => {\n bytes[index * 2] = (group >> 8) & 0xff;\n bytes[index * 2 + 1] = group & 0xff;\n });\n return bytes;\n}\n\nfunction parseGroups(text: string): number[] {\n if (text.length === 0) {\n return [];\n }\n return text.split(':').map((group) => {\n if (!/^[0-9a-f]{1,4}$/u.test(group)) {\n throw invalid(message('invalidV6Group', { group }));\n }\n return Number.parseInt(group, 16);\n });\n}\n\nfunction ipv4Rule(base: string, bits: number, reason: NettoolsMessage): PrefixRule {\n return Object.freeze({ prefix: ipv4ToBytes(base), bits, reason });\n}\n\nfunction ipv6Rule(base: string, bits: number, reason: NettoolsMessage): PrefixRule {\n return Object.freeze({ prefix: ipv6ToBytes(base), bits, reason });\n}\n\n/** Build the PTR query name for an IP (`in-addr.arpa` / `ip6.arpa`). */\nexport function reverseLookupName(host: ParsedHost): string {\n if (host.kind === 'ipv4') {\n return `${Array.from(host.bytes).reverse().join('.')}.in-addr.arpa`;\n }\n if (host.kind === 'ipv6') {\n const nibbles: string[] = [];\n for (const byte of host.bytes) {\n nibbles.push((byte >> 4).toString(16));\n nibbles.push((byte & 0x0f).toString(16));\n }\n return `${nibbles.reverse().join('.')}.ip6.arpa`;\n }\n throw invalid(message('reverseNeedsIp'));\n}\n\n/** Human label for a parsed host. */\nexport function displayHost(host: ParsedHost): string {\n return host.kind === 'domain' ? host.name : host.address;\n}\n"]}
|
package/lib/cooldown.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { message } from './messages.js';
|
|
1
2
|
/**
|
|
2
3
|
* Per-user cooldown shared by every nettools command and Agent tool of one
|
|
3
4
|
* plugin instance. Each call to an external API spends the caller's budget
|
|
@@ -38,7 +39,7 @@ export class UserCooldown {
|
|
|
38
39
|
const remaining = this.remainingMs(callerKey);
|
|
39
40
|
if (remaining > 0) {
|
|
40
41
|
const seconds = Math.ceil(remaining / 1000);
|
|
41
|
-
throw new NettoolsError('cooldown',
|
|
42
|
+
throw new NettoolsError('cooldown', message('cooldown', { seconds }), { retryAfterMs: remaining });
|
|
42
43
|
}
|
|
43
44
|
this.#readyAt.delete(callerKey);
|
|
44
45
|
this.#readyAt.set(callerKey, this.#now() + this.#cooldownMs);
|
package/lib/cooldown.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cooldown.js","sourceRoot":"","sources":["../src/cooldown.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAEhD,MAAM,OAAO,YAAY;IACd,WAAW,CAAS;IACpB,SAAS,CAAS;IAClB,IAAI,CAAe;IAC5B,4DAA4D;IACnD,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE9C,YAAY,UAAkB,EAAE,MAAoB,IAAI,CAAC,GAAG,EAAE,QAAQ,GAAG,yBAAyB;QAChG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,sEAAsE;IACtE,WAAW,CAAC,SAAiB;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC;QACX,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,mEAAmE;IACnE,IAAI,CAAC,SAAiB;QACpB,IAAI,IAAI,CAAC,WAAW,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;YAC5C,MAAM,IAAI,aAAa,CAAC,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"cooldown.js","sourceRoot":"","sources":["../src/cooldown.ts"],"names":[],"mappings":"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","sourcesContent":["import { message } from './messages.js';\n/**\n * Per-user cooldown shared by every nettools command and Agent tool of one\n * plugin instance. Each call to an external API spends the caller's budget\n * at admission time (after input validation), so typos are free but a failed\n * lookup still counts — that is what protects the upstream free APIs.\n *\n * State is in-memory and bounded; it resets on restart.\n */\nimport { NettoolsError } from './errors.js';\n\nexport const DEFAULT_COOLDOWN_CAPACITY = 10_000;\n\nexport class UserCooldown {\n readonly #cooldownMs: number;\n readonly #capacity: number;\n readonly #now: () => number;\n /** caller key → epoch ms when the caller may call again. */\n readonly #readyAt = new Map<string, number>();\n\n constructor(cooldownMs: number, now: () => number = Date.now, capacity = DEFAULT_COOLDOWN_CAPACITY) {\n this.#cooldownMs = Math.max(0, cooldownMs);\n this.#now = now;\n this.#capacity = Math.max(1, capacity);\n }\n\n get cooldownMs(): number {\n return this.#cooldownMs;\n }\n\n /** Milliseconds until `callerKey` may call again (0 when allowed). */\n remainingMs(callerKey: string): number {\n const readyAt = this.#readyAt.get(callerKey);\n if (readyAt === undefined) {\n return 0;\n }\n return Math.max(0, readyAt - this.#now());\n }\n\n /** Spend one call for `callerKey`, or throw a `cooldown` error. */\n take(callerKey: string): void {\n if (this.#cooldownMs === 0) {\n return;\n }\n const remaining = this.remainingMs(callerKey);\n if (remaining > 0) {\n const seconds = Math.ceil(remaining / 1000);\n throw new NettoolsError('cooldown', message('cooldown', { seconds }), { retryAfterMs: remaining });\n }\n this.#readyAt.delete(callerKey);\n this.#readyAt.set(callerKey, this.#now() + this.#cooldownMs);\n this.#enforceCapacity();\n }\n\n #enforceCapacity(): void {\n if (this.#readyAt.size <= this.#capacity) {\n return;\n }\n const now = this.#now();\n for (const [key, readyAt] of this.#readyAt) {\n if (readyAt <= now) {\n this.#readyAt.delete(key);\n }\n }\n while (this.#readyAt.size > this.#capacity) {\n const oldest = this.#readyAt.keys().next().value;\n if (oldest === undefined) {\n break;\n }\n this.#readyAt.delete(oldest);\n }\n }\n}\n\n/** Cooldown key for a chat command: endpoint + sender, shared across conversations. */\nexport function commandCallerKey(input: {\n readonly metadata?: Readonly<Record<string, unknown>>;\n readonly conversation?: { readonly id?: unknown; readonly endpoint?: { readonly id?: unknown } };\n readonly sender?: { readonly id?: unknown };\n} | undefined): string {\n const senderId = input?.sender?.id;\n if (senderId === undefined || senderId === null || String(senderId) === '') {\n return 'anonymous';\n }\n const endpointId = input?.metadata?.endpointId ?? input?.conversation?.endpoint?.id ?? '';\n return `im:${String(endpointId)}:${String(senderId)}`;\n}\n\n/** Cooldown key for an Agent tool call: the invoking principal. */\nexport function toolCallerKey(context: { readonly principal?: { readonly subjectId?: string } } | undefined): string {\n const subjectId = context?.principal?.subjectId;\n return subjectId ? `principal:${subjectId}` : 'anonymous';\n}\n"]}
|
package/lib/dns.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Locale } from 'zhin.js/i18n';
|
|
1
2
|
import type { HttpClient } from './http.js';
|
|
2
3
|
export declare const DNS_RECORD_TYPES: {
|
|
3
4
|
readonly A: 1;
|
|
@@ -49,4 +50,4 @@ export declare class DohResolver {
|
|
|
49
50
|
}
|
|
50
51
|
/** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */
|
|
51
52
|
export declare function parseDohResponse(body: unknown, name: string, type: DnsRecordType, provider: string): DnsAnswer;
|
|
52
|
-
export declare function formatDnsAnswer(answer: DnsAnswer, maxRecords?: number): string;
|
|
53
|
+
export declare function formatDnsAnswer(answer: DnsAnswer, maxRecords?: number, locale?: Locale): string;
|
package/lib/dns.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { messages } from './presentation.js';
|
|
2
|
+
import { message } from './messages.js';
|
|
1
3
|
/**
|
|
2
4
|
* DNS lookups over HTTPS (DoH JSON API) with provider fallback.
|
|
3
5
|
*
|
|
@@ -59,7 +61,7 @@ export function parseDnsType(input) {
|
|
|
59
61
|
if (Object.hasOwn(DNS_RECORD_TYPES, upper)) {
|
|
60
62
|
return upper;
|
|
61
63
|
}
|
|
62
|
-
throw new NettoolsError('invalid_input',
|
|
64
|
+
throw new NettoolsError('invalid_input', message('dnsType', { type: input, types: SUPPORTED_DNS_TYPES.join(' / ') }));
|
|
63
65
|
}
|
|
64
66
|
export class DohResolver {
|
|
65
67
|
#http;
|
|
@@ -88,15 +90,15 @@ export class DohResolver {
|
|
|
88
90
|
}
|
|
89
91
|
const wrapped = error instanceof NettoolsError
|
|
90
92
|
? error
|
|
91
|
-
: new NettoolsError('network',
|
|
93
|
+
: new NettoolsError('network', message('queryFailed', { provider: provider.label }), { cause: error });
|
|
92
94
|
lastError = wrapped;
|
|
93
|
-
failures.push(wrapped.message);
|
|
95
|
+
failures.push(wrapped.detail ?? wrapped.message);
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
if (failures.length <= 1 && lastError) {
|
|
97
99
|
throw lastError;
|
|
98
100
|
}
|
|
99
|
-
throw new NettoolsError('network',
|
|
101
|
+
throw new NettoolsError('network', message('dnsUnavailable', { failures }), { cause: lastError });
|
|
100
102
|
}
|
|
101
103
|
/**
|
|
102
104
|
* When another provider is still available we retry at most once here and
|
|
@@ -120,12 +122,12 @@ export class DohResolver {
|
|
|
120
122
|
/** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */
|
|
121
123
|
export function parseDohResponse(body, name, type, provider) {
|
|
122
124
|
if (!isRecord(body) || typeof body.Status !== 'number') {
|
|
123
|
-
throw new NettoolsError('malformed',
|
|
125
|
+
throw new NettoolsError('malformed', message('dnsMalformed', { provider }));
|
|
124
126
|
}
|
|
125
127
|
const status = body.Status;
|
|
126
128
|
if (status !== 0 && status !== 3) {
|
|
127
129
|
const rcode = RCODE_NAMES[status] ?? `RCODE ${status}`;
|
|
128
|
-
throw new NettoolsError('http',
|
|
130
|
+
throw new NettoolsError('http', message('dnsFailed', { provider, rcode }));
|
|
129
131
|
}
|
|
130
132
|
const rawAnswers = Array.isArray(body.Answer) ? body.Answer : [];
|
|
131
133
|
const records = [];
|
|
@@ -156,15 +158,15 @@ function parseDnsRecord(raw) {
|
|
|
156
158
|
const name = typeof raw.name === 'string' ? raw.name.replace(/\.$/u, '') : '';
|
|
157
159
|
return Object.freeze({ name, type: typeName, ttl, data: raw.data });
|
|
158
160
|
}
|
|
159
|
-
export function formatDnsAnswer(answer, maxRecords = 20) {
|
|
160
|
-
const header =
|
|
161
|
+
export function formatDnsAnswer(answer, maxRecords = 20, locale = 'zh-CN') {
|
|
162
|
+
const header = messages(locale, 'dnsHeader', { name: answer.name, type: answer.type, provider: answer.provider, verified: answer.authenticated ? messages(locale, 'verified') : '' });
|
|
161
163
|
if (answer.status === 'NXDOMAIN') {
|
|
162
|
-
return `${header}\n
|
|
164
|
+
return `${header}\n${messages(locale, 'nxdomain')}`;
|
|
163
165
|
}
|
|
164
166
|
const matching = answer.records.filter((record) => record.type === answer.type);
|
|
165
167
|
const chain = answer.records.filter((record) => record.type !== answer.type);
|
|
166
168
|
if (matching.length === 0 && chain.length === 0) {
|
|
167
|
-
return `${header}\n
|
|
169
|
+
return `${header}\n${messages(locale, 'noRecord', { type: answer.type })}`;
|
|
168
170
|
}
|
|
169
171
|
const lines = [header];
|
|
170
172
|
for (const record of chain.slice(0, maxRecords)) {
|
|
@@ -175,10 +177,10 @@ export function formatDnsAnswer(answer, maxRecords = 20) {
|
|
|
175
177
|
lines.push(`• ${truncate(record.data)} (TTL ${record.ttl})`);
|
|
176
178
|
}
|
|
177
179
|
if (matching.length > shown.length) {
|
|
178
|
-
lines.push(
|
|
180
|
+
lines.push(messages(locale, 'moreRecords', { count: matching.length - shown.length }));
|
|
179
181
|
}
|
|
180
182
|
if (matching.length === 0) {
|
|
181
|
-
lines.push(
|
|
183
|
+
lines.push(messages(locale, 'noDirectRecord', { type: answer.type }));
|
|
182
184
|
}
|
|
183
185
|
return lines.join('\n');
|
|
184
186
|
}
|
package/lib/dns.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dns.js","sourceRoot":"","sources":["../src/dns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,CAAC,EAAE,CAAC;IACJ,EAAE,EAAE,CAAC;IACL,KAAK,EAAE,CAAC;IACR,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,EAAE;IACP,EAAE,EAAE,EAAE;IACN,GAAG,EAAE,EAAE;IACP,IAAI,EAAE,EAAE;IACR,GAAG,EAAE,EAAE;IACP,EAAE,EAAE,EAAE;IACN,MAAM,EAAE,EAAE;IACV,GAAG,EAAE,GAAG;CACA,CAAC;AAIX,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAoB,CAAC;AAUpF,MAAM,CAAC,MAAM,aAAa,GAAiD,MAAM,CAAC,MAAM,CAAC;IACvF,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC;QACxB,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,gBAAgB;QACvB,QAAQ,EAAE,sCAAsC;KACjD,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,YAAY;QACnB,QAAQ,EAAE,gCAAgC;KAC3C,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,YAAY;QACnB,QAAQ,EAAE,4BAA4B;KACvC,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAA6B,CAAC,YAAY,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAqBlG,MAAM,WAAW,GAAqC;IACpD,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,UAAU;IACb,CAAC,EAAE,UAAU;IACb,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,SAAS;CACb,CAAC;AAEF,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAChC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CACrE,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,KAAyB;IACpD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/C,OAAO,GAAG,CAAC;IACb,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACzC,IAAI,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,KAAsB,CAAC;IAChC,CAAC;IACD,MAAM,IAAI,aAAa,CACrB,eAAe,EACf,YAAY,KAAK,OAAO,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAC1D,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,WAAW;IACb,KAAK,CAAa;IAClB,UAAU,CAAyB;IAE5C,YAAY,IAAgB,EAAE,cAAwC,qBAAqB;QACzF,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC;QACzE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,6FAA6F;IAC7F,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAAmB,EAAE,MAAoB;QACnE,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,SAAoC,CAAC;QACzC,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC;YAC1D,MAAM,WAAW,GAAG,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;YACvD,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;YAC5E,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,aAAa,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;oBAC/D,MAAM,KAAK,CAAC;gBACd,CAAC;gBACD,MAAM,OAAO,GAAG,KAAK,YAAY,aAAa;oBAC5C,CAAC,CAAC,KAAK;oBACP,CAAC,CAAC,IAAI,aAAa,CAAC,SAAS,EAAE,GAAG,QAAQ,CAAC,KAAK,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC7E,SAAS,GAAG,OAAO,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,SAAS,EAAE,CAAC;YACtC,MAAM,SAAS,CAAC;QAClB,CAAC;QACD,MAAM,IAAI,aAAa,CAAC,SAAS,EAAE,iBAAiB,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAClG,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAChB,QAAqB,EACrB,IAAY,EACZ,IAAmB,EACnB,WAAoB,EACpB,MAAoB;QAEpB,MAAM,iBAAiB,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC;QACvD,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;QACpF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACvC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;YAC/C,QAAQ,EAAE,QAAQ,CAAC,KAAK;YACxB,MAAM,EAAE,sBAAsB;YAC9B,UAAU;YACV,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9B,CAAC,CAAC;QACH,OAAO,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACrE,CAAC;CACF;AAED,8FAA8F;AAC9F,MAAM,UAAU,gBAAgB,CAAC,IAAa,EAAE,IAAY,EAAE,IAAmB,EAAE,QAAgB;IACjG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACvD,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,GAAG,QAAQ,kBAAkB,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,IAAI,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,SAAS,MAAM,EAAE,CAAC;QACvD,MAAM,IAAI,aAAa,CAAC,MAAM,EAAE,GAAG,QAAQ,SAAS,KAAK,GAAG,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI;QACJ,IAAI;QACJ,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;QAC7C,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;QAC/B,aAAa,EAAE,IAAI,CAAC,EAAE,KAAK,IAAI;QAC/B,QAAQ;KACT,CAAC,CAAC;AACL,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IAClC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACjE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;IACvE,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9E,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAiB,EAAE,UAAU,GAAG,EAAE;IAChE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,OAAO,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;IACrH,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,GAAG,MAAM,oBAAoB,CAAC;IACvC,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC;IAChF,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,GAAG,MAAM,QAAQ,MAAM,CAAC,IAAI,KAAK,CAAC;IAC3C,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC5C,KAAK,MAAM,MAAM,IAAI,KAAK,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,OAAO,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG;IACvC,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC","sourcesContent":["/**\n * DNS lookups over HTTPS (DoH JSON API) with provider fallback.\n *\n * Cloudflare (`cloudflare-dns.com/dns-query`), AliDNS (`dns.alidns.com/resolve`,\n * reachable from mainland China) and Google (`dns.google/resolve`) speak the\n * same JSON dialect. Providers are tried in\n * order; we move on to the next one when a provider is unreachable, times out,\n * returns an HTTP error, returns malformed JSON or answers SERVFAIL/REFUSED.\n * NXDOMAIN and \"no records\" are authoritative answers and are returned as-is.\n */\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const DNS_RECORD_TYPES = {\n A: 1,\n NS: 2,\n CNAME: 5,\n SOA: 6,\n PTR: 12,\n MX: 15,\n TXT: 16,\n AAAA: 28,\n SRV: 33,\n DS: 43,\n DNSKEY: 48,\n CAA: 257,\n} as const;\n\nexport type DnsRecordType = keyof typeof DNS_RECORD_TYPES;\n\nexport const SUPPORTED_DNS_TYPES = Object.keys(DNS_RECORD_TYPES) as DnsRecordType[];\n\nexport type DohProviderId = 'cloudflare' | 'alidns' | 'google';\n\nexport interface DohProvider {\n readonly id: DohProviderId;\n readonly label: string;\n readonly endpoint: string;\n}\n\nexport const DOH_PROVIDERS: Readonly<Record<DohProviderId, DohProvider>> = Object.freeze({\n cloudflare: Object.freeze({\n id: 'cloudflare',\n label: 'Cloudflare DoH',\n endpoint: 'https://cloudflare-dns.com/dns-query',\n }),\n alidns: Object.freeze({\n id: 'alidns',\n label: 'AliDNS DoH',\n endpoint: 'https://dns.alidns.com/resolve',\n }),\n google: Object.freeze({\n id: 'google',\n label: 'Google DoH',\n endpoint: 'https://dns.google/resolve',\n }),\n});\n\nexport const DEFAULT_DOH_PROVIDERS: readonly DohProviderId[] = ['cloudflare', 'alidns', 'google'];\n\nexport interface DnsRecord {\n readonly name: string;\n readonly type: string;\n readonly ttl: number;\n readonly data: string;\n}\n\nexport type DnsStatus = 'NOERROR' | 'NXDOMAIN';\n\nexport interface DnsAnswer {\n readonly name: string;\n readonly type: DnsRecordType;\n readonly status: DnsStatus;\n readonly records: readonly DnsRecord[];\n /** Whether the resolver validated the answer with DNSSEC (AD bit). */\n readonly authenticated: boolean;\n readonly provider: string;\n}\n\nconst RCODE_NAMES: Readonly<Record<number, string>> = {\n 0: 'NOERROR',\n 1: 'FORMERR',\n 2: 'SERVFAIL',\n 3: 'NXDOMAIN',\n 4: 'NOTIMP',\n 5: 'REFUSED',\n};\n\nconst TYPE_NAMES_BY_CODE = new Map<number, string>(\n Object.entries(DNS_RECORD_TYPES).map(([name, code]) => [code, name]),\n);\n\nexport function parseDnsType(input: string | undefined): DnsRecordType {\n if (input === undefined || input.trim() === '') {\n return 'A';\n }\n const upper = input.trim().toUpperCase();\n if (Object.hasOwn(DNS_RECORD_TYPES, upper)) {\n return upper as DnsRecordType;\n }\n throw new NettoolsError(\n 'invalid_input',\n `不支持的记录类型 ${input},可选:${SUPPORTED_DNS_TYPES.join(' / ')}`,\n );\n}\n\nexport class DohResolver {\n readonly #http: HttpClient;\n readonly #providers: readonly DohProvider[];\n\n constructor(http: HttpClient, providerIds: readonly DohProviderId[] = DEFAULT_DOH_PROVIDERS) {\n const unique = [...new Set(providerIds)];\n const selected = unique.length > 0 ? unique : [...DEFAULT_DOH_PROVIDERS];\n this.#http = http;\n this.#providers = selected.map((id) => DOH_PROVIDERS[id]);\n }\n\n get providers(): readonly DohProvider[] {\n return this.#providers;\n }\n\n /** Resolve `name` (already validated) for one record type, falling back across providers. */\n async resolve(name: string, type: DnsRecordType, signal?: AbortSignal): Promise<DnsAnswer> {\n const failures: string[] = [];\n let lastError: NettoolsError | undefined;\n for (const [index, provider] of this.#providers.entries()) {\n const hasFallback = index < this.#providers.length - 1;\n try {\n return await this.#resolveWith(provider, name, type, hasFallback, signal);\n } catch (error) {\n if (error instanceof NettoolsError && error.kind === 'aborted') {\n throw error;\n }\n const wrapped = error instanceof NettoolsError\n ? error\n : new NettoolsError('network', `${provider.label} 查询失败`, { cause: error });\n lastError = wrapped;\n failures.push(wrapped.message);\n }\n }\n if (failures.length <= 1 && lastError) {\n throw lastError;\n }\n throw new NettoolsError('network', `所有 DNS 服务均不可用:${failures.join(';')}`, { cause: lastError });\n }\n\n /**\n * When another provider is still available we retry at most once here and\n * let the fallback absorb the rest, keeping the worst-case latency bounded.\n */\n async #resolveWith(\n provider: DohProvider,\n name: string,\n type: DnsRecordType,\n hasFallback: boolean,\n signal?: AbortSignal,\n ): Promise<DnsAnswer> {\n const configuredRetries = this.#http.policy.maxRetries;\n const maxRetries = hasFallback ? Math.min(1, configuredRetries) : configuredRetries;\n const url = new URL(provider.endpoint);\n url.searchParams.set('name', name);\n url.searchParams.set('type', type);\n const response = await this.#http.fetchJson(url, {\n provider: provider.label,\n accept: 'application/dns-json',\n maxRetries,\n ...(signal ? { signal } : {}),\n });\n return parseDohResponse(response.body, name, type, provider.label);\n }\n}\n\n/** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */\nexport function parseDohResponse(body: unknown, name: string, type: DnsRecordType, provider: string): DnsAnswer {\n if (!isRecord(body) || typeof body.Status !== 'number') {\n throw new NettoolsError('malformed', `${provider} 返回了无法识别的 DNS 响应`);\n }\n const status = body.Status;\n if (status !== 0 && status !== 3) {\n const rcode = RCODE_NAMES[status] ?? `RCODE ${status}`;\n throw new NettoolsError('http', `${provider} 解析失败(${rcode})`);\n }\n const rawAnswers = Array.isArray(body.Answer) ? body.Answer : [];\n const records: DnsRecord[] = [];\n for (const raw of rawAnswers) {\n const record = parseDnsRecord(raw);\n if (record) {\n records.push(record);\n }\n }\n return Object.freeze({\n name,\n type,\n status: status === 3 ? 'NXDOMAIN' : 'NOERROR',\n records: Object.freeze(records),\n authenticated: body.AD === true,\n provider,\n });\n}\n\nfunction parseDnsRecord(raw: unknown): DnsRecord | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n if (typeof raw.data !== 'string' || typeof raw.type !== 'number') {\n return undefined;\n }\n const typeName = TYPE_NAMES_BY_CODE.get(raw.type) ?? `TYPE${raw.type}`;\n const ttl = typeof raw.TTL === 'number' && Number.isFinite(raw.TTL) ? raw.TTL : 0;\n const name = typeof raw.name === 'string' ? raw.name.replace(/\\.$/u, '') : '';\n return Object.freeze({ name, type: typeName, ttl, data: raw.data });\n}\n\nexport function formatDnsAnswer(answer: DnsAnswer, maxRecords = 20): string {\n const header = `🔎 ${answer.name} ${answer.type} 记录(${answer.provider}${answer.authenticated ? ',DNSSEC 已验证' : ''})`;\n if (answer.status === 'NXDOMAIN') {\n return `${header}\\n域名不存在 (NXDOMAIN)`;\n }\n const matching = answer.records.filter((record) => record.type === answer.type);\n const chain = answer.records.filter((record) => record.type !== answer.type);\n if (matching.length === 0 && chain.length === 0) {\n return `${header}\\n没有 ${answer.type} 记录`;\n }\n const lines = [header];\n for (const record of chain.slice(0, maxRecords)) {\n lines.push(`↪ ${record.name} ${record.type} ${truncate(record.data)}`);\n }\n const shown = matching.slice(0, maxRecords);\n for (const record of shown) {\n lines.push(`• ${truncate(record.data)} (TTL ${record.ttl})`);\n }\n if (matching.length > shown.length) {\n lines.push(`…另有 ${matching.length - shown.length} 条`);\n }\n if (matching.length === 0) {\n lines.push(`没有直接的 ${answer.type} 记录`);\n }\n return lines.join('\\n');\n}\n\nfunction truncate(text: string, max = 300): string {\n return text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dns.js","sourceRoot":"","sources":["../src/dns.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC;;;;;;;;;GASG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,CAAC,EAAE,CAAC;IACJ,EAAE,EAAE,CAAC;IACL,KAAK,EAAE,CAAC;IACR,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,EAAE;IACP,EAAE,EAAE,EAAE;IACN,GAAG,EAAE,EAAE;IACP,IAAI,EAAE,EAAE;IACR,GAAG,EAAE,EAAE;IACP,EAAE,EAAE,EAAE;IACN,MAAM,EAAE,EAAE;IACV,GAAG,EAAE,GAAG;CACA,CAAC;AAIX,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAoB,CAAC;AAUpF,MAAM,CAAC,MAAM,aAAa,GAAiD,MAAM,CAAC,MAAM,CAAC;IACvF,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC;QACxB,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,gBAAgB;QACvB,QAAQ,EAAE,sCAAsC;KACjD,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,YAAY;QACnB,QAAQ,EAAE,gCAAgC;KAC3C,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,YAAY;QACnB,QAAQ,EAAE,4BAA4B;KACvC,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAA6B,CAAC,YAAY,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAqBlG,MAAM,WAAW,GAAqC;IACpD,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,UAAU;IACb,CAAC,EAAE,UAAU;IACb,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,SAAS;CACb,CAAC;AAEF,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAChC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CACrE,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,KAAyB;IACpD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/C,OAAO,GAAG,CAAC;IACb,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACzC,IAAI,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,KAAsB,CAAC;IAChC,CAAC;IACD,MAAM,IAAI,aAAa,CACrB,eAAe,EACf,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAC5E,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,WAAW;IACb,KAAK,CAAa;IAClB,UAAU,CAAyB;IAE5C,YAAY,IAAgB,EAAE,cAAwC,qBAAqB;QACzF,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC;QACzE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,6FAA6F;IAC7F,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAAmB,EAAE,MAAoB;QACnE,MAAM,QAAQ,GAAyD,EAAE,CAAC;QAC1E,IAAI,SAAoC,CAAC;QACzC,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC;YAC1D,MAAM,WAAW,GAAG,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;YACvD,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;YAC5E,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,aAAa,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;oBAC/D,MAAM,KAAK,CAAC;gBACd,CAAC;gBACD,MAAM,OAAO,GAAG,KAAK,YAAY,aAAa;oBAC5C,CAAC,CAAC,KAAK;oBACP,CAAC,CAAC,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;gBACzG,SAAS,GAAG,OAAO,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;YACnD,CAAC;QACH,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,SAAS,EAAE,CAAC;YACtC,MAAM,SAAS,CAAC;QAClB,CAAC;QACD,MAAM,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IACpG,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAChB,QAAqB,EACrB,IAAY,EACZ,IAAmB,EACnB,WAAoB,EACpB,MAAoB;QAEpB,MAAM,iBAAiB,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC;QACvD,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;QACpF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACvC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;YAC/C,QAAQ,EAAE,QAAQ,CAAC,KAAK;YACxB,MAAM,EAAE,sBAAsB;YAC9B,UAAU;YACV,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9B,CAAC,CAAC;QACH,OAAO,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACrE,CAAC;CACF;AAED,8FAA8F;AAC9F,MAAM,UAAU,gBAAgB,CAAC,IAAa,EAAE,IAAY,EAAE,IAAmB,EAAE,QAAgB;IACjG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACvD,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,IAAI,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,SAAS,MAAM,EAAE,CAAC;QACvD,MAAM,IAAI,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI;QACJ,IAAI;QACJ,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;QAC7C,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;QAC/B,aAAa,EAAE,IAAI,CAAC,EAAE,KAAK,IAAI;QAC/B,QAAQ;KACT,CAAC,CAAC;AACL,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IAClC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACjE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;IACvE,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9E,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAiB,EAAE,UAAU,GAAG,EAAE,EAAE,SAAiB,OAAO;IAC1F,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACtL,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,GAAG,MAAM,KAAK,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC;IACtD,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC;IAChF,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,GAAG,MAAM,KAAK,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;IAC7E,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC5C,KAAK,MAAM,MAAM,IAAI,KAAK,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG;IACvC,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC","sourcesContent":["import { type Locale } from 'zhin.js/i18n';\nimport { messages } from './presentation.js';\nimport { message } from './messages.js';\n/**\n * DNS lookups over HTTPS (DoH JSON API) with provider fallback.\n *\n * Cloudflare (`cloudflare-dns.com/dns-query`), AliDNS (`dns.alidns.com/resolve`,\n * reachable from mainland China) and Google (`dns.google/resolve`) speak the\n * same JSON dialect. Providers are tried in\n * order; we move on to the next one when a provider is unreachable, times out,\n * returns an HTTP error, returns malformed JSON or answers SERVFAIL/REFUSED.\n * NXDOMAIN and \"no records\" are authoritative answers and are returned as-is.\n */\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const DNS_RECORD_TYPES = {\n A: 1,\n NS: 2,\n CNAME: 5,\n SOA: 6,\n PTR: 12,\n MX: 15,\n TXT: 16,\n AAAA: 28,\n SRV: 33,\n DS: 43,\n DNSKEY: 48,\n CAA: 257,\n} as const;\n\nexport type DnsRecordType = keyof typeof DNS_RECORD_TYPES;\n\nexport const SUPPORTED_DNS_TYPES = Object.keys(DNS_RECORD_TYPES) as DnsRecordType[];\n\nexport type DohProviderId = 'cloudflare' | 'alidns' | 'google';\n\nexport interface DohProvider {\n readonly id: DohProviderId;\n readonly label: string;\n readonly endpoint: string;\n}\n\nexport const DOH_PROVIDERS: Readonly<Record<DohProviderId, DohProvider>> = Object.freeze({\n cloudflare: Object.freeze({\n id: 'cloudflare',\n label: 'Cloudflare DoH',\n endpoint: 'https://cloudflare-dns.com/dns-query',\n }),\n alidns: Object.freeze({\n id: 'alidns',\n label: 'AliDNS DoH',\n endpoint: 'https://dns.alidns.com/resolve',\n }),\n google: Object.freeze({\n id: 'google',\n label: 'Google DoH',\n endpoint: 'https://dns.google/resolve',\n }),\n});\n\nexport const DEFAULT_DOH_PROVIDERS: readonly DohProviderId[] = ['cloudflare', 'alidns', 'google'];\n\nexport interface DnsRecord {\n readonly name: string;\n readonly type: string;\n readonly ttl: number;\n readonly data: string;\n}\n\nexport type DnsStatus = 'NOERROR' | 'NXDOMAIN';\n\nexport interface DnsAnswer {\n readonly name: string;\n readonly type: DnsRecordType;\n readonly status: DnsStatus;\n readonly records: readonly DnsRecord[];\n /** Whether the resolver validated the answer with DNSSEC (AD bit). */\n readonly authenticated: boolean;\n readonly provider: string;\n}\n\nconst RCODE_NAMES: Readonly<Record<number, string>> = {\n 0: 'NOERROR',\n 1: 'FORMERR',\n 2: 'SERVFAIL',\n 3: 'NXDOMAIN',\n 4: 'NOTIMP',\n 5: 'REFUSED',\n};\n\nconst TYPE_NAMES_BY_CODE = new Map<number, string>(\n Object.entries(DNS_RECORD_TYPES).map(([name, code]) => [code, name]),\n);\n\nexport function parseDnsType(input: string | undefined): DnsRecordType {\n if (input === undefined || input.trim() === '') {\n return 'A';\n }\n const upper = input.trim().toUpperCase();\n if (Object.hasOwn(DNS_RECORD_TYPES, upper)) {\n return upper as DnsRecordType;\n }\n throw new NettoolsError(\n 'invalid_input',\n message('dnsType', { type: input, types: SUPPORTED_DNS_TYPES.join(' / ') }),\n );\n}\n\nexport class DohResolver {\n readonly #http: HttpClient;\n readonly #providers: readonly DohProvider[];\n\n constructor(http: HttpClient, providerIds: readonly DohProviderId[] = DEFAULT_DOH_PROVIDERS) {\n const unique = [...new Set(providerIds)];\n const selected = unique.length > 0 ? unique : [...DEFAULT_DOH_PROVIDERS];\n this.#http = http;\n this.#providers = selected.map((id) => DOH_PROVIDERS[id]);\n }\n\n get providers(): readonly DohProvider[] {\n return this.#providers;\n }\n\n /** Resolve `name` (already validated) for one record type, falling back across providers. */\n async resolve(name: string, type: DnsRecordType, signal?: AbortSignal): Promise<DnsAnswer> {\n const failures: (string | import('./messages.js').NettoolsMessage)[] = [];\n let lastError: NettoolsError | undefined;\n for (const [index, provider] of this.#providers.entries()) {\n const hasFallback = index < this.#providers.length - 1;\n try {\n return await this.#resolveWith(provider, name, type, hasFallback, signal);\n } catch (error) {\n if (error instanceof NettoolsError && error.kind === 'aborted') {\n throw error;\n }\n const wrapped = error instanceof NettoolsError\n ? error\n : new NettoolsError('network', message('queryFailed', { provider: provider.label }), { cause: error });\n lastError = wrapped;\n failures.push(wrapped.detail ?? wrapped.message);\n }\n }\n if (failures.length <= 1 && lastError) {\n throw lastError;\n }\n throw new NettoolsError('network', message('dnsUnavailable', { failures }), { cause: lastError });\n }\n\n /**\n * When another provider is still available we retry at most once here and\n * let the fallback absorb the rest, keeping the worst-case latency bounded.\n */\n async #resolveWith(\n provider: DohProvider,\n name: string,\n type: DnsRecordType,\n hasFallback: boolean,\n signal?: AbortSignal,\n ): Promise<DnsAnswer> {\n const configuredRetries = this.#http.policy.maxRetries;\n const maxRetries = hasFallback ? Math.min(1, configuredRetries) : configuredRetries;\n const url = new URL(provider.endpoint);\n url.searchParams.set('name', name);\n url.searchParams.set('type', type);\n const response = await this.#http.fetchJson(url, {\n provider: provider.label,\n accept: 'application/dns-json',\n maxRetries,\n ...(signal ? { signal } : {}),\n });\n return parseDohResponse(response.body, name, type, provider.label);\n }\n}\n\n/** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */\nexport function parseDohResponse(body: unknown, name: string, type: DnsRecordType, provider: string): DnsAnswer {\n if (!isRecord(body) || typeof body.Status !== 'number') {\n throw new NettoolsError('malformed', message('dnsMalformed', { provider }));\n }\n const status = body.Status;\n if (status !== 0 && status !== 3) {\n const rcode = RCODE_NAMES[status] ?? `RCODE ${status}`;\n throw new NettoolsError('http', message('dnsFailed', { provider, rcode }));\n }\n const rawAnswers = Array.isArray(body.Answer) ? body.Answer : [];\n const records: DnsRecord[] = [];\n for (const raw of rawAnswers) {\n const record = parseDnsRecord(raw);\n if (record) {\n records.push(record);\n }\n }\n return Object.freeze({\n name,\n type,\n status: status === 3 ? 'NXDOMAIN' : 'NOERROR',\n records: Object.freeze(records),\n authenticated: body.AD === true,\n provider,\n });\n}\n\nfunction parseDnsRecord(raw: unknown): DnsRecord | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n if (typeof raw.data !== 'string' || typeof raw.type !== 'number') {\n return undefined;\n }\n const typeName = TYPE_NAMES_BY_CODE.get(raw.type) ?? `TYPE${raw.type}`;\n const ttl = typeof raw.TTL === 'number' && Number.isFinite(raw.TTL) ? raw.TTL : 0;\n const name = typeof raw.name === 'string' ? raw.name.replace(/\\.$/u, '') : '';\n return Object.freeze({ name, type: typeName, ttl, data: raw.data });\n}\n\nexport function formatDnsAnswer(answer: DnsAnswer, maxRecords = 20, locale: Locale = 'zh-CN'): string {\n const header = messages(locale, 'dnsHeader', { name: answer.name, type: answer.type, provider: answer.provider, verified: answer.authenticated ? messages(locale, 'verified') : '' });\n if (answer.status === 'NXDOMAIN') {\n return `${header}\\n${messages(locale, 'nxdomain')}`;\n }\n const matching = answer.records.filter((record) => record.type === answer.type);\n const chain = answer.records.filter((record) => record.type !== answer.type);\n if (matching.length === 0 && chain.length === 0) {\n return `${header}\\n${messages(locale, 'noRecord', { type: answer.type })}`;\n }\n const lines = [header];\n for (const record of chain.slice(0, maxRecords)) {\n lines.push(`↪ ${record.name} ${record.type} ${truncate(record.data)}`);\n }\n const shown = matching.slice(0, maxRecords);\n for (const record of shown) {\n lines.push(`• ${truncate(record.data)} (TTL ${record.ttl})`);\n }\n if (matching.length > shown.length) {\n lines.push(messages(locale, 'moreRecords', { count: matching.length - shown.length }));\n }\n if (matching.length === 0) {\n lines.push(messages(locale, 'noDirectRecord', { type: answer.type }));\n }\n return lines.join('\\n');\n}\n\nfunction truncate(text: string, max = 300): string {\n return text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|
package/lib/domain-price.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Locale } from 'zhin.js/i18n';
|
|
1
2
|
import type { HttpClient } from './http.js';
|
|
2
3
|
export declare const NAZHUMI_API_URL = "https://www.nazhumi.com/api/v1";
|
|
3
4
|
export type PriceKind = 'register' | 'renew' | 'transfer';
|
|
@@ -25,4 +26,4 @@ export declare function parsePriceKind(input: string | undefined): PriceKind;
|
|
|
25
26
|
export declare function parseDomainSuffix(input: string): string;
|
|
26
27
|
export declare function comparePrices(http: HttpClient, suffix: string, kind: PriceKind, signal?: AbortSignal): Promise<PriceComparison>;
|
|
27
28
|
export declare function parsePriceResponse(body: unknown, suffix: string, kind: PriceKind): PriceComparison;
|
|
28
|
-
export declare function formatPriceComparison(comparison: PriceComparison, limit: number): string;
|
|
29
|
+
export declare function formatPriceComparison(comparison: PriceComparison, limit: number, locale?: Locale): string;
|
package/lib/domain-price.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { messages } from './presentation.js';
|
|
2
|
+
import { message } from './messages.js';
|
|
1
3
|
/**
|
|
2
4
|
* Domain registration / renewal / transfer price comparison through the
|
|
3
5
|
* public nazhumi.com API (`GET /api/v1?domain=<suffix>&order=register`).
|
|
@@ -25,18 +27,13 @@ const PRICE_KIND_ALIASES = {
|
|
|
25
27
|
transfer: 'transfer',
|
|
26
28
|
转入: 'transfer',
|
|
27
29
|
};
|
|
28
|
-
const PRICE_KIND_LABELS = {
|
|
29
|
-
register: '注册',
|
|
30
|
-
renew: '续费',
|
|
31
|
-
transfer: '转入',
|
|
32
|
-
};
|
|
33
30
|
export function parsePriceKind(input) {
|
|
34
31
|
if (input === undefined || input.trim() === '') {
|
|
35
32
|
return 'register';
|
|
36
33
|
}
|
|
37
34
|
const kind = PRICE_KIND_ALIASES[input.trim().toLowerCase()];
|
|
38
35
|
if (!kind) {
|
|
39
|
-
throw new NettoolsError('invalid_input', '
|
|
36
|
+
throw new NettoolsError('invalid_input', message('priceKind'));
|
|
40
37
|
}
|
|
41
38
|
return kind;
|
|
42
39
|
}
|
|
@@ -51,11 +48,11 @@ export function parseDomainSuffix(input) {
|
|
|
51
48
|
const explicitSuffix = trimmed.startsWith('.');
|
|
52
49
|
const withoutDot = explicitSuffix ? trimmed.slice(1) : trimmed;
|
|
53
50
|
if (withoutDot.length === 0) {
|
|
54
|
-
throw new NettoolsError('invalid_input', '
|
|
51
|
+
throw new NettoolsError('invalid_input', message('suffixRequired'));
|
|
55
52
|
}
|
|
56
53
|
const parsed = parseHostInput(withoutDot, { minLabels: 1 });
|
|
57
54
|
if (parsed.kind !== 'domain') {
|
|
58
|
-
throw new NettoolsError('invalid_input', '
|
|
55
|
+
throw new NettoolsError('invalid_input', message('suffixNotIp'));
|
|
59
56
|
}
|
|
60
57
|
const labels = parsed.name.split('.');
|
|
61
58
|
if (explicitSuffix || labels.length === 1) {
|
|
@@ -75,11 +72,11 @@ export async function comparePrices(http, suffix, kind, signal) {
|
|
|
75
72
|
}
|
|
76
73
|
export function parsePriceResponse(body, suffix, kind) {
|
|
77
74
|
if (!isRecord(body)) {
|
|
78
|
-
throw new NettoolsError('malformed', '
|
|
75
|
+
throw new NettoolsError('malformed', message('priceMalformed'));
|
|
79
76
|
}
|
|
80
77
|
if (typeof body.code === 'number' && body.code !== 100 && body.code !== 200 && body.code !== 0) {
|
|
81
|
-
const
|
|
82
|
-
throw new NettoolsError('http',
|
|
78
|
+
const providerMessage = typeof body.msg === 'string' ? body.msg : typeof body.message === 'string' ? body.message : '';
|
|
79
|
+
throw new NettoolsError('http', message('priceError', { code: body.code, detail: providerMessage ? `: ${providerMessage}` : '' }));
|
|
83
80
|
}
|
|
84
81
|
const data = isRecord(body.data) ? body.data : {};
|
|
85
82
|
const rawPrices = Array.isArray(data.price) ? data.price : [];
|
|
@@ -141,22 +138,22 @@ function compareByCnyPrice(left, right) {
|
|
|
141
138
|
}
|
|
142
139
|
return left.priceCny - right.priceCny;
|
|
143
140
|
}
|
|
144
|
-
export function formatPriceComparison(comparison, limit) {
|
|
145
|
-
const label =
|
|
141
|
+
export function formatPriceComparison(comparison, limit, locale = 'zh-CN') {
|
|
142
|
+
const label = messages(locale, comparison.kind);
|
|
146
143
|
if (comparison.entries.length === 0) {
|
|
147
|
-
return
|
|
144
|
+
return messages(locale, 'noPrice', { suffix: comparison.suffix, kind: label });
|
|
148
145
|
}
|
|
149
|
-
const lines = [
|
|
146
|
+
const lines = [messages(locale, 'priceHeader', { suffix: comparison.suffix, kind: label })];
|
|
150
147
|
comparison.entries.slice(0, limit).forEach((entry, index) => {
|
|
151
148
|
const cny = entry.priceCny === undefined ? '' : `(≈¥${entry.priceCny.toFixed(2)})`;
|
|
152
|
-
const promo = entry.hasPromoCode ?
|
|
149
|
+
const promo = entry.hasPromoCode ? messages(locale, 'promo') : '';
|
|
153
150
|
lines.push(`${index + 1}. ${entry.registrar}:${entry.price} ${entry.currency}${cny}${promo}`);
|
|
154
151
|
});
|
|
155
152
|
if (comparison.entries.length > limit) {
|
|
156
|
-
lines.push(
|
|
153
|
+
lines.push(messages(locale, 'registrars', { count: comparison.entries.length }));
|
|
157
154
|
}
|
|
158
|
-
const updated = comparison.updatedAt ?
|
|
159
|
-
lines.push(
|
|
155
|
+
const updated = comparison.updatedAt ? messages(locale, 'priceUpdated', { date: comparison.updatedAt }) : '';
|
|
156
|
+
lines.push(messages(locale, 'priceNote', { updated }));
|
|
160
157
|
return lines.join('\n');
|
|
161
158
|
}
|
|
162
159
|
function isRecord(value) {
|
package/lib/domain-price.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain-price.js","sourceRoot":"","sources":["../src/domain-price.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Domain registration / renewal / transfer price comparison through the\n * public nazhumi.com API (`GET /api/v1?domain=<suffix>&order=register`).\n * `register` is the only documented `order` value, so renew/transfer\n * comparisons read the matching `renew` / `transfer` (+ `_cny`) fields and are\n * sorted locally.\n *\n * There is no second keyless provider with a comparable dataset, so instead\n * of a provider fallback this module tolerates partial data: entries with a\n * missing or non-numeric price are skipped, and an empty list is reported as\n * \"no data\" rather than an error.\n */\nimport { parseHostInput } from './address-policy.js';\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const NAZHUMI_API_URL = 'https://www.nazhumi.com/api/v1';\nconst PROVIDER = 'nazhumi.com';\n\nexport type PriceKind = 'register' | 'renew' | 'transfer';\n\nconst PRICE_KIND_ALIASES: Readonly<Record<string, PriceKind>> = {\n register: 'register',\n new: 'register',\n reg: 'register',\n 注册: 'register',\n 新购: 'register',\n renew: 'renew',\n 续费: 'renew',\n transfer: 'transfer',\n 转入: 'transfer',\n};\n\nconst PRICE_KIND_LABELS: Readonly<Record<PriceKind, string>> = {\n register: '注册',\n renew: '续费',\n transfer: '转入',\n};\n\nexport interface PriceEntry {\n readonly registrar: string;\n readonly website: string | undefined;\n readonly price: number;\n readonly currency: string;\n readonly priceCny: number | undefined;\n readonly hasPromoCode: boolean;\n}\n\nexport interface PriceComparison {\n readonly suffix: string;\n readonly kind: PriceKind;\n readonly entries: readonly PriceEntry[];\n readonly updatedAt: string | undefined;\n}\n\nexport function parsePriceKind(input: string | undefined): PriceKind {\n if (input === undefined || input.trim() === '') {\n return 'register';\n }\n const kind = PRICE_KIND_ALIASES[input.trim().toLowerCase()];\n if (!kind) {\n throw new NettoolsError('invalid_input', '价格类型可选:注册(register) / 续费(renew) / 转入(transfer)');\n }\n return kind;\n}\n\n/**\n * Normalise user input to a domain suffix.\n * - `com`, `.com`, `.co.uk`: a leading dot or a single label is taken verbatim;\n * - `example.com`, `www.example.co.uk`: the first label is dropped\n * (`com`, `example.co.uk` → use `.co.uk` explicitly for multi-level suffixes).\n */\nexport function parseDomainSuffix(input: string): string {\n const trimmed = input.trim();\n const explicitSuffix = trimmed.startsWith('.');\n const withoutDot = explicitSuffix ? trimmed.slice(1) : trimmed;\n if (withoutDot.length === 0) {\n throw new NettoolsError('invalid_input', '请提供域名后缀,例如 com 或 .cn');\n }\n const parsed = parseHostInput(withoutDot, { minLabels: 1 });\n if (parsed.kind !== 'domain') {\n throw new NettoolsError('invalid_input', '请提供域名或后缀,而不是 IP 地址');\n }\n const labels = parsed.name.split('.');\n if (explicitSuffix || labels.length === 1) {\n return parsed.name;\n }\n return labels.slice(1).join('.');\n}\n\nexport async function comparePrices(\n http: HttpClient,\n suffix: string,\n kind: PriceKind,\n signal?: AbortSignal,\n): Promise<PriceComparison> {\n const url = new URL(NAZHUMI_API_URL);\n url.searchParams.set('domain', suffix);\n url.searchParams.set('order', 'register');\n const response = await http.fetchJson(url, {\n provider: PROVIDER,\n ...(signal ? { signal } : {}),\n });\n return parsePriceResponse(response.body, suffix, kind);\n}\n\nexport function parsePriceResponse(body: unknown, suffix: string, kind: PriceKind): PriceComparison {\n if (!isRecord(body)) {\n throw new NettoolsError('malformed', 'nazhumi.com 返回了无法识别的数据');\n }\n if (typeof body.code === 'number' && body.code !== 100 && body.code !== 200 && body.code !== 0) {\n const message = typeof body.msg === 'string' ? body.msg : typeof body.message === 'string' ? body.message : '';\n throw new NettoolsError('http', `nazhumi.com 返回错误 ${body.code}${message ? `:${message}` : ''}`);\n }\n const data = isRecord(body.data) ? body.data : {};\n const rawPrices = Array.isArray(data.price) ? data.price : [];\n const entries: PriceEntry[] = [];\n let updatedAt: string | undefined;\n for (const raw of rawPrices) {\n const entry = parsePriceEntry(raw, kind);\n if (entry) {\n entries.push(entry);\n }\n if (isRecord(raw) && typeof raw.updatedtime === 'string' && (!updatedAt || raw.updatedtime > updatedAt)) {\n updatedAt = raw.updatedtime;\n }\n }\n entries.sort(compareByCnyPrice);\n return Object.freeze({ suffix, kind, entries: Object.freeze(entries), updatedAt });\n}\n\nfunction parsePriceEntry(raw: unknown, kind: PriceKind): PriceEntry | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n const price = raw[kind];\n if (typeof price !== 'number' || !Number.isFinite(price) || price <= 0) {\n return undefined;\n }\n const registrar = typeof raw.registrarname === 'string' && raw.registrarname.trim()\n ? raw.registrarname.trim()\n : typeof raw.registrar === 'string' ? raw.registrar : '';\n if (!registrar) {\n return undefined;\n }\n const cny = raw[`${kind}_cny`];\n const promo = isRecord(raw.promocode) ? raw.promocode[kind] === true : false;\n const website = typeof raw.registrarweb === 'string' && raw.registrarweb.startsWith('https://')\n ? raw.registrarweb\n : undefined;\n const currency = typeof raw.currencyname === 'string' && raw.currencyname\n ? raw.currencyname\n : typeof raw.currency === 'string' ? raw.currency.toUpperCase() : '';\n return Object.freeze({\n registrar,\n website,\n price,\n currency,\n priceCny: typeof cny === 'number' && Number.isFinite(cny) ? cny : undefined,\n hasPromoCode: promo,\n });\n}\n\n/** Cheapest first by CNY equivalent; entries without CNY go last, keeping API order. */\nfunction compareByCnyPrice(left: PriceEntry, right: PriceEntry): number {\n if (left.priceCny === undefined && right.priceCny === undefined) {\n return 0;\n }\n if (left.priceCny === undefined) {\n return 1;\n }\n if (right.priceCny === undefined) {\n return -1;\n }\n return left.priceCny - right.priceCny;\n}\n\nexport function formatPriceComparison(comparison: PriceComparison, limit: number): string {\n const label = PRICE_KIND_LABELS[comparison.kind];\n if (comparison.entries.length === 0) {\n return `💰 .${comparison.suffix} 暂无${label}价格数据`;\n }\n const lines = [`💰 .${comparison.suffix} ${label}价格(低→高,nazhumi.com)`];\n comparison.entries.slice(0, limit).forEach((entry, index) => {\n const cny = entry.priceCny === undefined ? '' : `(≈¥${entry.priceCny.toFixed(2)})`;\n const promo = entry.hasPromoCode ? ' 🎟️有优惠码' : '';\n lines.push(`${index + 1}. ${entry.registrar}:${entry.price} ${entry.currency}${cny}${promo}`);\n });\n if (comparison.entries.length > limit) {\n lines.push(`…共 ${comparison.entries.length} 家`);\n }\n const updated = comparison.updatedAt ? `,数据更新于 ${comparison.updatedAt}` : '';\n lines.push(`价格仅供参考${updated},以注册商官网为准`);\n return lines.join('\\n');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"domain-price.js","sourceRoot":"","sources":["../src/domain-price.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Locale } from 'zhin.js/i18n';\nimport { messages } from './presentation.js';\nimport { message } from './messages.js';\n/**\n * Domain registration / renewal / transfer price comparison through the\n * public nazhumi.com API (`GET /api/v1?domain=<suffix>&order=register`).\n * `register` is the only documented `order` value, so renew/transfer\n * comparisons read the matching `renew` / `transfer` (+ `_cny`) fields and are\n * sorted locally.\n *\n * There is no second keyless provider with a comparable dataset, so instead\n * of a provider fallback this module tolerates partial data: entries with a\n * missing or non-numeric price are skipped, and an empty list is reported as\n * \"no data\" rather than an error.\n */\nimport { parseHostInput } from './address-policy.js';\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const NAZHUMI_API_URL = 'https://www.nazhumi.com/api/v1';\nconst PROVIDER = 'nazhumi.com';\n\nexport type PriceKind = 'register' | 'renew' | 'transfer';\n\nconst PRICE_KIND_ALIASES: Readonly<Record<string, PriceKind>> = {\n register: 'register',\n new: 'register',\n reg: 'register',\n 注册: 'register',\n 新购: 'register',\n renew: 'renew',\n 续费: 'renew',\n transfer: 'transfer',\n 转入: 'transfer',\n};\n\n\n\nexport interface PriceEntry {\n readonly registrar: string;\n readonly website: string | undefined;\n readonly price: number;\n readonly currency: string;\n readonly priceCny: number | undefined;\n readonly hasPromoCode: boolean;\n}\n\nexport interface PriceComparison {\n readonly suffix: string;\n readonly kind: PriceKind;\n readonly entries: readonly PriceEntry[];\n readonly updatedAt: string | undefined;\n}\n\nexport function parsePriceKind(input: string | undefined): PriceKind {\n if (input === undefined || input.trim() === '') {\n return 'register';\n }\n const kind = PRICE_KIND_ALIASES[input.trim().toLowerCase()];\n if (!kind) {\n throw new NettoolsError('invalid_input', message('priceKind'));\n }\n return kind;\n}\n\n/**\n * Normalise user input to a domain suffix.\n * - `com`, `.com`, `.co.uk`: a leading dot or a single label is taken verbatim;\n * - `example.com`, `www.example.co.uk`: the first label is dropped\n * (`com`, `example.co.uk` → use `.co.uk` explicitly for multi-level suffixes).\n */\nexport function parseDomainSuffix(input: string): string {\n const trimmed = input.trim();\n const explicitSuffix = trimmed.startsWith('.');\n const withoutDot = explicitSuffix ? trimmed.slice(1) : trimmed;\n if (withoutDot.length === 0) {\n throw new NettoolsError('invalid_input', message('suffixRequired'));\n }\n const parsed = parseHostInput(withoutDot, { minLabels: 1 });\n if (parsed.kind !== 'domain') {\n throw new NettoolsError('invalid_input', message('suffixNotIp'));\n }\n const labels = parsed.name.split('.');\n if (explicitSuffix || labels.length === 1) {\n return parsed.name;\n }\n return labels.slice(1).join('.');\n}\n\nexport async function comparePrices(\n http: HttpClient,\n suffix: string,\n kind: PriceKind,\n signal?: AbortSignal,\n): Promise<PriceComparison> {\n const url = new URL(NAZHUMI_API_URL);\n url.searchParams.set('domain', suffix);\n url.searchParams.set('order', 'register');\n const response = await http.fetchJson(url, {\n provider: PROVIDER,\n ...(signal ? { signal } : {}),\n });\n return parsePriceResponse(response.body, suffix, kind);\n}\n\nexport function parsePriceResponse(body: unknown, suffix: string, kind: PriceKind): PriceComparison {\n if (!isRecord(body)) {\n throw new NettoolsError('malformed', message('priceMalformed'));\n }\n if (typeof body.code === 'number' && body.code !== 100 && body.code !== 200 && body.code !== 0) {\n const providerMessage = typeof body.msg === 'string' ? body.msg : typeof body.message === 'string' ? body.message : '';\n throw new NettoolsError('http', message('priceError', { code: body.code, detail: providerMessage ? `: ${providerMessage}` : '' }));\n }\n const data = isRecord(body.data) ? body.data : {};\n const rawPrices = Array.isArray(data.price) ? data.price : [];\n const entries: PriceEntry[] = [];\n let updatedAt: string | undefined;\n for (const raw of rawPrices) {\n const entry = parsePriceEntry(raw, kind);\n if (entry) {\n entries.push(entry);\n }\n if (isRecord(raw) && typeof raw.updatedtime === 'string' && (!updatedAt || raw.updatedtime > updatedAt)) {\n updatedAt = raw.updatedtime;\n }\n }\n entries.sort(compareByCnyPrice);\n return Object.freeze({ suffix, kind, entries: Object.freeze(entries), updatedAt });\n}\n\nfunction parsePriceEntry(raw: unknown, kind: PriceKind): PriceEntry | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n const price = raw[kind];\n if (typeof price !== 'number' || !Number.isFinite(price) || price <= 0) {\n return undefined;\n }\n const registrar = typeof raw.registrarname === 'string' && raw.registrarname.trim()\n ? raw.registrarname.trim()\n : typeof raw.registrar === 'string' ? raw.registrar : '';\n if (!registrar) {\n return undefined;\n }\n const cny = raw[`${kind}_cny`];\n const promo = isRecord(raw.promocode) ? raw.promocode[kind] === true : false;\n const website = typeof raw.registrarweb === 'string' && raw.registrarweb.startsWith('https://')\n ? raw.registrarweb\n : undefined;\n const currency = typeof raw.currencyname === 'string' && raw.currencyname\n ? raw.currencyname\n : typeof raw.currency === 'string' ? raw.currency.toUpperCase() : '';\n return Object.freeze({\n registrar,\n website,\n price,\n currency,\n priceCny: typeof cny === 'number' && Number.isFinite(cny) ? cny : undefined,\n hasPromoCode: promo,\n });\n}\n\n/** Cheapest first by CNY equivalent; entries without CNY go last, keeping API order. */\nfunction compareByCnyPrice(left: PriceEntry, right: PriceEntry): number {\n if (left.priceCny === undefined && right.priceCny === undefined) {\n return 0;\n }\n if (left.priceCny === undefined) {\n return 1;\n }\n if (right.priceCny === undefined) {\n return -1;\n }\n return left.priceCny - right.priceCny;\n}\n\nexport function formatPriceComparison(comparison: PriceComparison, limit: number, locale: Locale = 'zh-CN'): string {\n const label = messages(locale, comparison.kind);\n if (comparison.entries.length === 0) {\n return messages(locale, 'noPrice', { suffix: comparison.suffix, kind: label });\n }\n const lines = [messages(locale, 'priceHeader', { suffix: comparison.suffix, kind: label })];\n comparison.entries.slice(0, limit).forEach((entry, index) => {\n const cny = entry.priceCny === undefined ? '' : `(≈¥${entry.priceCny.toFixed(2)})`;\n const promo = entry.hasPromoCode ? messages(locale, 'promo') : '';\n lines.push(`${index + 1}. ${entry.registrar}:${entry.price} ${entry.currency}${cny}${promo}`);\n });\n if (comparison.entries.length > limit) {\n lines.push(messages(locale, 'registrars', { count: comparison.entries.length }));\n }\n const updated = comparison.updatedAt ? messages(locale, 'priceUpdated', { date: comparison.updatedAt }) : '';\n lines.push(messages(locale, 'priceNote', { updated }));\n return lines.join('\\n');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
|
package/lib/errors.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type Locale } from 'zhin.js/i18n';
|
|
2
|
+
import { type NettoolsMessage } from './messages.js';
|
|
1
3
|
/**
|
|
2
4
|
* Error categories surfaced by @zhin.js/plugin-nettools.
|
|
3
5
|
*
|
|
@@ -8,11 +10,12 @@
|
|
|
8
10
|
export type NettoolsErrorKind = 'invalid_input' | 'blocked_target' | 'cooldown' | 'timeout' | 'rate_limited' | 'http' | 'network' | 'malformed' | 'not_found' | 'aborted';
|
|
9
11
|
export declare class NettoolsError extends Error {
|
|
10
12
|
readonly kind: NettoolsErrorKind;
|
|
13
|
+
readonly detail?: NettoolsMessage;
|
|
11
14
|
/** HTTP status when the failure came from a provider response. */
|
|
12
15
|
readonly status?: number;
|
|
13
16
|
/** Milliseconds the caller should wait before retrying (cooldown / 429). */
|
|
14
17
|
readonly retryAfterMs?: number;
|
|
15
|
-
constructor(kind: NettoolsErrorKind, message: string, options?: {
|
|
18
|
+
constructor(kind: NettoolsErrorKind, message: string | NettoolsMessage, options?: {
|
|
16
19
|
status?: number;
|
|
17
20
|
retryAfterMs?: number;
|
|
18
21
|
cause?: unknown;
|
|
@@ -20,4 +23,4 @@ export declare class NettoolsError extends Error {
|
|
|
20
23
|
}
|
|
21
24
|
export declare function isNettoolsError(error: unknown): error is NettoolsError;
|
|
22
25
|
/** Render any thrown value as a short user-facing sentence. */
|
|
23
|
-
export declare function describeFailure(error: unknown): string;
|
|
26
|
+
export declare function describeFailure(error: unknown, locale?: Locale): string;
|