@fredcallagan/arn-spark 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +9 -0
- package/.opencode/plugins/arn-spark.js +272 -0
- package/package.json +17 -0
- package/plugins/arn-spark/.claude-plugin/plugin.json +9 -0
- package/plugins/arn-spark/LICENSE +21 -0
- package/plugins/arn-spark/README.md +25 -0
- package/plugins/arn-spark/agents/arn-spark-brand-strategist.md +299 -0
- package/plugins/arn-spark/agents/arn-spark-dev-env-builder.md +228 -0
- package/plugins/arn-spark/agents/arn-spark-doctor.md +92 -0
- package/plugins/arn-spark/agents/arn-spark-forensic-investigator.md +181 -0
- package/plugins/arn-spark/agents/arn-spark-market-researcher.md +232 -0
- package/plugins/arn-spark/agents/arn-spark-marketing-pm.md +225 -0
- package/plugins/arn-spark/agents/arn-spark-persona-architect.md +259 -0
- package/plugins/arn-spark/agents/arn-spark-persona-impersonator.md +183 -0
- package/plugins/arn-spark/agents/arn-spark-product-strategist.md +191 -0
- package/plugins/arn-spark/agents/arn-spark-prototype-builder.md +497 -0
- package/plugins/arn-spark/agents/arn-spark-scaffolder.md +228 -0
- package/plugins/arn-spark/agents/arn-spark-spike-runner.md +209 -0
- package/plugins/arn-spark/agents/arn-spark-style-capture.md +196 -0
- package/plugins/arn-spark/agents/arn-spark-tech-evaluator.md +229 -0
- package/plugins/arn-spark/agents/arn-spark-ui-interactor.md +235 -0
- package/plugins/arn-spark/agents/arn-spark-use-case-writer.md +280 -0
- package/plugins/arn-spark/agents/arn-spark-ux-judge.md +215 -0
- package/plugins/arn-spark/agents/arn-spark-ux-specialist.md +200 -0
- package/plugins/arn-spark/agents/arn-spark-visual-sketcher.md +285 -0
- package/plugins/arn-spark/agents/arn-spark-visual-test-engineer.md +224 -0
- package/plugins/arn-spark/references/copilot-tools.md +62 -0
- package/plugins/arn-spark/skills/arn-brainstorming/SKILL.md +520 -0
- package/plugins/arn-spark/skills/arn-brainstorming/references/add-feature-flow.md +155 -0
- package/plugins/arn-spark/skills/arn-spark-arch-vision/SKILL.md +226 -0
- package/plugins/arn-spark/skills/arn-spark-arch-vision/references/architecture-vision-template.md +153 -0
- package/plugins/arn-spark/skills/arn-spark-arch-vision/references/technology-evaluation-guide.md +86 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype/SKILL.md +471 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype/references/clickable-prototype-criteria.md +65 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype/references/journey-template.md +62 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype/references/review-report-template.md +75 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype/references/showcase-capture-guide.md +213 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype-teams/SKILL.md +642 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype-teams/references/debate-protocol.md +242 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype-teams/references/debate-review-report-template.md +161 -0
- package/plugins/arn-spark/skills/arn-spark-clickable-prototype-teams/references/expert-interaction-review-template.md +152 -0
- package/plugins/arn-spark/skills/arn-spark-concept-review/SKILL.md +350 -0
- package/plugins/arn-spark/skills/arn-spark-concept-review/references/conflict-resolution-protocol.md +145 -0
- package/plugins/arn-spark/skills/arn-spark-concept-review/references/review-report-template.md +185 -0
- package/plugins/arn-spark/skills/arn-spark-dev-setup/SKILL.md +366 -0
- package/plugins/arn-spark/skills/arn-spark-dev-setup/references/dev-setup-checklist.md +84 -0
- package/plugins/arn-spark/skills/arn-spark-dev-setup/references/dev-setup-template.md +205 -0
- package/plugins/arn-spark/skills/arn-spark-discover/SKILL.md +303 -0
- package/plugins/arn-spark/skills/arn-spark-discover/references/competitive-landscape-template.md +87 -0
- package/plugins/arn-spark/skills/arn-spark-discover/references/discovery-questions.md +120 -0
- package/plugins/arn-spark/skills/arn-spark-discover/references/persona-profile-template.md +97 -0
- package/plugins/arn-spark/skills/arn-spark-discover/references/product-concept-template.md +253 -0
- package/plugins/arn-spark/skills/arn-spark-ensure-config/SKILL.md +23 -0
- package/plugins/arn-spark/skills/arn-spark-ensure-config/references/ensure-config.md +388 -0
- package/plugins/arn-spark/skills/arn-spark-ensure-config/references/step-0-fast-path.md +25 -0
- package/plugins/arn-spark/skills/arn-spark-ensure-config/scripts/cache-check.sh +127 -0
- package/plugins/arn-spark/skills/arn-spark-feature-extract/SKILL.md +483 -0
- package/plugins/arn-spark/skills/arn-spark-feature-extract/references/feature-backlog-template.md +176 -0
- package/plugins/arn-spark/skills/arn-spark-feature-extract/references/feature-entry-template.md +209 -0
- package/plugins/arn-spark/skills/arn-spark-help/SKILL.md +149 -0
- package/plugins/arn-spark/skills/arn-spark-help/references/pipeline-map.md +211 -0
- package/plugins/arn-spark/skills/arn-spark-init/SKILL.md +312 -0
- package/plugins/arn-spark/skills/arn-spark-init/references/agent-models-presets/all-opus.md +23 -0
- package/plugins/arn-spark/skills/arn-spark-init/references/agent-models-presets/balanced.md +23 -0
- package/plugins/arn-spark/skills/arn-spark-init/references/bkt-setup.md +55 -0
- package/plugins/arn-spark/skills/arn-spark-init/references/jira-mcp-setup.md +61 -0
- package/plugins/arn-spark/skills/arn-spark-init/references/platform-labels.md +97 -0
- package/plugins/arn-spark/skills/arn-spark-naming/SKILL.md +275 -0
- package/plugins/arn-spark/skills/arn-spark-naming/references/creative-brief-template.md +146 -0
- package/plugins/arn-spark/skills/arn-spark-naming/references/naming-methodology.md +237 -0
- package/plugins/arn-spark/skills/arn-spark-naming/references/naming-report-template.md +122 -0
- package/plugins/arn-spark/skills/arn-spark-naming/references/trademark-databases.md +88 -0
- package/plugins/arn-spark/skills/arn-spark-naming/references/whois-server-map.md +164 -0
- package/plugins/arn-spark/skills/arn-spark-naming/scripts/whois-check.js +502 -0
- package/plugins/arn-spark/skills/arn-spark-naming/scripts/whois-check.py +533 -0
- package/plugins/arn-spark/skills/arn-spark-prototype-lock/SKILL.md +260 -0
- package/plugins/arn-spark/skills/arn-spark-prototype-lock/references/lock-report-template.md +68 -0
- package/plugins/arn-spark/skills/arn-spark-prototype-lock/references/pretooluse-hook-template.json +35 -0
- package/plugins/arn-spark/skills/arn-spark-prototype-lock/references/prototype-guardrail-rules.md +38 -0
- package/plugins/arn-spark/skills/arn-spark-report/SKILL.md +144 -0
- package/plugins/arn-spark/skills/arn-spark-report/references/issue-template.md +81 -0
- package/plugins/arn-spark/skills/arn-spark-report/references/spark-knowledge-base.md +293 -0
- package/plugins/arn-spark/skills/arn-spark-scaffold/SKILL.md +239 -0
- package/plugins/arn-spark/skills/arn-spark-scaffold/references/scaffold-checklist.md +79 -0
- package/plugins/arn-spark/skills/arn-spark-scaffold/references/scaffold-summary-template.md +74 -0
- package/plugins/arn-spark/skills/arn-spark-spike/SKILL.md +209 -0
- package/plugins/arn-spark/skills/arn-spark-spike/references/spike-report-template.md +123 -0
- package/plugins/arn-spark/skills/arn-spark-static-prototype/SKILL.md +362 -0
- package/plugins/arn-spark/skills/arn-spark-static-prototype/references/review-report-template.md +65 -0
- package/plugins/arn-spark/skills/arn-spark-static-prototype/references/showcase-capture-guide.md +153 -0
- package/plugins/arn-spark/skills/arn-spark-static-prototype/references/static-prototype-criteria.md +54 -0
- package/plugins/arn-spark/skills/arn-spark-static-prototype-teams/SKILL.md +518 -0
- package/plugins/arn-spark/skills/arn-spark-static-prototype-teams/references/debate-protocol.md +230 -0
- package/plugins/arn-spark/skills/arn-spark-static-prototype-teams/references/debate-review-report-template.md +148 -0
- package/plugins/arn-spark/skills/arn-spark-static-prototype-teams/references/expert-visual-review-template.md +130 -0
- package/plugins/arn-spark/skills/arn-spark-stress-competitive/SKILL.md +166 -0
- package/plugins/arn-spark/skills/arn-spark-stress-competitive/references/competitive-report-template.md +139 -0
- package/plugins/arn-spark/skills/arn-spark-stress-competitive/references/gap-analysis-framework.md +111 -0
- package/plugins/arn-spark/skills/arn-spark-stress-interview/SKILL.md +257 -0
- package/plugins/arn-spark/skills/arn-spark-stress-interview/references/interview-protocol.md +140 -0
- package/plugins/arn-spark/skills/arn-spark-stress-interview/references/interview-report-template.md +165 -0
- package/plugins/arn-spark/skills/arn-spark-stress-interview/references/persona-casting-spec.md +138 -0
- package/plugins/arn-spark/skills/arn-spark-stress-premortem/SKILL.md +181 -0
- package/plugins/arn-spark/skills/arn-spark-stress-premortem/references/premortem-protocol.md +112 -0
- package/plugins/arn-spark/skills/arn-spark-stress-premortem/references/premortem-report-template.md +158 -0
- package/plugins/arn-spark/skills/arn-spark-stress-prfaq/SKILL.md +206 -0
- package/plugins/arn-spark/skills/arn-spark-stress-prfaq/references/prfaq-report-template.md +139 -0
- package/plugins/arn-spark/skills/arn-spark-stress-prfaq/references/prfaq-workflow.md +118 -0
- package/plugins/arn-spark/skills/arn-spark-style-explore/SKILL.md +281 -0
- package/plugins/arn-spark/skills/arn-spark-style-explore/references/style-brief-template.md +198 -0
- package/plugins/arn-spark/skills/arn-spark-use-cases/SKILL.md +359 -0
- package/plugins/arn-spark/skills/arn-spark-use-cases/references/expert-review-template.md +94 -0
- package/plugins/arn-spark/skills/arn-spark-use-cases/references/review-protocol.md +150 -0
- package/plugins/arn-spark/skills/arn-spark-use-cases/references/use-case-index-template.md +108 -0
- package/plugins/arn-spark/skills/arn-spark-use-cases/references/use-case-template.md +125 -0
- package/plugins/arn-spark/skills/arn-spark-use-cases-teams/SKILL.md +306 -0
- package/plugins/arn-spark/skills/arn-spark-use-cases-teams/references/debate-protocol.md +272 -0
- package/plugins/arn-spark/skills/arn-spark-use-cases-teams/references/review-report-template.md +112 -0
- package/plugins/arn-spark/skills/arn-spark-visual-readiness/SKILL.md +293 -0
- package/plugins/arn-spark/skills/arn-spark-visual-readiness/references/readiness-checklist.md +196 -0
- package/plugins/arn-spark/skills/arn-spark-visual-sketch/SKILL.md +376 -0
- package/plugins/arn-spark/skills/arn-spark-visual-sketch/references/aesthetic-philosophy.md +210 -0
- package/plugins/arn-spark/skills/arn-spark-visual-sketch/references/sketch-gallery-guide.md +282 -0
- package/plugins/arn-spark/skills/arn-spark-visual-sketch/references/visual-direction-template.md +174 -0
- package/plugins/arn-spark/skills/arn-spark-visual-strategy/SKILL.md +447 -0
- package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/baseline-capture-script-template.js +89 -0
- package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/journey-schema.md +375 -0
- package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/spike-checklist.md +122 -0
- package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/strategy-layers-guide.md +132 -0
- package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/visual-strategy-template.md +141 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Domain availability checker using RDAP (primary) with WHOIS fallback.
|
|
4
|
+
* Reads JSON from stdin, outputs JSON to stdout.
|
|
5
|
+
* Uses only Node.js stdlib (https, net, child_process).
|
|
6
|
+
*
|
|
7
|
+
* RDAP (Registration Data Access Protocol) is the IETF standard replacement
|
|
8
|
+
* for port-43 WHOIS. It returns structured JSON over HTTPS, with 404 = available.
|
|
9
|
+
* IANA bootstrap file provides RDAP servers for all TLDs — no hardcoded map needed.
|
|
10
|
+
*
|
|
11
|
+
* Fallback chain per domain:
|
|
12
|
+
* 1. RDAP via IANA bootstrap → structured JSON, 200=taken, 404=available
|
|
13
|
+
* 2. Port-43 WHOIS via socket → pattern matching
|
|
14
|
+
* 3. System `whois` command → pattern matching on stdout
|
|
15
|
+
*
|
|
16
|
+
* Circuit breaker: stops ALL remaining queries on RDAP 429 only.
|
|
17
|
+
* Port-43 rate limits are per-server — they skip the domain, not the batch.
|
|
18
|
+
*
|
|
19
|
+
* Input: {"domains": ["example.com", ...], "delay_seconds": 2}
|
|
20
|
+
* Output: [{"domain": "example.com", "tld": "com", "available": true, ...}, ...]
|
|
21
|
+
*
|
|
22
|
+
* Exit code 0 = all queries completed. Exit code 1 = stopped early (rate limit).
|
|
23
|
+
* Exit code 2 = invalid input.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const https = require('https');
|
|
27
|
+
const net = require('net');
|
|
28
|
+
const { execFileSync, execSync } = require('child_process');
|
|
29
|
+
|
|
30
|
+
// ─── RDAP Bootstrap ──────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const IANA_BOOTSTRAP_URL = 'https://data.iana.org/rdap/dns.json';
|
|
33
|
+
const rdapServers = {};
|
|
34
|
+
let bootstrapLoaded = false;
|
|
35
|
+
|
|
36
|
+
function loadRdapBootstrap() {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
if (bootstrapLoaded) return resolve();
|
|
39
|
+
|
|
40
|
+
log('Loading IANA RDAP bootstrap...');
|
|
41
|
+
httpsGetJson(IANA_BOOTSTRAP_URL)
|
|
42
|
+
.then((data) => {
|
|
43
|
+
for (const service of data.services || []) {
|
|
44
|
+
const tlds = service[0];
|
|
45
|
+
const servers = service[1];
|
|
46
|
+
if (servers && servers.length > 0) {
|
|
47
|
+
const baseUrl = servers[0].replace(/\/+$/, '');
|
|
48
|
+
for (const tld of tlds) {
|
|
49
|
+
rdapServers[tld.toLowerCase()] = baseUrl;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
log(` Loaded ${Object.keys(rdapServers).length} TLD RDAP servers from bootstrap`);
|
|
54
|
+
bootstrapLoaded = true;
|
|
55
|
+
resolve();
|
|
56
|
+
})
|
|
57
|
+
.catch((err) => {
|
|
58
|
+
log(` WARNING: RDAP bootstrap failed (${err.message}). Will use WHOIS fallback.`);
|
|
59
|
+
bootstrapLoaded = true;
|
|
60
|
+
resolve();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getRdapServer(tld) {
|
|
66
|
+
return rdapServers[tld.toLowerCase()] || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Port-43 WHOIS (fallback) ────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const WHOIS_SERVERS = {
|
|
72
|
+
com: 'whois.verisign-grs.com',
|
|
73
|
+
net: 'whois.verisign-grs.com',
|
|
74
|
+
org: 'whois.pir.org',
|
|
75
|
+
io: 'whois.nic.io',
|
|
76
|
+
co: 'whois.registry.co',
|
|
77
|
+
// .dev and .app are RDAP-only — no port-43 WHOIS server exists
|
|
78
|
+
ai: 'whois.nic.ai',
|
|
79
|
+
me: 'whois.nic.me',
|
|
80
|
+
xyz: 'whois.nic.xyz',
|
|
81
|
+
tech: 'whois.nic.tech',
|
|
82
|
+
so: 'whois.nic.so',
|
|
83
|
+
sh: 'whois.nic.sh',
|
|
84
|
+
to: 'whois.tonic.to',
|
|
85
|
+
gg: 'whois.gg',
|
|
86
|
+
ly: 'whois.nic.ly',
|
|
87
|
+
is: 'whois.isnic.is',
|
|
88
|
+
fm: 'whois.nic.fm',
|
|
89
|
+
tv: 'whois.nic.tv',
|
|
90
|
+
cc: 'ccwhois.verisign-grs.com',
|
|
91
|
+
de: 'whois.denic.de',
|
|
92
|
+
fr: 'whois.nic.fr',
|
|
93
|
+
eu: 'whois.eu',
|
|
94
|
+
us: 'whois.nic.us',
|
|
95
|
+
ca: 'whois.cira.ca',
|
|
96
|
+
uk: 'whois.nic.uk',
|
|
97
|
+
'co.uk': 'whois.nic.uk',
|
|
98
|
+
'org.uk': 'whois.nic.uk',
|
|
99
|
+
// EU market TLDs
|
|
100
|
+
it: 'whois.nic.it',
|
|
101
|
+
es: 'whois.nic.es',
|
|
102
|
+
nl: 'whois.domain-registry.nl',
|
|
103
|
+
pt: 'whois.dns.pt',
|
|
104
|
+
pl: 'whois.dns.pl',
|
|
105
|
+
se: 'whois.iis.se',
|
|
106
|
+
ch: 'whois.nic.ch',
|
|
107
|
+
at: 'whois.nic.at',
|
|
108
|
+
be: 'whois.dns.be',
|
|
109
|
+
// Asia-Pacific TLDs
|
|
110
|
+
jp: 'whois.jprs.jp',
|
|
111
|
+
in: 'whois.registry.in',
|
|
112
|
+
au: 'whois.auda.org.au',
|
|
113
|
+
kr: 'whois.kr',
|
|
114
|
+
// Latin America TLDs
|
|
115
|
+
br: 'whois.registro.br',
|
|
116
|
+
mx: 'whois.mx',
|
|
117
|
+
ar: 'whois.nic.ar',
|
|
118
|
+
cl: 'whois.nic.cl',
|
|
119
|
+
// Compound ccTLDs
|
|
120
|
+
'com.br': 'whois.registro.br',
|
|
121
|
+
'com.au': 'whois.auda.org.au',
|
|
122
|
+
'co.jp': 'whois.jprs.jp',
|
|
123
|
+
'co.in': 'whois.registry.in',
|
|
124
|
+
'com.mx': 'whois.mx',
|
|
125
|
+
'com.ar': 'whois.nic.ar',
|
|
126
|
+
'co.kr': 'whois.kr',
|
|
127
|
+
'com.pt': 'whois.dns.pt',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const AVAILABLE_PATTERNS = [
|
|
131
|
+
'no match for', 'not found', 'no entries found', 'no data found',
|
|
132
|
+
'domain not found', 'no information available', 'status: available',
|
|
133
|
+
'the queried object does not exist', 'no object found',
|
|
134
|
+
'is available for', 'domain name has not been registered',
|
|
135
|
+
'status: free',
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const TAKEN_PATTERNS = [
|
|
139
|
+
'domain name:', 'registrar:', 'creation date:', 'registry expiry date:',
|
|
140
|
+
'updated date:', 'name server:', 'registrant:', 'status: ok',
|
|
141
|
+
'status: active', 'status: clienttransferprohibited',
|
|
142
|
+
'nserver:', 'status: connect', 'registered on:', 'expires on:',
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const RATE_LIMIT_PATTERNS = [
|
|
146
|
+
'quota exceeded', 'rate limit', 'too many requests',
|
|
147
|
+
'please try again later', 'your request has been throttled',
|
|
148
|
+
'connection limit reached', 'access denied', 'query rate exceeded',
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const NOT_SUPPORTED_PATTERNS = [
|
|
152
|
+
'tld is not supported', 'this tld has no whois server',
|
|
153
|
+
'no whois server is known', 'unknown tld',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const QUERY_TIMEOUT = 10000;
|
|
157
|
+
|
|
158
|
+
// ─── Shared Utilities ────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
function log(msg) {
|
|
161
|
+
process.stderr.write(msg + '\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sleep(ms) {
|
|
165
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getTld(domain) {
|
|
169
|
+
const parts = domain.toLowerCase().split('.');
|
|
170
|
+
if (parts.length < 2) return null;
|
|
171
|
+
if (parts.length >= 3) {
|
|
172
|
+
const compound = parts[parts.length - 2] + '.' + parts[parts.length - 1];
|
|
173
|
+
if (WHOIS_SERVERS[compound] || rdapServers[compound]) return compound;
|
|
174
|
+
}
|
|
175
|
+
return parts[parts.length - 1];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function manualCheckUrl(domain) {
|
|
179
|
+
return `https://www.whois.com/whois/${domain}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class RateLimitError extends Error {
|
|
183
|
+
constructor(message) {
|
|
184
|
+
super(message);
|
|
185
|
+
this.name = 'RateLimitError';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── HTTPS GET helper ────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
function httpsGetJson(url, timeout = QUERY_TIMEOUT) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const req = https.get(url, { headers: { Accept: 'application/rdap+json, application/json' }, timeout }, (res) => {
|
|
194
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
195
|
+
// Follow redirects (rdap.org uses 302)
|
|
196
|
+
const location = res.headers.location;
|
|
197
|
+
if (location) {
|
|
198
|
+
httpsGetJson(location, timeout).then(resolve).catch(reject);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (res.statusCode === 404) {
|
|
204
|
+
reject({ code: 404 });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (res.statusCode === 429) {
|
|
208
|
+
reject({ code: 429 });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (res.statusCode >= 400) {
|
|
212
|
+
reject({ code: res.statusCode });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let body = '';
|
|
217
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
218
|
+
res.on('end', () => {
|
|
219
|
+
try {
|
|
220
|
+
resolve(JSON.parse(body));
|
|
221
|
+
} catch (e) {
|
|
222
|
+
reject(new Error(`Invalid JSON from ${url}`));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
req.on('error', reject);
|
|
228
|
+
req.on('timeout', () => {
|
|
229
|
+
req.destroy();
|
|
230
|
+
reject(new Error(`Timeout fetching ${url}`));
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── RDAP Query ──────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
async function rdapQuery(domain, tld) {
|
|
238
|
+
const baseUrl = getRdapServer(tld);
|
|
239
|
+
if (!baseUrl) return null;
|
|
240
|
+
|
|
241
|
+
const url = `${baseUrl}/domain/${domain}`;
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const data = await httpsGetJson(url);
|
|
245
|
+
|
|
246
|
+
// 200 = domain exists (taken)
|
|
247
|
+
let registrar = null;
|
|
248
|
+
for (const entity of data.entities || []) {
|
|
249
|
+
if ((entity.roles || []).includes('registrar')) {
|
|
250
|
+
const vcard = entity.vcardArray;
|
|
251
|
+
if (Array.isArray(vcard) && vcard.length >= 2) {
|
|
252
|
+
for (const field of vcard[1]) {
|
|
253
|
+
if (field[0] === 'fn') { registrar = field[3]; break; }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!registrar) registrar = entity.handle || null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { domain, tld, available: false, registrar, method: 'rdap', error: null };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (err.code === 404) {
|
|
263
|
+
return { domain, tld, available: true, registrar: null, method: 'rdap', error: null };
|
|
264
|
+
}
|
|
265
|
+
if (err.code === 429) {
|
|
266
|
+
throw new RateLimitError(`RDAP rate limit (429) from ${baseUrl} for ${domain}`);
|
|
267
|
+
}
|
|
268
|
+
// Other errors — fall through to WHOIS
|
|
269
|
+
log(` RDAP failed for ${domain} (${err.message || err.code}), falling back to WHOIS...`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Port-43 WHOIS Query ─────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
function whoisQuery(domain, server, port = 43, timeout = QUERY_TIMEOUT) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const chunks = [];
|
|
279
|
+
const client = new net.Socket();
|
|
280
|
+
|
|
281
|
+
const timer = setTimeout(() => {
|
|
282
|
+
client.destroy();
|
|
283
|
+
reject(new Error(`Timeout connecting to ${server}`));
|
|
284
|
+
}, timeout);
|
|
285
|
+
|
|
286
|
+
client.on('error', (err) => {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
reject(err);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
client.on('data', (data) => { chunks.push(data); });
|
|
292
|
+
|
|
293
|
+
client.on('end', () => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
client.connect(port, server, () => {
|
|
299
|
+
client.write(domain + '\r\n');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function parseWhoisAvailability(response, tld) {
|
|
305
|
+
if (!response || !response.trim()) return null;
|
|
306
|
+
const lower = response.toLowerCase();
|
|
307
|
+
|
|
308
|
+
for (const p of NOT_SUPPORTED_PATTERNS) { if (lower.includes(p)) return null; }
|
|
309
|
+
for (const p of RATE_LIMIT_PATTERNS) { if (lower.includes(p)) return null; }
|
|
310
|
+
|
|
311
|
+
const availCount = AVAILABLE_PATTERNS.filter((p) => lower.includes(p)).length;
|
|
312
|
+
const takenCount = TAKEN_PATTERNS.filter((p) => lower.includes(p)).length;
|
|
313
|
+
|
|
314
|
+
// Verisign fix: .com/.net/.cc require zero TAKEN patterns alongside AVAILABLE
|
|
315
|
+
if (['com', 'net', 'cc'].includes(tld)) {
|
|
316
|
+
if (availCount > 0 && takenCount === 0) return true;
|
|
317
|
+
if (takenCount >= 1) return false;
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (availCount > 0 && takenCount === 0) return true;
|
|
322
|
+
if (takenCount >= 2) return false;
|
|
323
|
+
if (availCount > 0 && takenCount > 0) return takenCount >= 2 ? false : null;
|
|
324
|
+
if (takenCount >= 1) return false;
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function extractRegistrar(response) {
|
|
329
|
+
for (const line of response.split('\n')) {
|
|
330
|
+
const stripped = line.trim();
|
|
331
|
+
if (stripped.toLowerCase().startsWith('registrar:')) {
|
|
332
|
+
return stripped.split(':').slice(1).join(':').trim();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function whoisPort43Query(domain, tld) {
|
|
339
|
+
const server = WHOIS_SERVERS[tld];
|
|
340
|
+
if (!server) return null;
|
|
341
|
+
|
|
342
|
+
let response;
|
|
343
|
+
try {
|
|
344
|
+
response = await whoisQuery(domain, server);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
log(` WHOIS port-43 failed for ${domain} via ${server}: ${err.message}`);
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!response || !response.trim()) return null;
|
|
351
|
+
|
|
352
|
+
// Per-server rate limit — NOT circuit-breaking
|
|
353
|
+
const lower = response.toLowerCase();
|
|
354
|
+
for (const p of RATE_LIMIT_PATTERNS) {
|
|
355
|
+
if (lower.includes(p)) {
|
|
356
|
+
log(` WHOIS rate limit from ${server} for ${domain} (per-server, continuing batch)`);
|
|
357
|
+
return { domain, tld, available: null, registrar: null, method: 'whois', error: `Rate limited by ${server}` };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const available = parseWhoisAvailability(response, tld);
|
|
362
|
+
const registrar = !available ? extractRegistrar(response) : null;
|
|
363
|
+
|
|
364
|
+
return { domain, tld, available, registrar, method: 'whois', error: null };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ─── System WHOIS Fallback ───────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
function systemWhoisQuery(domain, tld) {
|
|
370
|
+
try {
|
|
371
|
+
// Check if whois is available (cross-platform)
|
|
372
|
+
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
373
|
+
execSync(`${whichCmd} whois`, { stdio: 'pipe' });
|
|
374
|
+
} catch {
|
|
375
|
+
return null; // whois not available (normal on Windows)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const output = execFileSync('whois', [domain], { timeout: 15000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
380
|
+
if (!output || !output.trim()) return null;
|
|
381
|
+
|
|
382
|
+
const available = parseWhoisAvailability(output, tld);
|
|
383
|
+
const registrar = !available ? extractRegistrar(output) : null;
|
|
384
|
+
|
|
385
|
+
return { domain, tld, available, registrar, method: 'system-whois', error: null };
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── Main Check (cascading fallback) ─────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
async function checkDomain(domain) {
|
|
394
|
+
const tld = getTld(domain);
|
|
395
|
+
if (!tld) {
|
|
396
|
+
return { domain, tld: null, available: null, registrar: null, method: 'none', error: 'Could not extract TLD' };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
log(`Checking ${domain}...`);
|
|
400
|
+
|
|
401
|
+
// 1. Try RDAP (primary)
|
|
402
|
+
const rdapResult = await rdapQuery(domain, tld);
|
|
403
|
+
if (rdapResult) {
|
|
404
|
+
const status = rdapResult.available === true ? 'available' : rdapResult.available === false ? 'taken' : 'unknown';
|
|
405
|
+
log(` ${domain}: ${status} (via RDAP)`);
|
|
406
|
+
return rdapResult;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 2. Try port-43 WHOIS (fallback)
|
|
410
|
+
const whoisResult = await whoisPort43Query(domain, tld);
|
|
411
|
+
if (whoisResult) {
|
|
412
|
+
const status = whoisResult.available === true ? 'available' : whoisResult.available === false ? 'taken' : 'unknown';
|
|
413
|
+
log(` ${domain}: ${status} (via WHOIS)`);
|
|
414
|
+
return whoisResult;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 3. Try system whois (last resort)
|
|
418
|
+
const sysResult = systemWhoisQuery(domain, tld);
|
|
419
|
+
if (sysResult) {
|
|
420
|
+
const status = sysResult.available === true ? 'available' : sysResult.available === false ? 'taken' : 'unknown';
|
|
421
|
+
log(` ${domain}: ${status} (via system whois)`);
|
|
422
|
+
return sysResult;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
log(` ${domain}: UNKNOWN (all methods failed)`);
|
|
426
|
+
return { domain, tld, available: null, registrar: null, method: 'none', error: 'All lookup methods failed' };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ─── Entry Point ─────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
async function main() {
|
|
432
|
+
let raw = '';
|
|
433
|
+
for await (const chunk of process.stdin) { raw += chunk; }
|
|
434
|
+
|
|
435
|
+
let config;
|
|
436
|
+
try {
|
|
437
|
+
config = JSON.parse(raw);
|
|
438
|
+
} catch (e) {
|
|
439
|
+
log(`ERROR: Invalid JSON input: ${e.message}`);
|
|
440
|
+
process.exit(2);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const domains = config.domains || [];
|
|
444
|
+
let delaySeconds = config.delay_seconds || 2;
|
|
445
|
+
|
|
446
|
+
if (!domains.length) {
|
|
447
|
+
process.stdout.write('[]\n');
|
|
448
|
+
process.exit(0);
|
|
449
|
+
}
|
|
450
|
+
if (delaySeconds < 1) delaySeconds = 1;
|
|
451
|
+
|
|
452
|
+
await loadRdapBootstrap();
|
|
453
|
+
|
|
454
|
+
log(`Checking ${domains.length} domain(s) with ${delaySeconds}s delay...`);
|
|
455
|
+
log(`RDAP servers loaded: ${Object.keys(rdapServers).length} TLDs`);
|
|
456
|
+
|
|
457
|
+
const results = [];
|
|
458
|
+
let circuitBroken = false;
|
|
459
|
+
|
|
460
|
+
for (let i = 0; i < domains.length; i++) {
|
|
461
|
+
const domain = domains[i].trim().toLowerCase();
|
|
462
|
+
if (!domain) continue;
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const result = await checkDomain(domain);
|
|
466
|
+
if (result.available === null && result.error) {
|
|
467
|
+
result.manual_url = manualCheckUrl(domain);
|
|
468
|
+
}
|
|
469
|
+
results.push(result);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (err instanceof RateLimitError) {
|
|
472
|
+
log(` CIRCUIT BREAKER (rate limit): ${err.message}`);
|
|
473
|
+
log(` Stopping. ${domains.length - i - 1} domain(s) not checked.`);
|
|
474
|
+
results.push({
|
|
475
|
+
domain, tld: getTld(domain), available: null, registrar: null,
|
|
476
|
+
method: 'none', error: err.message, manual_url: manualCheckUrl(domain),
|
|
477
|
+
});
|
|
478
|
+
circuitBroken = true;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (i < domains.length - 1) await sleep(delaySeconds * 1000);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
488
|
+
|
|
489
|
+
// Summary
|
|
490
|
+
const available = results.filter((r) => r.available === true).length;
|
|
491
|
+
const taken = results.filter((r) => r.available === false).length;
|
|
492
|
+
const unknown = results.filter((r) => r.available === null).length;
|
|
493
|
+
const methods = {};
|
|
494
|
+
for (const r of results) { methods[r.method] = (methods[r.method] || 0) + 1; }
|
|
495
|
+
|
|
496
|
+
log(`\nResults: ${results.length} checked — ${available} available, ${taken} taken, ${unknown} unknown`);
|
|
497
|
+
log(`Methods: ${Object.entries(methods).sort().map(([m, c]) => `${m}=${c}`).join(', ')}`);
|
|
498
|
+
|
|
499
|
+
process.exit(circuitBroken ? 1 : 0);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
main();
|