web-agent-bridge 3.4.0 → 3.9.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 (315) hide show
  1. package/LICENSE +84 -84
  2. package/README.ar.md +1565 -1304
  3. package/README.md +171 -298
  4. package/bin/agent-runner.js +474 -474
  5. package/bin/cli.js +237 -237
  6. package/bin/wab-init.js +244 -223
  7. package/bin/wab.js +80 -80
  8. package/examples/azure-dns-wab.js +83 -83
  9. package/examples/bidi-agent.js +119 -119
  10. package/examples/cloudflare-wab-dns.js +121 -121
  11. package/examples/cpanel-wab-dns.js +114 -114
  12. package/examples/cross-site-agent.js +91 -91
  13. package/examples/dns-discovery-agent.js +166 -166
  14. package/examples/gcp-dns-wab.js +76 -76
  15. package/examples/governance-agent.js +169 -169
  16. package/examples/mcp-agent.js +94 -94
  17. package/examples/next-app-router/README.md +44 -44
  18. package/examples/plesk-wab-dns.js +103 -103
  19. package/examples/puppeteer-agent.js +108 -108
  20. package/examples/route53-wab-dns.js +144 -144
  21. package/examples/saas-dashboard/README.md +55 -55
  22. package/examples/safe-mode-agent.js +96 -96
  23. package/examples/self-discovery.js +106 -0
  24. package/examples/shopify-hydrogen/README.md +74 -74
  25. package/examples/vision-agent.js +171 -171
  26. package/examples/wab-sign.js +74 -74
  27. package/examples/wab-verify.js +60 -60
  28. package/examples/wordpress-elementor/README.md +77 -77
  29. package/package.json +93 -93
  30. package/public/.well-known/agent-tools.json +180 -180
  31. package/public/.well-known/ai-assets.json +59 -59
  32. package/public/.well-known/security.txt +8 -8
  33. package/public/.well-known/wab.json +28 -28
  34. package/public/activate.html +448 -368
  35. package/public/adopt.html +236 -0
  36. package/public/adoption-metrics.html +188 -188
  37. package/public/agent-workspace.html +359 -349
  38. package/public/ai.html +198 -198
  39. package/public/api.html +397 -413
  40. package/public/atp.html +171 -0
  41. package/public/azure-dns-integration.html +289 -289
  42. package/public/browser.html +486 -486
  43. package/public/cloudflare-integration.html +380 -380
  44. package/public/commander-dashboard.html +243 -243
  45. package/public/cookies.html +210 -210
  46. package/public/cpanel-integration.html +398 -398
  47. package/public/css/agent-workspace.css +1713 -1713
  48. package/public/css/premium.css +317 -317
  49. package/public/css/styles.css +1401 -1263
  50. package/public/dashboard-shieldlink.html +295 -0
  51. package/public/dashboard.html +711 -707
  52. package/public/dns.html +436 -436
  53. package/public/docs.html +588 -588
  54. package/public/enterprise-mesh.ar.html +80 -0
  55. package/public/enterprise-mesh.html +81 -0
  56. package/public/feed.xml +89 -89
  57. package/public/gcp-dns-integration.html +318 -318
  58. package/public/governance.ar.html +70 -0
  59. package/public/governance.html +69 -0
  60. package/public/growth.html +465 -465
  61. package/public/index.html +1372 -1266
  62. package/public/integrations.html +556 -556
  63. package/public/js/activate.js +449 -145
  64. package/public/js/agent-workspace.js +1740 -1740
  65. package/public/js/auth-nav.js +117 -65
  66. package/public/js/auth-redirect.js +12 -12
  67. package/public/js/cookie-consent.js +56 -56
  68. package/public/js/dns.js +438 -438
  69. package/public/js/wab-demo-page.js +721 -721
  70. package/public/js/ws-client.js +74 -74
  71. package/public/l-preview.html +242 -0
  72. package/public/llms-full.txt +360 -360
  73. package/public/llms.txt +125 -125
  74. package/public/login.html +85 -85
  75. package/public/mesh-dashboard.html +328 -328
  76. package/public/milestones.html +346 -0
  77. package/public/one-click.html +779 -0
  78. package/public/openapi.json +669 -669
  79. package/public/partners.ar.html +145 -0
  80. package/public/partners.html +143 -0
  81. package/public/phone-shield.html +281 -281
  82. package/public/plesk-integration.html +375 -375
  83. package/public/premium-dashboard.html +2489 -2489
  84. package/public/premium.html +793 -793
  85. package/public/privacy.html +297 -297
  86. package/public/provider-onboarding.html +172 -172
  87. package/public/provider-sandbox.html +134 -134
  88. package/public/providers.html +359 -359
  89. package/public/refusals.html +172 -0
  90. package/public/register.html +105 -105
  91. package/public/registrar-integrations.html +141 -141
  92. package/public/ring4.html +292 -0
  93. package/public/robots.txt +99 -99
  94. package/public/route53-integration.html +531 -531
  95. package/public/score.html +263 -0
  96. package/public/script/wab-consent.d.ts +36 -36
  97. package/public/script/wab-consent.js +104 -104
  98. package/public/script/wab-schema.js +131 -131
  99. package/public/script/wab.d.ts +108 -108
  100. package/public/script/wab.min.js +580 -580
  101. package/public/security.txt +8 -8
  102. package/public/shieldlink.html +244 -0
  103. package/public/shieldqr.html +231 -231
  104. package/public/sitemap.xml +13 -1
  105. package/public/terms.html +256 -256
  106. package/public/trust-graph-api.ar.html +92 -0
  107. package/public/trust-graph-api.html +91 -0
  108. package/public/wab-features.html +560 -0
  109. package/public/wab-trust.html +200 -200
  110. package/public/wab-truth.html +375 -0
  111. package/public/wab-vs-protocols.html +210 -210
  112. package/public/whitepaper.html +449 -449
  113. package/script/ai-agent-bridge.js +1754 -1754
  114. package/sdk/README.md +99 -99
  115. package/sdk/agent-mesh.js +449 -449
  116. package/sdk/atp.js +103 -0
  117. package/sdk/auto-discovery.js +301 -288
  118. package/sdk/commander.js +262 -262
  119. package/sdk/governance.js +262 -262
  120. package/sdk/index.d.ts +464 -464
  121. package/sdk/index.js +653 -649
  122. package/sdk/multi-agent.js +318 -318
  123. package/sdk/safe-mode.js +221 -221
  124. package/sdk/safety-shield.js +219 -219
  125. package/sdk/schema-discovery.js +83 -83
  126. package/server/adapters/index.js +520 -520
  127. package/server/config/plans.js +412 -367
  128. package/server/config/secrets.js +102 -102
  129. package/server/control-plane/index.js +301 -301
  130. package/server/data-plane/index.js +354 -354
  131. package/server/index.js +793 -670
  132. package/server/llm/index.js +404 -404
  133. package/server/middleware/adminAuth.js +35 -35
  134. package/server/middleware/api-tier.js +170 -0
  135. package/server/middleware/auth.js +50 -50
  136. package/server/middleware/featureGate.js +88 -88
  137. package/server/middleware/rateLimits.js +100 -100
  138. package/server/middleware/sensitiveAction.js +157 -157
  139. package/server/middleware/wab-trust.js +141 -0
  140. package/server/migrations/001_add_analytics_indexes.sql +7 -7
  141. package/server/migrations/002_premium_features.sql +418 -418
  142. package/server/migrations/003_ads_integer_cents.sql +33 -33
  143. package/server/migrations/004_agent_os.sql +158 -158
  144. package/server/migrations/005_marketplace_metering.sql +126 -126
  145. package/server/migrations/006_growth_suite.sql +138 -0
  146. package/server/migrations/007_governance.sql +106 -106
  147. package/server/migrations/008_plans.sql +144 -144
  148. package/server/migrations/009_shieldqr.sql +30 -30
  149. package/server/migrations/010_extended_trust.sql +33 -33
  150. package/server/migrations/011_outreach.sql +47 -0
  151. package/server/migrations/012_shieldlink.sql +116 -0
  152. package/server/migrations/013_ct_monitor.sql +13 -0
  153. package/server/migrations/014_wab_advanced_features.sql +128 -0
  154. package/server/migrations/015_wab_truth_layer.sql +101 -0
  155. package/server/migrations/016_ring4_external_trust.sql +84 -0
  156. package/server/migrations/017_ring4_extensions.sql +69 -0
  157. package/server/migrations/018_commercial_foundations.sql +167 -0
  158. package/server/migrations/019_unify_tier_constraints.sql +133 -0
  159. package/server/migrations/020_agent_transaction_primitive.sql +119 -0
  160. package/server/models/adapters/index.js +33 -33
  161. package/server/models/adapters/mysql.js +183 -183
  162. package/server/models/adapters/postgresql.js +172 -172
  163. package/server/models/adapters/sqlite.js +7 -7
  164. package/server/models/db.js +740 -740
  165. package/server/observability/failure-analysis.js +337 -337
  166. package/server/observability/index.js +394 -394
  167. package/server/protocol/capabilities.js +223 -223
  168. package/server/protocol/index.js +243 -243
  169. package/server/protocol/schema.js +584 -584
  170. package/server/registry/certification.js +271 -271
  171. package/server/registry/index.js +326 -326
  172. package/server/routes/activate.js +478 -0
  173. package/server/routes/admin-outreach.js +239 -0
  174. package/server/routes/admin-plans.js +76 -76
  175. package/server/routes/admin-premium.js +674 -673
  176. package/server/routes/admin-shieldlink.js +137 -0
  177. package/server/routes/admin-shieldqr.js +90 -90
  178. package/server/routes/admin-trust-monitor.js +139 -83
  179. package/server/routes/admin.js +550 -549
  180. package/server/routes/adopt.js +61 -0
  181. package/server/routes/ads.js +130 -130
  182. package/server/routes/agent-workspace.js +540 -540
  183. package/server/routes/api-keys.js +127 -0
  184. package/server/routes/api.js +150 -150
  185. package/server/routes/auth.js +71 -71
  186. package/server/routes/billing.js +57 -57
  187. package/server/routes/commander.js +316 -316
  188. package/server/routes/customer-shieldlink.js +133 -0
  189. package/server/routes/demo-showcase.js +332 -332
  190. package/server/routes/demo-store.js +154 -154
  191. package/server/routes/diagnose.js +373 -0
  192. package/server/routes/discovery.js +2348 -2348
  193. package/server/routes/enterprise-mesh.js +170 -0
  194. package/server/routes/gateway.js +173 -173
  195. package/server/routes/governance-saas.js +203 -0
  196. package/server/routes/governance.js +208 -208
  197. package/server/routes/growth.js +1048 -0
  198. package/server/routes/intent.js +328 -0
  199. package/server/routes/license.js +251 -251
  200. package/server/routes/mesh.js +469 -469
  201. package/server/routes/noscript.js +543 -543
  202. package/server/routes/partners.js +201 -0
  203. package/server/routes/plans.js +33 -33
  204. package/server/routes/premium-v2.js +686 -686
  205. package/server/routes/premium.js +724 -724
  206. package/server/routes/providers.js +650 -650
  207. package/server/routes/reputation.js +411 -0
  208. package/server/routes/ring4.js +885 -0
  209. package/server/routes/runtime.js +2148 -2148
  210. package/server/routes/shieldlink.js +70 -0
  211. package/server/routes/shieldqr.js +88 -88
  212. package/server/routes/sovereign.js +465 -465
  213. package/server/routes/transactions.js +233 -0
  214. package/server/routes/truth-layer.js +670 -0
  215. package/server/routes/universal.js +200 -200
  216. package/server/routes/unsubscribe.js +51 -0
  217. package/server/routes/wab-api.js +850 -850
  218. package/server/routes/wab-cache.js +282 -0
  219. package/server/runtime/container-worker.js +111 -111
  220. package/server/runtime/container.js +448 -448
  221. package/server/runtime/distributed-worker.js +362 -362
  222. package/server/runtime/event-bus.js +210 -210
  223. package/server/runtime/index.js +253 -253
  224. package/server/runtime/queue.js +599 -599
  225. package/server/runtime/replay.js +666 -666
  226. package/server/runtime/sandbox.js +266 -266
  227. package/server/runtime/scheduler.js +534 -534
  228. package/server/runtime/session-engine.js +293 -293
  229. package/server/runtime/state-manager.js +188 -188
  230. package/server/secrets/wab-signing-key.pem +3 -0
  231. package/server/secrets/wab-signing-pub.pem +3 -0
  232. package/server/security/cross-site-redactor.js +196 -196
  233. package/server/security/dry-run.js +180 -180
  234. package/server/security/human-gate-rate-limit.js +147 -147
  235. package/server/security/human-gate-transports.js +178 -178
  236. package/server/security/human-gate.js +281 -281
  237. package/server/security/index.js +368 -368
  238. package/server/security/intent-engine.js +245 -245
  239. package/server/security/reward-guard.js +171 -171
  240. package/server/security/rollback-store.js +239 -239
  241. package/server/security/token-scope.js +404 -404
  242. package/server/security/url-policy.js +139 -139
  243. package/server/services/adoption-agent.js +182 -0
  244. package/server/services/agent-chat.js +506 -506
  245. package/server/services/agent-learning.js +601 -601
  246. package/server/services/agent-memory.js +625 -625
  247. package/server/services/agent-mesh.js +555 -555
  248. package/server/services/agent-symphony.js +717 -717
  249. package/server/services/agent-tasks.js +1807 -1807
  250. package/server/services/api-key-engine.js +292 -292
  251. package/server/services/cluster.js +894 -894
  252. package/server/services/commander.js +738 -738
  253. package/server/services/edge-compute.js +440 -440
  254. package/server/services/email.js +233 -233
  255. package/server/services/fairness-engine.js +409 -0
  256. package/server/services/fairness.js +420 -0
  257. package/server/services/governance.js +466 -466
  258. package/server/services/hosted-runtime.js +205 -205
  259. package/server/services/lfd.js +635 -635
  260. package/server/services/local-ai.js +389 -389
  261. package/server/services/marketplace.js +270 -270
  262. package/server/services/metering.js +182 -182
  263. package/server/services/modules/affiliate-intelligence.js +93 -93
  264. package/server/services/modules/agent-firewall.js +90 -90
  265. package/server/services/modules/bounty.js +89 -89
  266. package/server/services/modules/collective-bargaining.js +92 -92
  267. package/server/services/modules/dark-pattern.js +66 -66
  268. package/server/services/modules/gov-intelligence.js +45 -45
  269. package/server/services/modules/neural.js +55 -55
  270. package/server/services/modules/notary.js +49 -49
  271. package/server/services/modules/price-time-machine.js +86 -86
  272. package/server/services/modules/protocol.js +104 -104
  273. package/server/services/negotiation.js +439 -439
  274. package/server/services/outreach-agent.js +312 -0
  275. package/server/services/plans.js +214 -214
  276. package/server/services/plugins.js +771 -771
  277. package/server/services/price-intelligence.js +566 -566
  278. package/server/services/price-shield.js +1137 -1137
  279. package/server/services/provider-clients.js +740 -740
  280. package/server/services/reputation.js +465 -465
  281. package/server/services/search-engine.js +357 -357
  282. package/server/services/security.js +513 -513
  283. package/server/services/self-healing.js +843 -843
  284. package/server/services/shieldlink.js +492 -0
  285. package/server/services/shieldqr.js +322 -322
  286. package/server/services/sovereign-shield.js +542 -542
  287. package/server/services/ssl-ct-monitor.js +224 -0
  288. package/server/services/ssl-inspector.js +42 -42
  289. package/server/services/ssl-monitor.js +167 -167
  290. package/server/services/stripe.js +206 -205
  291. package/server/services/swarm.js +788 -788
  292. package/server/services/transactions.js +525 -0
  293. package/server/services/universal-scraper.js +662 -662
  294. package/server/services/verification.js +481 -481
  295. package/server/services/vision.js +1163 -1163
  296. package/server/services/wab-crypto.js +178 -178
  297. package/server/utils/cache.js +125 -125
  298. package/server/utils/migrate.js +81 -81
  299. package/server/utils/safe-fetch.js +228 -228
  300. package/server/utils/secureFields.js +50 -50
  301. package/server/ws.js +161 -161
  302. package/templates/artisan-marketplace.yaml +104 -104
  303. package/templates/book-price-scout.yaml +98 -98
  304. package/templates/electronics-price-tracker.yaml +108 -108
  305. package/templates/flight-deal-hunter.yaml +113 -113
  306. package/templates/freelancer-direct.yaml +116 -116
  307. package/templates/grocery-price-compare.yaml +93 -93
  308. package/templates/hotel-direct-booking.yaml +113 -113
  309. package/templates/local-services.yaml +98 -98
  310. package/templates/olive-oil-tunisia.yaml +88 -88
  311. package/templates/organic-farm-fresh.yaml +101 -101
  312. package/templates/restaurant-direct.yaml +97 -97
  313. package/templates/ring4/banking-sovereign.yaml +55 -0
  314. package/templates/ring4/ecommerce-sovereign.yaml +58 -0
  315. package/templates/ring4/healthcare-sovereign.yaml +60 -0
