@zhin.js/plugin-nettools 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -2
- package/commands/dns/[name]/[[type]]/index.js +18 -0
- package/commands/dns/[name]/[[type]]/index.ts +18 -0
- package/commands/domain-price/[suffix]/[[order]]/index.js +19 -0
- package/commands/domain-price/[suffix]/[[order]]/index.ts +19 -0
- package/commands/ping/[host]/index.js +14 -0
- package/commands/ping/[host]/index.ts +14 -0
- package/commands/whois/[domain]/index.js +14 -0
- package/commands/whois/[domain]/index.ts +14 -0
- package/lib/address-policy.d.ts +44 -0
- package/lib/address-policy.js +388 -0
- package/lib/address-policy.js.map +1 -0
- package/lib/config.d.ts +44 -0
- package/lib/config.js +38 -0
- package/lib/config.js.map +1 -0
- package/lib/cooldown.d.ts +29 -0
- package/lib/cooldown.js +80 -0
- package/lib/cooldown.js.map +1 -0
- package/lib/dns.d.ts +52 -0
- package/lib/dns.js +191 -0
- package/lib/dns.js.map +1 -0
- package/lib/domain-price.d.ts +28 -0
- package/lib/domain-price.js +165 -0
- package/lib/domain-price.js.map +1 -0
- package/lib/errors.d.ts +23 -0
- package/lib/errors.js +30 -0
- package/lib/errors.js.map +1 -0
- package/lib/http.d.ts +50 -0
- package/lib/http.js +242 -0
- package/lib/http.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -0
- package/lib/ping.d.ts +45 -0
- package/lib/ping.js +195 -0
- package/lib/ping.js.map +1 -0
- package/lib/runtime.d.ts +3 -0
- package/lib/runtime.js +4 -0
- package/lib/runtime.js.map +1 -0
- package/lib/service.d.ts +18 -0
- package/lib/service.js +96 -0
- package/lib/service.js.map +1 -0
- package/lib/target-guard.d.ts +24 -0
- package/lib/target-guard.js +59 -0
- package/lib/target-guard.js.map +1 -0
- package/lib/whois.d.ts +31 -0
- package/lib/whois.js +345 -0
- package/lib/whois.js.map +1 -0
- package/package.json +105 -3
- package/plugin.js +14 -0
- package/schema.json +62 -0
- package/skills/nettools/SKILL.md +44 -0
- package/skills/nettools/tools/dns_lookup/index.js +20 -0
- package/skills/nettools/tools/domain_price/index.js +19 -0
- package/skills/nettools/tools/net_ping/index.js +16 -0
- package/skills/nettools/tools/whois_lookup/index.js +16 -0
|
@@ -0,0 +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"]}
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration. Every field is optional; defaults are deliberately
|
|
3
|
+
* conservative because all upstream services are free, keyless and shared.
|
|
4
|
+
* Out-of-range values are clamped to the documented bounds.
|
|
5
|
+
*/
|
|
6
|
+
import { type DohProviderId } from './dns.js';
|
|
7
|
+
export interface NettoolsConfig {
|
|
8
|
+
/** Per-user cooldown across all nettools commands/tools, seconds. 0 disables. Default 10, max 3600. */
|
|
9
|
+
readonly cooldownSeconds?: number;
|
|
10
|
+
/** Per-attempt HTTP timeout, ms. Default 8000, range 1000–30000. */
|
|
11
|
+
readonly requestTimeoutMs?: number;
|
|
12
|
+
/** Retries for transient failures (network, timeout, 429, 5xx). Default 2, range 0–5. */
|
|
13
|
+
readonly maxRetries?: number;
|
|
14
|
+
/** Number of check-host.net probe nodes for ping. Default 4, range 1–10. */
|
|
15
|
+
readonly pingMaxNodes?: number;
|
|
16
|
+
/** Wait between ping result polls, ms. Default 2000, range 500–10000. */
|
|
17
|
+
readonly pingPollIntervalMs?: number;
|
|
18
|
+
/** Maximum number of ping result polls. Default 5, range 1–10. */
|
|
19
|
+
readonly pingMaxPolls?: number;
|
|
20
|
+
/** DoH providers in fallback order. Default ["cloudflare", "alidns", "google"]. */
|
|
21
|
+
readonly dohProviders?: readonly DohProviderId[];
|
|
22
|
+
/** Registrars shown in a price comparison. Default 8, range 1–20. */
|
|
23
|
+
readonly priceListLimit?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ResolvedNettoolsConfig {
|
|
26
|
+
readonly cooldownMs: number;
|
|
27
|
+
readonly requestTimeoutMs: number;
|
|
28
|
+
readonly maxRetries: number;
|
|
29
|
+
readonly pingMaxNodes: number;
|
|
30
|
+
readonly pingPollIntervalMs: number;
|
|
31
|
+
readonly pingMaxPolls: number;
|
|
32
|
+
readonly dohProviders: readonly DohProviderId[];
|
|
33
|
+
readonly priceListLimit: number;
|
|
34
|
+
}
|
|
35
|
+
export declare const NETTOOLS_DEFAULTS: Readonly<{
|
|
36
|
+
cooldownSeconds: 10;
|
|
37
|
+
requestTimeoutMs: 8000;
|
|
38
|
+
maxRetries: 2;
|
|
39
|
+
pingMaxNodes: 4;
|
|
40
|
+
pingPollIntervalMs: 2000;
|
|
41
|
+
pingMaxPolls: 5;
|
|
42
|
+
priceListLimit: 8;
|
|
43
|
+
}>;
|
|
44
|
+
export declare function resolveNettoolsConfig(config: NettoolsConfig | undefined): ResolvedNettoolsConfig;
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration. Every field is optional; defaults are deliberately
|
|
3
|
+
* conservative because all upstream services are free, keyless and shared.
|
|
4
|
+
* Out-of-range values are clamped to the documented bounds.
|
|
5
|
+
*/
|
|
6
|
+
import { DEFAULT_DOH_PROVIDERS, DOH_PROVIDERS } from './dns.js';
|
|
7
|
+
export const NETTOOLS_DEFAULTS = Object.freeze({
|
|
8
|
+
cooldownSeconds: 10,
|
|
9
|
+
requestTimeoutMs: 8_000,
|
|
10
|
+
maxRetries: 2,
|
|
11
|
+
pingMaxNodes: 4,
|
|
12
|
+
pingPollIntervalMs: 2_000,
|
|
13
|
+
pingMaxPolls: 5,
|
|
14
|
+
priceListLimit: 8,
|
|
15
|
+
});
|
|
16
|
+
export function resolveNettoolsConfig(config) {
|
|
17
|
+
const source = config ?? {};
|
|
18
|
+
const cooldownSeconds = clampInteger(source.cooldownSeconds, NETTOOLS_DEFAULTS.cooldownSeconds, 0, 3_600);
|
|
19
|
+
const providers = (source.dohProviders ?? []).filter((id) => Object.hasOwn(DOH_PROVIDERS, id));
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
cooldownMs: cooldownSeconds * 1000,
|
|
22
|
+
requestTimeoutMs: clampInteger(source.requestTimeoutMs, NETTOOLS_DEFAULTS.requestTimeoutMs, 1_000, 30_000),
|
|
23
|
+
maxRetries: clampInteger(source.maxRetries, NETTOOLS_DEFAULTS.maxRetries, 0, 5),
|
|
24
|
+
pingMaxNodes: clampInteger(source.pingMaxNodes, NETTOOLS_DEFAULTS.pingMaxNodes, 1, 10),
|
|
25
|
+
pingPollIntervalMs: clampInteger(source.pingPollIntervalMs, NETTOOLS_DEFAULTS.pingPollIntervalMs, 500, 10_000),
|
|
26
|
+
pingMaxPolls: clampInteger(source.pingMaxPolls, NETTOOLS_DEFAULTS.pingMaxPolls, 1, 10),
|
|
27
|
+
dohProviders: Object.freeze(providers.length > 0 ? [...new Set(providers)] : [...DEFAULT_DOH_PROVIDERS]),
|
|
28
|
+
priceListLimit: clampInteger(source.priceListLimit, NETTOOLS_DEFAULTS.priceListLimit, 1, 20),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function clampInteger(value, fallback, min, max) {
|
|
32
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
const rounded = Math.round(value);
|
|
36
|
+
return Math.min(max, Math.max(min, rounded));
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAsB,MAAM,UAAU,CAAC;AAgCpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,eAAe,EAAE,EAAE;IACnB,gBAAgB,EAAE,KAAK;IACvB,UAAU,EAAE,CAAC;IACb,YAAY,EAAE,CAAC;IACf,kBAAkB,EAAE,KAAK;IACzB,YAAY,EAAE,CAAC;IACf,cAAc,EAAE,CAAC;CAClB,CAAC,CAAC;AAEH,MAAM,UAAU,qBAAqB,CAAC,MAAkC;IACtE,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;IAC5B,MAAM,eAAe,GAAG,YAAY,CAAC,MAAM,CAAC,eAAe,EAAE,iBAAiB,CAAC,eAAe,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;IAC1G,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAuB,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,CAAC;IACpH,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,UAAU,EAAE,eAAe,GAAG,IAAI;QAClC,gBAAgB,EAAE,YAAY,CAAC,MAAM,CAAC,gBAAgB,EAAE,iBAAiB,CAAC,gBAAgB,EAAE,KAAK,EAAE,MAAM,CAAC;QAC1G,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC,UAAU,EAAE,iBAAiB,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QAC/E,YAAY,EAAE,YAAY,CAAC,MAAM,CAAC,YAAY,EAAE,iBAAiB,CAAC,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACtF,kBAAkB,EAAE,YAAY,CAAC,MAAM,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,kBAAkB,EAAE,GAAG,EAAE,MAAM,CAAC;QAC9G,YAAY,EAAE,YAAY,CAAC,MAAM,CAAC,YAAY,EAAE,iBAAiB,CAAC,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACtF,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC;QACxG,cAAc,EAAE,YAAY,CAAC,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,cAAc,EAAE,CAAC,EAAE,EAAE,CAAC;KAC7F,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY,CAAC,KAAc,EAAE,QAAgB,EAAE,GAAW,EAAE,GAAW;IAC9E,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAC/C,CAAC","sourcesContent":["/**\n * Plugin configuration. Every field is optional; defaults are deliberately\n * conservative because all upstream services are free, keyless and shared.\n * Out-of-range values are clamped to the documented bounds.\n */\nimport { DEFAULT_DOH_PROVIDERS, DOH_PROVIDERS, type DohProviderId } from './dns.js';\n\nexport interface NettoolsConfig {\n /** Per-user cooldown across all nettools commands/tools, seconds. 0 disables. Default 10, max 3600. */\n readonly cooldownSeconds?: number;\n /** Per-attempt HTTP timeout, ms. Default 8000, range 1000–30000. */\n readonly requestTimeoutMs?: number;\n /** Retries for transient failures (network, timeout, 429, 5xx). Default 2, range 0–5. */\n readonly maxRetries?: number;\n /** Number of check-host.net probe nodes for ping. Default 4, range 1–10. */\n readonly pingMaxNodes?: number;\n /** Wait between ping result polls, ms. Default 2000, range 500–10000. */\n readonly pingPollIntervalMs?: number;\n /** Maximum number of ping result polls. Default 5, range 1–10. */\n readonly pingMaxPolls?: number;\n /** DoH providers in fallback order. Default [\"cloudflare\", \"alidns\", \"google\"]. */\n readonly dohProviders?: readonly DohProviderId[];\n /** Registrars shown in a price comparison. Default 8, range 1–20. */\n readonly priceListLimit?: number;\n}\n\nexport interface ResolvedNettoolsConfig {\n readonly cooldownMs: number;\n readonly requestTimeoutMs: number;\n readonly maxRetries: number;\n readonly pingMaxNodes: number;\n readonly pingPollIntervalMs: number;\n readonly pingMaxPolls: number;\n readonly dohProviders: readonly DohProviderId[];\n readonly priceListLimit: number;\n}\n\nexport const NETTOOLS_DEFAULTS = Object.freeze({\n cooldownSeconds: 10,\n requestTimeoutMs: 8_000,\n maxRetries: 2,\n pingMaxNodes: 4,\n pingPollIntervalMs: 2_000,\n pingMaxPolls: 5,\n priceListLimit: 8,\n});\n\nexport function resolveNettoolsConfig(config: NettoolsConfig | undefined): ResolvedNettoolsConfig {\n const source = config ?? {};\n const cooldownSeconds = clampInteger(source.cooldownSeconds, NETTOOLS_DEFAULTS.cooldownSeconds, 0, 3_600);\n const providers = (source.dohProviders ?? []).filter((id): id is DohProviderId => Object.hasOwn(DOH_PROVIDERS, id));\n return Object.freeze({\n cooldownMs: cooldownSeconds * 1000,\n requestTimeoutMs: clampInteger(source.requestTimeoutMs, NETTOOLS_DEFAULTS.requestTimeoutMs, 1_000, 30_000),\n maxRetries: clampInteger(source.maxRetries, NETTOOLS_DEFAULTS.maxRetries, 0, 5),\n pingMaxNodes: clampInteger(source.pingMaxNodes, NETTOOLS_DEFAULTS.pingMaxNodes, 1, 10),\n pingPollIntervalMs: clampInteger(source.pingPollIntervalMs, NETTOOLS_DEFAULTS.pingPollIntervalMs, 500, 10_000),\n pingMaxPolls: clampInteger(source.pingMaxPolls, NETTOOLS_DEFAULTS.pingMaxPolls, 1, 10),\n dohProviders: Object.freeze(providers.length > 0 ? [...new Set(providers)] : [...DEFAULT_DOH_PROVIDERS]),\n priceListLimit: clampInteger(source.priceListLimit, NETTOOLS_DEFAULTS.priceListLimit, 1, 20),\n });\n}\n\nfunction clampInteger(value: unknown, fallback: number, min: number, max: number): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n return fallback;\n }\n const rounded = Math.round(value);\n return Math.min(max, Math.max(min, rounded));\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const DEFAULT_COOLDOWN_CAPACITY = 10000;
|
|
2
|
+
export declare class UserCooldown {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(cooldownMs: number, now?: () => number, capacity?: number);
|
|
5
|
+
get cooldownMs(): number;
|
|
6
|
+
/** Milliseconds until `callerKey` may call again (0 when allowed). */
|
|
7
|
+
remainingMs(callerKey: string): number;
|
|
8
|
+
/** Spend one call for `callerKey`, or throw a `cooldown` error. */
|
|
9
|
+
take(callerKey: string): void;
|
|
10
|
+
}
|
|
11
|
+
/** Cooldown key for a chat command: endpoint + sender, shared across conversations. */
|
|
12
|
+
export declare function commandCallerKey(input: {
|
|
13
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
14
|
+
readonly conversation?: {
|
|
15
|
+
readonly id?: unknown;
|
|
16
|
+
readonly endpoint?: {
|
|
17
|
+
readonly id?: unknown;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
readonly sender?: {
|
|
21
|
+
readonly id?: unknown;
|
|
22
|
+
};
|
|
23
|
+
} | undefined): string;
|
|
24
|
+
/** Cooldown key for an Agent tool call: the invoking principal. */
|
|
25
|
+
export declare function toolCallerKey(context: {
|
|
26
|
+
readonly principal?: {
|
|
27
|
+
readonly subjectId?: string;
|
|
28
|
+
};
|
|
29
|
+
} | undefined): string;
|
package/lib/cooldown.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-user cooldown shared by every nettools command and Agent tool of one
|
|
3
|
+
* plugin instance. Each call to an external API spends the caller's budget
|
|
4
|
+
* at admission time (after input validation), so typos are free but a failed
|
|
5
|
+
* lookup still counts — that is what protects the upstream free APIs.
|
|
6
|
+
*
|
|
7
|
+
* State is in-memory and bounded; it resets on restart.
|
|
8
|
+
*/
|
|
9
|
+
import { NettoolsError } from './errors.js';
|
|
10
|
+
export const DEFAULT_COOLDOWN_CAPACITY = 10_000;
|
|
11
|
+
export class UserCooldown {
|
|
12
|
+
#cooldownMs;
|
|
13
|
+
#capacity;
|
|
14
|
+
#now;
|
|
15
|
+
/** caller key → epoch ms when the caller may call again. */
|
|
16
|
+
#readyAt = new Map();
|
|
17
|
+
constructor(cooldownMs, now = Date.now, capacity = DEFAULT_COOLDOWN_CAPACITY) {
|
|
18
|
+
this.#cooldownMs = Math.max(0, cooldownMs);
|
|
19
|
+
this.#now = now;
|
|
20
|
+
this.#capacity = Math.max(1, capacity);
|
|
21
|
+
}
|
|
22
|
+
get cooldownMs() {
|
|
23
|
+
return this.#cooldownMs;
|
|
24
|
+
}
|
|
25
|
+
/** Milliseconds until `callerKey` may call again (0 when allowed). */
|
|
26
|
+
remainingMs(callerKey) {
|
|
27
|
+
const readyAt = this.#readyAt.get(callerKey);
|
|
28
|
+
if (readyAt === undefined) {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
return Math.max(0, readyAt - this.#now());
|
|
32
|
+
}
|
|
33
|
+
/** Spend one call for `callerKey`, or throw a `cooldown` error. */
|
|
34
|
+
take(callerKey) {
|
|
35
|
+
if (this.#cooldownMs === 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const remaining = this.remainingMs(callerKey);
|
|
39
|
+
if (remaining > 0) {
|
|
40
|
+
const seconds = Math.ceil(remaining / 1000);
|
|
41
|
+
throw new NettoolsError('cooldown', `操作太频繁,请 ${seconds} 秒后再试`, { retryAfterMs: remaining });
|
|
42
|
+
}
|
|
43
|
+
this.#readyAt.delete(callerKey);
|
|
44
|
+
this.#readyAt.set(callerKey, this.#now() + this.#cooldownMs);
|
|
45
|
+
this.#enforceCapacity();
|
|
46
|
+
}
|
|
47
|
+
#enforceCapacity() {
|
|
48
|
+
if (this.#readyAt.size <= this.#capacity) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const now = this.#now();
|
|
52
|
+
for (const [key, readyAt] of this.#readyAt) {
|
|
53
|
+
if (readyAt <= now) {
|
|
54
|
+
this.#readyAt.delete(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
while (this.#readyAt.size > this.#capacity) {
|
|
58
|
+
const oldest = this.#readyAt.keys().next().value;
|
|
59
|
+
if (oldest === undefined) {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
this.#readyAt.delete(oldest);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Cooldown key for a chat command: endpoint + sender, shared across conversations. */
|
|
67
|
+
export function commandCallerKey(input) {
|
|
68
|
+
const senderId = input?.sender?.id;
|
|
69
|
+
if (senderId === undefined || senderId === null || String(senderId) === '') {
|
|
70
|
+
return 'anonymous';
|
|
71
|
+
}
|
|
72
|
+
const endpointId = input?.metadata?.endpointId ?? input?.conversation?.endpoint?.id ?? '';
|
|
73
|
+
return `im:${String(endpointId)}:${String(senderId)}`;
|
|
74
|
+
}
|
|
75
|
+
/** Cooldown key for an Agent tool call: the invoking principal. */
|
|
76
|
+
export function toolCallerKey(context) {
|
|
77
|
+
const subjectId = context?.principal?.subjectId;
|
|
78
|
+
return subjectId ? `principal:${subjectId}` : 'anonymous';
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=cooldown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cooldown.js","sourceRoot":"","sources":["../src/cooldown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\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', `操作太频繁,请 ${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
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { HttpClient } from './http.js';
|
|
2
|
+
export declare const DNS_RECORD_TYPES: {
|
|
3
|
+
readonly A: 1;
|
|
4
|
+
readonly NS: 2;
|
|
5
|
+
readonly CNAME: 5;
|
|
6
|
+
readonly SOA: 6;
|
|
7
|
+
readonly PTR: 12;
|
|
8
|
+
readonly MX: 15;
|
|
9
|
+
readonly TXT: 16;
|
|
10
|
+
readonly AAAA: 28;
|
|
11
|
+
readonly SRV: 33;
|
|
12
|
+
readonly DS: 43;
|
|
13
|
+
readonly DNSKEY: 48;
|
|
14
|
+
readonly CAA: 257;
|
|
15
|
+
};
|
|
16
|
+
export type DnsRecordType = keyof typeof DNS_RECORD_TYPES;
|
|
17
|
+
export declare const SUPPORTED_DNS_TYPES: DnsRecordType[];
|
|
18
|
+
export type DohProviderId = 'cloudflare' | 'alidns' | 'google';
|
|
19
|
+
export interface DohProvider {
|
|
20
|
+
readonly id: DohProviderId;
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly endpoint: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const DOH_PROVIDERS: Readonly<Record<DohProviderId, DohProvider>>;
|
|
25
|
+
export declare const DEFAULT_DOH_PROVIDERS: readonly DohProviderId[];
|
|
26
|
+
export interface DnsRecord {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly type: string;
|
|
29
|
+
readonly ttl: number;
|
|
30
|
+
readonly data: string;
|
|
31
|
+
}
|
|
32
|
+
export type DnsStatus = 'NOERROR' | 'NXDOMAIN';
|
|
33
|
+
export interface DnsAnswer {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly type: DnsRecordType;
|
|
36
|
+
readonly status: DnsStatus;
|
|
37
|
+
readonly records: readonly DnsRecord[];
|
|
38
|
+
/** Whether the resolver validated the answer with DNSSEC (AD bit). */
|
|
39
|
+
readonly authenticated: boolean;
|
|
40
|
+
readonly provider: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function parseDnsType(input: string | undefined): DnsRecordType;
|
|
43
|
+
export declare class DohResolver {
|
|
44
|
+
#private;
|
|
45
|
+
constructor(http: HttpClient, providerIds?: readonly DohProviderId[]);
|
|
46
|
+
get providers(): readonly DohProvider[];
|
|
47
|
+
/** Resolve `name` (already validated) for one record type, falling back across providers. */
|
|
48
|
+
resolve(name: string, type: DnsRecordType, signal?: AbortSignal): Promise<DnsAnswer>;
|
|
49
|
+
}
|
|
50
|
+
/** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */
|
|
51
|
+
export declare function parseDohResponse(body: unknown, name: string, type: DnsRecordType, provider: string): DnsAnswer;
|
|
52
|
+
export declare function formatDnsAnswer(answer: DnsAnswer, maxRecords?: number): string;
|
package/lib/dns.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNS lookups over HTTPS (DoH JSON API) with provider fallback.
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare (`cloudflare-dns.com/dns-query`), AliDNS (`dns.alidns.com/resolve`,
|
|
5
|
+
* reachable from mainland China) and Google (`dns.google/resolve`) speak the
|
|
6
|
+
* same JSON dialect. Providers are tried in
|
|
7
|
+
* order; we move on to the next one when a provider is unreachable, times out,
|
|
8
|
+
* returns an HTTP error, returns malformed JSON or answers SERVFAIL/REFUSED.
|
|
9
|
+
* NXDOMAIN and "no records" are authoritative answers and are returned as-is.
|
|
10
|
+
*/
|
|
11
|
+
import { NettoolsError } from './errors.js';
|
|
12
|
+
export const DNS_RECORD_TYPES = {
|
|
13
|
+
A: 1,
|
|
14
|
+
NS: 2,
|
|
15
|
+
CNAME: 5,
|
|
16
|
+
SOA: 6,
|
|
17
|
+
PTR: 12,
|
|
18
|
+
MX: 15,
|
|
19
|
+
TXT: 16,
|
|
20
|
+
AAAA: 28,
|
|
21
|
+
SRV: 33,
|
|
22
|
+
DS: 43,
|
|
23
|
+
DNSKEY: 48,
|
|
24
|
+
CAA: 257,
|
|
25
|
+
};
|
|
26
|
+
export const SUPPORTED_DNS_TYPES = Object.keys(DNS_RECORD_TYPES);
|
|
27
|
+
export const DOH_PROVIDERS = Object.freeze({
|
|
28
|
+
cloudflare: Object.freeze({
|
|
29
|
+
id: 'cloudflare',
|
|
30
|
+
label: 'Cloudflare DoH',
|
|
31
|
+
endpoint: 'https://cloudflare-dns.com/dns-query',
|
|
32
|
+
}),
|
|
33
|
+
alidns: Object.freeze({
|
|
34
|
+
id: 'alidns',
|
|
35
|
+
label: 'AliDNS DoH',
|
|
36
|
+
endpoint: 'https://dns.alidns.com/resolve',
|
|
37
|
+
}),
|
|
38
|
+
google: Object.freeze({
|
|
39
|
+
id: 'google',
|
|
40
|
+
label: 'Google DoH',
|
|
41
|
+
endpoint: 'https://dns.google/resolve',
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
export const DEFAULT_DOH_PROVIDERS = ['cloudflare', 'alidns', 'google'];
|
|
45
|
+
const RCODE_NAMES = {
|
|
46
|
+
0: 'NOERROR',
|
|
47
|
+
1: 'FORMERR',
|
|
48
|
+
2: 'SERVFAIL',
|
|
49
|
+
3: 'NXDOMAIN',
|
|
50
|
+
4: 'NOTIMP',
|
|
51
|
+
5: 'REFUSED',
|
|
52
|
+
};
|
|
53
|
+
const TYPE_NAMES_BY_CODE = new Map(Object.entries(DNS_RECORD_TYPES).map(([name, code]) => [code, name]));
|
|
54
|
+
export function parseDnsType(input) {
|
|
55
|
+
if (input === undefined || input.trim() === '') {
|
|
56
|
+
return 'A';
|
|
57
|
+
}
|
|
58
|
+
const upper = input.trim().toUpperCase();
|
|
59
|
+
if (Object.hasOwn(DNS_RECORD_TYPES, upper)) {
|
|
60
|
+
return upper;
|
|
61
|
+
}
|
|
62
|
+
throw new NettoolsError('invalid_input', `不支持的记录类型 ${input},可选:${SUPPORTED_DNS_TYPES.join(' / ')}`);
|
|
63
|
+
}
|
|
64
|
+
export class DohResolver {
|
|
65
|
+
#http;
|
|
66
|
+
#providers;
|
|
67
|
+
constructor(http, providerIds = DEFAULT_DOH_PROVIDERS) {
|
|
68
|
+
const unique = [...new Set(providerIds)];
|
|
69
|
+
const selected = unique.length > 0 ? unique : [...DEFAULT_DOH_PROVIDERS];
|
|
70
|
+
this.#http = http;
|
|
71
|
+
this.#providers = selected.map((id) => DOH_PROVIDERS[id]);
|
|
72
|
+
}
|
|
73
|
+
get providers() {
|
|
74
|
+
return this.#providers;
|
|
75
|
+
}
|
|
76
|
+
/** Resolve `name` (already validated) for one record type, falling back across providers. */
|
|
77
|
+
async resolve(name, type, signal) {
|
|
78
|
+
const failures = [];
|
|
79
|
+
let lastError;
|
|
80
|
+
for (const [index, provider] of this.#providers.entries()) {
|
|
81
|
+
const hasFallback = index < this.#providers.length - 1;
|
|
82
|
+
try {
|
|
83
|
+
return await this.#resolveWith(provider, name, type, hasFallback, signal);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof NettoolsError && error.kind === 'aborted') {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const wrapped = error instanceof NettoolsError
|
|
90
|
+
? error
|
|
91
|
+
: new NettoolsError('network', `${provider.label} 查询失败`, { cause: error });
|
|
92
|
+
lastError = wrapped;
|
|
93
|
+
failures.push(wrapped.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (failures.length <= 1 && lastError) {
|
|
97
|
+
throw lastError;
|
|
98
|
+
}
|
|
99
|
+
throw new NettoolsError('network', `所有 DNS 服务均不可用:${failures.join(';')}`, { cause: lastError });
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* When another provider is still available we retry at most once here and
|
|
103
|
+
* let the fallback absorb the rest, keeping the worst-case latency bounded.
|
|
104
|
+
*/
|
|
105
|
+
async #resolveWith(provider, name, type, hasFallback, signal) {
|
|
106
|
+
const configuredRetries = this.#http.policy.maxRetries;
|
|
107
|
+
const maxRetries = hasFallback ? Math.min(1, configuredRetries) : configuredRetries;
|
|
108
|
+
const url = new URL(provider.endpoint);
|
|
109
|
+
url.searchParams.set('name', name);
|
|
110
|
+
url.searchParams.set('type', type);
|
|
111
|
+
const response = await this.#http.fetchJson(url, {
|
|
112
|
+
provider: provider.label,
|
|
113
|
+
accept: 'application/dns-json',
|
|
114
|
+
maxRetries,
|
|
115
|
+
...(signal ? { signal } : {}),
|
|
116
|
+
});
|
|
117
|
+
return parseDohResponse(response.body, name, type, provider.label);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */
|
|
121
|
+
export function parseDohResponse(body, name, type, provider) {
|
|
122
|
+
if (!isRecord(body) || typeof body.Status !== 'number') {
|
|
123
|
+
throw new NettoolsError('malformed', `${provider} 返回了无法识别的 DNS 响应`);
|
|
124
|
+
}
|
|
125
|
+
const status = body.Status;
|
|
126
|
+
if (status !== 0 && status !== 3) {
|
|
127
|
+
const rcode = RCODE_NAMES[status] ?? `RCODE ${status}`;
|
|
128
|
+
throw new NettoolsError('http', `${provider} 解析失败(${rcode})`);
|
|
129
|
+
}
|
|
130
|
+
const rawAnswers = Array.isArray(body.Answer) ? body.Answer : [];
|
|
131
|
+
const records = [];
|
|
132
|
+
for (const raw of rawAnswers) {
|
|
133
|
+
const record = parseDnsRecord(raw);
|
|
134
|
+
if (record) {
|
|
135
|
+
records.push(record);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
name,
|
|
140
|
+
type,
|
|
141
|
+
status: status === 3 ? 'NXDOMAIN' : 'NOERROR',
|
|
142
|
+
records: Object.freeze(records),
|
|
143
|
+
authenticated: body.AD === true,
|
|
144
|
+
provider,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function parseDnsRecord(raw) {
|
|
148
|
+
if (!isRecord(raw)) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (typeof raw.data !== 'string' || typeof raw.type !== 'number') {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const typeName = TYPE_NAMES_BY_CODE.get(raw.type) ?? `TYPE${raw.type}`;
|
|
155
|
+
const ttl = typeof raw.TTL === 'number' && Number.isFinite(raw.TTL) ? raw.TTL : 0;
|
|
156
|
+
const name = typeof raw.name === 'string' ? raw.name.replace(/\.$/u, '') : '';
|
|
157
|
+
return Object.freeze({ name, type: typeName, ttl, data: raw.data });
|
|
158
|
+
}
|
|
159
|
+
export function formatDnsAnswer(answer, maxRecords = 20) {
|
|
160
|
+
const header = `🔎 ${answer.name} ${answer.type} 记录(${answer.provider}${answer.authenticated ? ',DNSSEC 已验证' : ''})`;
|
|
161
|
+
if (answer.status === 'NXDOMAIN') {
|
|
162
|
+
return `${header}\n域名不存在 (NXDOMAIN)`;
|
|
163
|
+
}
|
|
164
|
+
const matching = answer.records.filter((record) => record.type === answer.type);
|
|
165
|
+
const chain = answer.records.filter((record) => record.type !== answer.type);
|
|
166
|
+
if (matching.length === 0 && chain.length === 0) {
|
|
167
|
+
return `${header}\n没有 ${answer.type} 记录`;
|
|
168
|
+
}
|
|
169
|
+
const lines = [header];
|
|
170
|
+
for (const record of chain.slice(0, maxRecords)) {
|
|
171
|
+
lines.push(`↪ ${record.name} ${record.type} ${truncate(record.data)}`);
|
|
172
|
+
}
|
|
173
|
+
const shown = matching.slice(0, maxRecords);
|
|
174
|
+
for (const record of shown) {
|
|
175
|
+
lines.push(`• ${truncate(record.data)} (TTL ${record.ttl})`);
|
|
176
|
+
}
|
|
177
|
+
if (matching.length > shown.length) {
|
|
178
|
+
lines.push(`…另有 ${matching.length - shown.length} 条`);
|
|
179
|
+
}
|
|
180
|
+
if (matching.length === 0) {
|
|
181
|
+
lines.push(`没有直接的 ${answer.type} 记录`);
|
|
182
|
+
}
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
function truncate(text, max = 300) {
|
|
186
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
187
|
+
}
|
|
188
|
+
function isRecord(value) {
|
|
189
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=dns.js.map
|
package/lib/dns.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dns.js","sourceRoot":"","sources":["../src/dns.ts"],"names":[],"mappings":"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","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"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HttpClient } from './http.js';
|
|
2
|
+
export declare const NAZHUMI_API_URL = "https://www.nazhumi.com/api/v1";
|
|
3
|
+
export type PriceKind = 'register' | 'renew' | 'transfer';
|
|
4
|
+
export interface PriceEntry {
|
|
5
|
+
readonly registrar: string;
|
|
6
|
+
readonly website: string | undefined;
|
|
7
|
+
readonly price: number;
|
|
8
|
+
readonly currency: string;
|
|
9
|
+
readonly priceCny: number | undefined;
|
|
10
|
+
readonly hasPromoCode: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface PriceComparison {
|
|
13
|
+
readonly suffix: string;
|
|
14
|
+
readonly kind: PriceKind;
|
|
15
|
+
readonly entries: readonly PriceEntry[];
|
|
16
|
+
readonly updatedAt: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare function parsePriceKind(input: string | undefined): PriceKind;
|
|
19
|
+
/**
|
|
20
|
+
* Normalise user input to a domain suffix.
|
|
21
|
+
* - `com`, `.com`, `.co.uk`: a leading dot or a single label is taken verbatim;
|
|
22
|
+
* - `example.com`, `www.example.co.uk`: the first label is dropped
|
|
23
|
+
* (`com`, `example.co.uk` → use `.co.uk` explicitly for multi-level suffixes).
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseDomainSuffix(input: string): string;
|
|
26
|
+
export declare function comparePrices(http: HttpClient, suffix: string, kind: PriceKind, signal?: AbortSignal): Promise<PriceComparison>;
|
|
27
|
+
export declare function parsePriceResponse(body: unknown, suffix: string, kind: PriceKind): PriceComparison;
|
|
28
|
+
export declare function formatPriceComparison(comparison: PriceComparison, limit: number): string;
|