web-agent-bridge 3.3.0 → 3.4.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 (83) hide show
  1. package/LICENSE +12 -0
  2. package/README.ar.md +18 -0
  3. package/README.md +198 -1664
  4. package/bin/wab-init.js +223 -0
  5. package/examples/azure-dns-wab.js +83 -0
  6. package/examples/cloudflare-wab-dns.js +121 -0
  7. package/examples/cpanel-wab-dns.js +114 -0
  8. package/examples/dns-discovery-agent.js +166 -0
  9. package/examples/gcp-dns-wab.js +76 -0
  10. package/examples/governance-agent.js +169 -0
  11. package/examples/plesk-wab-dns.js +103 -0
  12. package/examples/route53-wab-dns.js +144 -0
  13. package/examples/safe-mode-agent.js +96 -0
  14. package/examples/wab-sign.js +74 -0
  15. package/examples/wab-verify.js +60 -0
  16. package/package.json +5 -5
  17. package/public/.well-known/wab.json +28 -0
  18. package/public/activate.html +368 -0
  19. package/public/adoption-metrics.html +188 -0
  20. package/public/api.html +1 -1
  21. package/public/azure-dns-integration.html +289 -0
  22. package/public/cloudflare-integration.html +380 -0
  23. package/public/cpanel-integration.html +398 -0
  24. package/public/css/styles.css +28 -0
  25. package/public/dashboard.html +1 -0
  26. package/public/dns.html +101 -172
  27. package/public/docs.html +1 -0
  28. package/public/gcp-dns-integration.html +318 -0
  29. package/public/growth.html +4 -2
  30. package/public/index.html +227 -31
  31. package/public/integrations.html +1 -1
  32. package/public/js/activate.js +145 -0
  33. package/public/js/auth-nav.js +34 -0
  34. package/public/js/dns.js +438 -0
  35. package/public/openapi.json +89 -0
  36. package/public/plesk-integration.html +375 -0
  37. package/public/premium.html +1 -1
  38. package/public/provider-onboarding.html +172 -0
  39. package/public/provider-sandbox.html +134 -0
  40. package/public/providers.html +359 -0
  41. package/public/registrar-integrations.html +141 -0
  42. package/public/robots.txt +12 -0
  43. package/public/route53-integration.html +531 -0
  44. package/public/shieldqr.html +231 -0
  45. package/public/sitemap.xml +6 -0
  46. package/public/wab-trust.html +200 -0
  47. package/public/wab-vs-protocols.html +210 -0
  48. package/public/whitepaper.html +449 -0
  49. package/sdk/auto-discovery.js +288 -0
  50. package/sdk/governance.js +262 -0
  51. package/sdk/index.js +13 -0
  52. package/sdk/package.json +2 -2
  53. package/sdk/safe-mode.js +221 -0
  54. package/server/index.js +144 -5
  55. package/server/migrations/007_governance.sql +106 -0
  56. package/server/migrations/008_plans.sql +144 -0
  57. package/server/migrations/009_shieldqr.sql +30 -0
  58. package/server/migrations/010_extended_trust.sql +33 -0
  59. package/server/models/adapters/mysql.js +1 -1
  60. package/server/models/adapters/postgresql.js +1 -1
  61. package/server/models/db.js +60 -1
  62. package/server/routes/admin-plans.js +76 -0
  63. package/server/routes/admin-premium.js +4 -2
  64. package/server/routes/admin-shieldqr.js +90 -0
  65. package/server/routes/admin-trust-monitor.js +83 -0
  66. package/server/routes/admin.js +289 -1
  67. package/server/routes/billing.js +16 -4
  68. package/server/routes/discovery.js +1933 -2
  69. package/server/routes/governance.js +208 -0
  70. package/server/routes/plans.js +33 -0
  71. package/server/routes/providers.js +650 -0
  72. package/server/routes/shieldqr.js +88 -0
  73. package/server/services/email.js +29 -0
  74. package/server/services/governance.js +466 -0
  75. package/server/services/plans.js +214 -0
  76. package/server/services/premium.js +1 -1
  77. package/server/services/provider-clients.js +740 -0
  78. package/server/services/shieldqr.js +322 -0
  79. package/server/services/ssl-inspector.js +42 -0
  80. package/server/services/ssl-monitor.js +167 -0
  81. package/server/services/stripe.js +18 -5
  82. package/server/services/vision.js +1 -1
  83. package/server/services/wab-crypto.js +178 -0
