@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,533 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Domain availability checker using RDAP (primary) with WHOIS fallback.
|
|
4
|
+
Reads JSON from stdin, outputs JSON to stdout.
|
|
5
|
+
Uses only Python stdlib (urllib, socket, json, sys, time, subprocess, shutil).
|
|
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 (same as whois-check.py)
|
|
14
|
+
3. System `whois` command → pattern matching on stdout
|
|
15
|
+
|
|
16
|
+
Circuit breaker: stops ALL remaining queries on RATE LIMIT responses only.
|
|
17
|
+
DNS/connection errors are per-domain — they try the next fallback, not break 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
|
+
import json
|
|
27
|
+
import socket
|
|
28
|
+
import subprocess
|
|
29
|
+
import shutil
|
|
30
|
+
import ssl
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
import urllib.error
|
|
34
|
+
import urllib.request
|
|
35
|
+
|
|
36
|
+
# ─── RDAP Bootstrap ──────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
IANA_BOOTSTRAP_URL = "https://data.iana.org/rdap/dns.json"
|
|
39
|
+
|
|
40
|
+
# Cache: populated once from IANA bootstrap, maps TLD → RDAP base URL
|
|
41
|
+
_rdap_servers = {}
|
|
42
|
+
_bootstrap_loaded = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_rdap_bootstrap():
|
|
46
|
+
"""
|
|
47
|
+
Fetch the IANA RDAP bootstrap file and build a TLD → RDAP server map.
|
|
48
|
+
Called once at startup. Failures are non-fatal — we fall back to WHOIS.
|
|
49
|
+
"""
|
|
50
|
+
global _rdap_servers, _bootstrap_loaded
|
|
51
|
+
if _bootstrap_loaded:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
log("Loading IANA RDAP bootstrap...")
|
|
56
|
+
req = urllib.request.Request(IANA_BOOTSTRAP_URL)
|
|
57
|
+
ctx = ssl.create_default_context()
|
|
58
|
+
with urllib.request.urlopen(req, timeout=10, context=ctx) as resp:
|
|
59
|
+
data = json.loads(resp.read())
|
|
60
|
+
|
|
61
|
+
for service in data.get("services", []):
|
|
62
|
+
tlds = service[0] # list of TLD strings
|
|
63
|
+
servers = service[1] # list of RDAP base URLs
|
|
64
|
+
if servers:
|
|
65
|
+
base_url = servers[0].rstrip("/")
|
|
66
|
+
for tld in tlds:
|
|
67
|
+
_rdap_servers[tld.lower()] = base_url
|
|
68
|
+
|
|
69
|
+
log(f" Loaded {len(_rdap_servers)} TLD RDAP servers from bootstrap")
|
|
70
|
+
_bootstrap_loaded = True
|
|
71
|
+
except Exception as e:
|
|
72
|
+
log(f" WARNING: RDAP bootstrap failed ({e}). Will use WHOIS fallback for all domains.")
|
|
73
|
+
_bootstrap_loaded = True # Don't retry
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_rdap_server(tld):
|
|
77
|
+
"""Look up the RDAP base URL for a TLD from the bootstrap cache."""
|
|
78
|
+
return _rdap_servers.get(tld.lower())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ─── Port-43 WHOIS (fallback) ────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
WHOIS_SERVERS = {
|
|
84
|
+
"com": "whois.verisign-grs.com",
|
|
85
|
+
"net": "whois.verisign-grs.com",
|
|
86
|
+
"org": "whois.pir.org",
|
|
87
|
+
"io": "whois.nic.io",
|
|
88
|
+
"co": "whois.registry.co",
|
|
89
|
+
# .dev and .app are RDAP-only — no port-43 WHOIS server exists
|
|
90
|
+
"ai": "whois.nic.ai",
|
|
91
|
+
"me": "whois.nic.me",
|
|
92
|
+
"xyz": "whois.nic.xyz",
|
|
93
|
+
"tech": "whois.nic.tech",
|
|
94
|
+
"so": "whois.nic.so",
|
|
95
|
+
"sh": "whois.nic.sh",
|
|
96
|
+
"to": "whois.tonic.to",
|
|
97
|
+
"gg": "whois.gg",
|
|
98
|
+
"ly": "whois.nic.ly",
|
|
99
|
+
"is": "whois.isnic.is",
|
|
100
|
+
"fm": "whois.nic.fm",
|
|
101
|
+
"tv": "whois.nic.tv",
|
|
102
|
+
"cc": "ccwhois.verisign-grs.com",
|
|
103
|
+
"de": "whois.denic.de",
|
|
104
|
+
"fr": "whois.nic.fr",
|
|
105
|
+
"eu": "whois.eu",
|
|
106
|
+
"us": "whois.nic.us",
|
|
107
|
+
"ca": "whois.cira.ca",
|
|
108
|
+
"uk": "whois.nic.uk",
|
|
109
|
+
"co.uk": "whois.nic.uk",
|
|
110
|
+
"org.uk": "whois.nic.uk",
|
|
111
|
+
# EU market TLDs
|
|
112
|
+
"it": "whois.nic.it",
|
|
113
|
+
"es": "whois.nic.es",
|
|
114
|
+
"nl": "whois.domain-registry.nl",
|
|
115
|
+
"pt": "whois.dns.pt",
|
|
116
|
+
"pl": "whois.dns.pl",
|
|
117
|
+
"se": "whois.iis.se",
|
|
118
|
+
"ch": "whois.nic.ch",
|
|
119
|
+
"at": "whois.nic.at",
|
|
120
|
+
"be": "whois.dns.be",
|
|
121
|
+
# Asia-Pacific TLDs
|
|
122
|
+
"jp": "whois.jprs.jp",
|
|
123
|
+
"in": "whois.registry.in",
|
|
124
|
+
"au": "whois.auda.org.au",
|
|
125
|
+
"kr": "whois.kr",
|
|
126
|
+
# Latin America TLDs
|
|
127
|
+
"br": "whois.registro.br",
|
|
128
|
+
"mx": "whois.mx",
|
|
129
|
+
"ar": "whois.nic.ar",
|
|
130
|
+
"cl": "whois.nic.cl",
|
|
131
|
+
# Compound ccTLDs
|
|
132
|
+
"com.br": "whois.registro.br",
|
|
133
|
+
"com.au": "whois.auda.org.au",
|
|
134
|
+
"co.jp": "whois.jprs.jp",
|
|
135
|
+
"co.in": "whois.registry.in",
|
|
136
|
+
"com.mx": "whois.mx",
|
|
137
|
+
"com.ar": "whois.nic.ar",
|
|
138
|
+
"co.kr": "whois.kr",
|
|
139
|
+
"com.pt": "whois.dns.pt",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
AVAILABLE_PATTERNS = [
|
|
143
|
+
"no match for", "not found", "no entries found", "no data found",
|
|
144
|
+
"domain not found", "no information available", "status: available",
|
|
145
|
+
"the queried object does not exist", "no object found",
|
|
146
|
+
"is available for", "domain name has not been registered",
|
|
147
|
+
"status: free",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
TAKEN_PATTERNS = [
|
|
151
|
+
"domain name:", "registrar:", "creation date:", "registry expiry date:",
|
|
152
|
+
"updated date:", "name server:", "registrant:", "status: ok",
|
|
153
|
+
"status: active", "status: clienttransferprohibited",
|
|
154
|
+
"nserver:", "status: connect", "registered on:", "expires on:",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
RATE_LIMIT_PATTERNS = [
|
|
158
|
+
"quota exceeded", "rate limit", "too many requests",
|
|
159
|
+
"please try again later", "your request has been throttled",
|
|
160
|
+
"connection limit reached", "access denied", "query rate exceeded",
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
NOT_SUPPORTED_PATTERNS = [
|
|
164
|
+
"tld is not supported", "this tld has no whois server",
|
|
165
|
+
"no whois server is known", "unknown tld",
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
QUERY_TIMEOUT = 10
|
|
169
|
+
DNS_TIMEOUT = 5
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ─── Shared Utilities ────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
def log(msg):
|
|
175
|
+
print(msg, file=sys.stderr, flush=True)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_tld(domain):
|
|
179
|
+
parts = domain.lower().split(".")
|
|
180
|
+
if len(parts) < 2:
|
|
181
|
+
return None
|
|
182
|
+
if len(parts) >= 3:
|
|
183
|
+
compound = parts[-2] + "." + parts[-1]
|
|
184
|
+
if compound in WHOIS_SERVERS or compound in _rdap_servers:
|
|
185
|
+
return compound
|
|
186
|
+
return parts[-1]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def manual_check_url(domain):
|
|
190
|
+
return f"https://www.whois.com/whois/{domain}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RateLimitError(Exception):
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ─── RDAP Query ──────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
def rdap_query(domain, tld):
|
|
200
|
+
"""
|
|
201
|
+
Query RDAP for a domain. Returns a result dict.
|
|
202
|
+
Raises RateLimitError on 429. Returns None on failure (fall through to WHOIS).
|
|
203
|
+
"""
|
|
204
|
+
base_url = get_rdap_server(tld)
|
|
205
|
+
if not base_url:
|
|
206
|
+
return None # No RDAP server known — fall through
|
|
207
|
+
|
|
208
|
+
url = f"{base_url}/domain/{domain}"
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
req = urllib.request.Request(url, headers={
|
|
212
|
+
"Accept": "application/rdap+json, application/json",
|
|
213
|
+
})
|
|
214
|
+
ctx = ssl.create_default_context()
|
|
215
|
+
with urllib.request.urlopen(req, timeout=QUERY_TIMEOUT, context=ctx) as resp:
|
|
216
|
+
data = json.loads(resp.read())
|
|
217
|
+
|
|
218
|
+
# 200 = domain exists (taken)
|
|
219
|
+
registrar = None
|
|
220
|
+
entities = data.get("entities", [])
|
|
221
|
+
for entity in entities:
|
|
222
|
+
roles = entity.get("roles", [])
|
|
223
|
+
if "registrar" in roles:
|
|
224
|
+
vcard = entity.get("vcardArray", [])
|
|
225
|
+
if len(vcard) >= 2:
|
|
226
|
+
for field in vcard[1]:
|
|
227
|
+
if field[0] == "fn":
|
|
228
|
+
registrar = field[3]
|
|
229
|
+
break
|
|
230
|
+
if not registrar:
|
|
231
|
+
registrar = entity.get("handle", None)
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"domain": domain,
|
|
235
|
+
"tld": tld,
|
|
236
|
+
"available": False,
|
|
237
|
+
"registrar": registrar,
|
|
238
|
+
"method": "rdap",
|
|
239
|
+
"error": None,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
except urllib.error.HTTPError as e:
|
|
243
|
+
if e.code == 404:
|
|
244
|
+
# 404 = domain not found in registry = available
|
|
245
|
+
return {
|
|
246
|
+
"domain": domain,
|
|
247
|
+
"tld": tld,
|
|
248
|
+
"available": True,
|
|
249
|
+
"registrar": None,
|
|
250
|
+
"method": "rdap",
|
|
251
|
+
"error": None,
|
|
252
|
+
}
|
|
253
|
+
if e.code == 429:
|
|
254
|
+
raise RateLimitError(f"RDAP rate limit (429) from {base_url} for {domain}")
|
|
255
|
+
# Other HTTP errors — fall through to WHOIS
|
|
256
|
+
log(f" RDAP HTTP {e.code} for {domain}, falling back to WHOIS...")
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
except (urllib.error.URLError, socket.timeout, OSError) as e:
|
|
260
|
+
# DNS or connection error — fall through to WHOIS
|
|
261
|
+
log(f" RDAP failed for {domain} ({e}), falling back to WHOIS...")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ─── Port-43 WHOIS Query ─────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
def whois_query(domain, server, port=43, timeout=QUERY_TIMEOUT):
|
|
268
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
269
|
+
sock.settimeout(timeout)
|
|
270
|
+
try:
|
|
271
|
+
sock.connect((server, port))
|
|
272
|
+
sock.sendall((domain + "\r\n").encode("utf-8"))
|
|
273
|
+
response = b""
|
|
274
|
+
while True:
|
|
275
|
+
chunk = sock.recv(4096)
|
|
276
|
+
if not chunk:
|
|
277
|
+
break
|
|
278
|
+
response += chunk
|
|
279
|
+
return response.decode("utf-8", errors="replace")
|
|
280
|
+
finally:
|
|
281
|
+
sock.close()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def parse_whois_availability(response, tld=None):
|
|
285
|
+
if not response or not response.strip():
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
lower = response.lower()
|
|
289
|
+
|
|
290
|
+
for pattern in NOT_SUPPORTED_PATTERNS:
|
|
291
|
+
if pattern in lower:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
for pattern in RATE_LIMIT_PATTERNS:
|
|
295
|
+
if pattern in lower:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
available_matches = sum(1 for p in AVAILABLE_PATTERNS if p in lower)
|
|
299
|
+
taken_matches = sum(1 for p in TAKEN_PATTERNS if p in lower)
|
|
300
|
+
|
|
301
|
+
# Verisign fix: require no TAKEN patterns alongside AVAILABLE
|
|
302
|
+
if tld in {"com", "net", "cc"}:
|
|
303
|
+
if available_matches > 0 and taken_matches == 0:
|
|
304
|
+
return True
|
|
305
|
+
if taken_matches >= 1:
|
|
306
|
+
return False
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
if available_matches > 0 and taken_matches == 0:
|
|
310
|
+
return True
|
|
311
|
+
if taken_matches >= 2:
|
|
312
|
+
return False
|
|
313
|
+
if available_matches > 0 and taken_matches > 0:
|
|
314
|
+
return False if taken_matches >= 2 else None
|
|
315
|
+
if taken_matches >= 1:
|
|
316
|
+
return False
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def extract_registrar(response):
|
|
321
|
+
for line in response.splitlines():
|
|
322
|
+
stripped = line.strip()
|
|
323
|
+
if stripped.lower().startswith("registrar:"):
|
|
324
|
+
return stripped.split(":", 1)[1].strip()
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def whois_port43_query(domain, tld):
|
|
329
|
+
"""
|
|
330
|
+
Query port-43 WHOIS. Returns a result dict or None on failure.
|
|
331
|
+
Raises RateLimitError on rate limit response.
|
|
332
|
+
"""
|
|
333
|
+
server = WHOIS_SERVERS.get(tld)
|
|
334
|
+
if not server:
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
# Check DNS resolution first
|
|
338
|
+
try:
|
|
339
|
+
socket.setdefaulttimeout(DNS_TIMEOUT)
|
|
340
|
+
socket.getaddrinfo(server, 43)
|
|
341
|
+
except (socket.gaierror, socket.timeout, OSError):
|
|
342
|
+
log(f" WHOIS DNS failed for {server}, skipping port-43...")
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
response = whois_query(domain, server)
|
|
347
|
+
except (socket.timeout, ConnectionRefusedError, OSError):
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
if not response or not response.strip():
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
# Check rate limit — for port-43 WHOIS, rate limits are per-server,
|
|
354
|
+
# so we return unknown instead of triggering the global circuit breaker.
|
|
355
|
+
# Only RDAP 429 (which affects all queries from the same IP) triggers circuit break.
|
|
356
|
+
lower = response.lower()
|
|
357
|
+
for pattern in RATE_LIMIT_PATTERNS:
|
|
358
|
+
if pattern in lower:
|
|
359
|
+
log(f" WHOIS rate limit from {server} for {domain} (per-server, not circuit-breaking)")
|
|
360
|
+
return {
|
|
361
|
+
"domain": domain, "tld": tld, "available": None,
|
|
362
|
+
"registrar": None, "method": "whois",
|
|
363
|
+
"error": f"Rate limited by {server}",
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
available = parse_whois_availability(response, tld)
|
|
367
|
+
registrar = extract_registrar(response) if not available else None
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
"domain": domain,
|
|
371
|
+
"tld": tld,
|
|
372
|
+
"available": available,
|
|
373
|
+
"registrar": registrar,
|
|
374
|
+
"method": "whois",
|
|
375
|
+
"error": None,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ─── System WHOIS Fallback ───────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
def system_whois_query(domain, tld):
|
|
382
|
+
"""Last resort: system `whois` command."""
|
|
383
|
+
whois_cmd = shutil.which("whois")
|
|
384
|
+
if not whois_cmd:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
result = subprocess.run(
|
|
389
|
+
[whois_cmd, domain],
|
|
390
|
+
capture_output=True, text=True, timeout=15,
|
|
391
|
+
)
|
|
392
|
+
response = result.stdout
|
|
393
|
+
if not response or not response.strip():
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
available = parse_whois_availability(response, tld)
|
|
397
|
+
registrar = extract_registrar(response) if not available else None
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
"domain": domain,
|
|
401
|
+
"tld": tld,
|
|
402
|
+
"available": available,
|
|
403
|
+
"registrar": registrar,
|
|
404
|
+
"method": "system-whois",
|
|
405
|
+
"error": None,
|
|
406
|
+
}
|
|
407
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ─── Main Check (cascading fallback) ─────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
def check_domain(domain):
|
|
414
|
+
"""
|
|
415
|
+
Check a single domain using the fallback chain: RDAP → WHOIS → system whois.
|
|
416
|
+
Returns a result dict. Raises RateLimitError on rate limit (circuit breaker).
|
|
417
|
+
"""
|
|
418
|
+
tld = get_tld(domain)
|
|
419
|
+
if not tld:
|
|
420
|
+
return {
|
|
421
|
+
"domain": domain, "tld": None, "available": None,
|
|
422
|
+
"registrar": None, "method": "none",
|
|
423
|
+
"error": "Could not extract TLD from domain",
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# 1. Try RDAP (primary)
|
|
427
|
+
log(f"Checking {domain}...")
|
|
428
|
+
result = rdap_query(domain, tld)
|
|
429
|
+
if result is not None:
|
|
430
|
+
status = "available" if result["available"] else ("taken" if result["available"] is False else "unknown")
|
|
431
|
+
log(f" {domain}: {status} (via RDAP)")
|
|
432
|
+
return result
|
|
433
|
+
|
|
434
|
+
# 2. Try port-43 WHOIS (fallback)
|
|
435
|
+
result = whois_port43_query(domain, tld)
|
|
436
|
+
if result is not None:
|
|
437
|
+
status = "available" if result["available"] else ("taken" if result["available"] is False else "unknown")
|
|
438
|
+
log(f" {domain}: {status} (via WHOIS)")
|
|
439
|
+
return result
|
|
440
|
+
|
|
441
|
+
# 3. Try system whois (last resort)
|
|
442
|
+
result = system_whois_query(domain, tld)
|
|
443
|
+
if result is not None:
|
|
444
|
+
status = "available" if result["available"] else ("taken" if result["available"] is False else "unknown")
|
|
445
|
+
log(f" {domain}: {status} (via system whois)")
|
|
446
|
+
return result
|
|
447
|
+
|
|
448
|
+
# All methods failed
|
|
449
|
+
log(f" {domain}: UNKNOWN (all methods failed)")
|
|
450
|
+
return {
|
|
451
|
+
"domain": domain, "tld": tld, "available": None,
|
|
452
|
+
"registrar": None, "method": "none",
|
|
453
|
+
"error": "All lookup methods failed (RDAP, WHOIS port-43, system whois)",
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# ─── Entry Point ─────────────────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
def main():
|
|
460
|
+
try:
|
|
461
|
+
raw = sys.stdin.read()
|
|
462
|
+
config = json.loads(raw)
|
|
463
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
464
|
+
log(f"ERROR: Invalid JSON input: {e}")
|
|
465
|
+
sys.exit(2)
|
|
466
|
+
|
|
467
|
+
domains = config.get("domains", [])
|
|
468
|
+
delay_seconds = config.get("delay_seconds", 2)
|
|
469
|
+
|
|
470
|
+
if not domains:
|
|
471
|
+
log("No domains provided")
|
|
472
|
+
json.dump([], sys.stdout, indent=2)
|
|
473
|
+
sys.exit(0)
|
|
474
|
+
|
|
475
|
+
if delay_seconds < 1:
|
|
476
|
+
delay_seconds = 1
|
|
477
|
+
|
|
478
|
+
# Load RDAP bootstrap
|
|
479
|
+
load_rdap_bootstrap()
|
|
480
|
+
|
|
481
|
+
log(f"Checking {len(domains)} domain(s) with {delay_seconds}s delay...")
|
|
482
|
+
log(f"RDAP servers loaded: {len(_rdap_servers)} TLDs")
|
|
483
|
+
log(f"System whois available: {shutil.which('whois') is not None}")
|
|
484
|
+
|
|
485
|
+
results = []
|
|
486
|
+
circuit_broken = False
|
|
487
|
+
|
|
488
|
+
for i, domain in enumerate(domains):
|
|
489
|
+
domain = domain.strip().lower()
|
|
490
|
+
if not domain:
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
try:
|
|
494
|
+
result = check_domain(domain)
|
|
495
|
+
if result.get("available") is None and result.get("error"):
|
|
496
|
+
result["manual_url"] = manual_check_url(domain)
|
|
497
|
+
results.append(result)
|
|
498
|
+
except RateLimitError as e:
|
|
499
|
+
log(f" CIRCUIT BREAKER (rate limit): {e}")
|
|
500
|
+
log(f" Stopping. {len(domains) - i - 1} domain(s) not checked.")
|
|
501
|
+
results.append({
|
|
502
|
+
"domain": domain, "tld": get_tld(domain), "available": None,
|
|
503
|
+
"registrar": None, "method": "none",
|
|
504
|
+
"error": str(e), "manual_url": manual_check_url(domain),
|
|
505
|
+
})
|
|
506
|
+
circuit_broken = True
|
|
507
|
+
break
|
|
508
|
+
|
|
509
|
+
if i < len(domains) - 1:
|
|
510
|
+
time.sleep(delay_seconds)
|
|
511
|
+
|
|
512
|
+
json.dump(results, sys.stdout, indent=2)
|
|
513
|
+
print()
|
|
514
|
+
|
|
515
|
+
# Summary stats
|
|
516
|
+
available = sum(1 for r in results if r["available"] is True)
|
|
517
|
+
taken = sum(1 for r in results if r["available"] is False)
|
|
518
|
+
unknown = sum(1 for r in results if r["available"] is None)
|
|
519
|
+
methods = {}
|
|
520
|
+
for r in results:
|
|
521
|
+
m = r.get("method", "none")
|
|
522
|
+
methods[m] = methods.get(m, 0) + 1
|
|
523
|
+
|
|
524
|
+
log(f"\nResults: {len(results)} checked — {available} available, {taken} taken, {unknown} unknown")
|
|
525
|
+
log(f"Methods: {', '.join(f'{m}={c}' for m, c in sorted(methods.items()))}")
|
|
526
|
+
|
|
527
|
+
if circuit_broken:
|
|
528
|
+
sys.exit(1)
|
|
529
|
+
sys.exit(0)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
if __name__ == "__main__":
|
|
533
|
+
main()
|