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,223 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * wab-init — Zero-Config WAB initializer
4
+ *
5
+ * Usage:
6
+ * npx wab-init # interactive
7
+ * npx wab-init --site=https://acme.com --name="Acme" --yes
8
+ *
9
+ * Detects project type (Next.js, Nuxt, Laravel, WordPress, static) and
10
+ * scaffolds /.well-known/wab.json with sensible defaults plus prints
11
+ * platform-specific DNS TXT instructions.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const readline = require('readline');
19
+
20
+ const ARG = parseArgs(process.argv.slice(2));
21
+
22
+ function parseArgs(argv) {
23
+ const out = { _: [] };
24
+ for (const a of argv) {
25
+ if (a.startsWith('--')) {
26
+ const [k, ...rest] = a.slice(2).split('=');
27
+ out[k] = rest.length ? rest.join('=') : true;
28
+ } else out._.push(a);
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function ask(rl, q, def) {
34
+ return new Promise((resolve) => {
35
+ const prompt = def ? `${q} [${def}]: ` : `${q}: `;
36
+ rl.question(prompt, (ans) => resolve((ans || '').trim() || def || ''));
37
+ });
38
+ }
39
+
40
+ /* ------------------------------------------------------------------ */
41
+ /* Project detection */
42
+ /* ------------------------------------------------------------------ */
43
+
44
+ function detectProject(cwd) {
45
+ const has = (f) => fs.existsSync(path.join(cwd, f));
46
+ const pkgPath = path.join(cwd, 'package.json');
47
+ let pkg = null;
48
+ if (has('package.json')) {
49
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch {}
50
+ }
51
+ const deps = pkg ? Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {}) : {};
52
+
53
+ if (deps.next) return { type: 'nextjs', pkg };
54
+ if (deps.nuxt || deps.nuxt3 || deps['nuxt-edge']) return { type: 'nuxt', pkg };
55
+ if (deps['@sveltejs/kit']) return { type: 'sveltekit', pkg };
56
+ if (deps.astro) return { type: 'astro', pkg };
57
+ if (deps.gatsby) return { type: 'gatsby', pkg };
58
+ if (deps['react-scripts'] || deps.vite) return { type: 'spa', pkg };
59
+ if (has('wp-config.php') || has('wp-content')) return { type: 'wordpress', pkg };
60
+ if (has('artisan') && has('composer.json')) return { type: 'laravel', pkg };
61
+ if (has('composer.json')) return { type: 'php', pkg };
62
+ if (has('manage.py')) return { type: 'django', pkg };
63
+ if (has('Gemfile')) return { type: 'rails', pkg };
64
+ if (pkg) return { type: 'node', pkg };
65
+ return { type: 'static', pkg: null };
66
+ }
67
+
68
+ function publicDirFor(type, cwd) {
69
+ const candidates = {
70
+ nextjs: ['public'],
71
+ nuxt: ['public', 'static'],
72
+ sveltekit: ['static'],
73
+ astro: ['public'],
74
+ gatsby: ['static'],
75
+ spa: ['public'],
76
+ wordpress: ['.'],
77
+ laravel: ['public'],
78
+ php: ['public', '.'],
79
+ django: ['static'],
80
+ rails: ['public'],
81
+ node: ['public', '.'],
82
+ static: ['.']
83
+ };
84
+ for (const c of candidates[type] || ['.']) {
85
+ if (fs.existsSync(path.join(cwd, c))) return c;
86
+ }
87
+ return '.';
88
+ }
89
+
90
+ /* ------------------------------------------------------------------ */
91
+ /* wab.json builder */
92
+ /* ------------------------------------------------------------------ */
93
+
94
+ function buildWabJson({ siteUrl, name, description, projectType }) {
95
+ const baseActions = [
96
+ { name: 'home', description: 'Open homepage', url: siteUrl }
97
+ ];
98
+
99
+ // Project-aware default action hints (heuristic, user can edit).
100
+ const hints = {
101
+ wordpress: [
102
+ { name: 'browseBlog', description: 'Browse blog posts', url: `${siteUrl}/?feed=rss2` },
103
+ { name: 'searchSite', description: 'Search the site', urlTemplate: `${siteUrl}/?s={query}` }
104
+ ],
105
+ nextjs: [
106
+ { name: 'browseSitemap', description: 'Site URL inventory', url: `${siteUrl}/sitemap.xml` }
107
+ ],
108
+ laravel: [
109
+ { name: 'browseSitemap', description: 'Site URL inventory', url: `${siteUrl}/sitemap.xml` }
110
+ ],
111
+ static: []
112
+ };
113
+
114
+ return {
115
+ version: '1.0',
116
+ site: name,
117
+ description: description || `${name} — managed by Web Agent Bridge`,
118
+ url: siteUrl,
119
+ project_type: projectType,
120
+ generated_at: new Date().toISOString(),
121
+ generator: 'wab-init',
122
+ discovery: {
123
+ well_known: `${siteUrl}/.well-known/wab.json`,
124
+ dns_txt_record: `_wab.${new URL(siteUrl).hostname}`
125
+ },
126
+ actions: [...baseActions, ...(hints[projectType] || [])],
127
+ trust: {
128
+ signed: false,
129
+ note: 'Run `npm run wab:sign` (or scripts/sign-wab-domain.js) to add an Ed25519 signature.'
130
+ }
131
+ };
132
+ }
133
+
134
+ /* ------------------------------------------------------------------ */
135
+ /* DNS instructions */
136
+ /* ------------------------------------------------------------------ */
137
+
138
+ function dnsInstructions(host) {
139
+ return `
140
+ DNS Discovery (recommended for full WAB Trust):
141
+ Add this TXT record at your DNS provider:
142
+
143
+ Name: _wab.${host}
144
+ Type: TXT
145
+ Value: v=wab1; well-known=https://${host}/.well-known/wab.json
146
+
147
+ Cloudflare: DNS → Records → Add record (Type=TXT)
148
+ Route 53: Hosted zones → Create record (Type=TXT)
149
+ Namecheap: Advanced DNS → Add New Record (Type=TXT)
150
+ GoDaddy: DNS Management → Add → TXT
151
+ cPanel: Zone Editor → Add Record → TXT
152
+
153
+ After publishing, verify at:
154
+ https://www.webagentbridge.com/check?host=${host}
155
+ `;
156
+ }
157
+
158
+ /* ------------------------------------------------------------------ */
159
+ /* Main */
160
+ /* ------------------------------------------------------------------ */
161
+
162
+ async function main() {
163
+ const cwd = process.cwd();
164
+ const detected = detectProject(cwd);
165
+ console.log(`\n Web Agent Bridge — wab-init`);
166
+ console.log(` Detected project: ${detected.type}` +
167
+ (detected.pkg && detected.pkg.name ? ` (${detected.pkg.name})` : '') + '\n');
168
+
169
+ let siteUrl = ARG.site || ARG.url;
170
+ let name = ARG.name;
171
+ let description = ARG.description;
172
+
173
+ if (!ARG.yes && (!siteUrl || !name)) {
174
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
175
+ try {
176
+ siteUrl = siteUrl || await ask(rl, 'Site URL (https://example.com)', 'https://example.com');
177
+ name = name || await ask(rl, 'Site name', detected.pkg && detected.pkg.name || 'My Site');
178
+ description = description || await ask(rl, 'Short description', '');
179
+ } finally {
180
+ rl.close();
181
+ }
182
+ }
183
+
184
+ if (!siteUrl) siteUrl = 'https://example.com';
185
+ if (!name) name = (detected.pkg && detected.pkg.name) || 'My Site';
186
+
187
+ // Normalize URL
188
+ if (!/^https?:\/\//i.test(siteUrl)) siteUrl = `https://${siteUrl}`;
189
+ siteUrl = siteUrl.replace(/\/+$/, '');
190
+ const host = new URL(siteUrl).hostname;
191
+
192
+ const wab = buildWabJson({ siteUrl, name, description, projectType: detected.type });
193
+
194
+ const pubDir = publicDirFor(detected.type, cwd);
195
+ const wellKnownDir = path.join(cwd, pubDir, '.well-known');
196
+ const wabPath = path.join(wellKnownDir, 'wab.json');
197
+
198
+ if (fs.existsSync(wabPath) && !ARG.force && !ARG.yes) {
199
+ console.log(`\n ! ${path.relative(cwd, wabPath)} already exists. Use --force to overwrite.`);
200
+ process.exit(2);
201
+ }
202
+
203
+ fs.mkdirSync(wellKnownDir, { recursive: true });
204
+ fs.writeFileSync(wabPath, JSON.stringify(wab, null, 2) + '\n');
205
+
206
+ console.log(`\n Wrote: ${path.relative(cwd, wabPath)}`);
207
+ console.log(` URL: ${siteUrl}/.well-known/wab.json`);
208
+ console.log(dnsInstructions(host));
209
+ console.log(` Next steps:`);
210
+ console.log(` 1. Deploy your site so /.well-known/wab.json is publicly reachable.`);
211
+ console.log(` 2. Add the DNS TXT record above.`);
212
+ console.log(` 3. (Optional) Sign with Ed25519: see scripts/sign-wab-domain.js`);
213
+ console.log(` 4. Verify: https://www.webagentbridge.com/check?host=${host}\n`);
214
+ }
215
+
216
+ if (require.main === module) {
217
+ main().catch((e) => {
218
+ console.error('wab-init failed:', e.message);
219
+ process.exit(1);
220
+ });
221
+ }
222
+
223
+ module.exports = { detectProject, buildWabJson, publicDirFor };
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * azure-dns-wab.js — enable/disable WAB DNS Discovery TXT record on Azure DNS.
4
+ *
5
+ * Auth: Bearer token from Azure CLI:
6
+ * az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv
7
+ *
8
+ * Usage:
9
+ * AZURE_TOKEN="…" node azure-dns-wab.js enable example.com <subscription-id> <resource-group> <zone-name>
10
+ * AZURE_TOKEN="…" node azure-dns-wab.js disable example.com <subscription-id> <resource-group> <zone-name>
11
+ * node azure-dns-wab.js status example.com
12
+ *
13
+ * Required role on the DNS zone: "DNS Zone Contributor"
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const fetch = (() => { try { return require('node-fetch'); } catch { return globalThis.fetch; } })();
19
+
20
+ const [,, action, domain, subId, rg, zone] = process.argv;
21
+ const TOKEN = process.env.AZURE_TOKEN;
22
+ const WAB_BASE = process.env.WAB_BASE_URL || 'https://www.webagentbridge.com';
23
+ const ENDPOINT = process.env.WAB_ENDPOINT || `https://${domain}/.well-known/wab.json`;
24
+ const API_VER = '2018-05-01';
25
+
26
+ if (!action || !domain) { console.error('Usage: node azure-dns-wab.js <enable|disable|status> <domain> [subscription-id] [resource-group] [zone-name]'); process.exit(1); }
27
+ if (!['enable','disable','status'].includes(action)) { console.error('Action must be: enable | disable | status'); process.exit(1); }
28
+ if (action !== 'status') {
29
+ if (!TOKEN) { console.error('Set AZURE_TOKEN (run: az account get-access-token --resource https://management.azure.com/ --query accessToken -o tsv)'); process.exit(1); }
30
+ if (!subId || !rg || !zone) { console.error('Need <subscription-id> <resource-group> <zone-name>'); process.exit(1); }
31
+ }
32
+
33
+ const azPath = (subId, rg, zone) =>
34
+ `https://management.azure.com/subscriptions/${subId}/resourceGroups/${rg}` +
35
+ `/providers/Microsoft.Network/dnsZones/${zone}/TXT/_wab?api-version=${API_VER}`;
36
+
37
+ async function azReq(method, url, body) {
38
+ const r = await fetch(url, {
39
+ method,
40
+ headers: { 'Authorization': `Bearer ${TOKEN}`, 'Content-Type': 'application/json' },
41
+ body: body ? JSON.stringify(body) : undefined,
42
+ });
43
+ const text = await r.text();
44
+ if (!r.ok && !(method === 'DELETE' && r.status === 404)) {
45
+ throw new Error(`Azure ${method} ${r.status}: ${text}`);
46
+ }
47
+ return text ? JSON.parse(text) : {};
48
+ }
49
+
50
+ async function getTpl() {
51
+ const j = await (await fetch(`${WAB_BASE}/api/discovery/provider/record-template?domain=${encodeURIComponent(domain)}&endpoint=${encodeURIComponent(ENDPOINT)}`)).json();
52
+ if (!j.record || !j.record.value) throw new Error('Could not fetch WAB record template');
53
+ return j.record.value;
54
+ }
55
+
56
+ async function main() {
57
+ console.log(`[WAB] Action: ${action} | Domain: ${domain}`);
58
+
59
+ if (action === 'status') {
60
+ const j = await (await fetch(`${WAB_BASE}/api/discovery/provider/status?domain=${encodeURIComponent(domain)}`)).json();
61
+ console.log(`[WAB] Status: ${j.status}`);
62
+ console.log(JSON.stringify(j, null, 2));
63
+ return;
64
+ }
65
+
66
+ const url = azPath(subId, rg, zone);
67
+
68
+ if (action === 'enable') {
69
+ const txtVal = await getTpl();
70
+ console.log(`[WAB] TXT value: ${txtVal}`);
71
+ await azReq('PUT', url, { properties: { TTL: 3600, TXTRecords: [{ value: [txtVal] }] } });
72
+ console.log('[Azure DNS] PUT _wab TXT record done');
73
+ console.log('[WAB] WAB Discovery ENABLED.');
74
+ }
75
+
76
+ if (action === 'disable') {
77
+ await azReq('DELETE', url);
78
+ console.log('[Azure DNS] _wab TXT record deleted');
79
+ console.log('[WAB] WAB Discovery DISABLED.');
80
+ }
81
+ }
82
+
83
+ main().catch(err => { console.error('[ERROR]', err.message); process.exit(1); });
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cloudflare-wab-dns.js
4
+ * ---------------------
5
+ * CLI tool: enable or disable WAB DNS Discovery TXT record on Cloudflare.
6
+ *
7
+ * Usage:
8
+ * CF_API_TOKEN=<token> node cloudflare-wab-dns.js enable example.com
9
+ * CF_API_TOKEN=<token> node cloudflare-wab-dns.js disable example.com
10
+ * CF_API_TOKEN=<token> node cloudflare-wab-dns.js status example.com
11
+ *
12
+ * Optional: set WAB_ENDPOINT env var to override the endpoint URL in the TXT record.
13
+ *
14
+ * Required npm package: node-fetch (v2 for CommonJS):
15
+ * npm install node-fetch@2
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fetch = (() => {
21
+ try { return require('node-fetch'); }
22
+ catch { return globalThis.fetch; }
23
+ })();
24
+
25
+ const CF_TOKEN = process.env.CF_API_TOKEN;
26
+ const [,, action, domain] = process.argv;
27
+
28
+ if (!CF_TOKEN) { console.error('Error: CF_API_TOKEN env variable is required.'); process.exit(1); }
29
+ if (!action || !domain) {
30
+ console.error('Usage: node cloudflare-wab-dns.js <enable|disable|status> <domain>');
31
+ process.exit(1);
32
+ }
33
+ if (!['enable', 'disable', 'status'].includes(action)) {
34
+ console.error('Action must be one of: enable, disable, status');
35
+ process.exit(1);
36
+ }
37
+
38
+ const CF_BASE = 'https://api.cloudflare.com/client/v4';
39
+ const WAB_BASE = process.env.WAB_BASE_URL || 'https://www.webagentbridge.com';
40
+ const ENDPOINT = process.env.WAB_ENDPOINT || `https://${domain}/.well-known/wab.json`;
41
+
42
+ function cfHeaders() {
43
+ return { 'Authorization': `Bearer ${CF_TOKEN}`, 'Content-Type': 'application/json' };
44
+ }
45
+
46
+ async function cfGet(path) {
47
+ const r = await fetch(`${CF_BASE}${path}`, { headers: cfHeaders() });
48
+ const j = await r.json();
49
+ if (!j.success) throw new Error(`CF API error: ${JSON.stringify(j.errors)}`);
50
+ return j;
51
+ }
52
+
53
+ async function getZoneId() {
54
+ const j = await cfGet(`/zones?name=${encodeURIComponent(domain)}`);
55
+ if (!j.result[0]) throw new Error(`Zone not found for domain "${domain}"`);
56
+ return j.result[0].id;
57
+ }
58
+
59
+ async function getRecordTemplate() {
60
+ const url = `${WAB_BASE}/api/discovery/provider/record-template?domain=${encodeURIComponent(domain)}&endpoint=${encodeURIComponent(ENDPOINT)}`;
61
+ const r = await fetch(url);
62
+ const j = await r.json();
63
+ if (!j.record || !j.record.value) throw new Error('Could not fetch WAB record template');
64
+ return j.record.value;
65
+ }
66
+
67
+ async function findExistingRecord(zoneId) {
68
+ const j = await cfGet(`/zones/${zoneId}/dns_records?type=TXT&name=_wab.${domain}`);
69
+ return j.result && j.result[0] ? j.result[0] : null;
70
+ }
71
+
72
+ async function main() {
73
+ console.log(`[WAB] Action: ${action} | Domain: ${domain}`);
74
+
75
+ if (action === 'status') {
76
+ const url = `${WAB_BASE}/api/discovery/provider/status?domain=${encodeURIComponent(domain)}`;
77
+ const j = await (await fetch(url)).json();
78
+ console.log(`[WAB] Status: ${j.status}`);
79
+ console.log(JSON.stringify(j, null, 2));
80
+ return;
81
+ }
82
+
83
+ const txtValue = await getRecordTemplate();
84
+ console.log(`[WAB] TXT value: ${txtValue}`);
85
+
86
+ const zoneId = await getZoneId();
87
+ console.log(`[CF] Zone ID: ${zoneId}`);
88
+
89
+ const existing = await findExistingRecord(zoneId);
90
+ const recBase = `${CF_BASE}/zones/${zoneId}/dns_records`;
91
+
92
+ if (action === 'enable') {
93
+ const body = JSON.stringify({ type: 'TXT', name: `_wab.${domain}`, content: txtValue, ttl: 3600 });
94
+ if (existing) {
95
+ const r = await fetch(`${recBase}/${existing.id}`, { method: 'PUT', headers: cfHeaders(), body });
96
+ const j = await r.json();
97
+ if (!j.success) throw new Error(`PUT failed: ${JSON.stringify(j.errors)}`);
98
+ console.log(`[CF] Updated TXT record (id=${existing.id})`);
99
+ } else {
100
+ const r = await fetch(recBase, { method: 'POST', headers: cfHeaders(), body });
101
+ const j = await r.json();
102
+ if (!j.success) throw new Error(`POST failed: ${JSON.stringify(j.errors)}`);
103
+ console.log(`[CF] Created TXT record (id=${j.result.id})`);
104
+ }
105
+ console.log('[WAB] WAB Discovery ENABLED. Verification may take up to 60 s for DNS propagation.');
106
+ }
107
+
108
+ if (action === 'disable') {
109
+ if (!existing) {
110
+ console.log('[CF] No _wab TXT record found. Already disabled.');
111
+ return;
112
+ }
113
+ const r = await fetch(`${recBase}/${existing.id}`, { method: 'DELETE', headers: cfHeaders() });
114
+ const j = await r.json();
115
+ if (!j.success) throw new Error(`DELETE failed: ${JSON.stringify(j.errors)}`);
116
+ console.log(`[CF] Deleted TXT record (id=${existing.id})`);
117
+ console.log('[WAB] WAB Discovery DISABLED.');
118
+ }
119
+ }
120
+
121
+ main().catch(err => { console.error('[ERROR]', err.message); process.exit(1); });
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cpanel-wab-dns.js
4
+ * ------------------
5
+ * CLI tool: enable or disable WAB DNS Discovery TXT record via cPanel UAPI.
6
+ *
7
+ * Usage:
8
+ * CPANEL_API_TOKEN=<token> node cpanel-wab-dns.js enable example.com cpanel.example.com myuser
9
+ * CPANEL_API_TOKEN=<token> node cpanel-wab-dns.js disable example.com cpanel.example.com myuser
10
+ * CPANEL_API_TOKEN=<token> node cpanel-wab-dns.js status example.com
11
+ *
12
+ * Optional env vars:
13
+ * CPANEL_PORT=2083 (default: 2083)
14
+ * CPANEL_PASSWORD (used instead of API token when set; token preferred)
15
+ * WAB_BASE_URL (default: https://www.webagentbridge.com)
16
+ * WAB_ENDPOINT (override the wab.json endpoint URL in the TXT record)
17
+ * NODE_TLS_REJECT_UNAUTHORIZED=0 (set to bypass self-signed cert on test servers)
18
+ *
19
+ * Required: node-fetch v2 for CommonJS environments:
20
+ * npm install node-fetch@2
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const fetch = (() => {
26
+ try { return require('node-fetch'); }
27
+ catch { return globalThis.fetch; }
28
+ })();
29
+
30
+ const [,, action, domain, cpHost, cpUser] = process.argv;
31
+
32
+ const CP_PORT = process.env.CPANEL_PORT || '2083';
33
+ const CP_TOKEN = process.env.CPANEL_API_TOKEN;
34
+ const CP_PASS = process.env.CPANEL_PASSWORD;
35
+ const WAB_BASE = process.env.WAB_BASE_URL || 'https://www.webagentbridge.com';
36
+ const ENDPOINT = process.env.WAB_ENDPOINT || `https://${domain}/.well-known/wab.json`;
37
+
38
+ if (!action || !domain) { console.error('Usage: node cpanel-wab-dns.js <enable|disable|status> <domain> [cpanel-host] [username]'); process.exit(1); }
39
+ if (!['enable','disable','status'].includes(action)) { console.error('Action must be: enable | disable | status'); process.exit(1); }
40
+ if (action !== 'status' && (!cpHost || !cpUser)) { console.error('cpanel-host and username required for enable/disable'); process.exit(1); }
41
+ if (action !== 'status' && !CP_TOKEN && !CP_PASS) { console.error('Set CPANEL_API_TOKEN or CPANEL_PASSWORD env variable'); process.exit(1); }
42
+
43
+ function cpHeaders() {
44
+ if (CP_TOKEN) return { Authorization: `cpanel ${cpUser}:${CP_TOKEN}` };
45
+ const b64 = Buffer.from(`${cpUser}:${CP_PASS}`).toString('base64');
46
+ return { Authorization: `Basic ${b64}` };
47
+ }
48
+
49
+ function cpUrl(func, params = {}) {
50
+ const qs = new URLSearchParams({ domain, ...params }).toString();
51
+ return `https://${cpHost}:${CP_PORT}/execute/ZoneEdit/${func}?${qs}`;
52
+ }
53
+
54
+ async function cpCall(func, params = {}) {
55
+ const r = await fetch(cpUrl(func, params), { headers: cpHeaders() });
56
+ const j = await r.json();
57
+ if (j.errors && j.errors.length) throw new Error(`cPanel error: ${j.errors.join(', ')}`);
58
+ return j;
59
+ }
60
+
61
+ async function getRecordTemplate() {
62
+ const url = `${WAB_BASE}/api/discovery/provider/record-template?domain=${encodeURIComponent(domain)}&endpoint=${encodeURIComponent(ENDPOINT)}`;
63
+ const j = await (await fetch(url)).json();
64
+ if (!j.record || !j.record.value) throw new Error('Could not fetch WAB record template');
65
+ return j.record.value;
66
+ }
67
+
68
+ async function listWabRecords() {
69
+ const j = await cpCall('fetch_zone_records', { type: 'TXT', name: `_wab.${domain}.` });
70
+ return Array.isArray(j.data) ? j.data : [];
71
+ }
72
+
73
+ async function main() {
74
+ console.log(`[WAB] Action: ${action} | Domain: ${domain}`);
75
+
76
+ if (action === 'status') {
77
+ const url = `${WAB_BASE}/api/discovery/provider/status?domain=${encodeURIComponent(domain)}`;
78
+ const j = await (await fetch(url)).json();
79
+ console.log(`[WAB] Status: ${j.status}`);
80
+ console.log(JSON.stringify(j, null, 2));
81
+ return;
82
+ }
83
+
84
+ const txtValue = await getRecordTemplate();
85
+ console.log(`[WAB] TXT value: ${txtValue}`);
86
+
87
+ const records = await listWabRecords();
88
+ const existing = records[0] || null;
89
+ console.log(`[CP] Existing _wab TXT records: ${records.length}`);
90
+
91
+ if (action === 'enable') {
92
+ const payload = { type: 'TXT', name: `_wab.${domain}.`, txtdata: txtValue, ttl: 3600 };
93
+ if (existing) {
94
+ const j = await cpCall('edit_zone_record', { ...payload, line: existing.line });
95
+ console.log(`[CP] Updated TXT record (line=${existing.line})`);
96
+ console.log(JSON.stringify(j, null, 2));
97
+ } else {
98
+ const j = await cpCall('add_zone_record', payload);
99
+ console.log('[CP] Created TXT record');
100
+ console.log(JSON.stringify(j, null, 2));
101
+ }
102
+ console.log('[WAB] WAB Discovery ENABLED. Propagation may take up to 60 s.');
103
+ }
104
+
105
+ if (action === 'disable') {
106
+ if (!existing) { console.log('[CP] No _wab record found — already disabled.'); return; }
107
+ const j = await cpCall('remove_zone_record', { line: existing.line });
108
+ console.log(`[CP] Deleted TXT record (line=${existing.line})`);
109
+ console.log(JSON.stringify(j, null, 2));
110
+ console.log('[WAB] WAB Discovery DISABLED.');
111
+ }
112
+ }
113
+
114
+ main().catch(err => { console.error('[ERROR]', err.message); process.exit(1); });