@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.
Files changed (56) hide show
  1. package/README.md +70 -2
  2. package/commands/dns/[name]/[[type]]/index.js +18 -0
  3. package/commands/dns/[name]/[[type]]/index.ts +18 -0
  4. package/commands/domain-price/[suffix]/[[order]]/index.js +19 -0
  5. package/commands/domain-price/[suffix]/[[order]]/index.ts +19 -0
  6. package/commands/ping/[host]/index.js +14 -0
  7. package/commands/ping/[host]/index.ts +14 -0
  8. package/commands/whois/[domain]/index.js +14 -0
  9. package/commands/whois/[domain]/index.ts +14 -0
  10. package/lib/address-policy.d.ts +44 -0
  11. package/lib/address-policy.js +388 -0
  12. package/lib/address-policy.js.map +1 -0
  13. package/lib/config.d.ts +44 -0
  14. package/lib/config.js +38 -0
  15. package/lib/config.js.map +1 -0
  16. package/lib/cooldown.d.ts +29 -0
  17. package/lib/cooldown.js +80 -0
  18. package/lib/cooldown.js.map +1 -0
  19. package/lib/dns.d.ts +52 -0
  20. package/lib/dns.js +191 -0
  21. package/lib/dns.js.map +1 -0
  22. package/lib/domain-price.d.ts +28 -0
  23. package/lib/domain-price.js +165 -0
  24. package/lib/domain-price.js.map +1 -0
  25. package/lib/errors.d.ts +23 -0
  26. package/lib/errors.js +30 -0
  27. package/lib/errors.js.map +1 -0
  28. package/lib/http.d.ts +50 -0
  29. package/lib/http.js +242 -0
  30. package/lib/http.js.map +1 -0
  31. package/lib/index.d.ts +12 -0
  32. package/lib/index.js +13 -0
  33. package/lib/index.js.map +1 -0
  34. package/lib/ping.d.ts +45 -0
  35. package/lib/ping.js +195 -0
  36. package/lib/ping.js.map +1 -0
  37. package/lib/runtime.d.ts +3 -0
  38. package/lib/runtime.js +4 -0
  39. package/lib/runtime.js.map +1 -0
  40. package/lib/service.d.ts +18 -0
  41. package/lib/service.js +96 -0
  42. package/lib/service.js.map +1 -0
  43. package/lib/target-guard.d.ts +24 -0
  44. package/lib/target-guard.js +59 -0
  45. package/lib/target-guard.js.map +1 -0
  46. package/lib/whois.d.ts +31 -0
  47. package/lib/whois.js +345 -0
  48. package/lib/whois.js.map +1 -0
  49. package/package.json +105 -3
  50. package/plugin.js +14 -0
  51. package/schema.json +62 -0
  52. package/skills/nettools/SKILL.md +44 -0
  53. package/skills/nettools/tools/dns_lookup/index.js +20 -0
  54. package/skills/nettools/tools/domain_price/index.js +19 -0
  55. package/skills/nettools/tools/net_ping/index.js +16 -0
  56. package/skills/nettools/tools/whois_lookup/index.js +16 -0
