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,438 @@
1
+ // ═══════════ Bilingual toggle ═══════════
2
+ window.setLang = function (lang) {
3
+ const isAr = lang === 'ar';
4
+ document.documentElement.lang = isAr ? 'ar' : 'en';
5
+ document.documentElement.dir = isAr ? 'rtl' : 'ltr';
6
+ const enBtn = document.getElementById('enBtn');
7
+ const arBtn = document.getElementById('arBtn');
8
+ if (enBtn) enBtn.classList.toggle('active', !isAr);
9
+ if (arBtn) arBtn.classList.toggle('active', isAr);
10
+ document.querySelectorAll('[data-en]').forEach((el) => {
11
+ el.textContent = isAr ? (el.getAttribute('data-ar') || el.textContent) : (el.getAttribute('data-en') || el.textContent);
12
+ });
13
+ };
14
+
15
+ // ═══════════ Live DoH Verifier ═══════════
16
+ window.verifyDns = async function () {
17
+ const domain = (document.getElementById('dnsDomain').value || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
18
+ const resolver = document.getElementById('dnsResolver').value;
19
+ const status = document.getElementById('dnsStatus');
20
+ const out = document.getElementById('dnsOut');
21
+ if (!domain) {
22
+ status.innerHTML = '<span class="danger">Please enter a domain.</span>';
23
+ return;
24
+ }
25
+ const fqdn = '_wab.' + domain;
26
+ status.innerHTML = '<span class="warn">Querying ' + fqdn + ' …</span>';
27
+ out.textContent = '';
28
+ try {
29
+ const url = resolver + '?name=' + encodeURIComponent(fqdn) + '&type=TXT';
30
+ const res = await fetch(url, { headers: { accept: 'application/dns-json' } });
31
+ const data = await res.json();
32
+ out.textContent = JSON.stringify(data, null, 2);
33
+ const answers = (data.Answer || []).filter((a) => a.type === 16);
34
+ if (!answers.length) {
35
+ status.innerHTML = '<span class="danger">No _wab TXT record found for <b>' + domain + '</b>. The domain has not enabled WAB DNS Discovery (yet).</span>';
36
+ return;
37
+ }
38
+ const value = answers.map((a) => (a.data || '').replace(/^"|"$/g, '').replace(/" "/g, '')).join(' ');
39
+ const versionMatch = /v=([^;\s]+)/.exec(value);
40
+ const endpointMatch = /endpoint=([^;\s]+)/.exec(value);
41
+ if (versionMatch && endpointMatch && versionMatch[1].startsWith('wab')) {
42
+ status.innerHTML = '<span class="ok">✓ Valid WAB record found. Version: <b>' + versionMatch[1] + '</b> · Endpoint: <a href="' + endpointMatch[1] + '" target="_blank" style="color:#7dd3fc">' + endpointMatch[1] + '</a></span>';
43
+ } else {
44
+ status.innerHTML = '<span class="warn">TXT record found but it does not match the WAB format (v=wab1; endpoint=…).</span>';
45
+ }
46
+ } catch (err) {
47
+ status.innerHTML = '<span class="danger">Lookup failed: ' + ((err && err.message) || err) + '</span>';
48
+ }
49
+ };
50
+
51
+ window.copyExample = function () {
52
+ const txt = 'v=wab1; endpoint=https://yourdomain.com/.well-known/wab.json';
53
+ navigator.clipboard.writeText(txt).then(() => {
54
+ const s = document.getElementById('dnsStatus');
55
+ s.innerHTML = '<span class="ok">✓ Example record copied to clipboard.</span>';
56
+ });
57
+ };
58
+
59
+ function _statePill(label, ok) {
60
+ return '<span style="display:inline-block;margin:3px 6px 3px 0;padding:4px 9px;border-radius:999px;font-size:.74rem;border:1px solid ' +
61
+ (ok ? 'rgba(74,222,128,.5);color:#4ade80;background:rgba(74,222,128,.12)' : 'rgba(248,113,113,.4);color:#f87171;background:rgba(248,113,113,.1)') +
62
+ '">' + label + '</span>';
63
+ }
64
+
65
+ function renderProof(data) {
66
+ const status = document.getElementById('proofStatus');
67
+ const out = document.getElementById('proofOut');
68
+ const txt = document.getElementById('proofTxt');
69
+ const wab = document.getElementById('proofWabJson');
70
+ const use = document.getElementById('useCaseValue');
71
+ const badges = document.getElementById('stateBadges');
72
+ const discoverPathBadge = document.getElementById('proofDiscoverPathBadge');
73
+ const usageKpi = document.getElementById('usageKpiValue');
74
+ if (!status || !out || !txt || !wab || !use || !badges) return;
75
+
76
+ const states = data.statuses || {};
77
+ badges.innerHTML = [
78
+ _statePill('Registered', states.registered === 'yes'),
79
+ _statePill('DNS Verified', states.dns_verified === 'yes'),
80
+ _statePill('Agent-Ready', states.agent_ready === 'yes'),
81
+ _statePill('Production', states.production === 'yes'),
82
+ ].join('');
83
+
84
+ const rawTxt = ((data.dns && data.dns.records) || [])[0] || '—';
85
+ txt.textContent = rawTxt;
86
+ const wabUrl = data.wab_json && data.wab_json.url;
87
+ wab.innerHTML = wabUrl ? ('<a href="' + wabUrl + '" target="_blank" style="color:#7dd3fc">' + wabUrl + '</a>') : '—';
88
+ use.textContent = (data.wab_json && data.wab_json.use_case) || '—';
89
+ out.textContent = JSON.stringify(data, null, 2);
90
+
91
+ const agentOk = data.execution_proof && data.execution_proof.ok;
92
+ const coreOk = data.dns && data.dns.ok && data.wab_json && data.wab_json.ok;
93
+
94
+ if (discoverPathBadge) {
95
+ const discoverStep = data.execution_proof && data.execution_proof.steps
96
+ ? data.execution_proof.steps.find((s) => s.key === 'agent_discover_call')
97
+ : null;
98
+ const detail = discoverStep && typeof discoverStep.detail === 'string' ? discoverStep.detail : '';
99
+ const usedFallback = detail.includes('fallback /agent-bridge.json succeeded');
100
+ const usedPrimary = detail.includes('GET /api/wab/discover succeeded');
101
+
102
+ if (usedFallback) {
103
+ discoverPathBadge.style.display = 'block';
104
+ discoverPathBadge.innerHTML = '<span style="display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(250,204,21,.15);border:1px solid rgba(250,204,21,.45);color:#fde68a;font-size:.78rem;font-weight:700;letter-spacing:.03em">Fallback Used: /agent-bridge.json</span>';
105
+ } else if (usedPrimary) {
106
+ discoverPathBadge.style.display = 'block';
107
+ discoverPathBadge.innerHTML = '<span style="display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(34,197,94,.15);border:1px solid rgba(34,197,94,.45);color:#86efac;font-size:.78rem;font-weight:700;letter-spacing:.03em">Primary Path: /api/wab/discover</span>';
108
+ } else if (discoverStep && discoverStep.ok === false) {
109
+ const detailText = detail ? (' — ' + detail.replace(/"/g, '&quot;')) : '';
110
+ discoverPathBadge.style.display = 'block';
111
+ discoverPathBadge.innerHTML = '<span style="display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(239,68,68,.15);border:1px solid rgba(239,68,68,.45);color:#fca5a5;font-size:.78rem;font-weight:700;letter-spacing:.03em" title="' + detail.replace(/"/g, '&quot;') + '">Discovery Path Failed' + detailText + '</span>';
112
+ } else {
113
+ discoverPathBadge.style.display = 'none';
114
+ discoverPathBadge.innerHTML = '';
115
+ }
116
+ }
117
+
118
+ status.innerHTML = '<span class="' + ((agentOk || coreOk) ? 'ok' : 'danger') + '">' +
119
+ ((agentOk || coreOk)
120
+ ? '✓ Verifiable proof ready.'
121
+ : '✗ Verification incomplete. Check DNS record, endpoint, and agent flow.') +
122
+ '</span>';
123
+
124
+ if (usageKpi) {
125
+ if (data && data.kpi) {
126
+ usageKpi.textContent =
127
+ 'value_score=' + (data.kpi.value_score != null ? data.kpi.value_score : '—') +
128
+ ' · actions=' + (data.kpi.discovered_actions_count != null ? data.kpi.discovered_actions_count : '—') +
129
+ ' · business_cmds=' + (data.kpi.business_commands_count != null ? data.kpi.business_commands_count : '—') +
130
+ ' · e2e_ms=' + (data.kpi.end_to_end_ms != null ? data.kpi.end_to_end_ms : '—');
131
+ } else {
132
+ usageKpi.textContent = '—';
133
+ }
134
+ }
135
+ }
136
+
137
+ window.verifyLiveProof = async function () {
138
+ const domain = (document.getElementById('dnsDomain').value || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
139
+ const status = document.getElementById('proofStatus');
140
+ if (!status) return;
141
+ if (!domain) {
142
+ status.innerHTML = '<span class="danger">Please enter a domain.</span>';
143
+ return;
144
+ }
145
+ status.innerHTML = '<span class="warn">Running live verification…</span>';
146
+ try {
147
+ const res = await fetch('/api/discovery/verify-live?domain=' + encodeURIComponent(domain));
148
+ const data = await res.json();
149
+ renderProof(data);
150
+ } catch (err) {
151
+ status.innerHTML = '<span class="danger">Verification failed: ' + ((err && err.message) || err) + '</span>';
152
+ }
153
+ };
154
+
155
+ window.testWithAgent = async function () {
156
+ const domain = (document.getElementById('dnsDomain').value || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
157
+ const status = document.getElementById('proofStatus');
158
+ if (!status) return;
159
+ if (!domain) {
160
+ status.innerHTML = '<span class="danger">Please enter a domain.</span>';
161
+ return;
162
+ }
163
+ status.innerHTML = '<span class="warn">Running agent flow (discover → ping)…</span>';
164
+ try {
165
+ const res = await fetch('/api/discovery/test-agent?domain=' + encodeURIComponent(domain));
166
+ const data = await res.json();
167
+ renderProof(data);
168
+ } catch (err) {
169
+ status.innerHTML = '<span class="danger">Agent test failed: ' + ((err && err.message) || err) + '</span>';
170
+ }
171
+ };
172
+
173
+ window.runUsageProof = async function () {
174
+ const domain = (document.getElementById('dnsDomain').value || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
175
+ const apiKey = (document.getElementById('dnsUsageApiKey') && document.getElementById('dnsUsageApiKey').value || '').trim();
176
+ const preferredUseCase = (document.getElementById('dnsUsageUseCase') && document.getElementById('dnsUsageUseCase').value || '').trim();
177
+ const status = document.getElementById('proofStatus');
178
+ if (!status) return;
179
+ if (!domain) {
180
+ status.innerHTML = '<span class="danger">Please enter a domain.</span>';
181
+ return;
182
+ }
183
+ status.innerHTML = '<span class="warn">Running usage proof (' + (apiKey ? 'real execution' : 'readiness only') + ')…</span>';
184
+ try {
185
+ const res = await fetch('/api/discovery/usage-proof', {
186
+ method: 'POST',
187
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
188
+ body: JSON.stringify({
189
+ domain,
190
+ api_key: apiKey,
191
+ preferred_use_case: preferredUseCase || undefined,
192
+ }),
193
+ });
194
+ const data = await res.json();
195
+ renderProof(data);
196
+ if (data && data.usage_proof) {
197
+ const ok = data.usage_proof.ok || data.usage_proof.readiness_ok;
198
+ status.innerHTML = '<span class="' + (ok ? 'ok' : 'danger') + '">' +
199
+ (data.usage_proof.detail || (ok ? 'Usage proof completed.' : 'Usage proof failed.')) +
200
+ '</span>';
201
+ }
202
+ loadUsageTrend(domain).catch(() => {});
203
+ } catch (err) {
204
+ status.innerHTML = '<span class="danger">Usage proof failed: ' + ((err && err.message) || err) + '</span>';
205
+ }
206
+ };
207
+
208
+ async function loadUsageTrend(domain) {
209
+ const summary = document.getElementById('usageTrendSummary');
210
+ const list = document.getElementById('usageTrendList');
211
+ if (!summary || !list) return;
212
+
213
+ const cleanDomain = (domain || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
214
+ if (!cleanDomain) {
215
+ summary.textContent = 'Enter a domain to load usage trend.';
216
+ list.textContent = '—';
217
+ return;
218
+ }
219
+
220
+ summary.textContent = 'Loading usage trend…';
221
+ list.textContent = '';
222
+
223
+ try {
224
+ const res = await fetch('/api/discovery/usage-proof-runs?domain=' + encodeURIComponent(cleanDomain) + '&limit=20');
225
+ const data = await res.json();
226
+ const runs = Array.isArray(data && data.runs) ? data.runs : [];
227
+
228
+ if (!runs.length) {
229
+ summary.textContent = 'No usage proof runs yet for this domain.';
230
+ list.textContent = 'Run Usage Proof to start collecting trend data.';
231
+ return;
232
+ }
233
+
234
+ const avgScore = Math.round(runs.reduce((acc, r) => acc + Number(r.value_score || 0), 0) / runs.length);
235
+ const execSuccessCount = runs.filter((r) => r.execution_succeeded).length;
236
+ const readinessCount = runs.filter((r) => r.readiness_ok).length;
237
+ summary.textContent = 'runs=' + runs.length + ' · avg_score=' + avgScore + ' · readiness_ok=' + readinessCount + ' · execution_ok=' + execSuccessCount;
238
+
239
+ list.innerHTML = runs.slice(0, 20).map((r) => {
240
+ const state = r.execution_succeeded ? 'exec-ok' : (r.readiness_ok ? 'ready' : 'fail');
241
+ const action = r.selected_action || '—';
242
+ const t = r.created_at || 'unknown-time';
243
+ return '<div>[' + state + '] score=' + Number(r.value_score || 0) + ' · action=' + escapeHtml(action) + ' · ms=' + (r.end_to_end_ms != null ? r.end_to_end_ms : '—') + ' · ' + escapeHtml(t) + '</div>';
244
+ }).join('');
245
+ } catch (err) {
246
+ summary.textContent = 'Failed to load usage trend.';
247
+ list.textContent = ((err && err.message) || err);
248
+ }
249
+ }
250
+
251
+ window.toggleAdvanced = function () {
252
+ const blocks = document.querySelectorAll('.advanced-block');
253
+ blocks.forEach((el) => {
254
+ el.style.display = (el.style.display === 'none' || !el.style.display) ? 'block' : 'none';
255
+ });
256
+ };
257
+
258
+ // ═══════════ Canonical records — live status ═══════════
259
+ const RR_TYPE = { TXT: 16, CAA: 257, A: 1, AAAA: 28, CNAME: 5 };
260
+
261
+ function _decodeCAARdata(hex) {
262
+ try {
263
+ const bytes = hex.replace(/\\#\s*\d+\s*/, '').replace(/\s+/g, '');
264
+ const buf = bytes.match(/.{1,2}/g).map((b) => parseInt(b, 16));
265
+ const tagLen = buf[1];
266
+ let tag = '';
267
+ let val = '';
268
+ for (let i = 0; i < tagLen; i++) tag += String.fromCharCode(buf[2 + i]);
269
+ for (let i = 2 + tagLen; i < buf.length; i++) val += String.fromCharCode(buf[i]);
270
+ return { tag, value: val };
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ function _normalizeAnswer(answer, type) {
277
+ const data = answer.data || '';
278
+ if (type === 'CAA') {
279
+ const decoded = _decodeCAARdata(data);
280
+ if (decoded) return decoded.tag + ' ' + decoded.value;
281
+ return data;
282
+ }
283
+ return data.replace(/^"|"$/g, '').replace(/"\s*"/g, '');
284
+ }
285
+
286
+ async function _doh(name, type) {
287
+ const url = 'https://cloudflare-dns.com/dns-query?name=' +
288
+ encodeURIComponent(name) + '&type=' + encodeURIComponent(type) + '&do=1';
289
+ const res = await fetch(url, { headers: { accept: 'application/dns-json' } });
290
+ const data = await res.json();
291
+ const want = RR_TYPE[type];
292
+ const answers = (data.Answer || []).filter((a) => a.type === want)
293
+ .map((a) => _normalizeAnswer(a, type));
294
+ Object.defineProperty(answers, '_ad', { value: !!data.AD, enumerable: false });
295
+ return answers;
296
+ }
297
+
298
+ async function checkDnssecForWab() {
299
+ const el = document.getElementById('dnssecLiveStatus');
300
+ const rowState = document.getElementById('dnssecRowState');
301
+ if (!el) return;
302
+ try {
303
+ const ans = await _doh('_wab.webagentbridge.com', 'TXT');
304
+ if (ans._ad) {
305
+ el.className = 'ok';
306
+ el.textContent = '✓ DNSSEC validated (AD=1) at resolver';
307
+ if (rowState) {
308
+ rowState.className = 'ok';
309
+ rowState.textContent = '✓ DNSSEC validated';
310
+ }
311
+ } else {
312
+ el.className = 'warn';
313
+ el.textContent = '⚠ DNSSEC not yet enabled on this zone (AD=0). Roadmap: enable DS at registrar.';
314
+ }
315
+ } catch {
316
+ el.className = 'warn';
317
+ el.textContent = '… could not verify (DoH unreachable)';
318
+ }
319
+ }
320
+
321
+ async function checkCanonicalRecords() {
322
+ const rows = document.querySelectorAll('#recordsTable tr[data-record]');
323
+ const summary = document.getElementById('recordsLiveStatus');
324
+ let pass = 0;
325
+ let fail = 0;
326
+ const tasks = Array.from(rows).map(async (row) => {
327
+ const cell = row.querySelector('.live-cell');
328
+ const name = row.dataset.record;
329
+ const type = row.dataset.rtype;
330
+ const match = row.dataset.match;
331
+ try {
332
+ const answers = await _doh(name, type);
333
+ const hit = answers.some((a) => a.toLowerCase().includes(match.toLowerCase()));
334
+ if (hit) {
335
+ cell.innerHTML = '<span class="ok" title="Verified live via Cloudflare DoH">✓ live</span>';
336
+ pass++;
337
+ } else {
338
+ cell.innerHTML = '<span class="danger" title="Record not yet propagated or missing">✗ missing</span>';
339
+ fail++;
340
+ }
341
+ } catch {
342
+ cell.innerHTML = '<span class="warn" title="Lookup failed">… error</span>';
343
+ }
344
+ });
345
+ await Promise.allSettled(tasks);
346
+ const total = pass + fail;
347
+ summary.innerHTML = '<span class="' + (fail === 0 ? 'ok' : 'warn') + '">' +
348
+ (fail === 0
349
+ ? '✓ All ' + total + ' canonical records verified live (Cloudflare DoH).'
350
+ : '⚠ ' + pass + '/' + total + ' records live — ' + fail + ' missing or propagating.') + '</span>';
351
+ }
352
+
353
+ async function loadLiveAdoption() {
354
+ const status = document.getElementById('liveAdoptionStatus');
355
+ const list = document.getElementById('liveAdoptionList');
356
+ if (!status || !list) return;
357
+
358
+ status.innerHTML = '<span class="warn">Loading live registry…</span>';
359
+ list.style.display = 'none';
360
+ list.innerHTML = '';
361
+
362
+ try {
363
+ const res = await fetch('/api/discovery/registry?limit=24');
364
+ const data = await res.json();
365
+ const entries = Array.isArray(data && data.listings) ? data.listings : [];
366
+
367
+ if (!entries.length) {
368
+ status.innerHTML = '<span class="warn">No businesses are currently listed. New registrations will appear here automatically.</span>';
369
+ return;
370
+ }
371
+
372
+ const cards = entries.map((entry) => {
373
+ const safeName = escapeHtml(entry.name || entry.domain || 'Unnamed site');
374
+ const safeDomain = escapeHtml(entry.domain || '');
375
+ const safeDescription = escapeHtml(entry.description || 'WAB-enabled website');
376
+ const safeCategory = escapeHtml(entry.category || 'general');
377
+ const score = Number.isFinite(Number(entry.neutrality_score)) ? Number(entry.neutrality_score) : 0;
378
+
379
+ return '<article class="live-item">' +
380
+ '<h4>' + safeName + '</h4>' +
381
+ '<p>' + safeDescription + '</p>' +
382
+ '<div class="live-meta">' + safeDomain + ' · ' + safeCategory + ' · score ' + score + '</div>' +
383
+ '</article>';
384
+ }).join('');
385
+
386
+ list.innerHTML = cards;
387
+ list.style.display = 'grid';
388
+ status.innerHTML = '<span class="ok">✓ Live list loaded from real registrations.</span>';
389
+ } catch (err) {
390
+ status.innerHTML = '<span class="danger">Failed to load live registry: ' + ((err && err.message) || err) + '</span>';
391
+ }
392
+ }
393
+
394
+ function escapeHtml(value) {
395
+ return String(value)
396
+ .replace(/&/g, '&amp;')
397
+ .replace(/</g, '&lt;')
398
+ .replace(/>/g, '&gt;')
399
+ .replace(/\"/g, '&quot;')
400
+ .replace(/'/g, '&#39;');
401
+ }
402
+
403
+ function bindHandlers() {
404
+ const enBtn = document.getElementById('enBtn');
405
+ const arBtn = document.getElementById('arBtn');
406
+ const verifyBtn = document.getElementById('dnsVerifyBtn');
407
+ const copyBtn = document.getElementById('dnsCopyBtn');
408
+ const advancedBtn = document.getElementById('dnsAdvancedToggleBtn');
409
+ const proofVerifyBtn = document.getElementById('dnsProofVerifyBtn');
410
+ const proofAgentBtn = document.getElementById('dnsProofAgentBtn');
411
+ const usageProofBtn = document.getElementById('dnsUsageProofBtn');
412
+
413
+ if (enBtn) enBtn.addEventListener('click', () => window.setLang('en'));
414
+ if (arBtn) arBtn.addEventListener('click', () => window.setLang('ar'));
415
+ if (verifyBtn) verifyBtn.addEventListener('click', () => window.verifyDns());
416
+ if (copyBtn) copyBtn.addEventListener('click', () => window.copyExample());
417
+ if (advancedBtn) advancedBtn.addEventListener('click', () => window.toggleAdvanced());
418
+ if (proofVerifyBtn) proofVerifyBtn.addEventListener('click', () => window.verifyLiveProof());
419
+ if (proofAgentBtn) proofAgentBtn.addEventListener('click', () => window.testWithAgent());
420
+ if (usageProofBtn) usageProofBtn.addEventListener('click', () => window.runUsageProof());
421
+ }
422
+
423
+ document.addEventListener('DOMContentLoaded', () => {
424
+ bindHandlers();
425
+ checkCanonicalRecords().catch(() => {});
426
+ checkDnssecForWab().catch(() => {});
427
+ loadLiveAdoption().catch(() => {});
428
+ const initDomain = (document.getElementById('dnsDomain') && document.getElementById('dnsDomain').value || '').trim();
429
+ loadUsageTrend(initDomain).catch(() => {});
430
+ const navbar = document.getElementById('navbar');
431
+ window.addEventListener('scroll', () => {
432
+ if (!navbar) return;
433
+ navbar.style.background = window.scrollY > 50
434
+ ? 'rgba(7, 13, 25, 0.92)'
435
+ : 'rgba(7, 13, 25, 0.78)';
436
+ });
437
+ });
438
+
@@ -26,6 +26,7 @@
26
26
  ],
27
27
  "tags": [
28
28
  {"name": "Discovery", "description": "WAB Discovery Protocol endpoints"},
29
+ {"name": "Provider", "description": "DNS provider and registrar integration APIs"},
29
30
  {"name": "WAB Protocol", "description": "Core WAB command protocol over HTTP"},
30
31
  {"name": "Registry", "description": "Public directory of WAB-enabled sites"},
31
32
  {"name": "Plans", "description": "Subscription plan information"},
@@ -348,6 +349,94 @@
348
349
  }
349
350
  }
350
351
  },
352
+ "/api/discovery/provider/manifest": {
353
+ "get": {
354
+ "tags": ["Provider"],
355
+ "summary": "Provider protocol manifest",
356
+ "description": "Machine-readable contract for DNS providers and registrars implementing one-click WAB DNS Discovery.",
357
+ "operationId": "providerManifest",
358
+ "responses": {
359
+ "200": {"description": "Provider manifest"}
360
+ }
361
+ }
362
+ },
363
+ "/api/discovery/provider/record-template": {
364
+ "get": {
365
+ "tags": ["Provider"],
366
+ "summary": "Build record template",
367
+ "description": "Returns ready-to-write TXT record payload for a specific domain.",
368
+ "operationId": "providerRecordTemplate",
369
+ "parameters": [
370
+ {"name": "domain", "in": "query", "required": true, "schema": {"type": "string"}},
371
+ {"name": "endpoint", "in": "query", "required": false, "schema": {"type": "string"}}
372
+ ],
373
+ "responses": {
374
+ "200": {"description": "Record template payload"},
375
+ "400": {"description": "Invalid domain or endpoint"}
376
+ }
377
+ }
378
+ },
379
+ "/api/discovery/provider/enable-plan": {
380
+ "get": {
381
+ "tags": ["Provider"],
382
+ "summary": "Get one-click enable/disable plan",
383
+ "description": "Returns an implementation playbook with DNS operation, verify polling, and rollback hints.",
384
+ "operationId": "providerEnablePlan",
385
+ "parameters": [
386
+ {"name": "domain", "in": "query", "required": true, "schema": {"type": "string"}},
387
+ {"name": "action", "in": "query", "required": false, "schema": {"type": "string", "enum": ["enable", "disable"], "default": "enable"}},
388
+ {"name": "endpoint", "in": "query", "required": false, "schema": {"type": "string"}}
389
+ ],
390
+ "responses": {
391
+ "200": {"description": "Enable/disable execution plan"},
392
+ "400": {"description": "Invalid request"}
393
+ }
394
+ }
395
+ },
396
+ "/api/discovery/provider/status": {
397
+ "get": {
398
+ "tags": ["Provider"],
399
+ "summary": "Provider status check",
400
+ "description": "Returns machine-friendly state for UI toggles: enabled, partial, or disabled.",
401
+ "operationId": "providerStatus",
402
+ "parameters": [
403
+ {"name": "domain", "in": "query", "required": true, "schema": {"type": "string"}}
404
+ ],
405
+ "responses": {
406
+ "200": {"description": "Current provider status"},
407
+ "400": {"description": "Domain required"}
408
+ }
409
+ }
410
+ },
411
+ "/api/discovery/provider/verify-batch": {
412
+ "post": {
413
+ "tags": ["Provider"],
414
+ "summary": "Batch verify domains",
415
+ "description": "Verifies up to 50 domains in one request and optionally sends callback webhook with results.",
416
+ "operationId": "providerVerifyBatch",
417
+ "requestBody": {
418
+ "required": true,
419
+ "content": {
420
+ "application/json": {
421
+ "schema": {
422
+ "type": "object",
423
+ "required": ["domains"],
424
+ "properties": {
425
+ "domains": {"type": "array", "items": {"type": "string"}, "maxItems": 50},
426
+ "include_agent_run": {"type": "boolean", "default": false},
427
+ "callback_url": {"type": "string", "format": "uri"},
428
+ "callback_secret": {"type": "string"}
429
+ }
430
+ }
431
+ }
432
+ }
433
+ },
434
+ "responses": {
435
+ "200": {"description": "Batch verification results"},
436
+ "400": {"description": "Invalid request"}
437
+ }
438
+ }
439
+ },
351
440
  "/api/plans": {
352
441
  "get": {
353
442
  "tags": ["Plans"],