@namingsignal/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/core/batch.js +142 -0
- package/dist/core/brief-constraints.js +72 -0
- package/dist/core/brief.js +402 -0
- package/dist/core/candidate-sort.js +47 -0
- package/dist/core/candidates.js +210 -0
- package/dist/core/domain.js +607 -0
- package/dist/core/editorial-review.js +27 -0
- package/dist/core/export.js +235 -0
- package/dist/core/import-safety.js +298 -0
- package/dist/core/index.js +31 -0
- package/dist/core/namesake.js +53 -0
- package/dist/core/namespaces.js +295 -0
- package/dist/core/normalize.js +75 -0
- package/dist/core/scoring.js +409 -0
- package/dist/core/types.js +3 -0
- package/dist/core/url-input.js +28 -0
- package/dist/core/usage.js +271 -0
- package/dist/index.js +642 -0
- package/package.json +41 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_RDAP_CANARIES = void 0;
|
|
4
|
+
exports.createDomainChecker = createDomainChecker;
|
|
5
|
+
exports.checkDomain = checkDomain;
|
|
6
|
+
exports.checkDomains = checkDomains;
|
|
7
|
+
const normalize_1 = require("./normalize");
|
|
8
|
+
const IANA_RDAP_BOOTSTRAP = "https://data.iana.org/rdap/dns.json";
|
|
9
|
+
/** Known-registered canaries. In particular, github.io must never appear free. */
|
|
10
|
+
exports.DEFAULT_RDAP_CANARIES = Object.freeze({
|
|
11
|
+
com: "google.com",
|
|
12
|
+
net: "example.net",
|
|
13
|
+
org: "wikipedia.org",
|
|
14
|
+
io: "github.io",
|
|
15
|
+
dev: "google.dev",
|
|
16
|
+
app: "google.app",
|
|
17
|
+
ai: "google.ai",
|
|
18
|
+
co: "google.co",
|
|
19
|
+
me: "about.me",
|
|
20
|
+
xyz: "abc.xyz",
|
|
21
|
+
sh: "nic.sh",
|
|
22
|
+
});
|
|
23
|
+
class RequestTimeoutError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "RequestTimeoutError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function cloneResult(result, cached = result.cached) {
|
|
30
|
+
return { ...result, cached, evidence: { ...result.evidence } };
|
|
31
|
+
}
|
|
32
|
+
function iso(now) {
|
|
33
|
+
return now().toISOString();
|
|
34
|
+
}
|
|
35
|
+
function confidence(value) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function providerName(endpoint) {
|
|
39
|
+
try {
|
|
40
|
+
return `Registry RDAP (${new URL(endpoint).hostname})`;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return "Registry RDAP";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function safeRegistryEndpoint(input) {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(input);
|
|
49
|
+
if (url.protocol !== "https:")
|
|
50
|
+
return undefined;
|
|
51
|
+
const host = url.hostname.toLowerCase();
|
|
52
|
+
// Discovery must come from IANA; never fall back to a generic RDAP proxy.
|
|
53
|
+
if (host === "rdap.org" || host.endsWith(".rdap.org"))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (!url.pathname.endsWith("/"))
|
|
56
|
+
url.pathname += "/";
|
|
57
|
+
url.search = "";
|
|
58
|
+
url.hash = "";
|
|
59
|
+
return url.toString();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function fetchWithTimeout(fetcher, url, timeoutMs, init = {}) {
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
const upstreamSignal = init.signal;
|
|
68
|
+
const abortFromUpstream = () => controller.abort(upstreamSignal?.reason);
|
|
69
|
+
if (upstreamSignal) {
|
|
70
|
+
if (upstreamSignal.aborted)
|
|
71
|
+
abortFromUpstream();
|
|
72
|
+
else
|
|
73
|
+
upstreamSignal.addEventListener("abort", abortFromUpstream, { once: true });
|
|
74
|
+
}
|
|
75
|
+
let timeout;
|
|
76
|
+
try {
|
|
77
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
78
|
+
timeout = setTimeout(() => {
|
|
79
|
+
controller.abort(new RequestTimeoutError(`Request exceeded ${timeoutMs}ms.`));
|
|
80
|
+
reject(new RequestTimeoutError(`Request exceeded ${timeoutMs}ms.`));
|
|
81
|
+
}, timeoutMs);
|
|
82
|
+
});
|
|
83
|
+
return await Promise.race([
|
|
84
|
+
fetcher(url, { ...init, signal: controller.signal }),
|
|
85
|
+
timeoutPromise,
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
if (timeout)
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
upstreamSignal?.removeEventListener("abort", abortFromUpstream);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function endpointForDomain(base, domain) {
|
|
95
|
+
return new URL(`domain/${encodeURIComponent(domain)}`, base).toString();
|
|
96
|
+
}
|
|
97
|
+
function parseBootstrap(document) {
|
|
98
|
+
const endpoints = {};
|
|
99
|
+
if (!Array.isArray(document.services))
|
|
100
|
+
throw new Error("IANA RDAP bootstrap did not contain services.");
|
|
101
|
+
for (const service of document.services) {
|
|
102
|
+
if (!Array.isArray(service) || service.length < 2)
|
|
103
|
+
continue;
|
|
104
|
+
const [rawTlds, rawUrls] = service;
|
|
105
|
+
if (!Array.isArray(rawTlds) || !Array.isArray(rawUrls))
|
|
106
|
+
continue;
|
|
107
|
+
const endpoint = rawUrls
|
|
108
|
+
.filter((value) => typeof value === "string")
|
|
109
|
+
.map(safeRegistryEndpoint)
|
|
110
|
+
.find(Boolean);
|
|
111
|
+
if (!endpoint)
|
|
112
|
+
continue;
|
|
113
|
+
for (const rawTld of rawTlds) {
|
|
114
|
+
if (typeof rawTld !== "string")
|
|
115
|
+
continue;
|
|
116
|
+
const tld = rawTld.toLowerCase().replace(/^\./, "");
|
|
117
|
+
if (/^[a-z0-9-]+$/.test(tld))
|
|
118
|
+
endpoints[tld] = endpoint;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return endpoints;
|
|
122
|
+
}
|
|
123
|
+
async function cancelResponseBody(response) {
|
|
124
|
+
if (!response.body)
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
await response.body.cancel();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Status-only checks never need the upstream body. Cancellation is
|
|
131
|
+
// best-effort because some test and edge runtimes expose locked streams.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function readBoundedJson(response, maxBytes) {
|
|
135
|
+
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
136
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
137
|
+
await cancelResponseBody(response);
|
|
138
|
+
throw new Error("JSON response exceeded its byte limit.");
|
|
139
|
+
}
|
|
140
|
+
if (!response.body)
|
|
141
|
+
throw new Error("JSON response body was empty.");
|
|
142
|
+
const reader = response.body.getReader();
|
|
143
|
+
const chunks = [];
|
|
144
|
+
let total = 0;
|
|
145
|
+
try {
|
|
146
|
+
while (true) {
|
|
147
|
+
const { done, value } = await reader.read();
|
|
148
|
+
if (done)
|
|
149
|
+
break;
|
|
150
|
+
if (!value)
|
|
151
|
+
continue;
|
|
152
|
+
total += value.byteLength;
|
|
153
|
+
if (total > maxBytes) {
|
|
154
|
+
await reader.cancel("JSON response exceeded its byte limit.").catch(() => undefined);
|
|
155
|
+
throw new Error("JSON response exceeded its byte limit.");
|
|
156
|
+
}
|
|
157
|
+
chunks.push(value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
reader.releaseLock();
|
|
162
|
+
}
|
|
163
|
+
const bytes = new Uint8Array(total);
|
|
164
|
+
let offset = 0;
|
|
165
|
+
for (const chunk of chunks) {
|
|
166
|
+
bytes.set(chunk, offset);
|
|
167
|
+
offset += chunk.byteLength;
|
|
168
|
+
}
|
|
169
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
170
|
+
}
|
|
171
|
+
function manualLookup(domain) {
|
|
172
|
+
return `https://lookup.icann.org/en/lookup?name=${encodeURIComponent(domain)}`;
|
|
173
|
+
}
|
|
174
|
+
class RdapDomainChecker {
|
|
175
|
+
fetcher;
|
|
176
|
+
now;
|
|
177
|
+
bootstrapUrl;
|
|
178
|
+
timeoutMs;
|
|
179
|
+
cacheTtlMs;
|
|
180
|
+
negativeCacheTtlMs;
|
|
181
|
+
unknownCacheTtlMs;
|
|
182
|
+
bootstrapCacheTtlMs;
|
|
183
|
+
calibrationCacheTtlMs;
|
|
184
|
+
canaries;
|
|
185
|
+
requireCalibration;
|
|
186
|
+
maxDomains;
|
|
187
|
+
concurrency;
|
|
188
|
+
externalCache;
|
|
189
|
+
ledger;
|
|
190
|
+
resultCache = new Map();
|
|
191
|
+
calibrationCache = new Map();
|
|
192
|
+
inflight = new Map();
|
|
193
|
+
calibrationInflight = new Map();
|
|
194
|
+
bootstrap;
|
|
195
|
+
bootstrapInflight;
|
|
196
|
+
constructor(options = {}) {
|
|
197
|
+
const globalFetch = globalThis.fetch?.bind(globalThis);
|
|
198
|
+
if (!options.fetch && !globalFetch)
|
|
199
|
+
throw new Error("A Fetch-compatible function is required for domain checks.");
|
|
200
|
+
this.fetcher = options.fetch ?? globalFetch;
|
|
201
|
+
this.now = options.now ?? (() => new Date());
|
|
202
|
+
this.bootstrapUrl = options.bootstrapUrl ?? IANA_RDAP_BOOTSTRAP;
|
|
203
|
+
if (this.bootstrapUrl !== IANA_RDAP_BOOTSTRAP) {
|
|
204
|
+
const parsed = new URL(this.bootstrapUrl);
|
|
205
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "data.iana.org") {
|
|
206
|
+
throw new TypeError("RDAP bootstrap must use IANA data.iana.org/dns.json.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
this.timeoutMs = Math.max(50, options.timeoutMs ?? 5_000);
|
|
210
|
+
this.cacheTtlMs = Math.max(0, options.cacheTtlMs ?? 15 * 60_000);
|
|
211
|
+
this.negativeCacheTtlMs = Math.max(0, options.negativeCacheTtlMs ?? 60_000);
|
|
212
|
+
this.unknownCacheTtlMs = Math.max(0, options.unknownCacheTtlMs ?? 15_000);
|
|
213
|
+
this.bootstrapCacheTtlMs = Math.max(0, options.bootstrapCacheTtlMs ?? 24 * 60 * 60_000);
|
|
214
|
+
this.calibrationCacheTtlMs = Math.max(0, options.calibrationCacheTtlMs ?? 10 * 60_000);
|
|
215
|
+
this.canaries = { ...exports.DEFAULT_RDAP_CANARIES, ...(options.calibrationCanaries ?? {}) };
|
|
216
|
+
this.requireCalibration = options.requireCalibration ?? true;
|
|
217
|
+
this.maxDomains = Math.max(1, Math.min(1_000, options.maxDomains ?? 500));
|
|
218
|
+
this.concurrency = Math.max(1, Math.min(25, options.concurrency ?? 8));
|
|
219
|
+
this.externalCache = options.cache;
|
|
220
|
+
this.ledger = options.usageLedger;
|
|
221
|
+
}
|
|
222
|
+
cacheKey(domain) {
|
|
223
|
+
return `domain:rdap:v1:${domain}`;
|
|
224
|
+
}
|
|
225
|
+
async readResultCache(domain) {
|
|
226
|
+
const now = this.now().getTime();
|
|
227
|
+
const local = this.resultCache.get(domain);
|
|
228
|
+
if (local && local.expiresAt > now) {
|
|
229
|
+
this.ledger?.record({ kind: "domain", provider: "core-cache", task: "domain.cache", calls: 0, requests: 0, cacheHits: 1 });
|
|
230
|
+
return cloneResult(local.value, true);
|
|
231
|
+
}
|
|
232
|
+
if (local)
|
|
233
|
+
this.resultCache.delete(domain);
|
|
234
|
+
const external = await this.externalCache?.get(this.cacheKey(domain));
|
|
235
|
+
if (external) {
|
|
236
|
+
this.ledger?.record({ kind: "domain", provider: "core-cache", task: "domain.cache", calls: 0, requests: 0, cacheHits: 1 });
|
|
237
|
+
return cloneResult(external, true);
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
ttlFor(result) {
|
|
242
|
+
if (result.status === "likely_available")
|
|
243
|
+
return this.negativeCacheTtlMs;
|
|
244
|
+
if (result.status === "unknown")
|
|
245
|
+
return this.unknownCacheTtlMs;
|
|
246
|
+
if (result.status === "unsupported")
|
|
247
|
+
return this.calibrationCacheTtlMs;
|
|
248
|
+
return this.cacheTtlMs;
|
|
249
|
+
}
|
|
250
|
+
async writeResultCache(domain, result) {
|
|
251
|
+
const ttl = this.ttlFor(result);
|
|
252
|
+
if (ttl <= 0)
|
|
253
|
+
return;
|
|
254
|
+
const clean = cloneResult(result, false);
|
|
255
|
+
this.resultCache.set(domain, { value: clean, expiresAt: this.now().getTime() + ttl });
|
|
256
|
+
await this.externalCache?.set(this.cacheKey(domain), clean, ttl);
|
|
257
|
+
}
|
|
258
|
+
async loadBootstrap() {
|
|
259
|
+
const now = this.now().getTime();
|
|
260
|
+
if (this.bootstrap && this.bootstrap.expiresAt > now)
|
|
261
|
+
return this.bootstrap.value;
|
|
262
|
+
if (this.bootstrapInflight)
|
|
263
|
+
return this.bootstrapInflight;
|
|
264
|
+
this.bootstrapInflight = (async () => {
|
|
265
|
+
const cacheKey = "rdap:iana-bootstrap:v1";
|
|
266
|
+
const external = await this.externalCache?.get(cacheKey);
|
|
267
|
+
if (external && Object.keys(external).length) {
|
|
268
|
+
this.bootstrap = { value: external, expiresAt: this.now().getTime() + this.bootstrapCacheTtlMs };
|
|
269
|
+
return external;
|
|
270
|
+
}
|
|
271
|
+
const started = Date.now();
|
|
272
|
+
const response = await fetchWithTimeout(this.fetcher, this.bootstrapUrl, this.timeoutMs, {
|
|
273
|
+
headers: { accept: "application/json" },
|
|
274
|
+
});
|
|
275
|
+
this.ledger?.record({
|
|
276
|
+
kind: "fetch",
|
|
277
|
+
provider: "IANA",
|
|
278
|
+
task: "rdap.bootstrap",
|
|
279
|
+
requests: 1,
|
|
280
|
+
calls: 0,
|
|
281
|
+
failures: response.ok ? 0 : 1,
|
|
282
|
+
latencyMs: Date.now() - started,
|
|
283
|
+
});
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
await cancelResponseBody(response);
|
|
286
|
+
throw new Error(`IANA RDAP bootstrap returned HTTP ${response.status}.`);
|
|
287
|
+
}
|
|
288
|
+
const endpoints = parseBootstrap(await readBoundedJson(response, 1_000_000));
|
|
289
|
+
if (!Object.keys(endpoints).length)
|
|
290
|
+
throw new Error("IANA RDAP bootstrap did not contain usable registry endpoints.");
|
|
291
|
+
this.bootstrap = { value: endpoints, expiresAt: this.now().getTime() + this.bootstrapCacheTtlMs };
|
|
292
|
+
await this.externalCache?.set(cacheKey, endpoints, this.bootstrapCacheTtlMs);
|
|
293
|
+
return endpoints;
|
|
294
|
+
})().finally(() => {
|
|
295
|
+
this.bootstrapInflight = undefined;
|
|
296
|
+
});
|
|
297
|
+
return this.bootstrapInflight;
|
|
298
|
+
}
|
|
299
|
+
result(domain, status, provider, sourceUrl, evidenceStatus, evidenceConfidence, detail, responseCode, latencyMs) {
|
|
300
|
+
const checkedAt = iso(this.now);
|
|
301
|
+
return {
|
|
302
|
+
domain,
|
|
303
|
+
status,
|
|
304
|
+
checkedAt,
|
|
305
|
+
provider,
|
|
306
|
+
detail,
|
|
307
|
+
responseCode,
|
|
308
|
+
latencyMs,
|
|
309
|
+
manualUrl: manualLookup(domain),
|
|
310
|
+
evidence: {
|
|
311
|
+
id: `domain:${domain}`,
|
|
312
|
+
label: `${domain} registry status`,
|
|
313
|
+
provider,
|
|
314
|
+
sourceUrl,
|
|
315
|
+
checkedAt,
|
|
316
|
+
status: evidenceStatus,
|
|
317
|
+
confidence: evidenceConfidence,
|
|
318
|
+
value: { domain, status, responseCode },
|
|
319
|
+
detail,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
async calibrate(rawTld) {
|
|
324
|
+
const tld = rawTld.toLowerCase().replace(/^\./, "").split(".").pop() ?? "";
|
|
325
|
+
if (!/^[a-z0-9-]+$/.test(tld))
|
|
326
|
+
throw new TypeError(`Invalid TLD: ${rawTld}`);
|
|
327
|
+
const cached = this.calibrationCache.get(tld);
|
|
328
|
+
if (cached && cached.expiresAt > this.now().getTime())
|
|
329
|
+
return { ...cached.value, evidence: { ...cached.value.evidence } };
|
|
330
|
+
const existing = this.calibrationInflight.get(tld);
|
|
331
|
+
if (existing)
|
|
332
|
+
return existing;
|
|
333
|
+
const operation = (async () => {
|
|
334
|
+
const checkedAt = iso(this.now);
|
|
335
|
+
const canary = this.canaries[tld];
|
|
336
|
+
let endpoints;
|
|
337
|
+
try {
|
|
338
|
+
endpoints = await this.loadBootstrap();
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
const detail = `Could not load IANA RDAP discovery: ${error instanceof Error ? error.message : "unknown error"}`;
|
|
342
|
+
return {
|
|
343
|
+
tld,
|
|
344
|
+
canary,
|
|
345
|
+
healthy: false,
|
|
346
|
+
checkedAt,
|
|
347
|
+
detail,
|
|
348
|
+
evidence: {
|
|
349
|
+
provider: "IANA RDAP bootstrap",
|
|
350
|
+
sourceUrl: this.bootstrapUrl,
|
|
351
|
+
checkedAt,
|
|
352
|
+
status: "unknown",
|
|
353
|
+
confidence: confidence("low"),
|
|
354
|
+
detail,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const endpoint = endpoints[tld];
|
|
359
|
+
if (!endpoint) {
|
|
360
|
+
const detail = `IANA did not publish a usable authoritative RDAP endpoint for .${tld}.`;
|
|
361
|
+
return {
|
|
362
|
+
tld,
|
|
363
|
+
canary,
|
|
364
|
+
healthy: false,
|
|
365
|
+
checkedAt,
|
|
366
|
+
detail,
|
|
367
|
+
evidence: {
|
|
368
|
+
provider: "IANA RDAP bootstrap",
|
|
369
|
+
sourceUrl: this.bootstrapUrl,
|
|
370
|
+
checkedAt,
|
|
371
|
+
status: "manual",
|
|
372
|
+
confidence: confidence("high"),
|
|
373
|
+
detail,
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (!canary) {
|
|
378
|
+
const detail = `No known-registered canary is configured for .${tld}; absence cannot be called likely available.`;
|
|
379
|
+
return {
|
|
380
|
+
tld,
|
|
381
|
+
healthy: false,
|
|
382
|
+
checkedAt,
|
|
383
|
+
detail,
|
|
384
|
+
evidence: {
|
|
385
|
+
provider: providerName(endpoint),
|
|
386
|
+
sourceUrl: endpoint,
|
|
387
|
+
checkedAt,
|
|
388
|
+
status: "manual",
|
|
389
|
+
confidence: confidence("medium"),
|
|
390
|
+
detail,
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const url = endpointForDomain(endpoint, (0, normalize_1.normalizeDomain)(canary));
|
|
395
|
+
const started = Date.now();
|
|
396
|
+
try {
|
|
397
|
+
const response = await fetchWithTimeout(this.fetcher, url, this.timeoutMs, {
|
|
398
|
+
headers: { accept: "application/rdap+json, application/json" },
|
|
399
|
+
});
|
|
400
|
+
const latencyMs = Date.now() - started;
|
|
401
|
+
const healthy = response.status >= 200 && response.status < 300;
|
|
402
|
+
const detail = healthy
|
|
403
|
+
? `Canary ${canary} returned HTTP ${response.status}; the registry recognizes a known registered domain.`
|
|
404
|
+
: `Canary ${canary} returned HTTP ${response.status}; .${tld} absence results are not trusted.`;
|
|
405
|
+
this.ledger?.record({
|
|
406
|
+
kind: "domain",
|
|
407
|
+
provider: providerName(endpoint),
|
|
408
|
+
task: "domain.canary",
|
|
409
|
+
requests: 1,
|
|
410
|
+
calls: 0,
|
|
411
|
+
failures: healthy ? 0 : 1,
|
|
412
|
+
latencyMs,
|
|
413
|
+
});
|
|
414
|
+
await cancelResponseBody(response);
|
|
415
|
+
return {
|
|
416
|
+
tld,
|
|
417
|
+
canary,
|
|
418
|
+
healthy,
|
|
419
|
+
checkedAt,
|
|
420
|
+
detail,
|
|
421
|
+
evidence: {
|
|
422
|
+
provider: providerName(endpoint),
|
|
423
|
+
sourceUrl: url,
|
|
424
|
+
checkedAt,
|
|
425
|
+
status: healthy ? "confirmed" : "unknown",
|
|
426
|
+
confidence: healthy ? confidence("high") : confidence("low"),
|
|
427
|
+
value: { canary, responseCode: response.status },
|
|
428
|
+
detail,
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
const detail = `Canary ${canary} failed: ${error instanceof RequestTimeoutError || (error instanceof Error && error.name === "AbortError") ? "timeout" : "network error"}; absence results are not trusted.`;
|
|
434
|
+
this.ledger?.record({
|
|
435
|
+
kind: "domain",
|
|
436
|
+
provider: providerName(endpoint),
|
|
437
|
+
task: "domain.canary",
|
|
438
|
+
requests: 1,
|
|
439
|
+
calls: 0,
|
|
440
|
+
failures: 1,
|
|
441
|
+
latencyMs: Date.now() - started,
|
|
442
|
+
});
|
|
443
|
+
return {
|
|
444
|
+
tld,
|
|
445
|
+
canary,
|
|
446
|
+
healthy: false,
|
|
447
|
+
checkedAt,
|
|
448
|
+
detail,
|
|
449
|
+
evidence: {
|
|
450
|
+
provider: providerName(endpoint),
|
|
451
|
+
sourceUrl: url,
|
|
452
|
+
checkedAt,
|
|
453
|
+
status: "unknown",
|
|
454
|
+
confidence: confidence("low"),
|
|
455
|
+
detail,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
})();
|
|
460
|
+
this.calibrationInflight.set(tld, operation);
|
|
461
|
+
try {
|
|
462
|
+
const result = await operation;
|
|
463
|
+
const ttl = result.healthy ? this.calibrationCacheTtlMs : this.unknownCacheTtlMs;
|
|
464
|
+
if (ttl > 0)
|
|
465
|
+
this.calibrationCache.set(tld, { value: result, expiresAt: this.now().getTime() + ttl });
|
|
466
|
+
return { ...result, evidence: { ...result.evidence } };
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
this.calibrationInflight.delete(tld);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async checkUncached(domain) {
|
|
473
|
+
const tld = domain.split(".").pop();
|
|
474
|
+
let endpoints;
|
|
475
|
+
try {
|
|
476
|
+
endpoints = await this.loadBootstrap();
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
return this.result(domain, "unknown", "IANA RDAP bootstrap", this.bootstrapUrl, "unknown", confidence("low"), `Registry discovery failed: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
480
|
+
}
|
|
481
|
+
const endpoint = endpoints[tld];
|
|
482
|
+
if (!endpoint) {
|
|
483
|
+
return this.result(domain, "unsupported", "IANA RDAP bootstrap", this.bootstrapUrl, "manual", confidence("high"), `No usable authoritative RDAP endpoint is published for .${tld}; verify manually.`);
|
|
484
|
+
}
|
|
485
|
+
const url = endpointForDomain(endpoint, domain);
|
|
486
|
+
const provider = providerName(endpoint);
|
|
487
|
+
const started = Date.now();
|
|
488
|
+
try {
|
|
489
|
+
const response = await fetchWithTimeout(this.fetcher, url, this.timeoutMs, {
|
|
490
|
+
headers: { accept: "application/rdap+json, application/json" },
|
|
491
|
+
});
|
|
492
|
+
const latencyMs = Date.now() - started;
|
|
493
|
+
this.ledger?.record({
|
|
494
|
+
kind: "domain",
|
|
495
|
+
provider,
|
|
496
|
+
task: "domain.lookup",
|
|
497
|
+
requests: 1,
|
|
498
|
+
calls: 0,
|
|
499
|
+
failures: response.status === 429 || response.status >= 500 ? 1 : 0,
|
|
500
|
+
latencyMs,
|
|
501
|
+
});
|
|
502
|
+
await cancelResponseBody(response);
|
|
503
|
+
if (response.status >= 200 && response.status < 300) {
|
|
504
|
+
return this.result(domain, "registered", provider, url, "confirmed", confidence("high"), `Authoritative registry RDAP returned HTTP ${response.status}; the domain is registered.`, response.status, latencyMs);
|
|
505
|
+
}
|
|
506
|
+
if (response.status === 404) {
|
|
507
|
+
const calibration = this.requireCalibration ? await this.calibrate(tld) : undefined;
|
|
508
|
+
if (this.requireCalibration && !calibration?.healthy) {
|
|
509
|
+
return this.result(domain, "unknown", provider, url, "unknown", confidence("low"), `Registry returned 404, but the known-registered canary failed calibration. ${calibration?.detail ?? ""}`.trim(), response.status, latencyMs);
|
|
510
|
+
}
|
|
511
|
+
return this.result(domain, "likely_available", provider, url, "inferred", this.requireCalibration ? confidence("high") : confidence("medium"), this.requireCalibration
|
|
512
|
+
? `Registry returned 404 after its known-registered canary passed. The domain is likely unregistered, not guaranteed purchasable.`
|
|
513
|
+
: "Registry returned 404 without canary enforcement. This is an unverified availability signal, not a purchase guarantee.", response.status, latencyMs);
|
|
514
|
+
}
|
|
515
|
+
if ([405, 406, 501].includes(response.status)) {
|
|
516
|
+
return this.result(domain, "unsupported", provider, url, "manual", confidence("medium"), `Registry endpoint returned HTTP ${response.status}; this lookup requires manual verification.`, response.status, latencyMs);
|
|
517
|
+
}
|
|
518
|
+
const detail = response.status === 429
|
|
519
|
+
? "Registry rate-limited the request; status remains unknown."
|
|
520
|
+
: `Registry returned ambiguous HTTP ${response.status}; status remains unknown.`;
|
|
521
|
+
return this.result(domain, "unknown", provider, url, "unknown", confidence("low"), detail, response.status, latencyMs);
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
const timeout = error instanceof RequestTimeoutError || (error instanceof Error && error.name === "AbortError");
|
|
525
|
+
this.ledger?.record({
|
|
526
|
+
kind: "domain",
|
|
527
|
+
provider,
|
|
528
|
+
task: "domain.lookup",
|
|
529
|
+
requests: 1,
|
|
530
|
+
calls: 0,
|
|
531
|
+
failures: 1,
|
|
532
|
+
latencyMs: Date.now() - started,
|
|
533
|
+
});
|
|
534
|
+
return this.result(domain, "unknown", provider, url, "unknown", confidence("low"), timeout ? "Registry request timed out; status remains unknown." : "Registry request failed; status remains unknown.", undefined, Date.now() - started);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async check(rawDomain) {
|
|
538
|
+
const domain = (0, normalize_1.normalizeDomain)(rawDomain);
|
|
539
|
+
const cached = await this.readResultCache(domain);
|
|
540
|
+
if (cached)
|
|
541
|
+
return cached;
|
|
542
|
+
const existing = this.inflight.get(domain);
|
|
543
|
+
if (existing)
|
|
544
|
+
return cloneResult(await existing);
|
|
545
|
+
const operation = (async () => {
|
|
546
|
+
const result = await this.checkUncached(domain);
|
|
547
|
+
await this.writeResultCache(domain, result);
|
|
548
|
+
return result;
|
|
549
|
+
})();
|
|
550
|
+
this.inflight.set(domain, operation);
|
|
551
|
+
try {
|
|
552
|
+
return cloneResult(await operation);
|
|
553
|
+
}
|
|
554
|
+
finally {
|
|
555
|
+
this.inflight.delete(domain);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
checkDomain(domain) {
|
|
559
|
+
return this.check(domain);
|
|
560
|
+
}
|
|
561
|
+
async checkMany(domains) {
|
|
562
|
+
if (!Array.isArray(domains))
|
|
563
|
+
throw new TypeError("Domains must be an array.");
|
|
564
|
+
if (domains.length > this.maxDomains)
|
|
565
|
+
throw new RangeError(`At most ${this.maxDomains} domains may be checked at once.`);
|
|
566
|
+
const results = new Array(domains.length);
|
|
567
|
+
let next = 0;
|
|
568
|
+
const workers = Array.from({ length: Math.min(this.concurrency, Math.max(1, domains.length)) }, async () => {
|
|
569
|
+
while (true) {
|
|
570
|
+
const index = next;
|
|
571
|
+
next += 1;
|
|
572
|
+
if (index >= domains.length)
|
|
573
|
+
break;
|
|
574
|
+
results[index] = await this.check(domains[index]);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
await Promise.all(workers);
|
|
578
|
+
return results;
|
|
579
|
+
}
|
|
580
|
+
checkDomains(domains) {
|
|
581
|
+
return this.checkMany(domains);
|
|
582
|
+
}
|
|
583
|
+
clearCache() {
|
|
584
|
+
this.resultCache.clear();
|
|
585
|
+
this.calibrationCache.clear();
|
|
586
|
+
this.bootstrap = undefined;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function createDomainChecker(options = {}) {
|
|
590
|
+
return new RdapDomainChecker(options);
|
|
591
|
+
}
|
|
592
|
+
let defaultChecker;
|
|
593
|
+
function resolveChecker(checkerOrOptions) {
|
|
594
|
+
if (checkerOrOptions && "check" in checkerOrOptions && typeof checkerOrOptions.check === "function") {
|
|
595
|
+
return checkerOrOptions;
|
|
596
|
+
}
|
|
597
|
+
if (checkerOrOptions)
|
|
598
|
+
return createDomainChecker(checkerOrOptions);
|
|
599
|
+
defaultChecker ??= createDomainChecker();
|
|
600
|
+
return defaultChecker;
|
|
601
|
+
}
|
|
602
|
+
function checkDomain(domain, checkerOrOptions) {
|
|
603
|
+
return resolveChecker(checkerOrOptions).check(domain);
|
|
604
|
+
}
|
|
605
|
+
function checkDomains(domains, checkerOrOptions) {
|
|
606
|
+
return resolveChecker(checkerOrOptions).checkMany(domains);
|
|
607
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyEditorialPenalty = applyEditorialPenalty;
|
|
4
|
+
function scoreLabel(score) {
|
|
5
|
+
if (score >= 84)
|
|
6
|
+
return "Strong signal";
|
|
7
|
+
if (score >= 70)
|
|
8
|
+
return "Worth investigating";
|
|
9
|
+
if (score >= 55)
|
|
10
|
+
return "Mixed signal";
|
|
11
|
+
return "Needs work";
|
|
12
|
+
}
|
|
13
|
+
/** Apply the ranker's structured concern instead of leaving it as detached prose. */
|
|
14
|
+
function applyEditorialPenalty(score, review) {
|
|
15
|
+
if (review.penalty <= 0)
|
|
16
|
+
return score;
|
|
17
|
+
const penalty = Math.max(0, Math.min(30, Math.round(review.penalty)));
|
|
18
|
+
const overall = Math.max(0, Math.round(score.overall - penalty));
|
|
19
|
+
const warning = `AI editorial ${review.verdict}: ${review.concern} (${penalty}-point penalty).`;
|
|
20
|
+
return {
|
|
21
|
+
...score,
|
|
22
|
+
overall,
|
|
23
|
+
label: scoreLabel(overall),
|
|
24
|
+
warnings: [...new Set([...(score.warnings ?? []), warning])],
|
|
25
|
+
summary: `${score.summary ?? "Preliminary score."} ${warning}`,
|
|
26
|
+
};
|
|
27
|
+
}
|