@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.
Files changed (130) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/.opencode/plugins/arn-spark.js +272 -0
  3. package/package.json +17 -0
  4. package/plugins/arn-spark/.claude-plugin/plugin.json +9 -0
  5. package/plugins/arn-spark/LICENSE +21 -0
  6. package/plugins/arn-spark/README.md +25 -0
  7. package/plugins/arn-spark/agents/arn-spark-brand-strategist.md +299 -0
  8. package/plugins/arn-spark/agents/arn-spark-dev-env-builder.md +228 -0
  9. package/plugins/arn-spark/agents/arn-spark-doctor.md +92 -0
  10. package/plugins/arn-spark/agents/arn-spark-forensic-investigator.md +181 -0
  11. package/plugins/arn-spark/agents/arn-spark-market-researcher.md +232 -0
  12. package/plugins/arn-spark/agents/arn-spark-marketing-pm.md +225 -0
  13. package/plugins/arn-spark/agents/arn-spark-persona-architect.md +259 -0
  14. package/plugins/arn-spark/agents/arn-spark-persona-impersonator.md +183 -0
  15. package/plugins/arn-spark/agents/arn-spark-product-strategist.md +191 -0
  16. package/plugins/arn-spark/agents/arn-spark-prototype-builder.md +497 -0
  17. package/plugins/arn-spark/agents/arn-spark-scaffolder.md +228 -0
  18. package/plugins/arn-spark/agents/arn-spark-spike-runner.md +209 -0
  19. package/plugins/arn-spark/agents/arn-spark-style-capture.md +196 -0
  20. package/plugins/arn-spark/agents/arn-spark-tech-evaluator.md +229 -0
  21. package/plugins/arn-spark/agents/arn-spark-ui-interactor.md +235 -0
  22. package/plugins/arn-spark/agents/arn-spark-use-case-writer.md +280 -0
  23. package/plugins/arn-spark/agents/arn-spark-ux-judge.md +215 -0
  24. package/plugins/arn-spark/agents/arn-spark-ux-specialist.md +200 -0
  25. package/plugins/arn-spark/agents/arn-spark-visual-sketcher.md +285 -0
  26. package/plugins/arn-spark/agents/arn-spark-visual-test-engineer.md +224 -0
  27. package/plugins/arn-spark/references/copilot-tools.md +62 -0
  28. package/plugins/arn-spark/skills/arn-brainstorming/SKILL.md +520 -0
  29. package/plugins/arn-spark/skills/arn-brainstorming/references/add-feature-flow.md +155 -0
  30. package/plugins/arn-spark/skills/arn-spark-arch-vision/SKILL.md +226 -0
  31. package/plugins/arn-spark/skills/arn-spark-arch-vision/references/architecture-vision-template.md +153 -0
  32. package/plugins/arn-spark/skills/arn-spark-arch-vision/references/technology-evaluation-guide.md +86 -0
  33. package/plugins/arn-spark/skills/arn-spark-clickable-prototype/SKILL.md +471 -0
  34. package/plugins/arn-spark/skills/arn-spark-clickable-prototype/references/clickable-prototype-criteria.md +65 -0
  35. package/plugins/arn-spark/skills/arn-spark-clickable-prototype/references/journey-template.md +62 -0
  36. package/plugins/arn-spark/skills/arn-spark-clickable-prototype/references/review-report-template.md +75 -0
  37. package/plugins/arn-spark/skills/arn-spark-clickable-prototype/references/showcase-capture-guide.md +213 -0
  38. package/plugins/arn-spark/skills/arn-spark-clickable-prototype-teams/SKILL.md +642 -0
  39. package/plugins/arn-spark/skills/arn-spark-clickable-prototype-teams/references/debate-protocol.md +242 -0
  40. package/plugins/arn-spark/skills/arn-spark-clickable-prototype-teams/references/debate-review-report-template.md +161 -0
  41. package/plugins/arn-spark/skills/arn-spark-clickable-prototype-teams/references/expert-interaction-review-template.md +152 -0
  42. package/plugins/arn-spark/skills/arn-spark-concept-review/SKILL.md +350 -0
  43. package/plugins/arn-spark/skills/arn-spark-concept-review/references/conflict-resolution-protocol.md +145 -0
  44. package/plugins/arn-spark/skills/arn-spark-concept-review/references/review-report-template.md +185 -0
  45. package/plugins/arn-spark/skills/arn-spark-dev-setup/SKILL.md +366 -0
  46. package/plugins/arn-spark/skills/arn-spark-dev-setup/references/dev-setup-checklist.md +84 -0
  47. package/plugins/arn-spark/skills/arn-spark-dev-setup/references/dev-setup-template.md +205 -0
  48. package/plugins/arn-spark/skills/arn-spark-discover/SKILL.md +303 -0
  49. package/plugins/arn-spark/skills/arn-spark-discover/references/competitive-landscape-template.md +87 -0
  50. package/plugins/arn-spark/skills/arn-spark-discover/references/discovery-questions.md +120 -0
  51. package/plugins/arn-spark/skills/arn-spark-discover/references/persona-profile-template.md +97 -0
  52. package/plugins/arn-spark/skills/arn-spark-discover/references/product-concept-template.md +253 -0
  53. package/plugins/arn-spark/skills/arn-spark-ensure-config/SKILL.md +23 -0
  54. package/plugins/arn-spark/skills/arn-spark-ensure-config/references/ensure-config.md +388 -0
  55. package/plugins/arn-spark/skills/arn-spark-ensure-config/references/step-0-fast-path.md +25 -0
  56. package/plugins/arn-spark/skills/arn-spark-ensure-config/scripts/cache-check.sh +127 -0
  57. package/plugins/arn-spark/skills/arn-spark-feature-extract/SKILL.md +483 -0
  58. package/plugins/arn-spark/skills/arn-spark-feature-extract/references/feature-backlog-template.md +176 -0
  59. package/plugins/arn-spark/skills/arn-spark-feature-extract/references/feature-entry-template.md +209 -0
  60. package/plugins/arn-spark/skills/arn-spark-help/SKILL.md +149 -0
  61. package/plugins/arn-spark/skills/arn-spark-help/references/pipeline-map.md +211 -0
  62. package/plugins/arn-spark/skills/arn-spark-init/SKILL.md +312 -0
  63. package/plugins/arn-spark/skills/arn-spark-init/references/agent-models-presets/all-opus.md +23 -0
  64. package/plugins/arn-spark/skills/arn-spark-init/references/agent-models-presets/balanced.md +23 -0
  65. package/plugins/arn-spark/skills/arn-spark-init/references/bkt-setup.md +55 -0
  66. package/plugins/arn-spark/skills/arn-spark-init/references/jira-mcp-setup.md +61 -0
  67. package/plugins/arn-spark/skills/arn-spark-init/references/platform-labels.md +97 -0
  68. package/plugins/arn-spark/skills/arn-spark-naming/SKILL.md +275 -0
  69. package/plugins/arn-spark/skills/arn-spark-naming/references/creative-brief-template.md +146 -0
  70. package/plugins/arn-spark/skills/arn-spark-naming/references/naming-methodology.md +237 -0
  71. package/plugins/arn-spark/skills/arn-spark-naming/references/naming-report-template.md +122 -0
  72. package/plugins/arn-spark/skills/arn-spark-naming/references/trademark-databases.md +88 -0
  73. package/plugins/arn-spark/skills/arn-spark-naming/references/whois-server-map.md +164 -0
  74. package/plugins/arn-spark/skills/arn-spark-naming/scripts/whois-check.js +502 -0
  75. package/plugins/arn-spark/skills/arn-spark-naming/scripts/whois-check.py +533 -0
  76. package/plugins/arn-spark/skills/arn-spark-prototype-lock/SKILL.md +260 -0
  77. package/plugins/arn-spark/skills/arn-spark-prototype-lock/references/lock-report-template.md +68 -0
  78. package/plugins/arn-spark/skills/arn-spark-prototype-lock/references/pretooluse-hook-template.json +35 -0
  79. package/plugins/arn-spark/skills/arn-spark-prototype-lock/references/prototype-guardrail-rules.md +38 -0
  80. package/plugins/arn-spark/skills/arn-spark-report/SKILL.md +144 -0
  81. package/plugins/arn-spark/skills/arn-spark-report/references/issue-template.md +81 -0
  82. package/plugins/arn-spark/skills/arn-spark-report/references/spark-knowledge-base.md +293 -0
  83. package/plugins/arn-spark/skills/arn-spark-scaffold/SKILL.md +239 -0
  84. package/plugins/arn-spark/skills/arn-spark-scaffold/references/scaffold-checklist.md +79 -0
  85. package/plugins/arn-spark/skills/arn-spark-scaffold/references/scaffold-summary-template.md +74 -0
  86. package/plugins/arn-spark/skills/arn-spark-spike/SKILL.md +209 -0
  87. package/plugins/arn-spark/skills/arn-spark-spike/references/spike-report-template.md +123 -0
  88. package/plugins/arn-spark/skills/arn-spark-static-prototype/SKILL.md +362 -0
  89. package/plugins/arn-spark/skills/arn-spark-static-prototype/references/review-report-template.md +65 -0
  90. package/plugins/arn-spark/skills/arn-spark-static-prototype/references/showcase-capture-guide.md +153 -0
  91. package/plugins/arn-spark/skills/arn-spark-static-prototype/references/static-prototype-criteria.md +54 -0
  92. package/plugins/arn-spark/skills/arn-spark-static-prototype-teams/SKILL.md +518 -0
  93. package/plugins/arn-spark/skills/arn-spark-static-prototype-teams/references/debate-protocol.md +230 -0
  94. package/plugins/arn-spark/skills/arn-spark-static-prototype-teams/references/debate-review-report-template.md +148 -0
  95. package/plugins/arn-spark/skills/arn-spark-static-prototype-teams/references/expert-visual-review-template.md +130 -0
  96. package/plugins/arn-spark/skills/arn-spark-stress-competitive/SKILL.md +166 -0
  97. package/plugins/arn-spark/skills/arn-spark-stress-competitive/references/competitive-report-template.md +139 -0
  98. package/plugins/arn-spark/skills/arn-spark-stress-competitive/references/gap-analysis-framework.md +111 -0
  99. package/plugins/arn-spark/skills/arn-spark-stress-interview/SKILL.md +257 -0
  100. package/plugins/arn-spark/skills/arn-spark-stress-interview/references/interview-protocol.md +140 -0
  101. package/plugins/arn-spark/skills/arn-spark-stress-interview/references/interview-report-template.md +165 -0
  102. package/plugins/arn-spark/skills/arn-spark-stress-interview/references/persona-casting-spec.md +138 -0
  103. package/plugins/arn-spark/skills/arn-spark-stress-premortem/SKILL.md +181 -0
  104. package/plugins/arn-spark/skills/arn-spark-stress-premortem/references/premortem-protocol.md +112 -0
  105. package/plugins/arn-spark/skills/arn-spark-stress-premortem/references/premortem-report-template.md +158 -0
  106. package/plugins/arn-spark/skills/arn-spark-stress-prfaq/SKILL.md +206 -0
  107. package/plugins/arn-spark/skills/arn-spark-stress-prfaq/references/prfaq-report-template.md +139 -0
  108. package/plugins/arn-spark/skills/arn-spark-stress-prfaq/references/prfaq-workflow.md +118 -0
  109. package/plugins/arn-spark/skills/arn-spark-style-explore/SKILL.md +281 -0
  110. package/plugins/arn-spark/skills/arn-spark-style-explore/references/style-brief-template.md +198 -0
  111. package/plugins/arn-spark/skills/arn-spark-use-cases/SKILL.md +359 -0
  112. package/plugins/arn-spark/skills/arn-spark-use-cases/references/expert-review-template.md +94 -0
  113. package/plugins/arn-spark/skills/arn-spark-use-cases/references/review-protocol.md +150 -0
  114. package/plugins/arn-spark/skills/arn-spark-use-cases/references/use-case-index-template.md +108 -0
  115. package/plugins/arn-spark/skills/arn-spark-use-cases/references/use-case-template.md +125 -0
  116. package/plugins/arn-spark/skills/arn-spark-use-cases-teams/SKILL.md +306 -0
  117. package/plugins/arn-spark/skills/arn-spark-use-cases-teams/references/debate-protocol.md +272 -0
  118. package/plugins/arn-spark/skills/arn-spark-use-cases-teams/references/review-report-template.md +112 -0
  119. package/plugins/arn-spark/skills/arn-spark-visual-readiness/SKILL.md +293 -0
  120. package/plugins/arn-spark/skills/arn-spark-visual-readiness/references/readiness-checklist.md +196 -0
  121. package/plugins/arn-spark/skills/arn-spark-visual-sketch/SKILL.md +376 -0
  122. package/plugins/arn-spark/skills/arn-spark-visual-sketch/references/aesthetic-philosophy.md +210 -0
  123. package/plugins/arn-spark/skills/arn-spark-visual-sketch/references/sketch-gallery-guide.md +282 -0
  124. package/plugins/arn-spark/skills/arn-spark-visual-sketch/references/visual-direction-template.md +174 -0
  125. package/plugins/arn-spark/skills/arn-spark-visual-strategy/SKILL.md +447 -0
  126. package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/baseline-capture-script-template.js +89 -0
  127. package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/journey-schema.md +375 -0
  128. package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/spike-checklist.md +122 -0
  129. package/plugins/arn-spark/skills/arn-spark-visual-strategy/references/strategy-layers-guide.md +132 -0
  130. 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()