@@ -1,513 +1,513 @@
1
- /**
2
- * WAB Security Layer — Command Signing, Audit Logging, and Agent Identity
3
- *
4
- * Implements:
5
- * - HMAC-SHA256 command signatures for non-repudiation
6
- * - Immutable audit log with tamper-evident chaining
7
- * - Agent identity verification (key pair registration)
8
- * - Capability-based tokens for fine-grained access control
9
- * - Timing-safe comparisons for secret validation
10
- */
11
-
12
- const crypto = require('crypto');
13
- const { db } = require('../models/db');
14
-
15
- // ─── Schema ──────────────────────────────────────────────────────────
16
-
17
- db.exec(`
18
- CREATE TABLE IF NOT EXISTS security_audit_log (
19
- id TEXT PRIMARY KEY,
20
- chain_hash TEXT NOT NULL,
21
- prev_hash TEXT,
22
- timestamp TEXT NOT NULL DEFAULT (datetime('now')),
23
- actor_type TEXT NOT NULL CHECK(actor_type IN ('agent','user','admin','system','plugin')),
24
- actor_id TEXT,
25
- action TEXT NOT NULL,
26
- resource TEXT,
27
- resource_id TEXT,
28
- ip_hash TEXT,
29
- signature TEXT,
30
- outcome TEXT DEFAULT 'success' CHECK(outcome IN ('success','denied','error','blocked')),
31
- details TEXT DEFAULT '{}',
32
- severity TEXT DEFAULT 'info' CHECK(severity IN ('info','warning','critical'))
33
- );
34
-
35
- CREATE TABLE IF NOT EXISTS registered_agents (
36
- id TEXT PRIMARY KEY,
37
- site_id TEXT NOT NULL,
38
- agent_name TEXT NOT NULL,
39
- public_key TEXT NOT NULL,
40
- key_algorithm TEXT DEFAULT 'hmac-sha256',
41
- capabilities TEXT DEFAULT '["read"]',
42
- status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended','revoked')),
43
- max_rate INTEGER DEFAULT 60,
44
- ip_allowlist TEXT DEFAULT '[]',
45
- total_commands INTEGER DEFAULT 0,
46
- last_command TEXT,
47
- created_at TEXT DEFAULT (datetime('now')),
48
- revoked_at TEXT,
49
- FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
50
- UNIQUE(site_id, agent_name)
51
- );
52
-
53
- CREATE TABLE IF NOT EXISTS capability_tokens (
54
- token_hash TEXT PRIMARY KEY,
55
- agent_id TEXT NOT NULL,
56
- site_id TEXT NOT NULL,
57
- capabilities TEXT NOT NULL DEFAULT '["read"]',
58
- allowed_actions TEXT DEFAULT '["*"]',
59
- selector_scope TEXT DEFAULT '[]',
60
- expires_at TEXT NOT NULL,
61
- revoked INTEGER DEFAULT 0,
62
- created_at TEXT DEFAULT (datetime('now')),
63
- FOREIGN KEY (agent_id) REFERENCES registered_agents(id) ON DELETE CASCADE,
64
- FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
65
- );
66
-
67
- CREATE TABLE IF NOT EXISTS revoked_tokens (
68
- token_hash TEXT PRIMARY KEY,
69
- revoked_at TEXT DEFAULT (datetime('now')),
70
- reason TEXT
71
- );
72
-
73
- CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON security_audit_log(timestamp);
74
- CREATE INDEX IF NOT EXISTS idx_audit_actor ON security_audit_log(actor_type, actor_id);
75
- CREATE INDEX IF NOT EXISTS idx_audit_action ON security_audit_log(action);
76
- CREATE INDEX IF NOT EXISTS idx_audit_severity ON security_audit_log(severity);
77
- CREATE INDEX IF NOT EXISTS idx_agents_site ON registered_agents(site_id);
78
- CREATE INDEX IF NOT EXISTS idx_cap_tokens_agent ON capability_tokens(agent_id);
79
- CREATE INDEX IF NOT EXISTS idx_cap_tokens_site ON capability_tokens(site_id);
80
- CREATE INDEX IF NOT EXISTS idx_revoked_tokens ON revoked_tokens(token_hash);
81
- `);
82
-
83
- // ─── Prepared Statements ─────────────────────────────────────────────
84
-
85
- const stmts = {
86
- insertAudit: db.prepare(`INSERT INTO security_audit_log
87
- (id, chain_hash, prev_hash, timestamp, actor_type, actor_id, action, resource, resource_id, ip_hash, signature, outcome, details, severity)
88
- VALUES (?, ?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
89
- getLastAudit: db.prepare(`SELECT chain_hash FROM security_audit_log ORDER BY rowid DESC LIMIT 1`),
90
- getAuditRange: db.prepare(`SELECT * FROM security_audit_log WHERE timestamp >= ? AND timestamp <= ? ORDER BY rowid`),
91
- getAuditByActor: db.prepare(`SELECT * FROM security_audit_log WHERE actor_type = ? AND actor_id = ? ORDER BY rowid DESC LIMIT ?`),
92
- getAuditBySeverity: db.prepare(`SELECT * FROM security_audit_log WHERE severity = ? ORDER BY rowid DESC LIMIT ?`),
93
-
94
- insertAgent: db.prepare(`INSERT INTO registered_agents
95
- (id, site_id, agent_name, public_key, key_algorithm, capabilities, max_rate, ip_allowlist)
96
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`),
97
- getAgent: db.prepare(`SELECT * FROM registered_agents WHERE id = ?`),
98
- getAgentByKey: db.prepare(`SELECT * FROM registered_agents WHERE site_id = ? AND public_key = ? AND status = 'active'`),
99
- getAgentByName: db.prepare(`SELECT * FROM registered_agents WHERE site_id = ? AND agent_name = ? AND status = 'active'`),
100
- getAgentsBySite: db.prepare(`SELECT * FROM registered_agents WHERE site_id = ? ORDER BY created_at DESC`),
101
- updateAgentStats: db.prepare(`UPDATE registered_agents SET total_commands = total_commands + 1, last_command = datetime('now') WHERE id = ?`),
102
- revokeAgent: db.prepare(`UPDATE registered_agents SET status = 'revoked', revoked_at = datetime('now') WHERE id = ?`),
103
- suspendAgent: db.prepare(`UPDATE registered_agents SET status = 'suspended' WHERE id = ?`),
104
-
105
- insertCapToken: db.prepare(`INSERT INTO capability_tokens
106
- (token_hash, agent_id, site_id, capabilities, allowed_actions, selector_scope, expires_at)
107
- VALUES (?, ?, ?, ?, ?, ?, ?)`),
108
- getCapToken: db.prepare(`SELECT ct.*, ra.status as agent_status, ra.public_key
109
- FROM capability_tokens ct
110
- JOIN registered_agents ra ON ct.agent_id = ra.id
111
- WHERE ct.token_hash = ? AND ct.revoked = 0
112
- AND ct.expires_at > datetime('now')
113
- AND ra.status = 'active'`),
114
- revokeCapToken: db.prepare(`UPDATE capability_tokens SET revoked = 1 WHERE token_hash = ?`),
115
-
116
- insertRevokedToken: db.prepare(`INSERT OR IGNORE INTO revoked_tokens (token_hash, reason) VALUES (?, ?)`),
117
- isTokenRevoked: db.prepare(`SELECT 1 FROM revoked_tokens WHERE token_hash = ? LIMIT 1`),
118
- };
119
-
120
- // ─── Audit Logging (Tamper-Evident Chain) ────────────────────────────
121
-
122
- let _lastHash = null;
123
-
124
- function _getLastHash() {
125
- if (_lastHash) return _lastHash;
126
- const row = stmts.getLastAudit.get();
127
- return row ? row.chain_hash : '0'.repeat(64);
128
- }
129
-
130
- /**
131
- * Log a security-critical event with hash chaining for tamper detection.
132
- */
133
- function auditLog(entry) {
134
- const id = crypto.randomUUID();
135
- const prevHash = _getLastHash();
136
- const payload = `${prevHash}|${entry.actorType}|${entry.actorId || ''}|${entry.action}|${entry.resource || ''}|${Date.now()}`;
137
- const chainHash = crypto.createHash('sha256').update(payload).digest('hex');
138
- _lastHash = chainHash;
139
-
140
- const ipHash = entry.ip ? crypto.createHash('sha256').update(entry.ip).digest('hex').slice(0, 16) : null;
141
-
142
- stmts.insertAudit.run(
143
- id, chainHash, prevHash,
144
- entry.actorType || 'system',
145
- entry.actorId || null,
146
- entry.action,
147
- entry.resource || null,
148
- entry.resourceId || null,
149
- ipHash,
150
- entry.signature || null,
151
- entry.outcome || 'success',
152
- JSON.stringify(entry.details || {}),
153
- entry.severity || 'info'
154
- );
155
-
156
- return { id, chainHash };
157
- }
158
-
159
- /**
160
- * Verify audit chain integrity — detect tampering.
161
- */
162
- function verifyAuditChain(startDate, endDate) {
163
- const logs = stmts.getAuditRange.all(startDate, endDate);
164
- if (logs.length === 0) return { valid: true, checked: 0 };
165
-
166
- let valid = true;
167
- let broken = null;
168
-
169
- for (let i = 1; i < logs.length; i++) {
170
- if (logs[i].prev_hash !== logs[i - 1].chain_hash) {
171
- valid = false;
172
- broken = { index: i, id: logs[i].id };
173
- break;
174
- }
175
- }
176
-
177
- return { valid, checked: logs.length, broken };
178
- }
179
-
180
- // ─── Command Signing ─────────────────────────────────────────────────
181
-
182
- /**
183
- * Sign a command payload with HMAC-SHA256.
184
- * @param {string} secretKey - Agent's secret key
185
- * @param {object} payload - { action, params, timestamp, nonce }
186
- * @returns {string} HMAC signature
187
- */
188
- function signCommand(secretKey, payload) {
189
- const canonical = JSON.stringify({
190
- action: payload.action,
191
- params: payload.params || {},
192
- timestamp: payload.timestamp,
193
- nonce: payload.nonce,
194
- });
195
- return crypto.createHmac('sha256', secretKey).update(canonical).digest('hex');
196
- }
197
-
198
- /**
199
- * Verify a command signature with timing-safe comparison.
200
- * Also validates timestamp freshness (< 5 minutes) and nonce uniqueness.
201
- */
202
- const _usedNonces = new Map();
203
- const NONCE_TTL = 5 * 60 * 1000;
204
-
205
- // Purge old nonces every 5 minutes
206
- setInterval(() => {
207
- const cutoff = Date.now() - NONCE_TTL;
208
- for (const [nonce, ts] of _usedNonces) {
209
- if (ts < cutoff) _usedNonces.delete(nonce);
210
- }
211
- }, NONCE_TTL);
212
-
213
- function verifyCommandSignature(publicKey, payload, signature) {
214
- // Check timestamp freshness (±5 minutes)
215
- const ts = payload.timestamp;
216
- if (!ts || Math.abs(Date.now() - ts) > NONCE_TTL) {
217
- return { valid: false, reason: 'timestamp_expired' };
218
- }
219
-
220
- // Check nonce uniqueness (replay protection)
221
- if (!payload.nonce) {
222
- return { valid: false, reason: 'nonce_required' };
223
- }
224
- if (_usedNonces.has(payload.nonce)) {
225
- return { valid: false, reason: 'nonce_reused' };
226
- }
227
-
228
- // Verify HMAC
229
- const expected = signCommand(publicKey, payload);
230
- const sigBuf = Buffer.from(signature, 'hex');
231
- const expBuf = Buffer.from(expected, 'hex');
232
-
233
- if (sigBuf.length !== expBuf.length) {
234
- return { valid: false, reason: 'invalid_signature' };
235
- }
236
-
237
- if (!crypto.timingSafeEqual(sigBuf, expBuf)) {
238
- return { valid: false, reason: 'invalid_signature' };
239
- }
240
-
241
- // Signature valid — record nonce
242
- _usedNonces.set(payload.nonce, Date.now());
243
- return { valid: true };
244
- }
245
-
246
- // ─── Agent Identity ──────────────────────────────────────────────────
247
-
248
- /**
249
- * Register a new agent with a cryptographic key.
250
- * Returns { agentId, secretKey } — the secret must be saved by the caller.
251
- */
252
- function registerAgent(siteId, agentName, options = {}) {
253
- const id = crypto.randomUUID();
254
- const secretKey = crypto.randomBytes(32).toString('hex');
255
- const publicKeyHash = crypto.createHash('sha256').update(secretKey).digest('hex');
256
-
257
- const capabilities = options.capabilities || ['read'];
258
- const validCaps = ['read', 'click', 'fill', 'scroll', 'navigate', 'execute', 'extract', 'api'];
259
- const filtered = capabilities.filter(c => validCaps.includes(c));
260
-
261
- stmts.insertAgent.run(
262
- id, siteId, agentName, publicKeyHash,
263
- 'hmac-sha256',
264
- JSON.stringify(filtered),
265
- options.maxRate || 60,
266
- JSON.stringify(options.ipAllowlist || [])
267
- );
268
-
269
- auditLog({
270
- actorType: 'system',
271
- action: 'agent_registered',
272
- resource: 'agent',
273
- resourceId: id,
274
- details: { agentName, siteId, capabilities: filtered },
275
- });
276
-
277
- return { agentId: id, secretKey };
278
- }
279
-
280
- /**
281
- * Authenticate an agent with its secret key.
282
- * Uses timing-safe comparison against stored public key hash.
283
- */
284
- function authenticateAgent(siteId, secretKey) {
285
- const publicKeyHash = crypto.createHash('sha256').update(secretKey).digest('hex');
286
- const agent = stmts.getAgentByKey.get(siteId, publicKeyHash);
287
-
288
- if (!agent) return null;
289
-
290
- stmts.updateAgentStats.run(agent.id);
291
- return {
292
- agentId: agent.id,
293
- agentName: agent.agent_name,
294
- capabilities: JSON.parse(agent.capabilities || '["read"]'),
295
- maxRate: agent.max_rate,
296
- };
297
- }
298
-
299
- // ─── Capability Tokens ───────────────────────────────────────────────
300
-
301
- /**
302
- * Issue a capability token with restricted scope.
303
- * @param {string} agentId
304
- * @param {string} siteId
305
- * @param {object} scope - { capabilities, allowedActions, selectorScope, ttlSeconds }
306
- */
307
- function issueCapabilityToken(agentId, siteId, scope = {}) {
308
- const rawToken = crypto.randomBytes(32).toString('hex');
309
- const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
310
- const ttl = Math.min(scope.ttlSeconds || 3600, 86400); // max 24h
311
- const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
312
-
313
- stmts.insertCapToken.run(
314
- tokenHash, agentId, siteId,
315
- JSON.stringify(scope.capabilities || ['read']),
316
- JSON.stringify(scope.allowedActions || ['*']),
317
- JSON.stringify(scope.selectorScope || []),
318
- expiresAt
319
- );
320
-
321
- auditLog({
322
- actorType: 'agent',
323
- actorId: agentId,
324
- action: 'capability_token_issued',
325
- resource: 'token',
326
- details: { siteId, capabilities: scope.capabilities, ttl },
327
- });
328
-
329
- return { token: rawToken, expiresAt };
330
- }
331
-
332
- /**
333
- * Validate a capability token and check if the requested action is allowed.
334
- * @returns {object|null} - Token info with capabilities, or null if invalid
335
- */
336
- function validateCapabilityToken(rawToken, requiredAction) {
337
- const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
338
-
339
- // Check revocation list
340
- if (stmts.isTokenRevoked.get(tokenHash)) return null;
341
-
342
- const token = stmts.getCapToken.get(tokenHash);
343
- if (!token) return null;
344
-
345
- const capabilities = JSON.parse(token.capabilities || '["read"]');
346
- const allowedActions = JSON.parse(token.allowed_actions || '["*"]');
347
-
348
- // Check if action is permitted
349
- if (requiredAction && !allowedActions.includes('*') && !allowedActions.includes(requiredAction)) {
350
- return null;
351
- }
352
-
353
- return {
354
- agentId: token.agent_id,
355
- siteId: token.site_id,
356
- capabilities,
357
- allowedActions,
358
- selectorScope: JSON.parse(token.selector_scope || '[]'),
359
- expiresAt: token.expires_at,
360
- };
361
- }
362
-
363
- /**
364
- * Revoke a specific token.
365
- */
366
- function revokeCapabilityToken(rawToken, reason) {
367
- const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
368
- stmts.revokeCapToken.run(tokenHash);
369
- stmts.insertRevokedToken.run(tokenHash, reason || 'manual_revocation');
370
- }
371
-
372
- // ─── JWT Revocation ──────────────────────────────────────────────────
373
-
374
- /**
375
- * Add a JWT to the revocation list (for logout / compromise).
376
- */
377
- function revokeJWT(token, reason) {
378
- const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
379
- stmts.insertRevokedToken.run(tokenHash, reason || 'manual_revocation');
380
- }
381
-
382
- function isJWTRevoked(token) {
383
- const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
384
- return !!stmts.isTokenRevoked.get(tokenHash);
385
- }
386
-
387
- // ─── Input Sanitizer ─────────────────────────────────────────────────
388
-
389
- const DOMAIN_RE = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
390
- const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,255}$/;
391
-
392
- function validateDomain(domain) {
393
- if (!domain || typeof domain !== 'string') return false;
394
- const clean = domain.replace(/^www\./, '').toLowerCase();
395
- return DOMAIN_RE.test(clean) && clean.length <= 253;
396
- }
397
-
398
- function validateEmail(email) {
399
- if (!email || typeof email !== 'string') return false;
400
- return EMAIL_RE.test(email) && email.length <= 320;
401
- }
402
-
403
- /**
404
- * Sanitize arbitrary string input — strip control characters, limit length.
405
- */
406
- function sanitizeInput(str, maxLength = 1000) {
407
- if (typeof str !== 'string') return '';
408
- // Remove control characters except newlines/tabs
409
- return str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').slice(0, maxLength);
410
- }
411
-
412
- /**
413
- * Validate and sanitize site configuration — strict schema enforcement.
414
- */
415
- function validateSiteConfig(config) {
416
- if (!config || typeof config !== 'object') return { valid: false, error: 'Config must be an object' };
417
-
418
- const allowed = {
419
- agentPermissions: 'object',
420
- restrictions: 'object',
421
- logging: 'object',
422
- stealth: 'object',
423
- };
424
-
425
- const validPermissions = ['readContent', 'click', 'fillForms', 'scroll', 'navigate', 'apiAccess', 'automatedLogin', 'extractData'];
426
-
427
- // Strip unknown top-level keys
428
- const cleaned = {};
429
- for (const [key, type] of Object.entries(allowed)) {
430
- if (config[key] !== undefined) {
431
- if (typeof config[key] !== type) {
432
- return { valid: false, error: `${key} must be ${type}` };
433
- }
434
- cleaned[key] = config[key];
435
- }
436
- }
437
-
438
- // Validate permissions — only allow known keys with boolean values
439
- if (cleaned.agentPermissions) {
440
- const perms = {};
441
- for (const [k, v] of Object.entries(cleaned.agentPermissions)) {
442
- if (validPermissions.includes(k) && typeof v === 'boolean') {
443
- perms[k] = v;
444
- }
445
- }
446
- cleaned.agentPermissions = perms;
447
- }
448
-
449
- // Validate restrictions
450
- if (cleaned.restrictions) {
451
- const r = cleaned.restrictions;
452
- if (r.allowedSelectors && !Array.isArray(r.allowedSelectors)) {
453
- return { valid: false, error: 'allowedSelectors must be an array' };
454
- }
455
- if (r.blockedSelectors && !Array.isArray(r.blockedSelectors)) {
456
- return { valid: false, error: 'blockedSelectors must be an array' };
457
- }
458
- }
459
-
460
- // Validate stealth consent requirement
461
- if (cleaned.stealth) {
462
- if (cleaned.stealth.enabled && !cleaned.stealth.consent) {
463
- return { valid: false, error: 'Stealth mode requires explicit consent: true' };
464
- }
465
- }
466
-
467
- // Reject configs > 10KB
468
- const serialized = JSON.stringify(cleaned);
469
- if (serialized.length > 10240) {
470
- return { valid: false, error: 'Config too large (max 10KB)' };
471
- }
472
-
473
- return { valid: true, config: cleaned };
474
- }
475
-
476
- // ─── IP Hashing ──────────────────────────────────────────────────────
477
-
478
- function hashIP(ip) {
479
- if (!ip) return null;
480
- return crypto.createHash('sha256').update(ip).digest('hex').slice(0, 16);
481
- }
482
-
483
- // ─── Exports ─────────────────────────────────────────────────────────
484
-
485
- module.exports = {
486
- // Audit
487
- auditLog,
488
- verifyAuditChain,
489
-
490
- // Command signing
491
- signCommand,
492
- verifyCommandSignature,
493
-
494
- // Agent identity
495
- registerAgent,
496
- authenticateAgent,
497
-
498
- // Capability tokens
499
- issueCapabilityToken,
500
- validateCapabilityToken,
501
- revokeCapabilityToken,
502
-
503
- // JWT revocation
504
- revokeJWT,
505
- isJWTRevoked,
506
-
507
- // Input validation
508
- validateDomain,
509
- validateEmail,
510
- sanitizeInput,
511
- validateSiteConfig,
512
- hashIP,
513
- };
1
+ /**
2
+ * WAB Security Layer — Command Signing, Audit Logging, and Agent Identity
3
+ *
4
+ * Implements:
5
+ * - HMAC-SHA256 command signatures for non-repudiation
6
+ * - Immutable audit log with tamper-evident chaining
7
+ * - Agent identity verification (key pair registration)
8
+ * - Capability-based tokens for fine-grained access control
9
+ * - Timing-safe comparisons for secret validation
10
+ */
11
+
12
+ const crypto = require('crypto');
13
+ const { db } = require('../models/db');
14
+
15
+ // ─── Schema ──────────────────────────────────────────────────────────
16
+
17
+ db.exec(`
18
+ CREATE TABLE IF NOT EXISTS security_audit_log (
19
+ id TEXT PRIMARY KEY,
20
+ chain_hash TEXT NOT NULL,
21
+ prev_hash TEXT,
22
+ timestamp TEXT NOT NULL DEFAULT (datetime('now')),
23
+ actor_type TEXT NOT NULL CHECK(actor_type IN ('agent','user','admin','system','plugin')),
24
+ actor_id TEXT,
25
+ action TEXT NOT NULL,
26
+ resource TEXT,
27
+ resource_id TEXT,
28
+ ip_hash TEXT,
29
+ signature TEXT,
30
+ outcome TEXT DEFAULT 'success' CHECK(outcome IN ('success','denied','error','blocked')),
31
+ details TEXT DEFAULT '{}',
32
+ severity TEXT DEFAULT 'info' CHECK(severity IN ('info','warning','critical'))
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS registered_agents (
36
+ id TEXT PRIMARY KEY,
37
+ site_id TEXT NOT NULL,
38
+ agent_name TEXT NOT NULL,
39
+ public_key TEXT NOT NULL,
40
+ key_algorithm TEXT DEFAULT 'hmac-sha256',
41
+ capabilities TEXT DEFAULT '["read"]',
42
+ status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended','revoked')),
43
+ max_rate INTEGER DEFAULT 60,
44
+ ip_allowlist TEXT DEFAULT '[]',
45
+ total_commands INTEGER DEFAULT 0,
46
+ last_command TEXT,
47
+ created_at TEXT DEFAULT (datetime('now')),
48
+ revoked_at TEXT,
49
+ FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
50
+ UNIQUE(site_id, agent_name)
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS capability_tokens (
54
+ token_hash TEXT PRIMARY KEY,
55
+ agent_id TEXT NOT NULL,
56
+ site_id TEXT NOT NULL,
57
+ capabilities TEXT NOT NULL DEFAULT '["read"]',
58
+ allowed_actions TEXT DEFAULT '["*"]',
59
+ selector_scope TEXT DEFAULT '[]',
60
+ expires_at TEXT NOT NULL,
61
+ revoked INTEGER DEFAULT 0,
62
+ created_at TEXT DEFAULT (datetime('now')),
63
+ FOREIGN KEY (agent_id) REFERENCES registered_agents(id) ON DELETE CASCADE,
64
+ FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS revoked_tokens (
68
+ token_hash TEXT PRIMARY KEY,
69
+ revoked_at TEXT DEFAULT (datetime('now')),
70
+ reason TEXT
71
+ );
72
+
73
+ CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON security_audit_log(timestamp);
74
+ CREATE INDEX IF NOT EXISTS idx_audit_actor ON security_audit_log(actor_type, actor_id);
75
+ CREATE INDEX IF NOT EXISTS idx_audit_action ON security_audit_log(action);
76
+ CREATE INDEX IF NOT EXISTS idx_audit_severity ON security_audit_log(severity);
77
+ CREATE INDEX IF NOT EXISTS idx_agents_site ON registered_agents(site_id);
78
+ CREATE INDEX IF NOT EXISTS idx_cap_tokens_agent ON capability_tokens(agent_id);
79
+ CREATE INDEX IF NOT EXISTS idx_cap_tokens_site ON capability_tokens(site_id);
80
+ CREATE INDEX IF NOT EXISTS idx_revoked_tokens ON revoked_tokens(token_hash);
81
+ `);
82
+
83
+ // ─── Prepared Statements ─────────────────────────────────────────────
84
+
85
+ const stmts = {
86
+ insertAudit: db.prepare(`INSERT INTO security_audit_log
87
+ (id, chain_hash, prev_hash, timestamp, actor_type, actor_id, action, resource, resource_id, ip_hash, signature, outcome, details, severity)
88
+ VALUES (?, ?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
89
+ getLastAudit: db.prepare(`SELECT chain_hash FROM security_audit_log ORDER BY rowid DESC LIMIT 1`),
90
+ getAuditRange: db.prepare(`SELECT * FROM security_audit_log WHERE timestamp >= ? AND timestamp <= ? ORDER BY rowid`),
91
+ getAuditByActor: db.prepare(`SELECT * FROM security_audit_log WHERE actor_type = ? AND actor_id = ? ORDER BY rowid DESC LIMIT ?`),
92
+ getAuditBySeverity: db.prepare(`SELECT * FROM security_audit_log WHERE severity = ? ORDER BY rowid DESC LIMIT ?`),
93
+
94
+ insertAgent: db.prepare(`INSERT INTO registered_agents
95
+ (id, site_id, agent_name, public_key, key_algorithm, capabilities, max_rate, ip_allowlist)
96
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`),
97
+ getAgent: db.prepare(`SELECT * FROM registered_agents WHERE id = ?`),
98
+ getAgentByKey: db.prepare(`SELECT * FROM registered_agents WHERE site_id = ? AND public_key = ? AND status = 'active'`),
99
+ getAgentByName: db.prepare(`SELECT * FROM registered_agents WHERE site_id = ? AND agent_name = ? AND status = 'active'`),
100
+ getAgentsBySite: db.prepare(`SELECT * FROM registered_agents WHERE site_id = ? ORDER BY created_at DESC`),
101
+ updateAgentStats: db.prepare(`UPDATE registered_agents SET total_commands = total_commands + 1, last_command = datetime('now') WHERE id = ?`),
102
+ revokeAgent: db.prepare(`UPDATE registered_agents SET status = 'revoked', revoked_at = datetime('now') WHERE id = ?`),
103
+ suspendAgent: db.prepare(`UPDATE registered_agents SET status = 'suspended' WHERE id = ?`),
104
+
105
+ insertCapToken: db.prepare(`INSERT INTO capability_tokens
106
+ (token_hash, agent_id, site_id, capabilities, allowed_actions, selector_scope, expires_at)
107
+ VALUES (?, ?, ?, ?, ?, ?, ?)`),
108
+ getCapToken: db.prepare(`SELECT ct.*, ra.status as agent_status, ra.public_key
109
+ FROM capability_tokens ct
110
+ JOIN registered_agents ra ON ct.agent_id = ra.id
111
+ WHERE ct.token_hash = ? AND ct.revoked = 0
112
+ AND ct.expires_at > datetime('now')
113
+ AND ra.status = 'active'`),
114
+ revokeCapToken: db.prepare(`UPDATE capability_tokens SET revoked = 1 WHERE token_hash = ?`),
115
+
116
+ insertRevokedToken: db.prepare(`INSERT OR IGNORE INTO revoked_tokens (token_hash, reason) VALUES (?, ?)`),
117
+ isTokenRevoked: db.prepare(`SELECT 1 FROM revoked_tokens WHERE token_hash = ? LIMIT 1`),
118
+ };
119
+
120
+ // ─── Audit Logging (Tamper-Evident Chain) ────────────────────────────
121
+
122
+ let _lastHash = null;
123
+
124
+ function _getLastHash() {
125
+ if (_lastHash) return _lastHash;
126
+ const row = stmts.getLastAudit.get();
127
+ return row ? row.chain_hash : '0'.repeat(64);
128
+ }
129
+
130
+ /**
131
+ * Log a security-critical event with hash chaining for tamper detection.
132
+ */
133
+ function auditLog(entry) {
134
+ const id = crypto.randomUUID();
135
+ const prevHash = _getLastHash();
136
+ const payload = `${prevHash}|${entry.actorType}|${entry.actorId || ''}|${entry.action}|${entry.resource || ''}|${Date.now()}`;
137
+ const chainHash = crypto.createHash('sha256').update(payload).digest('hex');
138
+ _lastHash = chainHash;
139
+
140
+ const ipHash = entry.ip ? crypto.createHash('sha256').update(entry.ip).digest('hex').slice(0, 16) : null;
141
+
142
+ stmts.insertAudit.run(
143
+ id, chainHash, prevHash,
144
+ entry.actorType || 'system',
145
+ entry.actorId || null,
146
+ entry.action,
147
+ entry.resource || null,
148
+ entry.resourceId || null,
149
+ ipHash,
150
+ entry.signature || null,
151
+ entry.outcome || 'success',
152
+ JSON.stringify(entry.details || {}),
153
+ entry.severity || 'info'
154
+ );
155
+
156
+ return { id, chainHash };
157
+ }
158
+
159
+ /**
160
+ * Verify audit chain integrity — detect tampering.
161
+ */
162
+ function verifyAuditChain(startDate, endDate) {
163
+ const logs = stmts.getAuditRange.all(startDate, endDate);
164
+ if (logs.length === 0) return { valid: true, checked: 0 };
165
+
166
+ let valid = true;
167
+ let broken = null;
168
+
169
+ for (let i = 1; i < logs.length; i++) {
170
+ if (logs[i].prev_hash !== logs[i - 1].chain_hash) {
171
+ valid = false;
172
+ broken = { index: i, id: logs[i].id };
173
+ break;
174
+ }
175
+ }
176
+
177
+ return { valid, checked: logs.length, broken };
178
+ }
179
+
180
+ // ─── Command Signing ─────────────────────────────────────────────────
181
+
182
+ /**
183
+ * Sign a command payload with HMAC-SHA256.
184
+ * @param {string} secretKey - Agent's secret key
185
+ * @param {object} payload - { action, params, timestamp, nonce }
186
+ * @returns {string} HMAC signature
187
+ */
188
+ function signCommand(secretKey, payload) {
189
+ const canonical = JSON.stringify({
190
+ action: payload.action,
191
+ params: payload.params || {},
192
+ timestamp: payload.timestamp,
193
+ nonce: payload.nonce,
194
+ });
195
+ return crypto.createHmac('sha256', secretKey).update(canonical).digest('hex');
196
+ }
197
+
198
+ /**
199
+ * Verify a command signature with timing-safe comparison.
200
+ * Also validates timestamp freshness (< 5 minutes) and nonce uniqueness.
201
+ */
202
+ const _usedNonces = new Map();
203
+ const NONCE_TTL = 5 * 60 * 1000;
204
+
205
+ // Purge old nonces every 5 minutes
206
+ setInterval(() => {
207
+ const cutoff = Date.now() - NONCE_TTL;
208
+ for (const [nonce, ts] of _usedNonces) {
209
+ if (ts < cutoff) _usedNonces.delete(nonce);
210
+ }
211
+ }, NONCE_TTL);
212
+
213
+ function verifyCommandSignature(publicKey, payload, signature) {
214
+ // Check timestamp freshness (±5 minutes)
215
+ const ts = payload.timestamp;
216
+ if (!ts || Math.abs(Date.now() - ts) > NONCE_TTL) {
217
+ return { valid: false, reason: 'timestamp_expired' };
218
+ }
219
+
220
+ // Check nonce uniqueness (replay protection)
221
+ if (!payload.nonce) {
222
+ return { valid: false, reason: 'nonce_required' };
223
+ }
224
+ if (_usedNonces.has(payload.nonce)) {
225
+ return { valid: false, reason: 'nonce_reused' };
226
+ }
227
+
228
+ // Verify HMAC
229
+ const expected = signCommand(publicKey, payload);
230
+ const sigBuf = Buffer.from(signature, 'hex');
231
+ const expBuf = Buffer.from(expected, 'hex');
232
+
233
+ if (sigBuf.length !== expBuf.length) {
234
+ return { valid: false, reason: 'invalid_signature' };
235
+ }
236
+
237
+ if (!crypto.timingSafeEqual(sigBuf, expBuf)) {
238
+ return { valid: false, reason: 'invalid_signature' };
239
+ }
240
+
241
+ // Signature valid — record nonce
242
+ _usedNonces.set(payload.nonce, Date.now());
243
+ return { valid: true };
244
+ }
245
+
246
+ // ─── Agent Identity ──────────────────────────────────────────────────
247
+
248
+ /**
249
+ * Register a new agent with a cryptographic key.
250
+ * Returns { agentId, secretKey } — the secret must be saved by the caller.
251
+ */
252
+ function registerAgent(siteId, agentName, options = {}) {
253
+ const id = crypto.randomUUID();
254
+ const secretKey = crypto.randomBytes(32).toString('hex');
255
+ const publicKeyHash = crypto.createHash('sha256').update(secretKey).digest('hex');
256
+
257
+ const capabilities = options.capabilities || ['read'];
258
+ const validCaps = ['read', 'click', 'fill', 'scroll', 'navigate', 'execute', 'extract', 'api'];
259
+ const filtered = capabilities.filter(c => validCaps.includes(c));
260
+
261
+ stmts.insertAgent.run(
262
+ id, siteId, agentName, publicKeyHash,
263
+ 'hmac-sha256',
264
+ JSON.stringify(filtered),
265
+ options.maxRate || 60,
266
+ JSON.stringify(options.ipAllowlist || [])
267
+ );
268
+
269
+ auditLog({
270
+ actorType: 'system',
271
+ action: 'agent_registered',
272
+ resource: 'agent',
273
+ resourceId: id,
274
+ details: { agentName, siteId, capabilities: filtered },
275
+ });
276
+
277
+ return { agentId: id, secretKey };
278
+ }
279
+
280
+ /**
281
+ * Authenticate an agent with its secret key.
282
+ * Uses timing-safe comparison against stored public key hash.
283
+ */
284
+ function authenticateAgent(siteId, secretKey) {
285
+ const publicKeyHash = crypto.createHash('sha256').update(secretKey).digest('hex');
286
+ const agent = stmts.getAgentByKey.get(siteId, publicKeyHash);
287
+
288
+ if (!agent) return null;
289
+
290
+ stmts.updateAgentStats.run(agent.id);
291
+ return {
292
+ agentId: agent.id,
293
+ agentName: agent.agent_name,
294
+ capabilities: JSON.parse(agent.capabilities || '["read"]'),
295
+ maxRate: agent.max_rate,
296
+ };
297
+ }
298
+
299
+ // ─── Capability Tokens ───────────────────────────────────────────────
300
+
301
+ /**
302
+ * Issue a capability token with restricted scope.
303
+ * @param {string} agentId
304
+ * @param {string} siteId
305
+ * @param {object} scope - { capabilities, allowedActions, selectorScope, ttlSeconds }
306
+ */
307
+ function issueCapabilityToken(agentId, siteId, scope = {}) {
308
+ const rawToken = crypto.randomBytes(32).toString('hex');
309
+ const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
310
+ const ttl = Math.min(scope.ttlSeconds || 3600, 86400); // max 24h
311
+ const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
312
+
313
+ stmts.insertCapToken.run(
314
+ tokenHash, agentId, siteId,
315
+ JSON.stringify(scope.capabilities || ['read']),
316
+ JSON.stringify(scope.allowedActions || ['*']),
317
+ JSON.stringify(scope.selectorScope || []),
318
+ expiresAt
319
+ );
320
+
321
+ auditLog({
322
+ actorType: 'agent',
323
+ actorId: agentId,
324
+ action: 'capability_token_issued',
325
+ resource: 'token',
326
+ details: { siteId, capabilities: scope.capabilities, ttl },
327
+ });
328
+
329
+ return { token: rawToken, expiresAt };
330
+ }
331
+
332
+ /**
333
+ * Validate a capability token and check if the requested action is allowed.
334
+ * @returns {object|null} - Token info with capabilities, or null if invalid
335
+ */
336
+ function validateCapabilityToken(rawToken, requiredAction) {
337
+ const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
338
+
339
+ // Check revocation list
340
+ if (stmts.isTokenRevoked.get(tokenHash)) return null;
341
+
342
+ const token = stmts.getCapToken.get(tokenHash);
343
+ if (!token) return null;
344
+
345
+ const capabilities = JSON.parse(token.capabilities || '["read"]');
346
+ const allowedActions = JSON.parse(token.allowed_actions || '["*"]');
347
+
348
+ // Check if action is permitted
349
+ if (requiredAction && !allowedActions.includes('*') && !allowedActions.includes(requiredAction)) {
350
+ return null;
351
+ }
352
+
353
+ return {
354
+ agentId: token.agent_id,
355
+ siteId: token.site_id,
356
+ capabilities,
357
+ allowedActions,
358
+ selectorScope: JSON.parse(token.selector_scope || '[]'),
359
+ expiresAt: token.expires_at,
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Revoke a specific token.
365
+ */
366
+ function revokeCapabilityToken(rawToken, reason) {
367
+ const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
368
+ stmts.revokeCapToken.run(tokenHash);
369
+ stmts.insertRevokedToken.run(tokenHash, reason || 'manual_revocation');
370
+ }
371
+
372
+ // ─── JWT Revocation ──────────────────────────────────────────────────
373
+
374
+ /**
375
+ * Add a JWT to the revocation list (for logout / compromise).
376
+ */
377
+ function revokeJWT(token, reason) {
378
+ const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
379
+ stmts.insertRevokedToken.run(tokenHash, reason || 'manual_revocation');
380
+ }
381
+
382
+ function isJWTRevoked(token) {
383
+ const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
384
+ return !!stmts.isTokenRevoked.get(tokenHash);
385
+ }
386
+
387
+ // ─── Input Sanitizer ─────────────────────────────────────────────────
388
+
389
+ const DOMAIN_RE = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
390
+ const EMAIL_RE = /^[^\s@]{1,64}@[^\s@]{1,255}$/;
391
+
392
+ function validateDomain(domain) {
393
+ if (!domain || typeof domain !== 'string') return false;
394
+ const clean = domain.replace(/^www\./, '').toLowerCase();
395
+ return DOMAIN_RE.test(clean) && clean.length <= 253;
396
+ }
397
+
398
+ function validateEmail(email) {
399
+ if (!email || typeof email !== 'string') return false;
400
+ return EMAIL_RE.test(email) && email.length <= 320;
401
+ }
402
+
403
+ /**
404
+ * Sanitize arbitrary string input — strip control characters, limit length.
405
+ */
406
+ function sanitizeInput(str, maxLength = 1000) {
407
+ if (typeof str !== 'string') return '';
408
+ // Remove control characters except newlines/tabs
409
+ return str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').slice(0, maxLength);
410
+ }
411
+
412
+ /**
413
+ * Validate and sanitize site configuration — strict schema enforcement.
414
+ */
415
+ function validateSiteConfig(config) {
416
+ if (!config || typeof config !== 'object') return { valid: false, error: 'Config must be an object' };
417
+
418
+ const allowed = {
419
+ agentPermissions: 'object',
420
+ restrictions: 'object',
421
+ logging: 'object',
422
+ stealth: 'object',
423
+ };
424
+
425
+ const validPermissions = ['readContent', 'click', 'fillForms', 'scroll', 'navigate', 'apiAccess', 'automatedLogin', 'extractData'];
426
+
427
+ // Strip unknown top-level keys
428
+ const cleaned = {};
429
+ for (const [key, type] of Object.entries(allowed)) {
430
+ if (config[key] !== undefined) {
431
+ if (typeof config[key] !== type) {
432
+ return { valid: false, error: `${key} must be ${type}` };
433
+ }
434
+ cleaned[key] = config[key];
435
+ }
436
+ }
437
+
438
+ // Validate permissions — only allow known keys with boolean values
439
+ if (cleaned.agentPermissions) {
440
+ const perms = {};
441
+ for (const [k, v] of Object.entries(cleaned.agentPermissions)) {
442
+ if (validPermissions.includes(k) && typeof v === 'boolean') {
443
+ perms[k] = v;
444
+ }
445
+ }
446
+ cleaned.agentPermissions = perms;
447
+ }
448
+
449
+ // Validate restrictions
450
+ if (cleaned.restrictions) {
451
+ const r = cleaned.restrictions;
452
+ if (r.allowedSelectors && !Array.isArray(r.allowedSelectors)) {
453
+ return { valid: false, error: 'allowedSelectors must be an array' };
454
+ }
455
+ if (r.blockedSelectors && !Array.isArray(r.blockedSelectors)) {
456
+ return { valid: false, error: 'blockedSelectors must be an array' };
457
+ }
458
+ }
459
+
460
+ // Validate stealth consent requirement
461
+ if (cleaned.stealth) {
462
+ if (cleaned.stealth.enabled && !cleaned.stealth.consent) {
463
+ return { valid: false, error: 'Stealth mode requires explicit consent: true' };
464
+ }
465
+ }
466
+
467
+ // Reject configs > 10KB
468
+ const serialized = JSON.stringify(cleaned);
469
+ if (serialized.length > 10240) {
470
+ return { valid: false, error: 'Config too large (max 10KB)' };
471
+ }
472
+
473
+ return { valid: true, config: cleaned };
474
+ }
475
+
476
+ // ─── IP Hashing ──────────────────────────────────────────────────────
477
+
478
+ function hashIP(ip) {
479
+ if (!ip) return null;
480
+ return crypto.createHash('sha256').update(ip).digest('hex').slice(0, 16);
481
+ }
482
+
483
+ // ─── Exports ─────────────────────────────────────────────────────────
484
+
485
+ module.exports = {
486
+ // Audit
487
+ auditLog,
488
+ verifyAuditChain,
489
+
490
+ // Command signing
491
+ signCommand,
492
+ verifyCommandSignature,
493
+
494
+ // Agent identity
495
+ registerAgent,
496
+ authenticateAgent,
497
+
498
+ // Capability tokens
499
+ issueCapabilityToken,
500
+ validateCapabilityToken,
501
+ revokeCapabilityToken,
502
+
503
+ // JWT revocation
504
+ revokeJWT,
505
+ isJWTRevoked,
506
+
507
+ // Input validation
508
+ validateDomain,
509
+ validateEmail,
510
+ sanitizeInput,
511
+ validateSiteConfig,
512
+ hashIP,
513
+ };