@zhin.js/plugin-nettools 0.0.0-stage → 0.1.1

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +70 -2
  3. package/commands/dns/[name]/[[type]]/index.js +18 -0
  4. package/commands/dns/[name]/[[type]]/index.ts +18 -0
  5. package/commands/domain-price/[suffix]/[[order]]/index.js +19 -0
  6. package/commands/domain-price/[suffix]/[[order]]/index.ts +19 -0
  7. package/commands/ping/[host]/index.js +14 -0
  8. package/commands/ping/[host]/index.ts +14 -0
  9. package/commands/whois/[domain]/index.js +14 -0
  10. package/commands/whois/[domain]/index.ts +14 -0
  11. package/lib/address-policy.d.ts +44 -0
  12. package/lib/address-policy.js +388 -0
  13. package/lib/address-policy.js.map +1 -0
  14. package/lib/config.d.ts +44 -0
  15. package/lib/config.js +38 -0
  16. package/lib/config.js.map +1 -0
  17. package/lib/cooldown.d.ts +29 -0
  18. package/lib/cooldown.js +80 -0
  19. package/lib/cooldown.js.map +1 -0
  20. package/lib/dns.d.ts +52 -0
  21. package/lib/dns.js +191 -0
  22. package/lib/dns.js.map +1 -0
  23. package/lib/domain-price.d.ts +28 -0
  24. package/lib/domain-price.js +165 -0
  25. package/lib/domain-price.js.map +1 -0
  26. package/lib/errors.d.ts +23 -0
  27. package/lib/errors.js +30 -0
  28. package/lib/errors.js.map +1 -0
  29. package/lib/http.d.ts +50 -0
  30. package/lib/http.js +242 -0
  31. package/lib/http.js.map +1 -0
  32. package/lib/index.d.ts +12 -0
  33. package/lib/index.js +13 -0
  34. package/lib/index.js.map +1 -0
  35. package/lib/ping.d.ts +45 -0
  36. package/lib/ping.js +195 -0
  37. package/lib/ping.js.map +1 -0
  38. package/lib/runtime.d.ts +3 -0
  39. package/lib/runtime.js +4 -0
  40. package/lib/runtime.js.map +1 -0
  41. package/lib/service.d.ts +18 -0
  42. package/lib/service.js +96 -0
  43. package/lib/service.js.map +1 -0
  44. package/lib/target-guard.d.ts +24 -0
  45. package/lib/target-guard.js +59 -0
  46. package/lib/target-guard.js.map +1 -0
  47. package/lib/whois.d.ts +31 -0
  48. package/lib/whois.js +345 -0
  49. package/lib/whois.js.map +1 -0
  50. package/package.json +105 -3
  51. package/plugin.js +14 -0
  52. package/schema.json +62 -0
  53. package/skills/nettools/SKILL.md +44 -0
  54. package/skills/nettools/tools/dns_lookup/index.js +20 -0
  55. package/skills/nettools/tools/domain_price/index.js +19 -0
  56. package/skills/nettools/tools/net_ping/index.js +16 -0
  57. package/skills/nettools/tools/whois_lookup/index.js +16 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Domain registration / renewal / transfer price comparison through the
