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
@@ -0,0 +1,525 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Transaction Primitive (ATP) — v3.9.0
5
+ *
6
+ * Promotes WAB from "discover + execute" to "trust + transaction" by giving
7
+ * agentic workflows four guarantees as first-class primitives:
8
+ *
9
+ * 1. Intent contracts — what the user authorized, with scope/cap/expiry/nonce.
10
+ * 2. Idempotent execution — same intent + idempotency_key never runs twice.
11
+ * 3. Signed receipts — Ed25519-signed canonical JSON of the outcome.
12
+ * 4. Compensation — explicit rollback path for each step.
13
+ *
14
+ * The DB-level CHECK constraints and UNIQUE (intent_id, idempotency_key)
15
+ * make illegal states unrepresentable, not just unlikely.
16
+ */
17
+
18
+ const crypto = require('crypto');
19
+ const { db } = require('../models/db');
20
+ const wabCrypto = require('./wab-crypto');
21
+
22
+ // ── ID helpers ───────────────────────────────────────────────────────────────
23
+ function ulid(prefix) {
24
+ // 26-char base32 ulid-ish (time-sortable + random). Not RFC-strict but stable.
25
+ const t = Date.now().toString(36).padStart(8, '0');
26
+ const r = crypto.randomBytes(10).toString('hex');
27
+ return `${prefix}_${t}${r}`;
28
+ }
29
+
30
+ function nowIso() { return new Date().toISOString(); }
31
+
32
+ // ── Intent lifecycle ─────────────────────────────────────────────────────────
33
+
34
+ const VALID_SCOPE_ACTIONS = new Set([
35
+ 'read', 'search', 'compare', 'select', 'add_to_cart', 'checkout',
36
+ 'submit_form', 'book', 'cancel', 'message', 'pay'
37
+ ]);
38
+
39
+ function validateScope(scope) {
40
+ if (!scope || typeof scope !== 'object') throw badRequest('scope must be an object');
41
+ const { actions, domains } = scope;
42
+ if (!Array.isArray(actions) || actions.length === 0) throw badRequest('scope.actions must be a non-empty array');
43
+ for (const a of actions) {
44
+ if (typeof a !== 'string' || !VALID_SCOPE_ACTIONS.has(a)) {
45
+ throw badRequest(`scope.actions contains invalid action: ${a}`);
46
+ }
47
+ }
48
+ if (domains !== undefined) {
49
+ if (!Array.isArray(domains)) throw badRequest('scope.domains must be an array of hostnames');
50
+ for (const d of domains) {
51
+ if (typeof d !== 'string' || d.length === 0 || d.length > 253) throw badRequest('scope.domains contains invalid hostname');
52
+ }
53
+ }
54
+ }
55
+
56
+ function badRequest(msg) {
57
+ const e = new Error(msg); e.statusCode = 400; e.code = 'invalid_request'; return e;
58
+ }
59
+ function notFound(msg) {
60
+ const e = new Error(msg); e.statusCode = 404; e.code = 'not_found'; return e;
61
+ }
62
+ function conflict(msg, code = 'conflict') {
63
+ const e = new Error(msg); e.statusCode = 409; e.code = code; return e;
64
+ }
65
+ function forbidden(msg, code = 'forbidden') {
66
+ const e = new Error(msg); e.statusCode = 403; e.code = code; return e;
67
+ }
68
+
69
+ /**
70
+ * Create a draft intent. Status starts at 'draft' and requires explicit
71
+ * authorize() before any transaction can be executed under it.
72
+ */
73
+ function createIntent(params) {
74
+ const {
75
+ userId, siteId = null, agentId = null,
76
+ purpose, scope,
77
+ spendCapCents = 0, spendCurrency = 'EUR',
78
+ maxExecutions = 1,
79
+ ttlSeconds = 3600,
80
+ metadata = {},
81
+ } = params;
82
+
83
+ if (!userId) throw badRequest('userId required');
84
+ if (!purpose || typeof purpose !== 'string' || purpose.length > 500) {
85
+ throw badRequest('purpose required (1-500 chars)');
86
+ }
87
+ validateScope(scope);
88
+ if (!Number.isInteger(spendCapCents) || spendCapCents < 0) throw badRequest('spendCapCents must be a non-negative integer');
89
+ if (!Number.isInteger(maxExecutions) || maxExecutions < 1 || maxExecutions > 1000) {
90
+ throw badRequest('maxExecutions must be 1..1000');
91
+ }
92
+ if (!Number.isInteger(ttlSeconds) || ttlSeconds < 30 || ttlSeconds > 7 * 24 * 3600) {
93
+ throw badRequest('ttlSeconds must be 30..604800');
94
+ }
95
+
96
+ const id = ulid('atp_int');
97
+ const nonce = crypto.randomBytes(16).toString('hex');
98
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
99
+
100
+ db.prepare(`
101
+ INSERT INTO atp_intents (
102
+ id, user_id, site_id, agent_id, purpose, scope,
103
+ spend_cap_cents, spend_currency, max_executions, expires_at, nonce, metadata
104
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
105
+ `).run(
106
+ id, userId, siteId, agentId, purpose, JSON.stringify(scope),
107
+ spendCapCents, spendCurrency, maxExecutions, expiresAt, nonce, JSON.stringify(metadata)
108
+ );
109
+
110
+ return getIntent(id);
111
+ }
112
+
113
+ function getIntent(id) {
114
+ const row = db.prepare('SELECT * FROM atp_intents WHERE id = ?').get(id);
115
+ if (!row) return null;
116
+ return hydrateIntent(row);
117
+ }
118
+
119
+ function hydrateIntent(row) {
120
+ return {
121
+ ...row,
122
+ scope: safeJson(row.scope, {}),
123
+ metadata: safeJson(row.metadata, {}),
124
+ };
125
+ }
126
+
127
+ function safeJson(s, fallback) {
128
+ try { return JSON.parse(s); } catch { return fallback; }
129
+ }
130
+
131
+ function listIntentsForUser(userId, { limit = 50, offset = 0 } = {}) {
132
+ const rows = db.prepare(`
133
+ SELECT * FROM atp_intents WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?
134
+ `).all(userId, limit, offset);
135
+ return rows.map(hydrateIntent);
136
+ }
137
+
138
+ /**
139
+ * Authorize an intent. The user (principal) confirms the contract.
140
+ * After this call, the intent's nonce is registered in atp_nonces to
141
+ * make it single-use, and the intent moves to 'authorized'.
142
+ */
143
+ function authorizeIntent(intentId, { userId }) {
144
+ const intent = getIntent(intentId);
145
+ if (!intent) throw notFound('intent not found');
146
+ if (intent.user_id !== userId) throw forbidden('not your intent');
147
+ if (intent.status !== 'draft') throw conflict(`cannot authorize intent in status '${intent.status}'`, 'invalid_state');
148
+ if (new Date(intent.expires_at).getTime() < Date.now()) {
149
+ db.prepare("UPDATE atp_intents SET status='expired', updated_at=? WHERE id=?").run(nowIso(), intentId);
150
+ throw conflict('intent expired before authorization', 'expired');
151
+ }
152
+
153
+ const tx = db.transaction(() => {
154
+ // Reserve the nonce — single use across the whole user.
155
+ try {
156
+ db.prepare('INSERT INTO atp_nonces (nonce, user_id) VALUES (?, ?)').run(intent.nonce, userId);
157
+ } catch (e) {
158
+ throw conflict('nonce already consumed', 'replay');
159
+ }
160
+ db.prepare(`
161
+ UPDATE atp_intents
162
+ SET status='authorized', authorized_at=?, authorized_by=?, updated_at=?
163
+ WHERE id=? AND status='draft'
164
+ `).run(nowIso(), userId, nowIso(), intentId);
165
+ });
166
+ tx();
167
+
168
+ return getIntent(intentId);
169
+ }
170
+
171
+ function revokeIntent(intentId, { userId, reason = 'user_revoked' }) {
172
+ const intent = getIntent(intentId);
173
+ if (!intent) throw notFound('intent not found');
174
+ if (intent.user_id !== userId) throw forbidden('not your intent');
175
+ if (intent.status === 'consumed' || intent.status === 'revoked' || intent.status === 'expired') {
176
+ throw conflict(`cannot revoke intent in status '${intent.status}'`, 'invalid_state');
177
+ }
178
+ db.prepare(`
179
+ UPDATE atp_intents
180
+ SET status='revoked', revoked_at=?, revoked_reason=?, updated_at=?
181
+ WHERE id=?
182
+ `).run(nowIso(), String(reason).slice(0, 500), nowIso(), intentId);
183
+ return getIntent(intentId);
184
+ }
185
+
186
+ // ── Transaction execution ────────────────────────────────────────────────────
187
+
188
+ /**
189
+ * Begin a transaction under an authorized intent. Idempotent on
190
+ * (intent_id, idempotency_key): replaying the same key returns the
191
+ * existing transaction instead of creating a new one.
192
+ */
193
+ function beginTransaction(params) {
194
+ const {
195
+ intentId, idempotencyKey, siteId = null, agentId = null,
196
+ amountCents = 0, currency = 'EUR', summary = null, metadata = {},
197
+ } = params;
198
+
199
+ if (!intentId) throw badRequest('intentId required');
200
+ if (!idempotencyKey || typeof idempotencyKey !== 'string' || idempotencyKey.length > 200) {
201
+ throw badRequest('idempotencyKey required (1-200 chars)');
202
+ }
203
+ if (!Number.isInteger(amountCents) || amountCents < 0) throw badRequest('amountCents must be a non-negative integer');
204
+
205
+ const intent = getIntent(intentId);
206
+ if (!intent) throw notFound('intent not found');
207
+ if (intent.status !== 'authorized') throw conflict(`intent not authorized (status='${intent.status}')`, 'invalid_state');
208
+ if (new Date(intent.expires_at).getTime() < Date.now()) {
209
+ db.prepare("UPDATE atp_intents SET status='expired', updated_at=? WHERE id=? AND status='authorized'").run(nowIso(), intentId);
210
+ throw conflict('intent expired', 'expired');
211
+ }
212
+ if (intent.used_executions >= intent.max_executions) {
213
+ throw conflict('intent execution cap reached', 'cap_reached');
214
+ }
215
+ if (intent.spend_cap_cents > 0 && (intent.spent_cents + amountCents) > intent.spend_cap_cents) {
216
+ throw conflict('spend cap would be exceeded', 'spend_cap');
217
+ }
218
+ if (intent.spend_currency !== currency) {
219
+ throw badRequest(`currency mismatch: intent='${intent.spend_currency}', tx='${currency}'`);
220
+ }
221
+
222
+ // Idempotency check — return the existing record if same key was used.
223
+ const existing = db.prepare(`
224
+ SELECT id FROM atp_transactions WHERE intent_id=? AND idempotency_key=?
225
+ `).get(intentId, idempotencyKey);
226
+ if (existing) return { ...getTransaction(existing.id), _idempotent_replay: true };
227
+
228
+ const id = ulid('atp_tx');
229
+ db.prepare(`
230
+ INSERT INTO atp_transactions (
231
+ id, intent_id, site_id, agent_id, idempotency_key,
232
+ amount_cents, currency, summary, metadata
233
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
234
+ `).run(id, intentId, siteId || intent.site_id, agentId || intent.agent_id, idempotencyKey,
235
+ amountCents, currency, summary, JSON.stringify(metadata));
236
+
237
+ return getTransaction(id);
238
+ }
239
+
240
+ function getTransaction(id) {
241
+ const row = db.prepare('SELECT * FROM atp_transactions WHERE id=?').get(id);
242
+ if (!row) return null;
243
+ return { ...row, metadata: safeJson(row.metadata, {}) };
244
+ }
245
+
246
+ function listTransactionsForIntent(intentId) {
247
+ return db.prepare('SELECT * FROM atp_transactions WHERE intent_id=? ORDER BY created_at ASC').all(intentId)
248
+ .map(r => ({ ...r, metadata: safeJson(r.metadata, {}) }));
249
+ }
250
+
251
+ const VALID_TX_TRANSITIONS = {
252
+ pending: ['executing', 'failed'],
253
+ executing: ['executed', 'failed'],
254
+ executed: ['settled', 'compensated', 'failed'],
255
+ settled: ['compensated'],
256
+ failed: ['compensated'],
257
+ compensated: [],
258
+ };
259
+
260
+ function transitionTransaction(txId, toStatus, patch = {}) {
261
+ const tx = getTransaction(txId);
262
+ if (!tx) throw notFound('transaction not found');
263
+ const allowed = VALID_TX_TRANSITIONS[tx.status] || [];
264
+ if (!allowed.includes(toStatus)) {
265
+ throw conflict(`illegal transition ${tx.status} → ${toStatus}`, 'invalid_state');
266
+ }
267
+
268
+ const fields = { status: toStatus, updated_at: nowIso() };
269
+ if (toStatus === 'executing') fields.started_at = nowIso();
270
+ if (toStatus === 'executed') fields.completed_at = nowIso();
271
+ if (toStatus === 'settled') fields.settled_at = nowIso();
272
+ if (toStatus === 'compensated') fields.compensated_at = nowIso();
273
+ if (patch.error !== undefined) fields.error = String(patch.error).slice(0, 2000);
274
+ if (patch.summary !== undefined) fields.summary = String(patch.summary).slice(0, 1000);
275
+
276
+ const sets = Object.keys(fields).map(k => `${k}=?`).join(', ');
277
+ const vals = Object.values(fields);
278
+ db.prepare(`UPDATE atp_transactions SET ${sets} WHERE id=?`).run(...vals, txId);
279
+
280
+ // On settled, charge the intent. On compensated, refund.
281
+ if (toStatus === 'settled') {
282
+ const updated = db.prepare(`
283
+ UPDATE atp_intents
284
+ SET spent_cents = spent_cents + ?,
285
+ used_executions = used_executions + 1,
286
+ updated_at = ?
287
+ WHERE id = ?
288
+ `).run(tx.amount_cents, nowIso(), tx.intent_id);
289
+ // Auto-consume intent if cap hit.
290
+ const intent = getIntent(tx.intent_id);
291
+ if (intent.used_executions >= intent.max_executions) {
292
+ db.prepare("UPDATE atp_intents SET status='consumed', updated_at=? WHERE id=? AND status='authorized'")
293
+ .run(nowIso(), tx.intent_id);
294
+ }
295
+ }
296
+ if (toStatus === 'compensated' && tx.status === 'settled') {
297
+ db.prepare(`
298
+ UPDATE atp_intents
299
+ SET spent_cents = MAX(0, spent_cents - ?),
300
+ updated_at = ?
301
+ WHERE id = ?
302
+ `).run(tx.amount_cents, nowIso(), tx.intent_id);
303
+ }
304
+
305
+ return getTransaction(txId);
306
+ }
307
+
308
+ // ── Step ledger ──────────────────────────────────────────────────────────────
309
+
310
+ function appendStep(txId, { action, evidence = null, before = null, after = null, compensation = null }) {
311
+ if (!action || typeof action !== 'string') throw badRequest('step.action required');
312
+ const tx = getTransaction(txId);
313
+ if (!tx) throw notFound('transaction not found');
314
+
315
+ const nextSeqRow = db.prepare('SELECT COALESCE(MAX(seq),0)+1 AS s FROM atp_steps WHERE transaction_id=?').get(txId);
316
+ const seq = nextSeqRow.s;
317
+ db.prepare(`
318
+ INSERT INTO atp_steps (transaction_id, seq, action, state, before_snapshot, after_snapshot, evidence, compensation, started_at, ended_at)
319
+ VALUES (?, ?, ?, 'succeeded', ?, ?, ?, ?, ?, ?)
320
+ `).run(txId, seq, action,
321
+ before ? JSON.stringify(before) : null,
322
+ after ? JSON.stringify(after) : null,
323
+ evidence ? JSON.stringify(evidence) : null,
324
+ compensation ? JSON.stringify(compensation) : null,
325
+ nowIso(), nowIso());
326
+ return getStep(txId, seq);
327
+ }
328
+
329
+ function getStep(txId, seq) {
330
+ const row = db.prepare('SELECT * FROM atp_steps WHERE transaction_id=? AND seq=?').get(txId, seq);
331
+ if (!row) return null;
332
+ return {
333
+ ...row,
334
+ before_snapshot: safeJson(row.before_snapshot, null),
335
+ after_snapshot: safeJson(row.after_snapshot, null),
336
+ evidence: safeJson(row.evidence, null),
337
+ compensation: safeJson(row.compensation, null),
338
+ };
339
+ }
340
+
341
+ function listSteps(txId) {
342
+ return db.prepare('SELECT * FROM atp_steps WHERE transaction_id=? ORDER BY seq ASC').all(txId)
343
+ .map(r => ({
344
+ ...r,
345
+ before_snapshot: safeJson(r.before_snapshot, null),
346
+ after_snapshot: safeJson(r.after_snapshot, null),
347
+ evidence: safeJson(r.evidence, null),
348
+ compensation: safeJson(r.compensation, null),
349
+ }));
350
+ }
351
+
352
+ // ── Receipts (signed proof of outcome) ───────────────────────────────────────
353
+
354
+ /**
355
+ * Issue a signed receipt for an executed transaction. The receipt body is
356
+ * canonicalized via wab-crypto and signed Ed25519 with the supplied private
357
+ * key (typically the site's key from `wab_signing_keys`).
358
+ *
359
+ * If no privateKey is supplied, an ephemeral keypair is generated and the
360
+ * public key is embedded in the receipt so verifiers can still check it.
361
+ * This keeps the free tier usable while encouraging Pro+ users to bind a
362
+ * persistent site key for trust continuity.
363
+ */
364
+ function issueReceipt(txId, { privateKeyB64 = null, embedPublicKey = true } = {}) {
365
+ const tx = getTransaction(txId);
366
+ if (!tx) throw notFound('transaction not found');
367
+ if (!['executed', 'settled', 'failed', 'compensated'].includes(tx.status)) {
368
+ throw conflict(`cannot issue receipt for status '${tx.status}'`, 'invalid_state');
369
+ }
370
+
371
+ // Refuse double-issuance.
372
+ const existing = db.prepare('SELECT id FROM atp_receipts WHERE transaction_id=?').get(txId);
373
+ if (existing) return getReceipt(existing.id);
374
+
375
+ const steps = listSteps(txId);
376
+ const intent = getIntent(tx.intent_id);
377
+
378
+ const body = {
379
+ type: 'atp.receipt.v1',
380
+ receipt_id: ulid('atp_rcpt'),
381
+ issued_at: nowIso(),
382
+ transaction: {
383
+ id: tx.id,
384
+ status: tx.status,
385
+ amount_cents: tx.amount_cents,
386
+ currency: tx.currency,
387
+ summary: tx.summary,
388
+ started_at: tx.started_at,
389
+ completed_at: tx.completed_at,
390
+ settled_at: tx.settled_at,
391
+ compensated_at: tx.compensated_at,
392
+ error: tx.error,
393
+ },
394
+ intent: {
395
+ id: intent.id,
396
+ purpose: intent.purpose,
397
+ scope: intent.scope,
398
+ spend_cap_cents: intent.spend_cap_cents,
399
+ currency: intent.spend_currency,
400
+ authorized_at: intent.authorized_at,
401
+ },
402
+ steps: steps.map(s => ({
403
+ seq: s.seq, action: s.action, state: s.state,
404
+ attempts: s.attempts,
405
+ started_at: s.started_at, ended_at: s.ended_at,
406
+ })),
407
+ site_id: tx.site_id || null,
408
+ agent_id: tx.agent_id || null,
409
+ };
410
+
411
+ // Decide signing key.
412
+ let signKey = privateKeyB64;
413
+ let publicKeyB64 = null;
414
+ let keyOrigin = 'supplied';
415
+ if (!signKey) {
416
+ const kp = wabCrypto.generateKeyPair();
417
+ signKey = kp.private_key;
418
+ publicKeyB64 = kp.public_key;
419
+ keyOrigin = 'ephemeral';
420
+ }
421
+
422
+ const signed = wabCrypto.signManifest(body, signKey, { embed_public_key: embedPublicKey });
423
+ // wab-crypto already embedded the pub key into signed.signature.public_key if requested,
424
+ // but we also want the raw pubkey for column storage:
425
+ if (!publicKeyB64) publicKeyB64 = signed.signature.public_key || null;
426
+
427
+ const canonical = canonicalizeForStorage(signed);
428
+
429
+ const id = body.receipt_id;
430
+ db.prepare(`
431
+ INSERT INTO atp_receipts (id, transaction_id, site_id, algorithm, key_id, canonical_body, signature, public_key)
432
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
433
+ `).run(id, txId, tx.site_id, 'ed25519',
434
+ signed.signature.key_id, canonical, signed.signature.value, publicKeyB64);
435
+
436
+ return { ...getReceipt(id), _key_origin: keyOrigin };
437
+ }
438
+
439
+ function canonicalizeForStorage(signedManifest) {
440
+ // Store the FULL signed object as JSON; verifiers recompute canonical from this.
441
+ return JSON.stringify(signedManifest);
442
+ }
443
+
444
+ function getReceipt(id) {
445
+ const row = db.prepare('SELECT * FROM atp_receipts WHERE id=?').get(id);
446
+ if (!row) return null;
447
+ let body = null;
448
+ try { body = JSON.parse(row.canonical_body); } catch { /* keep null */ }
449
+ return { ...row, body };
450
+ }
451
+
452
+ function getReceiptByTransaction(txId) {
453
+ const row = db.prepare('SELECT id FROM atp_receipts WHERE transaction_id=?').get(txId);
454
+ return row ? getReceipt(row.id) : null;
455
+ }
456
+
457
+ /**
458
+ * Verify a receipt. Accepts either:
459
+ * - a receipt id (looked up in DB), or
460
+ * - a raw signed receipt object (offline verification).
461
+ * Returns { ok, reason?, key_id?, age_seconds? }.
462
+ */
463
+ function verifyReceipt(input) {
464
+ let signed = null;
465
+ let stored = null;
466
+ if (typeof input === 'string') {
467
+ stored = getReceipt(input);
468
+ if (!stored) return { ok: false, reason: 'receipt not found' };
469
+ signed = stored.body;
470
+ } else if (input && typeof input === 'object') {
471
+ signed = input;
472
+ } else {
473
+ return { ok: false, reason: 'invalid input' };
474
+ }
475
+ if (!signed || !signed.signature) return { ok: false, reason: 'no signature' };
476
+
477
+ const pubB64 = signed.signature.public_key || (stored && stored.public_key) || null;
478
+ const result = wabCrypto.verifyManifest(signed, pubB64, { max_age_seconds: 365 * 24 * 3600 });
479
+ return result;
480
+ }
481
+
482
+ // ── Compensation ─────────────────────────────────────────────────────────────
483
+
484
+ /**
485
+ * Compensate a transaction: rolls back its effects. This is the explicit
486
+ * "undo" primitive that distinguishes WAB from naive scrapers — every
487
+ * executed step can carry its own compensation descriptor in its evidence.
488
+ *
489
+ * This function just transitions the state and unwinds the intent's spend
490
+ * counter. Actual site-side rollback (e.g. cancelling a booking) is the
491
+ * caller's responsibility and should be recorded as further steps before
492
+ * calling this function.
493
+ */
494
+ function compensateTransaction(txId, { reason = 'compensated' } = {}) {
495
+ return transitionTransaction(txId, 'compensated', { summary: String(reason).slice(0, 1000) });
496
+ }
497
+
498
+ // ── Periodic maintenance ─────────────────────────────────────────────────────
499
+
500
+ function expireOverdueIntents() {
501
+ const r = db.prepare(`
502
+ UPDATE atp_intents
503
+ SET status='expired', updated_at=datetime('now')
504
+ WHERE status IN ('draft','authorized')
505
+ AND datetime(expires_at) < datetime('now')
506
+ `).run();
507
+ return r.changes;
508
+ }
509
+
510
+ module.exports = {
511
+ // intents
512
+ createIntent, getIntent, listIntentsForUser, authorizeIntent, revokeIntent,
513
+ // transactions
514
+ beginTransaction, getTransaction, listTransactionsForIntent, transitionTransaction,
515
+ // steps
516
+ appendStep, getStep, listSteps,
517
+ // receipts
518
+ issueReceipt, getReceipt, getReceiptByTransaction, verifyReceipt,
519
+ // compensation
520
+ compensateTransaction,
521
+ // maintenance
522
+ expireOverdueIntents,
523
+ // re-exports for tests
524
+ _validateScope: validateScope,
525
+ };