@@ -0,0 +1,134 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WAB Provider Sandbox</title>
7
+ <meta name="description" content="Interactive sandbox for DNS providers and registrars to test WAB DNS Discovery APIs.">
8
+ <link rel="stylesheet" href="/css/styles.css?v=3.3.0">
9
+ <style>
10
+ body { background:#081123; color:#e8efff; }
11
+ .hero { padding:100px 20px 30px; text-align:center; }
12
+ .hero h1 { font-size:clamp(1.8rem,4vw,2.8rem); margin-bottom:8px; }
13
+ .hero p { color:#9eb1d8; max-width:820px; margin:0 auto; }
14
+ .wrap { max-width:1100px; margin:0 auto; padding:0 18px 70px; }
15
+ .panel { background:linear-gradient(180deg,rgba(17,24,39,.92),rgba(10,16,30,.96)); border:1px solid rgba(148,163,184,.2); border-radius:14px; padding:14px; margin-bottom:14px; }
16
+ .panel h3 { margin:0 0 8px; color:#fcd34d; }
17
+ .row { display:grid; grid-template-columns:2fr 1fr; gap:10px; }
18
+ @media (max-width:760px) { .row { grid-template-columns:1fr; } }
19
+ label { display:block; font-size:.8rem; color:#a8b7d7; margin-bottom:5px; }
20
+ input, select, textarea { width:100%; background:#0b162a; border:1px solid rgba(148,163,184,.28); color:#e8efff; border-radius:10px; padding:10px; font-family:Consolas, monospace; font-size:.85rem; }
21
+ textarea { min-height:90px; }
22
+ .actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
23
+ button { background:linear-gradient(135deg,#f59e0b,#b45309); color:#fff; border:none; border-radius:10px; padding:9px 12px; cursor:pointer; font-weight:700; font-size:.82rem; }
24
+ button.alt { background:linear-gradient(135deg,#334155,#1f2937); }
25
+ pre { background:#020617; border:1px solid rgba(148,163,184,.26); border-radius:10px; padding:12px; color:#c4b5fd; overflow:auto; min-height:180px; }
26
+ .hint { color:#9eb1d8; font-size:.84rem; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <section class="hero">
31
+ <h1>Provider Sandbox</h1>
32
+ <p>Run provider integration APIs end-to-end: manifest, record template, status, one-click plan, and batch verify.</p>
33
+ </section>
34
+
35
+ <div class="wrap">
36
+ <div class="panel">
37
+ <h3>Request Builder</h3>
38
+ <div class="row">
39
+ <div>
40
+ <label>Domain</label>
41
+ <input id="domain" value="webagentbridge.com" placeholder="example.com">
42
+ </div>
43
+ <div>
44
+ <label>Action</label>
45
+ <select id="action">
46
+ <option value="enable">enable</option>
47
+ <option value="disable">disable</option>
48
+ </select>
49
+ </div>
50
+ </div>
51
+ <div class="row" style="margin-top:8px;">
52
+ <div>
53
+ <label>Batch Domains (comma separated)</label>
54
+ <input id="batchDomains" value="webagentbridge.com, example.com">
55
+ </div>
56
+ <div>
57
+ <label>Include Agent Run</label>
58
+ <select id="includeAgentRun">
59
+ <option value="false" selected>false</option>
60
+ <option value="true">true</option>
61
+ </select>
62
+ </div>
63
+ </div>
64
+ <div class="actions">
65
+ <button onclick="runManifest()">Manifest</button>
66
+ <button onclick="runTemplate()">Record Template</button>
67
+ <button onclick="runStatus()">Status</button>
68
+ <button onclick="runPlan()">Enable/Disable Plan</button>
69
+ <button onclick="runBatch()">Verify Batch</button>
70
+ <button class="alt" onclick="clearOut()">Clear</button>
71
+ </div>
72
+ <p class="hint">Tip: use this page during provider integration QA before shipping one-click toggle in production.</p>
73
+ </div>
74
+
75
+ <div class="panel">
76
+ <h3>Response</h3>
77
+ <pre id="out">{}</pre>
78
+ </div>
79
+ </div>
80
+
81
+ <script>
82
+ async function call(url, opts) {
83
+ const out = document.getElementById('out');
84
+ out.textContent = 'Loading ' + url + ' ...';
85
+ try {
86
+ const res = await fetch(url, opts || {});
87
+ const data = await res.json().catch(() => ({}));
88
+ out.textContent = JSON.stringify({ http_status: res.status, data: data }, null, 2);
89
+ } catch (err) {
90
+ out.textContent = JSON.stringify({ error: String(err) }, null, 2);
91
+ }
92
+ }
93
+
94
+ function getDomain() {
95
+ return (document.getElementById('domain').value || '').trim();
96
+ }
97
+
98
+ function runManifest() {
99
+ call('/api/discovery/provider/manifest');
100
+ }
101
+
102
+ function runTemplate() {
103
+ const d = getDomain();
104
+ call('/api/discovery/provider/record-template?domain=' + encodeURIComponent(d));
105
+ }
106
+
107
+ function runStatus() {
108
+ const d = getDomain();
109
+ call('/api/discovery/provider/status?domain=' + encodeURIComponent(d));
110
+ }
111
+
112
+ function runPlan() {
113
+ const d = getDomain();
114
+ const action = document.getElementById('action').value;
115
+ call('/api/discovery/provider/enable-plan?domain=' + encodeURIComponent(d) + '&action=' + encodeURIComponent(action));
116
+ }
117
+
118
+ function runBatch() {
119
+ const raw = (document.getElementById('batchDomains').value || '');
120
+ const domains = raw.split(',').map((s) => s.trim()).filter(Boolean);
121
+ const includeAgentRun = document.getElementById('includeAgentRun').value === 'true';
122
+ call('/api/discovery/provider/verify-batch', {
123
+ method: 'POST',
124
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
125
+ body: JSON.stringify({ domains: domains, include_agent_run: includeAgentRun })
126
+ });
127
+ }
128
+
129
+ function clearOut() {
130
+ document.getElementById('out').textContent = '{}';
131
+ }
132
+ </script>
133
+ </body>
134
+ </html>
@@ -0,0 +1,359 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DNS Providers — Web Agent Bridge</title>
7
+ <meta name="description" content="Connect Cloudflare, Route 53, Azure DNS, GCP, cPanel, Plesk, GoDaddy, or Namecheap and toggle WAB DNS Discovery on every domain you own — server-side, encrypted, audit-logged.">
8
+ <script>
9
+ (function () {
10
+ try { if (!localStorage.getItem('wab_token')) window.location.replace('/login'); }
11
+ catch (e) { window.location.replace('/login'); }
12
+ })();
13
+ </script>
14
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
17
+ <link rel="stylesheet" href="/css/styles.css">
18
+ <style>
19
+ body { background: #0b0f17; color: #e5e7eb; font-family: 'Inter', system-ui, sans-serif; margin: 0; }
20
+ .wrap { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }
21
+ .topbar { display:flex; justify-content:space-between; align-items:center; padding-bottom:24px; border-bottom:1px solid #1f2937; margin-bottom:32px; }
22
+ .topbar h1 { font-size:1.6rem; margin:0; font-weight:700; }
23
+ .topbar .actions { display:flex; gap:12px; }
24
+ .btn { display:inline-block; padding:10px 18px; border-radius:8px; border:1px solid #374151; background:#1f2937; color:#e5e7eb; font-weight:600; cursor:pointer; font-size:0.92rem; text-decoration:none; transition:all 0.15s; }
25
+ .btn:hover { background:#374151; border-color:#4b5563; }
26
+ .btn-primary { background:linear-gradient(135deg,#3b82f6,#1d4ed8); border-color:transparent; color:#fff; }
27
+ .btn-primary:hover { filter:brightness(1.1); }
28
+ .btn-danger { background:#7f1d1d; border-color:#991b1b; color:#fee2e2; }
29
+ .btn-danger:hover { background:#991b1b; }
30
+ .btn-success { background:#065f46; border-color:#047857; color:#d1fae5; }
31
+ .btn-sm { padding:6px 12px; font-size:0.82rem; }
32
+ .grid { display:grid; gap:20px; }
33
+ .grid-2 { grid-template-columns: 1fr 1fr; }
34
+ .grid-3 { grid-template-columns: repeat(3, 1fr); }
35
+ .grid-4 { grid-template-columns: repeat(4, 1fr); }
36
+ @media(max-width:900px){ .grid-2,.grid-3,.grid-4{grid-template-columns:1fr;} }
37
+ .card { background:#111827; border:1px solid #1f2937; border-radius:12px; padding:24px; }
38
+ .card h2 { font-size:1.15rem; margin:0 0 16px; }
39
+ .stat { background:#111827; border:1px solid #1f2937; border-radius:12px; padding:18px; text-align:center; }
40
+ .stat .label { color:#94a3b8; font-size:0.78rem; text-transform:uppercase; letter-spacing:0.06em; }
41
+ .stat .value { font-size:1.8rem; font-weight:800; margin-top:6px; }
42
+ .provider-pick { border:1px solid #1f2937; border-radius:10px; padding:16px; cursor:pointer; transition:all 0.15s; background:#0f172a; }
43
+ .provider-pick:hover { border-color:#3b82f6; background:#111827; }
44
+ .provider-pick h3 { margin:0 0 4px; font-size:1rem; }
45
+ .provider-pick small { color:#94a3b8; }
46
+ table { width:100%; border-collapse:collapse; }
47
+ th, td { padding:10px 12px; text-align:left; border-bottom:1px solid #1f2937; font-size:0.92rem; }
48
+ th { color:#94a3b8; font-weight:600; font-size:0.78rem; text-transform:uppercase; letter-spacing:0.04em; }
49
+ .pill { display:inline-block; padding:3px 10px; border-radius:999px; font-size:0.75rem; font-weight:600; }
50
+ .pill-ok { background:#064e3b; color:#6ee7b7; }
51
+ .pill-err { background:#7f1d1d; color:#fecaca; }
52
+ .pill-pending { background:#374151; color:#cbd5e1; }
53
+ .form-group { margin-bottom:14px; }
54
+ .form-group label { display:block; margin-bottom:6px; font-size:0.85rem; color:#cbd5e1; font-weight:500; }
55
+ .form-group input, .form-group textarea, .form-group select {
56
+ width:100%; padding:10px 12px; background:#0f172a; border:1px solid #374151;
57
+ border-radius:8px; color:#e5e7eb; font-family:inherit; font-size:0.9rem;
58
+ }
59
+ .form-group input:focus, .form-group textarea:focus { outline:none; border-color:#3b82f6; }
60
+ .form-group small.help { display:block; color:#94a3b8; margin-top:4px; font-size:0.78rem; }
61
+ .modal { position:fixed; inset:0; background:rgba(0,0,0,0.7); display:none; align-items:center; justify-content:center; z-index:50; padding:24px; }
62
+ .modal.active { display:flex; }
63
+ .modal-content { background:#111827; border:1px solid #1f2937; border-radius:14px; padding:28px; max-width:560px; width:100%; max-height:85vh; overflow-y:auto; }
64
+ .modal-content h2 { margin-top:0; }
65
+ .alert { padding:12px 14px; border-radius:8px; margin-bottom:16px; font-size:0.9rem; }
66
+ .alert-ok { background:#064e3b; border:1px solid #047857; color:#d1fae5; }
67
+ .alert-err { background:#7f1d1d; border:1px solid #991b1b; color:#fecaca; }
68
+ .empty { text-align:center; color:#94a3b8; padding:40px 20px; }
69
+ code { background:#0f172a; padding:2px 6px; border-radius:4px; font-family:'JetBrains Mono', monospace; font-size:0.82rem; }
70
+ </style>
71
+ </head>
72
+ <body>
73
+ <div class="wrap">
74
+ <div class="topbar">
75
+ <div>
76
+ <h1>🔗 DNS Providers</h1>
77
+ <div style="color:#94a3b8;font-size:0.9rem;margin-top:4px;">Connect your DNS provider to enable one-click WAB Discovery on every domain you own.</div>
78
+ </div>
79
+ <div class="actions">
80
+ <a href="/dashboard" class="btn">← Dashboard</a>
81
+ <button class="btn btn-primary" onclick="openProviderPicker()">+ Connect Provider</button>
82
+ </div>
83
+ </div>
84
+
85
+ <div id="alertBox"></div>
86
+
87
+ <div class="grid grid-4" style="margin-bottom:32px;">
88
+ <div class="stat"><div class="label">Connected</div><div class="value" id="kAccounts">–</div></div>
89
+ <div class="stat"><div class="label">Active</div><div class="value" id="kActive" style="color:#4ade80">–</div></div>
90
+ <div class="stat"><div class="label">Domains</div><div class="value" id="kDomains">–</div></div>
91
+ <div class="stat"><div class="label">_wab Live</div><div class="value" id="kWab" style="color:#4ade80">–</div></div>
92
+ </div>
93
+
94
+ <div class="card" style="margin-bottom:32px;">
95
+ <h2>Your Connected Accounts</h2>
96
+ <div id="accountsList"></div>
97
+ </div>
98
+
99
+ <div class="card" id="domainsCard" style="display:none;">
100
+ <h2 id="domainsHeader">Domains</h2>
101
+ <div id="domainsList"></div>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Provider picker modal -->
106
+ <div class="modal" id="pickerModal">
107
+ <div class="modal-content">
108
+ <h2>Choose your DNS provider</h2>
109
+ <div class="grid grid-2" id="pickerGrid"></div>
110
+ <div style="text-align:right; margin-top:16px;"><button class="btn" onclick="closeModal('pickerModal')">Cancel</button></div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Connect form modal -->
115
+ <div class="modal" id="connectModal">
116
+ <div class="modal-content">
117
+ <h2 id="connectTitle">Connect Provider</h2>
118
+ <form id="connectForm" onsubmit="event.preventDefault(); submitConnect();">
119
+ <input type="hidden" id="connectType">
120
+ <div class="form-group">
121
+ <label>Label (your name for this account)</label>
122
+ <input type="text" id="connectLabel" placeholder="My Cloudflare account">
123
+ </div>
124
+ <div id="credFields"></div>
125
+ <div id="configFields"></div>
126
+ <div id="connectAlert"></div>
127
+ <div style="display:flex; gap:12px; justify-content:flex-end; margin-top:16px;">
128
+ <button type="button" class="btn" onclick="closeModal('connectModal')">Cancel</button>
129
+ <button type="submit" class="btn btn-primary" id="connectSubmit">Save & Test</button>
130
+ </div>
131
+ </form>
132
+ </div>
133
+ </div>
134
+
135
+ <script>
136
+ const API = '/api/providers';
137
+ const TOKEN = localStorage.getItem('wab_token');
138
+ let PROVIDER_TYPES = [];
139
+ let ACCOUNTS = [];
140
+ let SELECTED_ACCOUNT = null;
141
+
142
+ function H() { return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` }; }
143
+ function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
144
+ function openModal(id){ document.getElementById(id).classList.add('active'); }
145
+ function closeModal(id){ document.getElementById(id).classList.remove('active'); }
146
+ function flash(msg, ok=true){
147
+ const el = document.getElementById('alertBox');
148
+ el.innerHTML = `<div class="alert ${ok?'alert-ok':'alert-err'}">${esc(msg)}</div>`;
149
+ setTimeout(() => { el.innerHTML = ''; }, 6000);
150
+ }
151
+
152
+ async function loadTypes() {
153
+ const r = await fetch(`${API}/types`);
154
+ const j = await r.json();
155
+ PROVIDER_TYPES = j.providers || [];
156
+ }
157
+
158
+ async function loadAccounts() {
159
+ const r = await fetch(`${API}/accounts`, { headers: H() });
160
+ if (r.status === 401) { window.location = '/login'; return; }
161
+ const j = await r.json();
162
+ ACCOUNTS = j.accounts || [];
163
+ renderAccounts();
164
+ renderStats();
165
+ }
166
+
167
+ function renderStats() {
168
+ const total = ACCOUNTS.length;
169
+ const active = ACCOUNTS.filter(a => a.status === 'active').length;
170
+ const domains = ACCOUNTS.reduce((n, a) => n + (a.domains_count || 0), 0);
171
+ document.getElementById('kAccounts').textContent = total;
172
+ document.getElementById('kActive').textContent = active;
173
+ document.getElementById('kDomains').textContent = domains;
174
+ document.getElementById('kWab').textContent = '…';
175
+ // _wab live count comes from the currently-selected account view
176
+ }
177
+
178
+ function statusPill(a) {
179
+ if (a.status === 'active') return '<span class="pill pill-ok">● active</span>';
180
+ if (a.status === 'error') return `<span class="pill pill-err">● error</span>`;
181
+ return '<span class="pill pill-pending">○ pending</span>';
182
+ }
183
+
184
+ function renderAccounts() {
185
+ const root = document.getElementById('accountsList');
186
+ if (!ACCOUNTS.length) {
187
+ root.innerHTML = '<div class="empty">You have not connected any DNS provider yet.<br><br><button class="btn btn-primary" onclick="openProviderPicker()">+ Connect your first provider</button></div>';
188
+ return;
189
+ }
190
+ root.innerHTML = '<table><thead><tr><th>Provider</th><th>Label</th><th>Status</th><th>Domains</th><th>Last Test</th><th></th></tr></thead><tbody>' +
191
+ ACCOUNTS.map(a => {
192
+ const t = PROVIDER_TYPES.find(p => p.type === a.provider_type) || { label: a.provider_type };
193
+ const errMsg = a.last_test_error ? `<br><small style="color:#fca5a5">${esc(a.last_test_error)}</small>` : '';
194
+ return `<tr>
195
+ <td><strong>${esc(t.label)}</strong></td>
196
+ <td>${esc(a.label)}</td>
197
+ <td>${statusPill(a)}${errMsg}</td>
198
+ <td>${a.domains_count || 0}</td>
199
+ <td><small>${esc(a.last_test_at || 'never')}</small></td>
200
+ <td style="text-align:right; white-space:nowrap;">
201
+ <button class="btn btn-sm" onclick="testAccount('${a.id}')">Test</button>
202
+ <button class="btn btn-sm" onclick="syncAccount('${a.id}')">Sync</button>
203
+ <button class="btn btn-sm" onclick="viewDomains('${a.id}')">Domains</button>
204
+ <button class="btn btn-sm btn-danger" onclick="deleteAccount('${a.id}')">Delete</button>
205
+ </td>
206
+ </tr>`;
207
+ }).join('') + '</tbody></table>';
208
+ }
209
+
210
+ function openProviderPicker() {
211
+ const grid = document.getElementById('pickerGrid');
212
+ grid.innerHTML = PROVIDER_TYPES.map(p => `
213
+ <div class="provider-pick" onclick="openConnect('${p.type}')">
214
+ <h3>${esc(p.label)}</h3>
215
+ <small>${(p.credential_fields || []).map(f => esc(f.label)).join(' · ')}</small>
216
+ </div>`).join('');
217
+ openModal('pickerModal');
218
+ }
219
+
220
+ function openConnect(type) {
221
+ closeModal('pickerModal');
222
+ const p = PROVIDER_TYPES.find(x => x.type === type);
223
+ if (!p) return;
224
+ document.getElementById('connectType').value = type;
225
+ document.getElementById('connectTitle').textContent = `Connect ${p.label}`;
226
+ document.getElementById('connectLabel').value = p.label;
227
+ document.getElementById('connectAlert').innerHTML = '';
228
+ document.getElementById('credFields').innerHTML = (p.credential_fields || []).map(renderField).join('');
229
+ document.getElementById('configFields').innerHTML = (p.config_fields || []).length
230
+ ? '<hr style="border-color:#1f2937;margin:16px 0"><h3 style="font-size:0.95rem;color:#94a3b8;margin:0 0 12px">Configuration</h3>' + (p.config_fields).map(renderField).join('')
231
+ : '';
232
+ openModal('connectModal');
233
+ }
234
+
235
+ function renderField(f) {
236
+ const id = `f_${f.key}`;
237
+ const required = f.required ? 'required' : '';
238
+ const help = f.help ? `<small class="help">${esc(f.help)}</small>` : '';
239
+ if (f.type === 'textarea') {
240
+ return `<div class="form-group"><label>${esc(f.label)}${f.required?' *':''}</label><textarea id="${id}" name="${esc(f.key)}" rows="6" ${required}></textarea>${help}</div>`;
241
+ }
242
+ const t = f.type === 'password' ? 'password' : 'text';
243
+ return `<div class="form-group"><label>${esc(f.label)}${f.required?' *':''}</label><input type="${t}" id="${id}" name="${esc(f.key)}" ${required}>${help}</div>`;
244
+ }
245
+
246
+ async function submitConnect() {
247
+ const type = document.getElementById('connectType').value;
248
+ const p = PROVIDER_TYPES.find(x => x.type === type);
249
+ const credentials = {};
250
+ const config = {};
251
+ for (const f of (p.credential_fields || [])) credentials[f.key] = document.getElementById(`f_${f.key}`).value.trim();
252
+ for (const f of (p.config_fields || [])) config[f.key] = document.getElementById(`f_${f.key}`).value.trim();
253
+ const label = document.getElementById('connectLabel').value.trim() || p.label;
254
+ const alertEl = document.getElementById('connectAlert');
255
+ const submitBtn = document.getElementById('connectSubmit');
256
+ submitBtn.disabled = true; submitBtn.textContent = 'Saving…';
257
+ alertEl.innerHTML = '';
258
+ try {
259
+ const r = await fetch(`${API}/accounts`, {
260
+ method: 'POST', headers: H(),
261
+ body: JSON.stringify({ provider_type: type, label, credentials, config })
262
+ });
263
+ const j = await r.json();
264
+ if (!r.ok) throw new Error(j.error || 'Failed to save');
265
+ alertEl.innerHTML = '<div class="alert alert-ok">Saved. Testing connection…</div>';
266
+ const t = await fetch(`${API}/accounts/${j.account.id}/test`, { method: 'POST', headers: H() });
267
+ const tj = await t.json();
268
+ if (!t.ok) {
269
+ alertEl.innerHTML = `<div class="alert alert-err">Saved, but test failed: ${esc(tj.error)}</div>`;
270
+ } else {
271
+ alertEl.innerHTML = `<div class="alert alert-ok">✅ Connected (${esc(tj.detail)}). Syncing zones…</div>`;
272
+ await fetch(`${API}/accounts/${j.account.id}/sync`, { method: 'POST', headers: H() });
273
+ setTimeout(() => { closeModal('connectModal'); flash('Provider connected and synced.'); loadAccounts(); }, 600);
274
+ return;
275
+ }
276
+ } catch (e) {
277
+ alertEl.innerHTML = `<div class="alert alert-err">${esc(e.message)}</div>`;
278
+ } finally {
279
+ submitBtn.disabled = false; submitBtn.textContent = 'Save & Test';
280
+ }
281
+ loadAccounts();
282
+ }
283
+
284
+ async function testAccount(id) {
285
+ flash('Testing…');
286
+ const r = await fetch(`${API}/accounts/${id}/test`, { method: 'POST', headers: H() });
287
+ const j = await r.json();
288
+ flash(r.ok ? `✅ ${j.detail || 'OK'}` : `❌ ${j.error}`, r.ok);
289
+ loadAccounts();
290
+ }
291
+
292
+ async function syncAccount(id) {
293
+ flash('Syncing zones…');
294
+ const r = await fetch(`${API}/accounts/${id}/sync`, { method: 'POST', headers: H() });
295
+ const j = await r.json();
296
+ flash(r.ok ? `✅ Synced ${j.count} domains` : `❌ ${j.error}`, r.ok);
297
+ loadAccounts();
298
+ if (SELECTED_ACCOUNT === id) viewDomains(id);
299
+ }
300
+
301
+ async function deleteAccount(id) {
302
+ if (!confirm('Delete this provider account? Stored credentials will be erased. This will not remove DNS records you already published.')) return;
303
+ const r = await fetch(`${API}/accounts/${id}`, { method: 'DELETE', headers: H() });
304
+ flash(r.ok ? 'Account deleted.' : 'Delete failed.', r.ok);
305
+ if (SELECTED_ACCOUNT === id) {
306
+ SELECTED_ACCOUNT = null;
307
+ document.getElementById('domainsCard').style.display = 'none';
308
+ }
309
+ loadAccounts();
310
+ }
311
+
312
+ async function viewDomains(id) {
313
+ SELECTED_ACCOUNT = id;
314
+ const acc = ACCOUNTS.find(a => a.id === id);
315
+ const card = document.getElementById('domainsCard');
316
+ card.style.display = 'block';
317
+ const t = PROVIDER_TYPES.find(p => p.type === acc.provider_type) || { label: acc.provider_type };
318
+ document.getElementById('domainsHeader').textContent = `Domains — ${t.label} · ${acc.label}`;
319
+ document.getElementById('domainsList').innerHTML = '<div class="empty">Loading…</div>';
320
+ const r = await fetch(`${API}/accounts/${id}/domains`, { headers: H() });
321
+ const j = await r.json();
322
+ const domains = j.domains || [];
323
+ const wabLive = domains.filter(d => d.wab_enabled).length;
324
+ document.getElementById('kWab').textContent = wabLive;
325
+ if (!domains.length) {
326
+ document.getElementById('domainsList').innerHTML = '<div class="empty">No domains synced yet. Click <strong>Sync</strong> on this account to fetch them from your provider.</div>';
327
+ return;
328
+ }
329
+ document.getElementById('domainsList').innerHTML = '<table><thead><tr><th>Domain</th><th>WAB</th><th>Last Action</th><th></th></tr></thead><tbody>' +
330
+ domains.map(d => `<tr>
331
+ <td><strong>${esc(d.domain)}</strong>${d.zone_id ? `<br><small style="color:#94a3b8">zone: <code>${esc(d.zone_id)}</code></small>` : ''}</td>
332
+ <td>${d.wab_enabled ? '<span class="pill pill-ok">● ON</span>' : '<span class="pill pill-pending">○ OFF</span>'}</td>
333
+ <td>${d.last_action ? `${esc(d.last_action)} · <span style="color:${d.last_action_status==='ok'?'#6ee7b7':'#fca5a5'}">${esc(d.last_action_status||'—')}</span><br><small style="color:#94a3b8">${esc(d.last_action_at||'')}</small>${d.last_action_error?'<br><small style="color:#fca5a5">'+esc(d.last_action_error)+'</small>':''}` : '<small style="color:#94a3b8">—</small>'}</td>
334
+ <td style="text-align:right;white-space:nowrap;">
335
+ ${d.wab_enabled
336
+ ? `<button class="btn btn-sm btn-danger" onclick="toggleWab('${id}','${esc(d.domain)}',false)">Disable WAB</button>`
337
+ : `<button class="btn btn-sm btn-success" onclick="toggleWab('${id}','${esc(d.domain)}',true)">Enable WAB</button>`}
338
+ </td>
339
+ </tr>`).join('') + '</tbody></table>';
340
+ }
341
+
342
+ async function toggleWab(accountId, domain, on) {
343
+ const path = on ? 'enable-wab' : 'disable-wab';
344
+ flash(`${on?'Enabling':'Disabling'} WAB on ${domain}…`);
345
+ const r = await fetch(`${API}/accounts/${accountId}/domains/${encodeURIComponent(domain)}/${path}`, {
346
+ method: 'POST', headers: H()
347
+ });
348
+ const j = await r.json();
349
+ flash(r.ok ? `✅ ${domain}: ${j.detail || 'ok'}` : `❌ ${domain}: ${j.error}`, r.ok);
350
+ viewDomains(accountId);
351
+ }
352
+
353
+ (async function init() {
354
+ await loadTypes();
355
+ await loadAccounts();
356
+ })();
357
+ </script>
358
+ </body>
359
+ </html>
@@ -0,0 +1,141 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>WAB DNS — Domain Registrar Integrations</title>
7
+ <link rel="stylesheet" href="/css/main.css">
8
+ <style>
9
+ body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; padding: 0; }
10
+ .page { max-width: 880px; margin: 0 auto; padding: 40px 20px 80px; }
11
+ h1 { font-size: 1.7rem; margin-bottom: 6px; }
12
+ .sub { color: #94a3b8; margin-bottom: 32px; font-size: .97rem; }
13
+ .card { background: #1e293b; border-radius: 10px; padding: 24px; margin-bottom: 24px; }
14
+ h2 { font-size: 1.25rem; margin: 0 0 14px; display: flex; align-items: center; gap: 10px; }
15
+ .pill { display: inline-block; font-size: .68rem; font-weight: 700; padding: 3px 9px; border-radius: 12px; background: #334155; color: #cbd5e1; }
16
+ .pill.cli { background: #1e3a8a; color: #bfdbfe; }
17
+ .pill.warn { background: #7c2d12; color: #fdba74; }
18
+ pre { background: #0f172a; border-radius: 7px; padding: 14px 16px; font-size: .82rem; color: #94a3b8; overflow-x: auto; white-space: pre-wrap; word-break: break-word; margin: 14px 0 0; }
19
+ code { background: #0f172a; padding: 1px 5px; border-radius: 4px; font-size: .88em; }
20
+ .step { display: flex; gap: 14px; margin-bottom: 18px; }
21
+ .step-num { flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; background: #334155; color: #e2e8f0; font-size: .82rem; font-weight: 700; display: flex; align-items: center; justify-content: center; }
22
+ .step-body { flex: 1; padding-top: 3px; }
23
+ .info-box { background: #0c2340; border: 1px solid #1e3a5f; border-radius: 8px; padding: 12px 16px; font-size: .87rem; color: #93c5fd; margin-bottom: 18px; }
24
+ .warning-box { background: #431407; border: 1px solid #9a3412; border-radius: 8px; padding: 12px 16px; font-size: .87rem; color: #fdba74; margin-bottom: 18px; }
25
+ a { color: #818cf8; }
26
+ .download-btn { display: inline-block; padding: 7px 14px; background: #6366f1; color: #fff; border-radius: 6px; font-size: .85rem; text-decoration: none; margin-right: 8px; }
27
+ .download-btn:hover { opacity: .85; }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <div class="page">
32
+ <h1>Domain Registrar Integrations</h1>
33
+ <p class="sub">
34
+ One-line CLI scripts to enable/disable WAB DNS Discovery on the most popular domain registrars.
35
+ </p>
36
+
37
+ <div class="info-box">
38
+ ℹ <strong>Why CLI not browser?</strong>
39
+ Most registrar APIs require static IP allow-lists or block CORS, which prevents direct browser-side calls.
40
+ Run these scripts from a server or your local machine instead.
41
+ </div>
42
+
43
+ <!-- ── GODADDY ── -->
44
+ <div class="card">
45
+ <h2>GoDaddy <span class="pill cli">CLI</span></h2>
46
+ <p style="color:#cbd5e1;font-size:.92rem;margin:0 0 12px">
47
+ Uses the <a href="https://developer.godaddy.com/doc/endpoint/domains" target="_blank" rel="noopener">GoDaddy Domains API v1</a>.
48
+ Requires production API key + secret from <a href="https://developer.godaddy.com/keys" target="_blank" rel="noopener">developer.godaddy.com/keys</a>.
49
+ </p>
50
+ <div class="warning-box" style="margin-bottom:12px">
51
+ ⚠ GoDaddy now restricts the public API to accounts with 50+ domains or paid Reseller plans.
52
+ For smaller accounts, use the GoDaddy DNS web UI to add a TXT record at <code>_wab</code> with the value provided by WAB.
53
+ </div>
54
+ <div class="step">
55
+ <div class="step-num">1</div>
56
+ <div class="step-body">Download the script: <a class="download-btn" href="/downloads/godaddy-wab-dns.js" download>⬇ godaddy-wab-dns.js</a></div>
57
+ </div>
58
+ <div class="step">
59
+ <div class="step-num">2</div>
60
+ <div class="step-body">Install dependencies: <code>npm install node-fetch@2</code></div>
61
+ </div>
62
+ <div class="step">
63
+ <div class="step-num">3</div>
64
+ <div class="step-body">Run:
65
+ <pre>GODADDY_API_KEY=your-key GODADDY_API_SECRET=your-secret \
66
+ node godaddy-wab-dns.js enable example.com
67
+
68
+ GODADDY_API_KEY=your-key GODADDY_API_SECRET=your-secret \
69
+ node godaddy-wab-dns.js disable example.com</pre>
70
+ </div>
71
+ </div>
72
+ <h3 style="font-size:1rem;margin:18px 0 8px;color:#cbd5e1">Inline cURL alternative</h3>
73
+ <pre># 1. Get current TXT records for _wab
74
+ curl -s -H "Authorization: sso-key $KEY:$SECRET" \
75
+ "https://api.godaddy.com/v1/domains/example.com/records/TXT/_wab"
76
+
77
+ # 2. Replace TXT records (PUT replaces entire record set)
78
+ curl -s -X PUT -H "Authorization: sso-key $KEY:$SECRET" \
79
+ -H "Content-Type: application/json" \
80
+ "https://api.godaddy.com/v1/domains/example.com/records/TXT/_wab" \
81
+ -d '[{"data":"v=wab1; endpoint=https://example.com/.well-known/wab.json","ttl":3600}]'
82
+
83
+ # 3. Disable: replace with empty array
84
+ curl -s -X PUT -H "Authorization: sso-key $KEY:$SECRET" \
85
+ -H "Content-Type: application/json" \
86
+ "https://api.godaddy.com/v1/domains/example.com/records/TXT/_wab" \
87
+ -d '[]'</pre>
88
+ </div>
89
+
90
+ <!-- ── NAMECHEAP ── -->
91
+ <div class="card">
92
+ <h2>Namecheap <span class="pill cli">CLI</span></h2>
93
+ <p style="color:#cbd5e1;font-size:.92rem;margin:0 0 12px">
94
+ Uses the <a href="https://www.namecheap.com/support/api/intro/" target="_blank" rel="noopener">Namecheap API</a>.
95
+ Requires API enablement (Profile → Tools → API Access) and IP allow-listing.
96
+ </p>
97
+ <div class="warning-box" style="margin-bottom:12px">
98
+ ⚠ Namecheap API requires the calling IP to be added to the allow-list in your account profile.
99
+ Also, <code>setHosts</code> replaces ALL host records — the script preserves your existing records and only modifies <code>_wab</code>.
100
+ </div>
101
+ <div class="step">
102
+ <div class="step-num">1</div>
103
+ <div class="step-body">Download the script: <a class="download-btn" href="/downloads/namecheap-wab-dns.js" download>⬇ namecheap-wab-dns.js</a></div>
104
+ </div>
105
+ <div class="step">
106
+ <div class="step-num">2</div>
107
+ <div class="step-body">Install dependencies: <code>npm install node-fetch@2 fast-xml-parser</code></div>
108
+ </div>
109
+ <div class="step">
110
+ <div class="step-num">3</div>
111
+ <div class="step-body">Run:
112
+ <pre>NAMECHEAP_USER=your-user \
113
+ NAMECHEAP_API_KEY=your-api-key \
114
+ NAMECHEAP_CLIENT_IP=1.2.3.4 \
115
+ node namecheap-wab-dns.js enable example.com
116
+
117
+ # Same env vars, then:
118
+ node namecheap-wab-dns.js disable example.com</pre>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- ── SUMMARY TABLE ── -->
124
+ <div class="card">
125
+ <h2>How it works <span class="pill">all registrars</span></h2>
126
+ <div class="step"><div class="step-num">1</div><div class="step-body">Fetch WAB record template (<code>GET /api/discovery/provider/record-template</code>) for the TXT value.</div></div>
127
+ <div class="step"><div class="step-num">2</div><div class="step-body">Read existing TXT records via the registrar's API.</div></div>
128
+ <div class="step"><div class="step-num">3</div><div class="step-body">Add or replace the <code>_wab</code> TXT record (registrar-specific call).</div></div>
129
+ <div class="step"><div class="step-num">4</div><div class="step-body">Verify with <code>/api/discovery/provider/status</code> — propagation may take 5-30 minutes for some registrars.</div></div>
130
+ </div>
131
+
132
+ <p style="text-align:center;margin-top:30px;font-size:.85rem;color:#475569">
133
+ <a href="/provider-onboarding">← Provider Onboarding</a> ·
134
+ <a href="/cloudflare-integration">Cloudflare</a> ·
135
+ <a href="/route53-integration">Route 53</a> ·
136
+ <a href="/azure-dns-integration">Azure DNS</a> ·
137
+ <a href="/dns">DNS Discovery</a>
138
+ </p>
139
+ </div>
140
+ </body>
141
+ </html>
package/public/robots.txt CHANGED
@@ -25,6 +25,18 @@ Disallow: /admin/
25
25
  Disallow: /dashboard
26
26
  Disallow: /premium-dashboard
27
27
 
28
+ # Whitepaper: indexable but no archive / no snippet (rights protected)
29
+ Allow: /whitepaper
30
+ Disallow: /whitepaper.html
31
+
32
+ # Block archivers explicitly
33
+ User-agent: ia_archiver
34
+ Disallow: /whitepaper
35
+ Disallow: /whitepaper.html
36
+ User-agent: archive.org_bot
37
+ Disallow: /whitepaper
38
+ Disallow: /whitepaper.html
39
+
28
40
  # AI Crawlers — Welcome! We have structured data for you.
29
41
  User-agent: GPTBot
30
42
  Allow: /