3
+ * public nazhumi.com API (`GET /api/v1?domain=<suffix>&order=register`).
4
+ * `register` is the only documented `order` value, so renew/transfer
5
+ * comparisons read the matching `renew` / `transfer` (+ `_cny`) fields and are
6
+ * sorted locally.
7
+ *
8
+ * There is no second keyless provider with a comparable dataset, so instead
9
+ * of a provider fallback this module tolerates partial data: entries with a
10
+ * missing or non-numeric price are skipped, and an empty list is reported as
11
+ * "no data" rather than an error.
12
+ */
13
+ import { parseHostInput } from './address-policy.js';
14
+ import { NettoolsError } from './errors.js';
15
+ export const NAZHUMI_API_URL = 'https://www.nazhumi.com/api/v1';
16
+ const PROVIDER = 'nazhumi.com';
17
+ const PRICE_KIND_ALIASES = {
18
+ register: 'register',
19
+ new: 'register',
20
+ reg: 'register',
21
+ 注册: 'register',
22
+ 新购: 'register',
23
+ renew: 'renew',
24
+ 续费: 'renew',
25
+ transfer: 'transfer',
26
+ 转入: 'transfer',
27
+ };
28
+ const PRICE_KIND_LABELS = {
29
+ register: '注册',
30
+ renew: '续费',
31
+ transfer: '转入',
32
+ };
33
+ export function parsePriceKind(input) {
34
+ if (input === undefined || input.trim() === '') {
35
+ return 'register';
36
+ }
37
+ const kind = PRICE_KIND_ALIASES[input.trim().toLowerCase()];
38
+ if (!kind) {
39
+ throw new NettoolsError('invalid_input', '价格类型可选:注册(register) / 续费(renew) / 转入(transfer)');
40
+ }
41
+ return kind;
42
+ }
43
+ /**
44
+ * Normalise user input to a domain suffix.
45
+ * - `com`, `.com`, `.co.uk`: a leading dot or a single label is taken verbatim;
46
+ * - `example.com`, `www.example.co.uk`: the first label is dropped
47
+ * (`com`, `example.co.uk` → use `.co.uk` explicitly for multi-level suffixes).
48
+ */
49
+ export function parseDomainSuffix(input) {
50
+ const trimmed = input.trim();
51
+ const explicitSuffix = trimmed.startsWith('.');
52
+ const withoutDot = explicitSuffix ? trimmed.slice(1) : trimmed;
53
+ if (withoutDot.length === 0) {
54
+ throw new NettoolsError('invalid_input', '请提供域名后缀,例如 com 或 .cn');
55
+ }
56
+ const parsed = parseHostInput(withoutDot, { minLabels: 1 });
57
+ if (parsed.kind !== 'domain') {
58
+ throw new NettoolsError('invalid_input', '请提供域名或后缀,而不是 IP 地址');
59
+ }
60
+ const labels = parsed.name.split('.');
61
+ if (explicitSuffix || labels.length === 1) {
62
+ return parsed.name;
63
+ }
64
+ return labels.slice(1).join('.');
65
+ }
66
+ export async function comparePrices(http, suffix, kind, signal) {
67
+ const url = new URL(NAZHUMI_API_URL);
68
+ url.searchParams.set('domain', suffix);
69
+ url.searchParams.set('order', 'register');
70
+ const response = await http.fetchJson(url, {
71
+ provider: PROVIDER,
72
+ ...(signal ? { signal } : {}),
73
+ });
74
+ return parsePriceResponse(response.body, suffix, kind);
75
+ }
76
+ export function parsePriceResponse(body, suffix, kind) {
77
+ if (!isRecord(body)) {
78
+ throw new NettoolsError('malformed', 'nazhumi.com 返回了无法识别的数据');
79
+ }
80
+ if (typeof body.code === 'number' && body.code !== 100 && body.code !== 200 && body.code !== 0) {
81
+ const message = typeof body.msg === 'string' ? body.msg : typeof body.message === 'string' ? body.message : '';
82
+ throw new NettoolsError('http', `nazhumi.com 返回错误 ${body.code}${message ? `:${message}` : ''}`);
83
+ }
84
+ const data = isRecord(body.data) ? body.data : {};
85
+ const rawPrices = Array.isArray(data.price) ? data.price : [];
86
+ const entries = [];
87
+ let updatedAt;
88
+ for (const raw of rawPrices) {
89
+ const entry = parsePriceEntry(raw, kind);
90
+ if (entry) {
91
+ entries.push(entry);
92
+ }
93
+ if (isRecord(raw) && typeof raw.updatedtime === 'string' && (!updatedAt || raw.updatedtime > updatedAt)) {
94
+ updatedAt = raw.updatedtime;
95
+ }
96
+ }
97
+ entries.sort(compareByCnyPrice);
98
+ return Object.freeze({ suffix, kind, entries: Object.freeze(entries), updatedAt });
99
+ }
100
+ function parsePriceEntry(raw, kind) {
101
+ if (!isRecord(raw)) {
102
+ return undefined;
103
+ }
104
+ const price = raw[kind];
105
+ if (typeof price !== 'number' || !Number.isFinite(price) || price <= 0) {
106
+ return undefined;
107
+ }
108
+ const registrar = typeof raw.registrarname === 'string' && raw.registrarname.trim()
109
+ ? raw.registrarname.trim()
110
+ : typeof raw.registrar === 'string' ? raw.registrar : '';
111
+ if (!registrar) {
112
+ return undefined;
113
+ }
114
+ const cny = raw[`${kind}_cny`];
115
+ const promo = isRecord(raw.promocode) ? raw.promocode[kind] === true : false;
116
+ const website = typeof raw.registrarweb === 'string' && raw.registrarweb.startsWith('https://')
117
+ ? raw.registrarweb
118
+ : undefined;
119
+ const currency = typeof raw.currencyname === 'string' && raw.currencyname
120
+ ? raw.currencyname
121
+ : typeof raw.currency === 'string' ? raw.currency.toUpperCase() : '';
122
+ return Object.freeze({
123
+ registrar,
124
+ website,
125
+ price,
126
+ currency,
127
+ priceCny: typeof cny === 'number' && Number.isFinite(cny) ? cny : undefined,
128
+ hasPromoCode: promo,
129
+ });
130
+ }
131
+ /** Cheapest first by CNY equivalent; entries without CNY go last, keeping API order. */
132
+ function compareByCnyPrice(left, right) {
133
+ if (left.priceCny === undefined && right.priceCny === undefined) {
134
+ return 0;
135
+ }
136
+ if (left.priceCny === undefined) {
137
+ return 1;
138
+ }
139
+ if (right.priceCny === undefined) {
140
+ return -1;
141
+ }
142
+ return left.priceCny - right.priceCny;
143
+ }
144
+ export function formatPriceComparison(comparison, limit) {
145
+ const label = PRICE_KIND_LABELS[comparison.kind];
146
+ if (comparison.entries.length === 0) {
147
+ return `💰 .${comparison.suffix} 暂无${label}价格数据`;
148
+ }
149
+ const lines = [`💰 .${comparison.suffix} ${label}价格(低→高,nazhumi.com)`];
150
+ comparison.entries.slice(0, limit).forEach((entry, index) => {
151
+ const cny = entry.priceCny === undefined ? '' : `(≈¥${entry.priceCny.toFixed(2)})`;
152
+ const promo = entry.hasPromoCode ? ' 🎟️有优惠码' : '';
153
+ lines.push(`${index + 1}. ${entry.registrar}:${entry.price} ${entry.currency}${cny}${promo}`);
154
+ });
155
+ if (comparison.entries.length > limit) {
156
+ lines.push(`…共 ${comparison.entries.length} 家`);
157
+ }
158
+ const updated = comparison.updatedAt ? `,数据更新于 ${comparison.updatedAt}` : '';
159
+ lines.push(`价格仅供参考${updated},以注册商官网为准`);
160
+ return lines.join('\n');
161
+ }
162
+ function isRecord(value) {
163
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
164
+ }
165
+ //# sourceMappingURL=domain-price.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Error categories surfaced by @zhin.js/plugin-nettools.
3
+ *
4
+ * Every failure that reaches a user is a {@link NettoolsError} so commands and
5
+ * Agent tools can render one consistent, human-readable message instead of a
6
+ * raw stack trace or provider payload.
7
+ */
8
+ export type NettoolsErrorKind = 'invalid_input' | 'blocked_target' | 'cooldown' | 'timeout' | 'rate_limited' | 'http' | 'network' | 'malformed' | 'not_found' | 'aborted';
9
+ export declare class NettoolsError extends Error {
10
+ readonly kind: NettoolsErrorKind;
11
+ /** HTTP status when the failure came from a provider response. */
12
+ readonly status?: number;
13
+ /** Milliseconds the caller should wait before retrying (cooldown / 429). */
14
+ readonly retryAfterMs?: number;
15
+ constructor(kind: NettoolsErrorKind, message: string, options?: {
16
+ status?: number;
17
+ retryAfterMs?: number;
18
+ cause?: unknown;
19
+ });
20
+ }
21
+ export declare function isNettoolsError(error: unknown): error is NettoolsError;
22
+ /** Render any thrown value as a short user-facing sentence. */
23
+ export declare function describeFailure(error: unknown): string;
package/lib/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ export class NettoolsError extends Error {
2
+ kind;
3
+ /** HTTP status when the failure came from a provider response. */
4
+ status;
5
+ /** Milliseconds the caller should wait before retrying (cooldown / 429). */
6
+ retryAfterMs;
7
+ constructor(kind, message, options = {}) {
8
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
9
+ this.name = 'NettoolsError';
10
+ this.kind = kind;
11
+ if (options.status !== undefined) {
12
+ this.status = options.status;
13
+ }
14
+ if (options.retryAfterMs !== undefined) {
15
+ this.retryAfterMs = options.retryAfterMs;
16
+ }
17
+ }
18
+ }
19
+ export function isNettoolsError(error) {
20
+ return error instanceof NettoolsError;
21
+ }
22
+ /** Render any thrown value as a short user-facing sentence. */
23
+ export function describeFailure(error) {
24
+ if (!isNettoolsError(error)) {
25
+ const detail = error instanceof Error ? error.message : String(error);
26
+ return `出错了:${detail}`;
27
+ }
28
+ return error.message;
29
+ }
30
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAmBA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,IAAI,CAAoB;IACjC,kEAAkE;IACzD,MAAM,CAAU;IACzB,4EAA4E;IACnE,YAAY,CAAU;IAE/B,YACE,IAAuB,EACvB,OAAe,EACf,UAAuE,EAAE;QAEzE,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACnF,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC/B,CAAC;QACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACvC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QAC3C,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,KAAK,YAAY,aAAa,CAAC;AACxC,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,OAAO,OAAO,MAAM,EAAE,CAAC;IACzB,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC;AACvB,CAAC","sourcesContent":["/**\n * Error categories surfaced by @zhin.js/plugin-nettools.\n *\n * Every failure that reaches a user is a {@link NettoolsError} so commands and\n * Agent tools can render one consistent, human-readable message instead of a\n * raw stack trace or provider payload.\n */\nexport type NettoolsErrorKind =\n | 'invalid_input'\n | 'blocked_target'\n | 'cooldown'\n | 'timeout'\n | 'rate_limited'\n | 'http'\n | 'network'\n | 'malformed'\n | 'not_found'\n | 'aborted';\n\nexport class NettoolsError extends Error {\n readonly kind: NettoolsErrorKind;\n /** HTTP status when the failure came from a provider response. */\n readonly status?: number;\n /** Milliseconds the caller should wait before retrying (cooldown / 429). */\n readonly retryAfterMs?: number;\n\n constructor(\n kind: NettoolsErrorKind,\n message: string,\n options: { status?: number; retryAfterMs?: number; cause?: unknown } = {},\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = 'NettoolsError';\n this.kind = kind;\n if (options.status !== undefined) {\n this.status = options.status;\n }\n if (options.retryAfterMs !== undefined) {\n this.retryAfterMs = options.retryAfterMs;\n }\n }\n}\n\nexport function isNettoolsError(error: unknown): error is NettoolsError {\n return error instanceof NettoolsError;\n}\n\n/** Render any thrown value as a short user-facing sentence. */\nexport function describeFailure(error: unknown): string {\n if (!isNettoolsError(error)) {\n const detail = error instanceof Error ? error.message : String(error);\n return `出错了:${detail}`;\n }\n return error.message;\n}\n"]}
package/lib/http.d.ts ADDED
@@ -0,0 +1,50 @@
1
+ export type FetchLike = (input: URL, init: RequestInit) => Promise<Response>;
2
+ export interface HttpDependencies {
3
+ readonly fetch: FetchLike;
4
+ readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
5
+ readonly random: () => number;
6
+ readonly now: () => number;
7
+ }
8
+ export interface RetryPolicy {
9
+ /** Per-attempt timeout in ms. */
10
+ readonly timeoutMs: number;
11
+ /** Extra attempts after the first one. */
12
+ readonly maxRetries: number;
13
+ /** First backoff step in ms; doubles on every retry. */
14
+ readonly baseDelayMs: number;
15
+ /** Upper bound for one computed backoff step. */
16
+ readonly maxDelayMs: number;
17
+ /** Longest `Retry-After` we are willing to wait inside one chat request. */
18
+ readonly maxRetryAfterMs: number;
19
+ }
20
+ export interface FetchJsonOptions {
21
+ /** Human name of the provider, used in error messages. */
22
+ readonly provider: string;
23
+ readonly accept?: string;
24
+ readonly signal?: AbortSignal;
25
+ /** Statuses that are a valid answer rather than an error (e.g. RDAP 404). */
26
+ readonly passStatuses?: readonly number[];
27
+ readonly maxBodyBytes?: number;
28
+ /** Override {@link RetryPolicy.maxRetries} for this call (e.g. when a fallback provider exists). */
29
+ readonly maxRetries?: number;
30
+ }
31
+ export interface JsonResponse {
32
+ readonly status: number;
33
+ readonly body: unknown;
34
+ readonly url: string;
35
+ }
36
+ export declare const DEFAULT_RETRY_POLICY: RetryPolicy;
37
+ /** 1 MiB is far above any legitimate answer from the APIs we call. */
38
+ export declare const DEFAULT_MAX_BODY_BYTES: number;
39
+ export declare function defaultHttpDependencies(): HttpDependencies;
40
+ export declare class HttpClient {
41
+ #private;
42
+ constructor(policy?: RetryPolicy, deps?: HttpDependencies);
43
+ get policy(): RetryPolicy;
44
+ get dependencies(): HttpDependencies;
45
+ /** GET a JSON document with timeout, retry, backoff and Retry-After handling. */
46
+ fetchJson(url: URL, options: FetchJsonOptions): Promise<JsonResponse>;
47
+ }
48
+ /** Parse `Retry-After` as delta-seconds or an HTTP-date. Invalid values are ignored. */
49
+ export declare function parseRetryAfter(value: string | null, nowMs: number): number | undefined;
50
+ export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
package/lib/http.js ADDED
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Resilient JSON-over-HTTP for third-party, keyless public APIs.
3
+ *
4
+ * Every external call in this plugin goes through {@link fetchJson}:
5
+ * - per-attempt timeout (AbortSignal), combined with the caller's signal;
6
+ * - retry of transient failures only: network errors, timeouts, 408, 425, 429
7
+ * and 5xx gateway-ish statuses (500, 502, 503, 504);
8
+ * - exponential backoff with "equal jitter" (half fixed, half random) so many
9
+ * bots do not retry in lock-step;
10
+ * - `Retry-After` is honoured (seconds or HTTP-date) up to a cap; a longer
11
+ * server-requested wait fails fast with a clear rate-limit message instead of
12
+ * blocking a chat reply for minutes;
13
+ * - response bodies are size-capped and parsed defensively; malformed JSON is
14
+ * reported as `malformed`, never thrown as a raw SyntaxError.
15
+ *
16
+ * All clocks, sleeps, randomness and fetch are injectable for tests.
17
+ */
18
+ import { NettoolsError } from './errors.js';
19
+ export const DEFAULT_RETRY_POLICY = Object.freeze({
20
+ timeoutMs: 8_000,
21
+ maxRetries: 2,
22
+ baseDelayMs: 500,
23
+ maxDelayMs: 4_000,
24
+ maxRetryAfterMs: 10_000,
25
+ });
26
+ /** 1 MiB is far above any legitimate answer from the APIs we call. */
27
+ export const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
28
+ const RETRYABLE_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
29
+ export function defaultHttpDependencies() {
30
+ return {
31
+ fetch: (input, init) => fetch(input, init),
32
+ sleep: abortableSleep,
33
+ random: Math.random,
34
+ now: Date.now,
35
+ };
36
+ }
37
+ export class HttpClient {
38
+ #deps;
39
+ #policy;
40
+ constructor(policy = DEFAULT_RETRY_POLICY, deps = defaultHttpDependencies()) {
41
+ this.#policy = policy;
42
+ this.#deps = deps;
43
+ }
44
+ get policy() {
45
+ return this.#policy;
46
+ }
47
+ get dependencies() {
48
+ return this.#deps;
49
+ }
50
+ /** GET a JSON document with timeout, retry, backoff and Retry-After handling. */
51
+ async fetchJson(url, options) {
52
+ const maxRetries = options.maxRetries ?? this.#policy.maxRetries;
53
+ const attempts = Math.max(0, maxRetries) + 1;
54
+ let lastError;
55
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
56
+ throwIfAborted(options.signal, options.provider);
57
+ const outcome = await this.#attempt(url, options);
58
+ if (outcome.kind === 'ok') {
59
+ return outcome.response;
60
+ }
61
+ lastError = outcome.error;
62
+ const isLastAttempt = attempt === attempts - 1;
63
+ if (!outcome.retryable || isLastAttempt) {
64
+ break;
65
+ }
66
+ const delay = this.#computeDelay(attempt, outcome.retryAfterMs);
67
+ if (delay === undefined) {
68
+ break;
69
+ }
70
+ await this.#deps.sleep(delay, options.signal);
71
+ }
72
+ throw lastError ?? new NettoolsError('network', `${options.provider} 请求失败`);
73
+ }
74
+ async #attempt(url, options) {
75
+ const timeoutSignal = AbortSignal.timeout(this.#policy.timeoutMs);
76
+ const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
77
+ let response;
78
+ try {
79
+ response = await this.#deps.fetch(url, {
80
+ method: 'GET',
81
+ headers: { accept: options.accept ?? 'application/json' },
82
+ redirect: 'follow',
83
+ signal,
84
+ });
85
+ }
86
+ catch (error) {
87
+ return this.#classifyTransportError(error, options, timeoutSignal);
88
+ }
89
+ const passStatuses = options.passStatuses ?? [];
90
+ const statusIsAnswer = response.ok || passStatuses.includes(response.status);
91
+ if (!statusIsAnswer) {
92
+ await discardBody(response);
93
+ return this.#classifyStatus(response, options.provider);
94
+ }
95
+ let text;
96
+ try {
97
+ text = await readLimitedText(response, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
98
+ }
99
+ catch (error) {
100
+ if (error instanceof NettoolsError) {
101
+ return { kind: 'error', retryable: false, error };
102
+ }
103
+ return this.#classifyTransportError(error, options, timeoutSignal);
104
+ }
105
+ const body = parseJsonSafely(text);
106
+ if (body === undefined) {
107
+ const error = new NettoolsError('malformed', `${options.provider} 返回了无法解析的数据`);
108
+ return { kind: 'error', retryable: false, error };
109
+ }
110
+ const finalUrl = response.url || url.toString();
111
+ return { kind: 'ok', response: { status: response.status, body, url: finalUrl } };
112
+ }
113
+ #classifyTransportError(error, options, timeoutSignal) {
114
+ if (options.signal?.aborted) {
115
+ return { kind: 'error', retryable: false, error: abortedError(options.provider) };
116
+ }
117
+ if (timeoutSignal.aborted) {
118
+ const seconds = Math.round(this.#policy.timeoutMs / 1000);
119
+ const timeout = new NettoolsError('timeout', `${options.provider} 响应超时(${seconds}s)`, { cause: error });
120
+ return { kind: 'error', retryable: true, error: timeout };
121
+ }
122
+ const detail = error instanceof Error ? error.message : String(error);
123
+ const network = new NettoolsError('network', `无法连接 ${options.provider}:${detail}`, { cause: error });
124
+ return { kind: 'error', retryable: true, error: network };
125
+ }
126
+ #classifyStatus(response, provider) {
127
+ const status = response.status;
128
+ const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'), this.#deps.now());
129
+ if (status === 429) {
130
+ const error = new NettoolsError('rate_limited', `${provider} 请求过于频繁,请稍后再试`, {
131
+ status,
132
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
133
+ });
134
+ return { kind: 'error', retryable: true, error, retryAfterMs };
135
+ }
136
+ const error = new NettoolsError('http', `${provider} 返回错误 HTTP ${status}`, { status });
137
+ const retryable = RETRYABLE_STATUSES.has(status);
138
+ return { kind: 'error', retryable, error, retryAfterMs };
139
+ }
140
+ /**
141
+ * Backoff for retry number `attempt + 1`. Returns `undefined` when the
142
+ * server asked us to wait longer than {@link RetryPolicy.maxRetryAfterMs}.
143
+ */
144
+ #computeDelay(attempt, retryAfterMs) {
145
+ if (retryAfterMs !== undefined) {
146
+ if (retryAfterMs > this.#policy.maxRetryAfterMs) {
147
+ return undefined;
148
+ }
149
+ return retryAfterMs;
150
+ }
151
+ const exponential = this.#policy.baseDelayMs * 2 ** attempt;
152
+ const capped = Math.min(this.#policy.maxDelayMs, exponential);
153
+ const half = capped / 2;
154
+ return Math.round(half + this.#deps.random() * half);
155
+ }
156
+ }
157
+ /** Parse `Retry-After` as delta-seconds or an HTTP-date. Invalid values are ignored. */
158
+ export function parseRetryAfter(value, nowMs) {
159
+ if (value === null) {
160
+ return undefined;
161
+ }
162
+ const trimmed = value.trim();
163
+ if (/^\d+$/u.test(trimmed)) {
164
+ return Number(trimmed) * 1000;
165
+ }
166
+ const dateMs = Date.parse(trimmed);
167
+ if (Number.isNaN(dateMs)) {
168
+ return undefined;
169
+ }
170
+ return Math.max(0, dateMs - nowMs);
171
+ }
172
+ function parseJsonSafely(text) {
173
+ try {
174
+ const parsed = JSON.parse(text);
175
+ return parsed === undefined ? undefined : parsed;
176
+ }
177
+ catch {
178
+ return undefined;
179
+ }
180
+ }
181
+ async function readLimitedText(response, maxBytes) {
182
+ const declared = Number(response.headers.get('content-length') ?? '');
183
+ if (Number.isFinite(declared) && declared > maxBytes) {
184
+ await discardBody(response);
185
+ throw new NettoolsError('malformed', '响应体过大,已拒绝读取');
186
+ }
187
+ if (!response.body) {
188
+ return await response.text();
189
+ }
190
+ const reader = response.body.getReader();
191
+ const decoder = new TextDecoder();
192
+ let received = 0;
193
+ let text = '';
194
+ for (;;) {
195
+ const chunk = await reader.read();
196
+ if (chunk.done) {
197
+ break;
198
+ }
199
+ received += chunk.value.byteLength;
200
+ if (received > maxBytes) {
201
+ await reader.cancel();
202
+ throw new NettoolsError('malformed', '响应体过大,已拒绝读取');
203
+ }
204
+ text += decoder.decode(chunk.value, { stream: true });
205
+ }
206
+ text += decoder.decode();
207
+ return text;
208
+ }
209
+ async function discardBody(response) {
210
+ try {
211
+ await response.body?.cancel();
212
+ }
213
+ catch {
214
+ // Ignore: the body is irrelevant once we decided the status is an error.
215
+ }
216
+ }
217
+ function throwIfAborted(signal, provider) {
218
+ if (signal?.aborted) {
219
+ throw abortedError(provider);
220
+ }
221
+ }
222
+ function abortedError(provider) {
223
+ return new NettoolsError('aborted', `${provider} 请求已取消`);
224
+ }
225
+ export function abortableSleep(ms, signal) {
226
+ return new Promise((resolve, reject) => {
227
+ if (signal?.aborted) {
228
+ reject(new NettoolsError('aborted', '请求已取消'));
229
+ return;
230
+ }
231
+ const timer = setTimeout(() => {
232
+ signal?.removeEventListener('abort', onAbort);
233
+ resolve();
234
+ }, ms);
235
+ const onAbort = () => {
236
+ clearTimeout(timer);
237
+ reject(new NettoolsError('aborted', '请求已取消'));
238
+ };
239
+ signal?.addEventListener('abort', onAbort, { once: true });
240
+ });
241
+ }
242
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA0C5C,MAAM,CAAC,MAAM,oBAAoB,GAAgB,MAAM,CAAC,MAAM,CAAC;IAC7D,SAAS,EAAE,KAAK;IAChB,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,GAAG;IAChB,UAAU,EAAE,KAAK;IACjB,eAAe,EAAE,MAAM;CACxB,CAAC,CAAC;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAExE,MAAM,UAAU,uBAAuB;IACrC,OAAO;QACL,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC;QAC1C,KAAK,EAAE,cAAc;QACrB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,GAAG,EAAE,IAAI,CAAC,GAAG;KACd,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,UAAU;IACZ,KAAK,CAAmB;IACxB,OAAO,CAAc;IAE9B,YAAY,SAAsB,oBAAoB,EAAE,OAAyB,uBAAuB,EAAE;QACxG,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,iFAAiF;IACjF,KAAK,CAAC,SAAS,CAAC,GAAQ,EAAE,OAAyB;QACjD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;QACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;QAC7C,IAAI,SAAoC,CAAC;QACzC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;YACvD,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;YACjD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAClD,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;gBAC1B,OAAO,OAAO,CAAC,QAAQ,CAAC;YAC1B,CAAC;YACD,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC;YAC1B,MAAM,aAAa,GAAG,OAAO,KAAK,QAAQ,GAAG,CAAC,CAAC;YAC/C,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,aAAa,EAAE,CAAC;gBACxC,MAAM;YACR,CAAC;YACD,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;YAChE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,MAAM;YACR,CAAC;YACD,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,SAAS,IAAI,IAAI,aAAa,CAAC,SAAS,EAAE,GAAG,OAAO,CAAC,QAAQ,OAAO,CAAC,CAAC;IAC9E,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAQ,EAAE,OAAyB;QAChD,MAAM,aAAa,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC;QACjG,IAAI,QAAkB,CAAC;QACvB,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE;gBACrC,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,kBAAkB,EAAE;gBACzD,QAAQ,EAAE,QAAQ;gBAClB,MAAM;aACP,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QAChD,MAAM,cAAc,GAAG,QAAQ,CAAC,EAAE,IAAI,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC7E,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;YAC5B,OAAO,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC1D,CAAC;QAED,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,OAAO,CAAC,YAAY,IAAI,sBAAsB,CAAC,CAAC;QACzF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;gBACnC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;YACpD,CAAC;YACD,OAAO,IAAI,CAAC,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;QACrE,CAAC;QAED,MAAM,IAAI,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,IAAI,aAAa,CAAC,WAAW,EAAE,GAAG,OAAO,CAAC,QAAQ,aAAa,CAAC,CAAC;YAC/E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACpD,CAAC;QACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC;IACpF,CAAC;IAED,uBAAuB,CAAC,KAAc,EAAE,OAAyB,EAAE,aAA0B;QAC3F,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;YAC5B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpF,CAAC;QACD,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;YAC1D,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,SAAS,EAAE,GAAG,OAAO,CAAC,QAAQ,SAAS,OAAO,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;YACxG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAC5D,CAAC;QACD,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,SAAS,EAAE,QAAQ,OAAO,CAAC,QAAQ,IAAI,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QACrG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAC5D,CAAC;IAED,eAAe,CAAC,QAAkB,EAAE,QAAgB;QAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;QAC/B,MAAM,YAAY,GAAG,eAAe,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;QAC5F,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM,KAAK,GAAG,IAAI,aAAa,CAAC,cAAc,EAAE,GAAG,QAAQ,eAAe,EAAE;gBAC1E,MAAM;gBACN,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;aACxD,CAAC,CAAC;YACH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;QACjE,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,aAAa,CAAC,MAAM,EAAE,GAAG,QAAQ,cAAc,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACvF,MAAM,SAAS,GAAG,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;IAC3D,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,OAAe,EAAE,YAAgC;QAC7D,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,CAAC;gBAChD,OAAO,SAAS,CAAC;YACnB,CAAC;YACD,OAAO,YAAY,CAAC;QACtB,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,GAAG,CAAC,IAAI,OAAO,CAAC;QAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QAC9D,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC;QACxB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;IACvD,CAAC;CACF;AAWD,wFAAwF;AACxF,MAAM,UAAU,eAAe,CAAC,KAAoB,EAAE,KAAa;IACjE,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;IAChC,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,QAAkB,EAAE,QAAgB;IACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC;IACtE,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,QAAQ,EAAE,CAAC;QACrD,MAAM,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC5B,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,SAAS,CAAC;QACR,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAClC,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YACf,MAAM;QACR,CAAC;QACD,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC;QACnC,IAAI,QAAQ,GAAG,QAAQ,EAAE,CAAC;YACxB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;YACtB,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;QACtD,CAAC;QACD,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IACzB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,QAAkB;IAC3C,IAAI,CAAC;QACH,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,yEAAyE;IAC3E,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,MAA+B,EAAE,QAAgB;IACvE,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;QACpB,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,QAAgB;IACpC,OAAO,IAAI,aAAa,CAAC,SAAS,EAAE,GAAG,QAAQ,QAAQ,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAU,EAAE,MAAoB;IAC7D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;YAC9C,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;QAChD,CAAC,CAAC;QACF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Resilient JSON-over-HTTP for third-party, keyless public APIs.\n *\n * Every external call in this plugin goes through {@link fetchJson}:\n * - per-attempt timeout (AbortSignal), combined with the caller's signal;\n * - retry of transient failures only: network errors, timeouts, 408, 425, 429\n * and 5xx gateway-ish statuses (500, 502, 503, 504);\n * - exponential backoff with \"equal jitter\" (half fixed, half random) so many\n * bots do not retry in lock-step;\n * - `Retry-After` is honoured (seconds or HTTP-date) up to a cap; a longer\n * server-requested wait fails fast with a clear rate-limit message instead of\n * blocking a chat reply for minutes;\n * - response bodies are size-capped and parsed defensively; malformed JSON is\n * reported as `malformed`, never thrown as a raw SyntaxError.\n *\n * All clocks, sleeps, randomness and fetch are injectable for tests.\n */\nimport { NettoolsError } from './errors.js';\n\nexport type FetchLike = (input: URL, init: RequestInit) => Promise<Response>;\n\nexport interface HttpDependencies {\n readonly fetch: FetchLike;\n readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;\n readonly random: () => number;\n readonly now: () => number;\n}\n\nexport interface RetryPolicy {\n /** Per-attempt timeout in ms. */\n readonly timeoutMs: number;\n /** Extra attempts after the first one. */\n readonly maxRetries: number;\n /** First backoff step in ms; doubles on every retry. */\n readonly baseDelayMs: number;\n /** Upper bound for one computed backoff step. */\n readonly maxDelayMs: number;\n /** Longest `Retry-After` we are willing to wait inside one chat request. */\n readonly maxRetryAfterMs: number;\n}\n\nexport interface FetchJsonOptions {\n /** Human name of the provider, used in error messages. */\n readonly provider: string;\n readonly accept?: string;\n readonly signal?: AbortSignal;\n /** Statuses that are a valid answer rather than an error (e.g. RDAP 404). */\n readonly passStatuses?: readonly number[];\n readonly maxBodyBytes?: number;\n /** Override {@link RetryPolicy.maxRetries} for this call (e.g. when a fallback provider exists). */\n readonly maxRetries?: number;\n}\n\nexport interface JsonResponse {\n readonly status: number;\n readonly body: unknown;\n readonly url: string;\n}\n\nexport const DEFAULT_RETRY_POLICY: RetryPolicy = Object.freeze({\n timeoutMs: 8_000,\n maxRetries: 2,\n baseDelayMs: 500,\n maxDelayMs: 4_000,\n maxRetryAfterMs: 10_000,\n});\n\n/** 1 MiB is far above any legitimate answer from the APIs we call. */\nexport const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;\n\nconst RETRYABLE_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);\n\nexport function defaultHttpDependencies(): HttpDependencies {\n return {\n fetch: (input, init) => fetch(input, init),\n sleep: abortableSleep,\n random: Math.random,\n now: Date.now,\n };\n}\n\nexport class HttpClient {\n readonly #deps: HttpDependencies;\n readonly #policy: RetryPolicy;\n\n constructor(policy: RetryPolicy = DEFAULT_RETRY_POLICY, deps: HttpDependencies = defaultHttpDependencies()) {\n this.#policy = policy;\n this.#deps = deps;\n }\n\n get policy(): RetryPolicy {\n return this.#policy;\n }\n\n get dependencies(): HttpDependencies {\n return this.#deps;\n }\n\n /** GET a JSON document with timeout, retry, backoff and Retry-After handling. */\n async fetchJson(url: URL, options: FetchJsonOptions): Promise<JsonResponse> {\n const maxRetries = options.maxRetries ?? this.#policy.maxRetries;\n const attempts = Math.max(0, maxRetries) + 1;\n let lastError: NettoolsError | undefined;\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n throwIfAborted(options.signal, options.provider);\n const outcome = await this.#attempt(url, options);\n if (outcome.kind === 'ok') {\n return outcome.response;\n }\n lastError = outcome.error;\n const isLastAttempt = attempt === attempts - 1;\n if (!outcome.retryable || isLastAttempt) {\n break;\n }\n const delay = this.#computeDelay(attempt, outcome.retryAfterMs);\n if (delay === undefined) {\n break;\n }\n await this.#deps.sleep(delay, options.signal);\n }\n throw lastError ?? new NettoolsError('network', `${options.provider} 请求失败`);\n }\n\n async #attempt(url: URL, options: FetchJsonOptions): Promise<AttemptOutcome> {\n const timeoutSignal = AbortSignal.timeout(this.#policy.timeoutMs);\n const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;\n let response: Response;\n try {\n response = await this.#deps.fetch(url, {\n method: 'GET',\n headers: { accept: options.accept ?? 'application/json' },\n redirect: 'follow',\n signal,\n });\n } catch (error) {\n return this.#classifyTransportError(error, options, timeoutSignal);\n }\n\n const passStatuses = options.passStatuses ?? [];\n const statusIsAnswer = response.ok || passStatuses.includes(response.status);\n if (!statusIsAnswer) {\n await discardBody(response);\n return this.#classifyStatus(response, options.provider);\n }\n\n let text: string;\n try {\n text = await readLimitedText(response, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);\n } catch (error) {\n if (error instanceof NettoolsError) {\n return { kind: 'error', retryable: false, error };\n }\n return this.#classifyTransportError(error, options, timeoutSignal);\n }\n\n const body = parseJsonSafely(text);\n if (body === undefined) {\n const error = new NettoolsError('malformed', `${options.provider} 返回了无法解析的数据`);\n return { kind: 'error', retryable: false, error };\n }\n const finalUrl = response.url || url.toString();\n return { kind: 'ok', response: { status: response.status, body, url: finalUrl } };\n }\n\n #classifyTransportError(error: unknown, options: FetchJsonOptions, timeoutSignal: AbortSignal): AttemptOutcome {\n if (options.signal?.aborted) {\n return { kind: 'error', retryable: false, error: abortedError(options.provider) };\n }\n if (timeoutSignal.aborted) {\n const seconds = Math.round(this.#policy.timeoutMs / 1000);\n const timeout = new NettoolsError('timeout', `${options.provider} 响应超时(${seconds}s)`, { cause: error });\n return { kind: 'error', retryable: true, error: timeout };\n }\n const detail = error instanceof Error ? error.message : String(error);\n const network = new NettoolsError('network', `无法连接 ${options.provider}:${detail}`, { cause: error });\n return { kind: 'error', retryable: true, error: network };\n }\n\n #classifyStatus(response: Response, provider: string): AttemptOutcome {\n const status = response.status;\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'), this.#deps.now());\n if (status === 429) {\n const error = new NettoolsError('rate_limited', `${provider} 请求过于频繁,请稍后再试`, {\n status,\n ...(retryAfterMs === undefined ? {} : { retryAfterMs }),\n });\n return { kind: 'error', retryable: true, error, retryAfterMs };\n }\n const error = new NettoolsError('http', `${provider} 返回错误 HTTP ${status}`, { status });\n const retryable = RETRYABLE_STATUSES.has(status);\n return { kind: 'error', retryable, error, retryAfterMs };\n }\n\n /**\n * Backoff for retry number `attempt + 1`. Returns `undefined` when the\n * server asked us to wait longer than {@link RetryPolicy.maxRetryAfterMs}.\n */\n #computeDelay(attempt: number, retryAfterMs: number | undefined): number | undefined {\n if (retryAfterMs !== undefined) {\n if (retryAfterMs > this.#policy.maxRetryAfterMs) {\n return undefined;\n }\n return retryAfterMs;\n }\n const exponential = this.#policy.baseDelayMs * 2 ** attempt;\n const capped = Math.min(this.#policy.maxDelayMs, exponential);\n const half = capped / 2;\n return Math.round(half + this.#deps.random() * half);\n }\n}\n\ntype AttemptOutcome =\n | { readonly kind: 'ok'; readonly response: JsonResponse }\n | {\n readonly kind: 'error';\n readonly retryable: boolean;\n readonly error: NettoolsError;\n readonly retryAfterMs?: number | undefined;\n };\n\n/** Parse `Retry-After` as delta-seconds or an HTTP-date. Invalid values are ignored. */\nexport function parseRetryAfter(value: string | null, nowMs: number): number | undefined {\n if (value === null) {\n return undefined;\n }\n const trimmed = value.trim();\n if (/^\\d+$/u.test(trimmed)) {\n return Number(trimmed) * 1000;\n }\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) {\n return undefined;\n }\n return Math.max(0, dateMs - nowMs);\n}\n\nfunction parseJsonSafely(text: string): unknown {\n try {\n const parsed: unknown = JSON.parse(text);\n return parsed === undefined ? undefined : parsed;\n } catch {\n return undefined;\n }\n}\n\nasync function readLimitedText(response: Response, maxBytes: number): Promise<string> {\n const declared = Number(response.headers.get('content-length') ?? '');\n if (Number.isFinite(declared) && declared > maxBytes) {\n await discardBody(response);\n throw new NettoolsError('malformed', '响应体过大,已拒绝读取');\n }\n if (!response.body) {\n return await response.text();\n }\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let received = 0;\n let text = '';\n for (;;) {\n const chunk = await reader.read();\n if (chunk.done) {\n break;\n }\n received += chunk.value.byteLength;\n if (received > maxBytes) {\n await reader.cancel();\n throw new NettoolsError('malformed', '响应体过大,已拒绝读取');\n }\n text += decoder.decode(chunk.value, { stream: true });\n }\n text += decoder.decode();\n return text;\n}\n\nasync function discardBody(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // Ignore: the body is irrelevant once we decided the status is an error.\n }\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined, provider: string): void {\n if (signal?.aborted) {\n throw abortedError(provider);\n }\n}\n\nfunction abortedError(provider: string): NettoolsError {\n return new NettoolsError('aborted', `${provider} 请求已取消`);\n}\n\nexport function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new NettoolsError('aborted', '请求已取消'));\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n const onAbort = (): void => {\n clearTimeout(timer);\n reject(new NettoolsError('aborted', '请求已取消'));\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"]}
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { classifyAddress, classifyHostname, classifyParsedHost, displayHost, parseHostInput, reverseLookupName, type AddressVerdict, type ParsedHost, type ParseHostOptions, } from './address-policy.js';
2
+ export { NETTOOLS_DEFAULTS, resolveNettoolsConfig, type NettoolsConfig, type ResolvedNettoolsConfig } from './config.js';
3
+ export { commandCallerKey, toolCallerKey, UserCooldown } from './cooldown.js';
4
+ export { DEFAULT_DOH_PROVIDERS, DOH_PROVIDERS, DohResolver, formatDnsAnswer, parseDnsType, parseDohResponse, SUPPORTED_DNS_TYPES, type DnsAnswer, type DnsRecord, type DnsRecordType, type DohProviderId, } from './dns.js';
5
+ export { comparePrices, formatPriceComparison, parseDomainSuffix, parsePriceKind, parsePriceResponse, type PriceComparison, type PriceEntry, type PriceKind, } from './domain-price.js';
6
+ export { describeFailure, isNettoolsError, NettoolsError, type NettoolsErrorKind } from './errors.js';
7
+ export { DEFAULT_RETRY_POLICY, HttpClient, parseRetryAfter, type FetchLike, type HttpDependencies, type RetryPolicy, } from './http.js';
8
+ export { formatPingReport, parsePingResults, parseStartResponse, runMultiRegionPing, type PingReport } from './ping.js';
9
+ export { nettoolsServiceToken } from './runtime.js';
10
+ export { NettoolsService, type NettoolsServiceOptions } from './service.js';
11
+ export { assertPublicTarget, type CheckedTarget } from './target-guard.js';
12
+ export { formatWhoisRecord, parseBootstrap, parseRdapDomain, parseWhoisFallback, RdapClient, type WhoisRecord, } from './whois.js';
package/lib/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export { classifyAddress, classifyHostname, classifyParsedHost, displayHost, parseHostInput, reverseLookupName, } from './address-policy.js';
2
+ export { NETTOOLS_DEFAULTS, resolveNettoolsConfig } from './config.js';
3
+ export { commandCallerKey, toolCallerKey, UserCooldown } from './cooldown.js';
4
+ export { DEFAULT_DOH_PROVIDERS, DOH_PROVIDERS, DohResolver, formatDnsAnswer, parseDnsType, parseDohResponse, SUPPORTED_DNS_TYPES, } from './dns.js';
5
+ export { comparePrices, formatPriceComparison, parseDomainSuffix, parsePriceKind, parsePriceResponse, } from './domain-price.js';
6
+ export { describeFailure, isNettoolsError, NettoolsError } from './errors.js';
7
+ export { DEFAULT_RETRY_POLICY, HttpClient, parseRetryAfter, } from './http.js';
8
+ export { formatPingReport, parsePingResults, parseStartResponse, runMultiRegionPing } from './ping.js';
9
+ export { nettoolsServiceToken } from './runtime.js';
10
+ export { NettoolsService } from './service.js';
11
+ export { assertPublicTarget } from './target-guard.js';
12
+ export { formatWhoisRecord, parseBootstrap, parseRdapDomain, parseWhoisFallback, RdapClient, } from './whois.js';
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,WAAW,EACX,cAAc,EACd,iBAAiB,GAIlB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAoD,MAAM,aAAa,CAAC;AACzH,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,WAAW,EACX,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,GAKpB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,kBAAkB,GAInB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAA0B,MAAM,aAAa,CAAC;AACtG,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,eAAe,GAIhB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,kBAAkB,EAAmB,MAAM,WAAW,CAAC;AACxH,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,eAAe,EAA+B,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAsB,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,UAAU,GAEX,MAAM,YAAY,CAAC","sourcesContent":["export {\n classifyAddress,\n classifyHostname,\n classifyParsedHost,\n displayHost,\n parseHostInput,\n reverseLookupName,\n type AddressVerdict,\n type ParsedHost,\n type ParseHostOptions,\n} from './address-policy.js';\nexport { NETTOOLS_DEFAULTS, resolveNettoolsConfig, type NettoolsConfig, type ResolvedNettoolsConfig } from './config.js';\nexport { commandCallerKey, toolCallerKey, UserCooldown } from './cooldown.js';\nexport {\n DEFAULT_DOH_PROVIDERS,\n DOH_PROVIDERS,\n DohResolver,\n formatDnsAnswer,\n parseDnsType,\n parseDohResponse,\n SUPPORTED_DNS_TYPES,\n type DnsAnswer,\n type DnsRecord,\n type DnsRecordType,\n type DohProviderId,\n} from './dns.js';\nexport {\n comparePrices,\n formatPriceComparison,\n parseDomainSuffix,\n parsePriceKind,\n parsePriceResponse,\n type PriceComparison,\n type PriceEntry,\n type PriceKind,\n} from './domain-price.js';\nexport { describeFailure, isNettoolsError, NettoolsError, type NettoolsErrorKind } from './errors.js';\nexport {\n DEFAULT_RETRY_POLICY,\n HttpClient,\n parseRetryAfter,\n type FetchLike,\n type HttpDependencies,\n type RetryPolicy,\n} from './http.js';\nexport { formatPingReport, parsePingResults, parseStartResponse, runMultiRegionPing, type PingReport } from './ping.js';\nexport { nettoolsServiceToken } from './runtime.js';\nexport { NettoolsService, type NettoolsServiceOptions } from './service.js';\nexport { assertPublicTarget, type CheckedTarget } from './target-guard.js';\nexport {\n formatWhoisRecord,\n parseBootstrap,\n parseRdapDomain,\n parseWhoisFallback,\n RdapClient,\n type WhoisRecord,\n} from './whois.js';\n"]}