package/lib/ping.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ import type { HttpClient } from './http.js';
2
+ export declare const CHECK_HOST_BASE_URL = "https://check-host.net/";
3
+ export interface PingOptions {
4
+ readonly maxNodes: number;
5
+ readonly pollIntervalMs: number;
6
+ readonly maxPolls: number;
7
+ readonly signal?: AbortSignal;
8
+ }
9
+ export interface PingNode {
10
+ readonly id: string;
11
+ readonly countryCode: string;
12
+ readonly country: string;
13
+ readonly city: string;
14
+ }
15
+ export type PingNodeResult = {
16
+ readonly state: 'pending';
17
+ } | {
18
+ readonly state: 'unresolved';
19
+ } | {
20
+ readonly state: 'done';
21
+ readonly sent: number;
22
+ readonly received: number;
23
+ readonly averageMs: number | undefined;
24
+ readonly minMs: number | undefined;
25
+ readonly maxMs: number | undefined;
26
+ readonly address: string | undefined;
27
+ };
28
+ export interface PingReport {
29
+ readonly host: string;
30
+ readonly permanentLink: string | undefined;
31
+ readonly nodes: readonly {
32
+ readonly node: PingNode;
33
+ readonly result: PingNodeResult;
34
+ }[];
35
+ }
36
+ interface StartedCheck {
37
+ readonly requestId: string;
38
+ readonly permanentLink: string | undefined;
39
+ readonly nodes: readonly PingNode[];
40
+ }
41
+ export declare function runMultiRegionPing(http: HttpClient, host: string, options: PingOptions): Promise<PingReport>;
42
+ export declare function parseStartResponse(body: unknown): StartedCheck;
43
+ export declare function parsePingResults(body: unknown): Map<string, PingNodeResult>;
44
+ export declare function formatPingReport(report: PingReport): string;
45
+ export {};
package/lib/ping.js ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Multi-region ping through check-host.net's public API.
3
+ *
4
+ * Flow: `GET /check-ping?host=…&max_nodes=N` returns a request id plus the
5
+ * chosen probe nodes; `GET /check-result/<id>` returns one entry per node,
6
+ * `null` while that node is still working. We poll a bounded number of times
7
+ * and report whatever finished; unfinished nodes are shown as "no result".
8
+ *
9
+ * Result shape per node (tolerantly parsed, anything else is ignored):
10
+ * [[ ["OK", 0.044, "93.184.216.34"], ["TIMEOUT", 3.0], … ]] finished
11
+ * [[ null ]] could not resolve
12
+ * null still running
13
+ */
14
+ import { NettoolsError } from './errors.js';
15
+ export const CHECK_HOST_BASE_URL = 'https://check-host.net/';
16
+ const PROVIDER = 'check-host.net';
17
+ export async function runMultiRegionPing(http, host, options) {
18
+ const started = await startPingCheck(http, host, options);
19
+ let results = new Map();
20
+ let anyPollSucceeded = false;
21
+ let lastPollError;
22
+ for (let poll = 0; poll < options.maxPolls; poll += 1) {
23
+ await http.dependencies.sleep(options.pollIntervalMs, options.signal);
24
+ try {
25
+ results = await fetchPingResults(http, started.requestId, options.signal);
26
+ anyPollSucceeded = true;
27
+ }
28
+ catch (error) {
29
+ // One failed poll (after its own retries) should not discard a check
30
+ // that is still running remotely; keep polling until the budget is used.
31
+ if (error instanceof NettoolsError && error.kind === 'aborted') {
32
+ throw error;
33
+ }
34
+ lastPollError = error;
35
+ continue;
36
+ }
37
+ const allFinished = started.nodes.every((node) => {
38
+ const result = results.get(node.id);
39
+ return result !== undefined && result.state !== 'pending';
40
+ });
41
+ if (allFinished) {
42
+ break;
43
+ }
44
+ }
45
+ if (!anyPollSucceeded && lastPollError !== undefined) {
46
+ throw lastPollError;
47
+ }
48
+ const nodes = started.nodes.map((node) => ({
49
+ node,
50
+ result: results.get(node.id) ?? { state: 'pending' },
51
+ }));
52
+ return Object.freeze({ host, permanentLink: started.permanentLink, nodes: Object.freeze(nodes) });
53
+ }
54
+ async function startPingCheck(http, host, options) {
55
+ const url = new URL('check-ping', CHECK_HOST_BASE_URL);
56
+ url.searchParams.set('host', host);
57
+ url.searchParams.set('max_nodes', String(options.maxNodes));
58
+ const response = await http.fetchJson(url, {
59
+ provider: PROVIDER,
60
+ ...(options.signal ? { signal: options.signal } : {}),
61
+ });
62
+ return parseStartResponse(response.body);
63
+ }
64
+ export function parseStartResponse(body) {
65
+ if (!isRecord(body)) {
66
+ throw malformed();
67
+ }
68
+ if (typeof body.error === 'string' && body.error.length > 0) {
69
+ throw new NettoolsError('http', `check-host.net 拒绝了请求:${body.error}`);
70
+ }
71
+ const requestId = body.request_id;
72
+ if (typeof requestId !== 'string' || !/^[A-Za-z0-9]{1,64}$/u.test(requestId)) {
73
+ throw malformed();
74
+ }
75
+ const nodes = [];
76
+ if (isRecord(body.nodes)) {
77
+ for (const [id, info] of Object.entries(body.nodes)) {
78
+ nodes.push(parseNodeInfo(id, info));
79
+ }
80
+ }
81
+ if (nodes.length === 0) {
82
+ throw malformed();
83
+ }
84
+ const permanentLink = typeof body.permanent_link === 'string' && body.permanent_link.startsWith('https://')
85
+ ? body.permanent_link
86
+ : undefined;
87
+ return Object.freeze({ requestId, permanentLink, nodes: Object.freeze(nodes) });
88
+ }
89
+ function parseNodeInfo(id, info) {
90
+ const fields = Array.isArray(info) ? info : [];
91
+ const text = (index) => {
92
+ const value = fields[index];
93
+ return typeof value === 'string' ? value : '';
94
+ };
95
+ return Object.freeze({
96
+ id,
97
+ countryCode: text(0).toUpperCase(),
98
+ country: text(1),
99
+ city: text(2),
100
+ });
101
+ }
102
+ async function fetchPingResults(http, requestId, signal) {
103
+ const url = new URL(`check-result/${encodeURIComponent(requestId)}`, CHECK_HOST_BASE_URL);
104
+ const response = await http.fetchJson(url, {
105
+ provider: PROVIDER,
106
+ ...(signal ? { signal } : {}),
107
+ });
108
+ return parsePingResults(response.body);
109
+ }
110
+ export function parsePingResults(body) {
111
+ if (!isRecord(body)) {
112
+ throw malformed();
113
+ }
114
+ const results = new Map();
115
+ for (const [nodeId, raw] of Object.entries(body)) {
116
+ results.set(nodeId, parseNodeResult(raw));
117
+ }
118
+ return results;
119
+ }
120
+ function parseNodeResult(raw) {
121
+ if (raw === null || raw === undefined) {
122
+ return { state: 'pending' };
123
+ }
124
+ const outer = Array.isArray(raw) ? raw : [];
125
+ const probes = Array.isArray(outer[0]) ? outer[0] : [];
126
+ const firstProbe = probes[0];
127
+ if (probes.length === 0 || firstProbe === null) {
128
+ return { state: 'unresolved' };
129
+ }
130
+ const latencies = [];
131
+ let address;
132
+ let sent = 0;
133
+ for (const probe of probes) {
134
+ if (!Array.isArray(probe)) {
135
+ continue;
136
+ }
137
+ sent += 1;
138
+ const [status, seconds, ip] = probe;
139
+ if (address === undefined && typeof ip === 'string') {
140
+ address = ip;
141
+ }
142
+ if (status === 'OK' && typeof seconds === 'number' && Number.isFinite(seconds)) {
143
+ latencies.push(seconds * 1000);
144
+ }
145
+ }
146
+ if (sent === 0) {
147
+ return { state: 'unresolved' };
148
+ }
149
+ const received = latencies.length;
150
+ const total = latencies.reduce((sum, value) => sum + value, 0);
151
+ return {
152
+ state: 'done',
153
+ sent,
154
+ received,
155
+ averageMs: received > 0 ? total / received : undefined,
156
+ minMs: received > 0 ? Math.min(...latencies) : undefined,
157
+ maxMs: received > 0 ? Math.max(...latencies) : undefined,
158
+ address,
159
+ };
160
+ }
161
+ export function formatPingReport(report) {
162
+ const lines = [`📡 ${report.host} 多地 Ping(check-host.net)`];
163
+ for (const { node, result } of report.nodes) {
164
+ const place = [node.countryCode, node.city || node.country].filter(Boolean).join(' ') || node.id;
165
+ lines.push(`${place}:${describeNodeResult(result)}`);
166
+ }
167
+ if (report.permanentLink) {
168
+ lines.push(`详情:${report.permanentLink}`);
169
+ }
170
+ return lines.join('\n');
171
+ }
172
+ function describeNodeResult(result) {
173
+ if (result.state === 'pending') {
174
+ return '未返回结果(超时)';
175
+ }
176
+ if (result.state === 'unresolved') {
177
+ return '无法解析或不可达';
178
+ }
179
+ const loss = Math.round(((result.sent - result.received) / result.sent) * 100);
180
+ const via = result.address ? ` → ${result.address}` : '';
181
+ if (result.averageMs === undefined) {
182
+ return `全部超时(丢包 ${loss}%)${via}`;
183
+ }
184
+ const average = result.averageMs.toFixed(1);
185
+ const min = (result.minMs ?? result.averageMs).toFixed(1);
186
+ const max = (result.maxMs ?? result.averageMs).toFixed(1);
187
+ return `平均 ${average}ms(${min}/${max}),丢包 ${loss}%${via}`;
188
+ }
189
+ function malformed() {
190
+ return new NettoolsError('malformed', 'check-host.net 返回了无法识别的数据');
191
+ }
192
+ function isRecord(value) {
193
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
194
+ }
195
+ //# sourceMappingURL=ping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ping.js","sourceRoot":"","sources":["../src/ping.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Multi-region ping through check-host.net's public API.\n *\n * Flow: `GET /check-ping?host=…&max_nodes=N` returns a request id plus the\n * chosen probe nodes; `GET /check-result/<id>` returns one entry per node,\n * `null` while that node is still working. We poll a bounded number of times\n * and report whatever finished; unfinished nodes are shown as \"no result\".\n *\n * Result shape per node (tolerantly parsed, anything else is ignored):\n * [[ [\"OK\", 0.044, \"93.184.216.34\"], [\"TIMEOUT\", 3.0], … ]] finished\n * [[ null ]] could not resolve\n * null still running\n */\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const CHECK_HOST_BASE_URL = 'https://check-host.net/';\nconst PROVIDER = 'check-host.net';\n\nexport interface PingOptions {\n readonly maxNodes: number;\n readonly pollIntervalMs: number;\n readonly maxPolls: number;\n readonly signal?: AbortSignal;\n}\n\nexport interface PingNode {\n readonly id: string;\n readonly countryCode: string;\n readonly country: string;\n readonly city: string;\n}\n\nexport type PingNodeResult =\n | { readonly state: 'pending' }\n | { readonly state: 'unresolved' }\n | {\n readonly state: 'done';\n readonly sent: number;\n readonly received: number;\n readonly averageMs: number | undefined;\n readonly minMs: number | undefined;\n readonly maxMs: number | undefined;\n readonly address: string | undefined;\n };\n\nexport interface PingReport {\n readonly host: string;\n readonly permanentLink: string | undefined;\n readonly nodes: readonly { readonly node: PingNode; readonly result: PingNodeResult }[];\n}\n\ninterface StartedCheck {\n readonly requestId: string;\n readonly permanentLink: string | undefined;\n readonly nodes: readonly PingNode[];\n}\n\nexport async function runMultiRegionPing(http: HttpClient, host: string, options: PingOptions): Promise<PingReport> {\n const started = await startPingCheck(http, host, options);\n let results = new Map<string, PingNodeResult>();\n let anyPollSucceeded = false;\n let lastPollError: unknown;\n for (let poll = 0; poll < options.maxPolls; poll += 1) {\n await http.dependencies.sleep(options.pollIntervalMs, options.signal);\n try {\n results = await fetchPingResults(http, started.requestId, options.signal);\n anyPollSucceeded = true;\n } catch (error) {\n // One failed poll (after its own retries) should not discard a check\n // that is still running remotely; keep polling until the budget is used.\n if (error instanceof NettoolsError && error.kind === 'aborted') {\n throw error;\n }\n lastPollError = error;\n continue;\n }\n const allFinished = started.nodes.every((node) => {\n const result = results.get(node.id);\n return result !== undefined && result.state !== 'pending';\n });\n if (allFinished) {\n break;\n }\n }\n if (!anyPollSucceeded && lastPollError !== undefined) {\n throw lastPollError;\n }\n const nodes = started.nodes.map((node) => ({\n node,\n result: results.get(node.id) ?? ({ state: 'pending' } as const),\n }));\n return Object.freeze({ host, permanentLink: started.permanentLink, nodes: Object.freeze(nodes) });\n}\n\nasync function startPingCheck(http: HttpClient, host: string, options: PingOptions): Promise<StartedCheck> {\n const url = new URL('check-ping', CHECK_HOST_BASE_URL);\n url.searchParams.set('host', host);\n url.searchParams.set('max_nodes', String(options.maxNodes));\n const response = await http.fetchJson(url, {\n provider: PROVIDER,\n ...(options.signal ? { signal: options.signal } : {}),\n });\n return parseStartResponse(response.body);\n}\n\nexport function parseStartResponse(body: unknown): StartedCheck {\n if (!isRecord(body)) {\n throw malformed();\n }\n if (typeof body.error === 'string' && body.error.length > 0) {\n throw new NettoolsError('http', `check-host.net 拒绝了请求:${body.error}`);\n }\n const requestId = body.request_id;\n if (typeof requestId !== 'string' || !/^[A-Za-z0-9]{1,64}$/u.test(requestId)) {\n throw malformed();\n }\n const nodes: PingNode[] = [];\n if (isRecord(body.nodes)) {\n for (const [id, info] of Object.entries(body.nodes)) {\n nodes.push(parseNodeInfo(id, info));\n }\n }\n if (nodes.length === 0) {\n throw malformed();\n }\n const permanentLink = typeof body.permanent_link === 'string' && body.permanent_link.startsWith('https://')\n ? body.permanent_link\n : undefined;\n return Object.freeze({ requestId, permanentLink, nodes: Object.freeze(nodes) });\n}\n\nfunction parseNodeInfo(id: string, info: unknown): PingNode {\n const fields = Array.isArray(info) ? info : [];\n const text = (index: number): string => {\n const value = fields[index];\n return typeof value === 'string' ? value : '';\n };\n return Object.freeze({\n id,\n countryCode: text(0).toUpperCase(),\n country: text(1),\n city: text(2),\n });\n}\n\nasync function fetchPingResults(http: HttpClient, requestId: string, signal?: AbortSignal): Promise<Map<string, PingNodeResult>> {\n const url = new URL(`check-result/${encodeURIComponent(requestId)}`, CHECK_HOST_BASE_URL);\n const response = await http.fetchJson(url, {\n provider: PROVIDER,\n ...(signal ? { signal } : {}),\n });\n return parsePingResults(response.body);\n}\n\nexport function parsePingResults(body: unknown): Map<string, PingNodeResult> {\n if (!isRecord(body)) {\n throw malformed();\n }\n const results = new Map<string, PingNodeResult>();\n for (const [nodeId, raw] of Object.entries(body)) {\n results.set(nodeId, parseNodeResult(raw));\n }\n return results;\n}\n\nfunction parseNodeResult(raw: unknown): PingNodeResult {\n if (raw === null || raw === undefined) {\n return { state: 'pending' };\n }\n const outer = Array.isArray(raw) ? raw : [];\n const probes = Array.isArray(outer[0]) ? outer[0] : [];\n const firstProbe = probes[0];\n if (probes.length === 0 || firstProbe === null) {\n return { state: 'unresolved' };\n }\n const latencies: number[] = [];\n let address: string | undefined;\n let sent = 0;\n for (const probe of probes) {\n if (!Array.isArray(probe)) {\n continue;\n }\n sent += 1;\n const [status, seconds, ip] = probe as unknown[];\n if (address === undefined && typeof ip === 'string') {\n address = ip;\n }\n if (status === 'OK' && typeof seconds === 'number' && Number.isFinite(seconds)) {\n latencies.push(seconds * 1000);\n }\n }\n if (sent === 0) {\n return { state: 'unresolved' };\n }\n const received = latencies.length;\n const total = latencies.reduce((sum, value) => sum + value, 0);\n return {\n state: 'done',\n sent,\n received,\n averageMs: received > 0 ? total / received : undefined,\n minMs: received > 0 ? Math.min(...latencies) : undefined,\n maxMs: received > 0 ? Math.max(...latencies) : undefined,\n address,\n };\n}\n\nexport function formatPingReport(report: PingReport): string {\n const lines = [`📡 ${report.host} 多地 Ping(check-host.net)`];\n for (const { node, result } of report.nodes) {\n const place = [node.countryCode, node.city || node.country].filter(Boolean).join(' ') || node.id;\n lines.push(`${place}:${describeNodeResult(result)}`);\n }\n if (report.permanentLink) {\n lines.push(`详情:${report.permanentLink}`);\n }\n return lines.join('\\n');\n}\n\nfunction describeNodeResult(result: PingNodeResult): string {\n if (result.state === 'pending') {\n return '未返回结果(超时)';\n }\n if (result.state === 'unresolved') {\n return '无法解析或不可达';\n }\n const loss = Math.round(((result.sent - result.received) / result.sent) * 100);\n const via = result.address ? ` → ${result.address}` : '';\n if (result.averageMs === undefined) {\n return `全部超时(丢包 ${loss}%)${via}`;\n }\n const average = result.averageMs.toFixed(1);\n const min = (result.minMs ?? result.averageMs).toFixed(1);\n const max = (result.maxMs ?? result.averageMs).toFixed(1);\n return `平均 ${average}ms(${min}/${max}),丢包 ${loss}%${via}`;\n}\n\nfunction malformed(): NettoolsError {\n return new NettoolsError('malformed', 'check-host.net 返回了无法识别的数据');\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,3 @@
1
+ import type { NettoolsService } from './service.js';
2
+ /** Owner-scoped nettools service for one plugin instance. */
3
+ export declare const nettoolsServiceToken: import("zhin.js").Token<NettoolsService>;
package/lib/runtime.js ADDED
@@ -0,0 +1,4 @@
1
+ import { createToken } from 'zhin.js';
2
+ /** Owner-scoped nettools service for one plugin instance. */
3
+ export const nettoolsServiceToken = createToken('zhin.nettools.service', 'Owner-scoped network tools service');
4
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtC,6DAA6D;AAC7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAC7C,uBAAuB,EACvB,oCAAoC,CACrC,CAAC","sourcesContent":["import { createToken } from 'zhin.js';\nimport type { NettoolsService } from './service.js';\n\n/** Owner-scoped nettools service for one plugin instance. */\nexport const nettoolsServiceToken = createToken<NettoolsService>(\n 'zhin.nettools.service',\n 'Owner-scoped network tools service',\n);\n"]}
@@ -0,0 +1,18 @@
1
+ import { type NettoolsConfig, type ResolvedNettoolsConfig } from './config.js';
2
+ import { type HttpDependencies } from './http.js';
3
+ export interface NettoolsServiceOptions {
4
+ readonly config?: NettoolsConfig;
5
+ /** Injected for tests: fetch, sleep, clock and randomness. */
6
+ readonly dependencies?: HttpDependencies;
7
+ }
8
+ export declare class NettoolsService {
9
+ #private;
10
+ readonly config: ResolvedNettoolsConfig;
11
+ constructor(options?: NettoolsServiceOptions);
12
+ /** Run one operation and always resolve to a user-facing string. */
13
+ run(operation: () => Promise<string>): Promise<string>;
14
+ ping(hostInput: string, callerKey: string, signal?: AbortSignal): Promise<string>;
15
+ whois(domainInput: string, callerKey: string, signal?: AbortSignal): Promise<string>;
16
+ dns(nameInput: string, typeInput: string | undefined, callerKey: string, signal?: AbortSignal): Promise<string>;
17
+ domainPrice(suffixInput: string, kindInput: string | undefined, callerKey: string, signal?: AbortSignal): Promise<string>;
18
+ }
package/lib/service.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * NettoolsService: the single entry point used by chat commands and Agent
3
+ * tools. Each method validates input, spends the caller's cooldown, performs
4
+ * the lookup and returns a ready-to-send text reply. Failures are thrown as
5
+ * NettoolsError; {@link NettoolsService.run} turns them into friendly text.
6
+ */
7
+ import { classifyParsedHost, displayHost, parseHostInput, reverseLookupName, } from './address-policy.js';
8
+ import { resolveNettoolsConfig } from './config.js';
9
+ import { UserCooldown } from './cooldown.js';
10
+ import { DohResolver, formatDnsAnswer, parseDnsType } from './dns.js';
11
+ import { comparePrices, formatPriceComparison, parseDomainSuffix, parsePriceKind } from './domain-price.js';
12
+ import { describeFailure, NettoolsError } from './errors.js';
13
+ import { DEFAULT_RETRY_POLICY, defaultHttpDependencies, HttpClient } from './http.js';
14
+ import { formatPingReport, runMultiRegionPing } from './ping.js';
15
+ import { assertPublicTarget } from './target-guard.js';
16
+ import { formatWhoisRecord, RdapClient } from './whois.js';
17
+ export class NettoolsService {
18
+ config;
19
+ #http;
20
+ #resolver;
21
+ #rdap;
22
+ #cooldown;
23
+ constructor(options = {}) {
24
+ this.config = resolveNettoolsConfig(options.config);
25
+ const dependencies = options.dependencies ?? defaultHttpDependencies();
26
+ this.#http = new HttpClient({
27
+ ...DEFAULT_RETRY_POLICY,
28
+ timeoutMs: this.config.requestTimeoutMs,
29
+ maxRetries: this.config.maxRetries,
30
+ }, dependencies);
31
+ this.#resolver = new DohResolver(this.#http, this.config.dohProviders);
32
+ this.#rdap = new RdapClient(this.#http);
33
+ this.#cooldown = new UserCooldown(this.config.cooldownMs, dependencies.now);
34
+ }
35
+ /** Run one operation and always resolve to a user-facing string. */
36
+ async run(operation) {
37
+ try {
38
+ return await operation();
39
+ }
40
+ catch (error) {
41
+ return describeFailure(error);
42
+ }
43
+ }
44
+ async ping(hostInput, callerKey, signal) {
45
+ const host = parseHostInput(hostInput);
46
+ const verdict = classifyParsedHost(host);
47
+ if (verdict.blocked) {
48
+ throw new NettoolsError('blocked_target', `拒绝探测 ${displayHost(host)}:${verdict.reason}`);
49
+ }
50
+ this.#cooldown.take(callerKey);
51
+ const target = await assertPublicTarget(hostInput, this.#resolver, signal);
52
+ const report = await runMultiRegionPing(this.#http, target.display, {
53
+ maxNodes: this.config.pingMaxNodes,
54
+ pollIntervalMs: this.config.pingPollIntervalMs,
55
+ maxPolls: this.config.pingMaxPolls,
56
+ ...(signal ? { signal } : {}),
57
+ });
58
+ return formatPingReport(report);
59
+ }
60
+ async whois(domainInput, callerKey, signal) {
61
+ const host = parseHostInput(domainInput);
62
+ if (host.kind !== 'domain') {
63
+ throw new NettoolsError('invalid_input', 'whois 目前只支持域名查询');
64
+ }
65
+ const verdict = classifyParsedHost(host);
66
+ if (verdict.blocked) {
67
+ throw new NettoolsError('blocked_target', `拒绝查询 ${host.name}:${verdict.reason}`);
68
+ }
69
+ this.#cooldown.take(callerKey);
70
+ const record = await this.#rdap.lookup(host.name, signal);
71
+ return formatWhoisRecord(record);
72
+ }
73
+ async dns(nameInput, typeInput, callerKey, signal) {
74
+ const type = parseDnsType(typeInput);
75
+ const host = parseHostInput(nameInput, { allowUnderscore: true });
76
+ const verdict = classifyParsedHost(host);
77
+ if (verdict.blocked) {
78
+ throw new NettoolsError('blocked_target', `拒绝查询 ${displayHost(host)}:${verdict.reason}`);
79
+ }
80
+ if (host.kind !== 'domain' && type !== 'PTR') {
81
+ throw new NettoolsError('invalid_input', 'IP 地址只能做反向解析,请使用:dns <IP> PTR');
82
+ }
83
+ const queryName = host.kind === 'domain' ? host.name : reverseLookupName(host);
84
+ this.#cooldown.take(callerKey);
85
+ const answer = await this.#resolver.resolve(queryName, type, signal);
86
+ return formatDnsAnswer(answer);
87
+ }
88
+ async domainPrice(suffixInput, kindInput, callerKey, signal) {
89
+ const kind = parsePriceKind(kindInput);
90
+ const suffix = parseDomainSuffix(suffixInput);
91
+ this.#cooldown.take(callerKey);
92
+ const comparison = await comparePrices(this.#http, suffix, kind, signal);
93
+ return formatPriceComparison(comparison, this.config.priceListLimit);
94
+ }
95
+ }
96
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * NettoolsService: the single entry point used by chat commands and Agent\n * tools. Each method validates input, spends the caller's cooldown, performs\n * the lookup and returns a ready-to-send text reply. Failures are thrown as\n * NettoolsError; {@link NettoolsService.run} turns them into friendly text.\n */\nimport {\n classifyParsedHost,\n displayHost,\n parseHostInput,\n reverseLookupName,\n} from './address-policy.js';\nimport { resolveNettoolsConfig, type NettoolsConfig, type ResolvedNettoolsConfig } from './config.js';\nimport { UserCooldown } from './cooldown.js';\nimport { DohResolver, formatDnsAnswer, parseDnsType } from './dns.js';\nimport { comparePrices, formatPriceComparison, parseDomainSuffix, parsePriceKind } from './domain-price.js';\nimport { describeFailure, NettoolsError } from './errors.js';\nimport { DEFAULT_RETRY_POLICY, defaultHttpDependencies, HttpClient, type HttpDependencies } from './http.js';\nimport { formatPingReport, runMultiRegionPing } from './ping.js';\nimport { assertPublicTarget } from './target-guard.js';\nimport { formatWhoisRecord, RdapClient } from './whois.js';\n\nexport interface NettoolsServiceOptions {\n readonly config?: NettoolsConfig;\n /** Injected for tests: fetch, sleep, clock and randomness. */\n readonly dependencies?: HttpDependencies;\n}\n\nexport class NettoolsService {\n readonly config: ResolvedNettoolsConfig;\n readonly #http: HttpClient;\n readonly #resolver: DohResolver;\n readonly #rdap: RdapClient;\n readonly #cooldown: UserCooldown;\n\n constructor(options: NettoolsServiceOptions = {}) {\n this.config = resolveNettoolsConfig(options.config);\n const dependencies = options.dependencies ?? defaultHttpDependencies();\n this.#http = new HttpClient(\n {\n ...DEFAULT_RETRY_POLICY,\n timeoutMs: this.config.requestTimeoutMs,\n maxRetries: this.config.maxRetries,\n },\n dependencies,\n );\n this.#resolver = new DohResolver(this.#http, this.config.dohProviders);\n this.#rdap = new RdapClient(this.#http);\n this.#cooldown = new UserCooldown(this.config.cooldownMs, dependencies.now);\n }\n\n /** Run one operation and always resolve to a user-facing string. */\n async run(operation: () => Promise<string>): Promise<string> {\n try {\n return await operation();\n } catch (error) {\n return describeFailure(error);\n }\n }\n\n async ping(hostInput: string, callerKey: string, signal?: AbortSignal): Promise<string> {\n const host = parseHostInput(hostInput);\n const verdict = classifyParsedHost(host);\n if (verdict.blocked) {\n throw new NettoolsError('blocked_target', `拒绝探测 ${displayHost(host)}:${verdict.reason}`);\n }\n this.#cooldown.take(callerKey);\n const target = await assertPublicTarget(hostInput, this.#resolver, signal);\n const report = await runMultiRegionPing(this.#http, target.display, {\n maxNodes: this.config.pingMaxNodes,\n pollIntervalMs: this.config.pingPollIntervalMs,\n maxPolls: this.config.pingMaxPolls,\n ...(signal ? { signal } : {}),\n });\n return formatPingReport(report);\n }\n\n async whois(domainInput: string, callerKey: string, signal?: AbortSignal): Promise<string> {\n const host = parseHostInput(domainInput);\n if (host.kind !== 'domain') {\n throw new NettoolsError('invalid_input', 'whois 目前只支持域名查询');\n }\n const verdict = classifyParsedHost(host);\n if (verdict.blocked) {\n throw new NettoolsError('blocked_target', `拒绝查询 ${host.name}:${verdict.reason}`);\n }\n this.#cooldown.take(callerKey);\n const record = await this.#rdap.lookup(host.name, signal);\n return formatWhoisRecord(record);\n }\n\n async dns(nameInput: string, typeInput: string | undefined, callerKey: string, signal?: AbortSignal): Promise<string> {\n const type = parseDnsType(typeInput);\n const host = parseHostInput(nameInput, { allowUnderscore: true });\n const verdict = classifyParsedHost(host);\n if (verdict.blocked) {\n throw new NettoolsError('blocked_target', `拒绝查询 ${displayHost(host)}:${verdict.reason}`);\n }\n if (host.kind !== 'domain' && type !== 'PTR') {\n throw new NettoolsError('invalid_input', 'IP 地址只能做反向解析,请使用:dns <IP> PTR');\n }\n const queryName = host.kind === 'domain' ? host.name : reverseLookupName(host);\n this.#cooldown.take(callerKey);\n const answer = await this.#resolver.resolve(queryName, type, signal);\n return formatDnsAnswer(answer);\n }\n\n async domainPrice(suffixInput: string, kindInput: string | undefined, callerKey: string, signal?: AbortSignal): Promise<string> {\n const kind = parsePriceKind(kindInput);\n const suffix = parseDomainSuffix(suffixInput);\n this.#cooldown.take(callerKey);\n const comparison = await comparePrices(this.#http, suffix, kind, signal);\n return formatPriceComparison(comparison, this.config.priceListLimit);\n }\n}\n"]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Resolve-and-check guard for hosts that will be actively probed (ping).
3
+ *
4
+ * Literal IPs are classified directly. Hostnames are checked by name, then
5
+ * resolved for both A and AAAA through DoH; the target is refused when ANY
6
+ * returned address is blocked (an attacker controlling a zone could otherwise
7
+ * mix one public and one private record). Resolution failure fails closed.
8
+ *
9
+ * Note on DNS rebinding: the probe itself is executed by check-host.net, which
10
+ * resolves the name again on its own network. Our process never connects to
11
+ * the target, so a rebinding answer can only make check-host.net probe its own
12
+ * neighbourhood, never ours; the pre-check stops casual abuse and obvious
13
+ * internal names.
14
+ */
15
+ import { type ParsedHost } from './address-policy.js';
16
+ import type { DohResolver } from './dns.js';
17
+ export interface CheckedTarget {
18
+ readonly host: ParsedHost;
19
+ /** Canonical host string to send to the probing service. */
20
+ readonly display: string;
21
+ /** Addresses the name resolved to (empty for IP literals). */
22
+ readonly resolvedAddresses: readonly string[];
23
+ }
24
+ export declare function assertPublicTarget(input: string, resolver: DohResolver, signal?: AbortSignal): Promise<CheckedTarget>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Resolve-and-check guard for hosts that will be actively probed (ping).
3
+ *
4
+ * Literal IPs are classified directly. Hostnames are checked by name, then
5
+ * resolved for both A and AAAA through DoH; the target is refused when ANY
6
+ * returned address is blocked (an attacker controlling a zone could otherwise
7
+ * mix one public and one private record). Resolution failure fails closed.
8
+ *
9
+ * Note on DNS rebinding: the probe itself is executed by check-host.net, which
10
+ * resolves the name again on its own network. Our process never connects to
11
+ * the target, so a rebinding answer can only make check-host.net probe its own
12
+ * neighbourhood, never ours; the pre-check stops casual abuse and obvious
13
+ * internal names.
14
+ */
15
+ import { classifyAddress, classifyParsedHost, displayHost, parseHostInput, } from './address-policy.js';
16
+ import { NettoolsError } from './errors.js';
17
+ export async function assertPublicTarget(input, resolver, signal) {
18
+ const host = parseHostInput(input);
19
+ const display = displayHost(host);
20
+ const verdict = classifyParsedHost(host);
21
+ if (verdict.blocked) {
22
+ throw new NettoolsError('blocked_target', `拒绝探测 ${display}:${verdict.reason}`);
23
+ }
24
+ if (host.kind !== 'domain') {
25
+ return Object.freeze({ host, display, resolvedAddresses: Object.freeze([]) });
26
+ }
27
+ const [ipv4, ipv6] = await Promise.all([
28
+ resolver.resolve(host.name, 'A', signal),
29
+ resolver.resolve(host.name, 'AAAA', signal),
30
+ ]);
31
+ const addresses = [];
32
+ for (const answer of [ipv4, ipv6]) {
33
+ for (const record of answer.records) {
34
+ if (record.type === 'A' || record.type === 'AAAA') {
35
+ addresses.push(record.data);
36
+ }
37
+ }
38
+ }
39
+ if (addresses.length === 0) {
40
+ throw new NettoolsError('not_found', `无法解析 ${display}(没有 A/AAAA 记录)`);
41
+ }
42
+ for (const address of addresses) {
43
+ const addressVerdict = safeClassify(address);
44
+ if (addressVerdict.blocked) {
45
+ throw new NettoolsError('blocked_target', `拒绝探测 ${display}:解析到 ${address}(${addressVerdict.reason})`);
46
+ }
47
+ }
48
+ return Object.freeze({ host, display, resolvedAddresses: Object.freeze(addresses) });
49
+ }
50
+ /** A record value that is not a parsable IP is treated as blocked (fail closed). */
51
+ function safeClassify(address) {
52
+ try {
53
+ return classifyAddress(address);
54
+ }
55
+ catch {
56
+ return { blocked: true, reason: '无法识别的地址记录' };
57
+ }
58
+ }
59
+ //# sourceMappingURL=target-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target-guard.js","sourceRoot":"","sources":["../src/target-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,WAAW,EACX,cAAc,GAEf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAU5C,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAa,EACb,QAAqB,EACrB,MAAoB;IAEpB,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,aAAa,CAAC,gBAAgB,EAAE,QAAQ,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC3B,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACrC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC;QACxC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC;KAC5C,CAAC,CAAC;IACH,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAClC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACpC,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;gBAClD,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,QAAQ,OAAO,gBAAgB,CAAC,CAAC;IACxE,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;QAChC,MAAM,cAAc,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QAC7C,IAAI,cAAc,CAAC,OAAO,EAAE,CAAC;YAC3B,MAAM,IAAI,aAAa,CACrB,gBAAgB,EAChB,QAAQ,OAAO,QAAQ,OAAO,IAAI,cAAc,CAAC,MAAM,GAAG,CAC3D,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;AACvF,CAAC;AAED,oFAAoF;AACpF,SAAS,YAAY,CAAC,OAAe;IACnC,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAChD,CAAC;AACH,CAAC","sourcesContent":["/**\n * Resolve-and-check guard for hosts that will be actively probed (ping).\n *\n * Literal IPs are classified directly. Hostnames are checked by name, then\n * resolved for both A and AAAA through DoH; the target is refused when ANY\n * returned address is blocked (an attacker controlling a zone could otherwise\n * mix one public and one private record). Resolution failure fails closed.\n *\n * Note on DNS rebinding: the probe itself is executed by check-host.net, which\n * resolves the name again on its own network. Our process never connects to\n * the target, so a rebinding answer can only make check-host.net probe its own\n * neighbourhood, never ours; the pre-check stops casual abuse and obvious\n * internal names.\n */\nimport {\n classifyAddress,\n classifyParsedHost,\n displayHost,\n parseHostInput,\n type ParsedHost,\n} from './address-policy.js';\nimport type { DohResolver } from './dns.js';\nimport { NettoolsError } from './errors.js';\n\nexport interface CheckedTarget {\n readonly host: ParsedHost;\n /** Canonical host string to send to the probing service. */\n readonly display: string;\n /** Addresses the name resolved to (empty for IP literals). */\n readonly resolvedAddresses: readonly string[];\n}\n\nexport async function assertPublicTarget(\n input: string,\n resolver: DohResolver,\n signal?: AbortSignal,\n): Promise<CheckedTarget> {\n const host = parseHostInput(input);\n const display = displayHost(host);\n const verdict = classifyParsedHost(host);\n if (verdict.blocked) {\n throw new NettoolsError('blocked_target', `拒绝探测 ${display}:${verdict.reason}`);\n }\n if (host.kind !== 'domain') {\n return Object.freeze({ host, display, resolvedAddresses: Object.freeze([]) });\n }\n\n const [ipv4, ipv6] = await Promise.all([\n resolver.resolve(host.name, 'A', signal),\n resolver.resolve(host.name, 'AAAA', signal),\n ]);\n const addresses: string[] = [];\n for (const answer of [ipv4, ipv6]) {\n for (const record of answer.records) {\n if (record.type === 'A' || record.type === 'AAAA') {\n addresses.push(record.data);\n }\n }\n }\n if (addresses.length === 0) {\n throw new NettoolsError('not_found', `无法解析 ${display}(没有 A/AAAA 记录)`);\n }\n for (const address of addresses) {\n const addressVerdict = safeClassify(address);\n if (addressVerdict.blocked) {\n throw new NettoolsError(\n 'blocked_target',\n `拒绝探测 ${display}:解析到 ${address}(${addressVerdict.reason})`,\n );\n }\n }\n return Object.freeze({ host, display, resolvedAddresses: Object.freeze(addresses) });\n}\n\n/** A record value that is not a parsable IP is treated as blocked (fail closed). */\nfunction safeClassify(address: string): { blocked: boolean; reason?: string } {\n try {\n return classifyAddress(address);\n } catch {\n return { blocked: true, reason: '无法识别的地址记录' };\n }\n}\n"]}
package/lib/whois.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { HttpClient } from './http.js';
2
+ export declare const IANA_RDAP_BOOTSTRAP_URL = "https://data.iana.org/rdap/dns.json";
3
+ export declare const RDAP_FALLBACK_BASE_URL = "https://rdap.org/";
4
+ export declare const WHOIS_FALLBACK_URL = "https://60s.viki.moe/v2/whois";
5
+ export declare const DEFAULT_BOOTSTRAP_TTL_MS: number;
6
+ export interface WhoisRecord {
7
+ readonly domain: string;
8
+ readonly registered: boolean;
9
+ readonly registrar: string | undefined;
10
+ readonly statuses: readonly string[];
11
+ readonly createdAt: string | undefined;
12
+ readonly expiresAt: string | undefined;
13
+ readonly updatedAt: string | undefined;
14
+ readonly nameservers: readonly string[];
15
+ readonly dnssec: boolean | undefined;
16
+ readonly source: string;
17
+ }
18
+ export declare class RdapClient {
19
+ #private;
20
+ constructor(http: HttpClient, bootstrapTtlMs?: number);
21
+ lookup(domain: string, signal?: AbortSignal): Promise<WhoisRecord>;
22
+ }
23
+ /** Parse the IANA bootstrap file into suffix → https base URLs. */
24
+ export declare function parseBootstrap(body: unknown): ReadonlyMap<string, readonly string[]>;
25
+ export declare function parseRdapDomain(body: unknown, domain: string, source: string): WhoisRecord;
26
+ /**
27
+ * Parse the 60s API WHOIS answer. Only registry-level facts are kept;
28
+ * registrant name / organisation / e-mail are deliberately dropped.
29
+ */
30
+ export declare function parseWhoisFallback(body: unknown, domain: string, source: string): WhoisRecord;
31
+ export declare function formatWhoisRecord(record: WhoisRecord): string;