web-agent-bridge 3.3.0 → 3.8.1
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.
- package/LICENSE +84 -72
- package/README.ar.md +1563 -1286
- package/README.md +137 -1764
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -237
- package/bin/wab-init.js +244 -0
- package/bin/wab.js +80 -80
- package/examples/azure-dns-wab.js +83 -0
- package/examples/bidi-agent.js +119 -119
- package/examples/cloudflare-wab-dns.js +121 -0
- package/examples/cpanel-wab-dns.js +114 -0
- package/examples/cross-site-agent.js +91 -91
- package/examples/dns-discovery-agent.js +166 -0
- package/examples/gcp-dns-wab.js +76 -0
- package/examples/governance-agent.js +169 -0
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/plesk-wab-dns.js +103 -0
- package/examples/puppeteer-agent.js +108 -108
- package/examples/route53-wab-dns.js +144 -0
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/safe-mode-agent.js +96 -0
- package/examples/self-discovery.js +106 -0
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wab-sign.js +74 -0
- package/examples/wab-verify.js +60 -0
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +93 -93
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/security.txt +8 -8
- package/public/.well-known/wab.json +28 -0
- package/public/activate.html +448 -0
- package/public/adopt.html +236 -0
- package/public/adoption-metrics.html +188 -0
- package/public/agent-workspace.html +359 -349
- package/public/ai.html +198 -198
- package/public/api.html +397 -413
- package/public/azure-dns-integration.html +289 -0
- package/public/browser.html +486 -486
- package/public/cloudflare-integration.html +380 -0
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -210
- package/public/cpanel-integration.html +398 -0
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1401 -1235
- package/public/dashboard-shieldlink.html +295 -0
- package/public/dashboard.html +711 -706
- package/public/dns.html +436 -507
- package/public/docs.html +588 -587
- package/public/enterprise-mesh.ar.html +80 -0
- package/public/enterprise-mesh.html +81 -0
- package/public/feed.xml +89 -89
- package/public/gcp-dns-integration.html +318 -0
- package/public/governance.ar.html +70 -0
- package/public/governance.html +69 -0
- package/public/growth.html +465 -463
- package/public/index.html +1372 -1070
- package/public/integrations.html +556 -556
- package/public/js/activate.js +449 -0
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +117 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/dns.js +438 -0
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/l-preview.html +242 -0
- package/public/llms-full.txt +360 -360
- package/public/llms.txt +125 -125
- package/public/login.html +85 -85
- package/public/mesh-dashboard.html +328 -328
- package/public/milestones.html +346 -0
- package/public/one-click.html +779 -0
- package/public/openapi.json +669 -580
- package/public/partners.ar.html +145 -0
- package/public/partners.html +143 -0
- package/public/phone-shield.html +281 -281
- package/public/plesk-integration.html +375 -0
- package/public/premium-dashboard.html +2489 -2489
- package/public/premium.html +793 -793
- package/public/privacy.html +297 -297
- package/public/provider-onboarding.html +172 -0
- package/public/provider-sandbox.html +134 -0
- package/public/providers.html +359 -0
- package/public/refusals.html +172 -0
- package/public/register.html +105 -105
- package/public/registrar-integrations.html +141 -0
- package/public/ring4.html +292 -0
- package/public/robots.txt +99 -87
- package/public/route53-integration.html +531 -0
- package/public/score.html +263 -0
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -8
- package/public/shieldlink.html +244 -0
- package/public/shieldqr.html +231 -0
- package/public/sitemap.xml +19 -1
- package/public/terms.html +256 -256
- package/public/trust-graph-api.ar.html +92 -0
- package/public/trust-graph-api.html +91 -0
- package/public/wab-features.html +560 -0
- package/public/wab-trust.html +200 -0
- package/public/wab-truth.html +375 -0
- package/public/wab-vs-protocols.html +210 -0
- package/public/whitepaper.html +449 -0
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/auto-discovery.js +301 -0
- package/sdk/commander.js +262 -262
- package/sdk/governance.js +262 -0
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +649 -636
- package/sdk/multi-agent.js +318 -318
- package/sdk/package.json +2 -2
- package/sdk/safe-mode.js +221 -0
- package/sdk/safety-shield.js +219 -219
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +412 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +790 -531
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/api-tier.js +170 -0
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -157
- package/server/middleware/wab-trust.js +141 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- package/server/migrations/006_growth_suite.sql +138 -0
- package/server/migrations/007_governance.sql +106 -0
- package/server/migrations/008_plans.sql +144 -0
- package/server/migrations/009_shieldqr.sql +30 -0
- package/server/migrations/010_extended_trust.sql +33 -0
- package/server/migrations/011_outreach.sql +47 -0
- package/server/migrations/012_shieldlink.sql +116 -0
- package/server/migrations/013_ct_monitor.sql +13 -0
- package/server/migrations/014_wab_advanced_features.sql +128 -0
- package/server/migrations/015_wab_truth_layer.sql +101 -0
- package/server/migrations/016_ring4_external_trust.sql +84 -0
- package/server/migrations/017_ring4_extensions.sql +69 -0
- package/server/migrations/018_commercial_foundations.sql +167 -0
- package/server/migrations/019_unify_tier_constraints.sql +133 -0
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +740 -681
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/activate.js +478 -0
- package/server/routes/admin-outreach.js +239 -0
- package/server/routes/admin-plans.js +76 -0
- package/server/routes/admin-premium.js +674 -671
- package/server/routes/admin-shieldlink.js +137 -0
- package/server/routes/admin-shieldqr.js +90 -0
- package/server/routes/admin-trust-monitor.js +139 -0
- package/server/routes/admin.js +550 -261
- package/server/routes/adopt.js +61 -0
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -540
- package/server/routes/api-keys.js +127 -0
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +57 -45
- package/server/routes/commander.js +316 -316
- package/server/routes/customer-shieldlink.js +133 -0
- package/server/routes/demo-showcase.js +332 -332
- package/server/routes/demo-store.js +154 -154
- package/server/routes/diagnose.js +373 -0
- package/server/routes/discovery.js +2348 -417
- package/server/routes/enterprise-mesh.js +170 -0
- package/server/routes/gateway.js +173 -173
- package/server/routes/governance-saas.js +203 -0
- package/server/routes/governance.js +208 -0
- package/server/routes/growth.js +1048 -0
- package/server/routes/intent.js +328 -0
- package/server/routes/license.js +251 -251
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/partners.js +201 -0
- package/server/routes/plans.js +33 -0
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/providers.js +650 -0
- package/server/routes/reputation.js +411 -0
- package/server/routes/ring4.js +885 -0
- package/server/routes/runtime.js +2148 -2148
- package/server/routes/shieldlink.js +70 -0
- package/server/routes/shieldqr.js +88 -0
- package/server/routes/sovereign.js +465 -465
- package/server/routes/truth-layer.js +670 -0
- package/server/routes/universal.js +200 -200
- package/server/routes/unsubscribe.js +51 -0
- package/server/routes/wab-api.js +850 -850
- package/server/routes/wab-cache.js +282 -0
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/secrets/wab-signing-key.pem +3 -0
- package/server/secrets/wab-signing-pub.pem +3 -0
- package/server/security/cross-site-redactor.js +196 -196
- package/server/security/dry-run.js +180 -180
- package/server/security/human-gate-rate-limit.js +147 -147
- package/server/security/human-gate-transports.js +178 -178
- package/server/security/human-gate.js +281 -281
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -245
- package/server/security/reward-guard.js +171 -171
- package/server/security/rollback-store.js +239 -239
- package/server/security/token-scope.js +404 -404
- package/server/security/url-policy.js +139 -139
- package/server/services/adoption-agent.js +182 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -601
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -555
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -292
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +233 -204
- package/server/services/fairness-engine.js +409 -0
- package/server/services/fairness.js +420 -0
- package/server/services/governance.js +466 -0
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -635
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -93
- package/server/services/modules/agent-firewall.js +90 -90
- package/server/services/modules/bounty.js +89 -89
- package/server/services/modules/collective-bargaining.js +92 -92
- package/server/services/modules/dark-pattern.js +66 -66
- package/server/services/modules/gov-intelligence.js +45 -45
- package/server/services/modules/neural.js +55 -55
- package/server/services/modules/notary.js +49 -49
- package/server/services/modules/price-time-machine.js +86 -86
- package/server/services/modules/protocol.js +104 -104
- package/server/services/negotiation.js +439 -439
- package/server/services/outreach-agent.js +312 -0
- package/server/services/plans.js +214 -0
- package/server/services/plugins.js +771 -771
- package/server/services/premium.js +1 -1
- package/server/services/price-intelligence.js +566 -566
- package/server/services/price-shield.js +1137 -1137
- package/server/services/provider-clients.js +740 -0
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/shieldlink.js +492 -0
- package/server/services/shieldqr.js +322 -0
- package/server/services/sovereign-shield.js +542 -542
- package/server/services/ssl-ct-monitor.js +224 -0
- package/server/services/ssl-inspector.js +42 -0
- package/server/services/ssl-monitor.js +167 -0
- package/server/services/stripe.js +206 -192
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +662 -662
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/services/wab-crypto.js +178 -0
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -228
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/templates/ring4/banking-sovereign.yaml +55 -0
- package/templates/ring4/ecommerce-sovereign.yaml +58 -0
- package/templates/ring4/healthcare-sovereign.yaml +60 -0
|
@@ -1,239 +1,239 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* WAB Safety Shield — Snapshot & Rollback Store (SPEC §8.13)
|
|
5
|
-
*
|
|
6
|
-
* Persists a "before-image" snapshot for every destructive action that
|
|
7
|
-
* site adapters opt into, plus a forward audit linking the snapshot to
|
|
8
|
-
* the agent action that created it. Operators (or the site admin UI)
|
|
9
|
-
* can then `restore` a snapshot to undo agent damage.
|
|
10
|
-
*
|
|
11
|
-
* The snapshot payload is opaque JSON provided by the site adapter
|
|
12
|
-
* (e.g. a serialized DB row, a tombstoned file URL, an S3 version-id).
|
|
13
|
-
* This module owns ONLY the durable index + lifecycle; it does not
|
|
14
|
-
* know how to actually restore site data — that contract is delegated
|
|
15
|
-
* to a per-site `restorer` callable registered via `setRestorer`.
|
|
16
|
-
*
|
|
17
|
-
* Tier: Enterprise.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const crypto = require('crypto');
|
|
21
|
-
const { db } = require('../models/db');
|
|
22
|
-
|
|
23
|
-
// Schema is created lazily so this module can be required even on
|
|
24
|
-
// installations that do not enable rollback.
|
|
25
|
-
let _initialized = false;
|
|
26
|
-
function _ensureSchema() {
|
|
27
|
-
if (_initialized) return;
|
|
28
|
-
db.exec(`
|
|
29
|
-
CREATE TABLE IF NOT EXISTS wab_snapshots (
|
|
30
|
-
id TEXT PRIMARY KEY,
|
|
31
|
-
site_id TEXT NOT NULL,
|
|
32
|
-
action_name TEXT NOT NULL,
|
|
33
|
-
actor_id TEXT,
|
|
34
|
-
actor_type TEXT NOT NULL DEFAULT 'agent',
|
|
35
|
-
session_fingerprint TEXT,
|
|
36
|
-
params_hash TEXT,
|
|
37
|
-
snapshot TEXT NOT NULL,
|
|
38
|
-
meta TEXT DEFAULT '{}',
|
|
39
|
-
reversible INTEGER NOT NULL DEFAULT 1,
|
|
40
|
-
status TEXT NOT NULL DEFAULT 'recorded'
|
|
41
|
-
CHECK(status IN ('recorded','restored','expired','failed')),
|
|
42
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
43
|
-
restored_at TEXT,
|
|
44
|
-
expires_at TEXT
|
|
45
|
-
);
|
|
46
|
-
CREATE INDEX IF NOT EXISTS idx_wab_snapshots_site ON wab_snapshots (site_id, created_at DESC);
|
|
47
|
-
CREATE INDEX IF NOT EXISTS idx_wab_snapshots_status ON wab_snapshots (status);
|
|
48
|
-
`);
|
|
49
|
-
_initialized = true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ─── per-site restorers ──────────────────────────────────────────────
|
|
53
|
-
const _restorers = new Map();
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Register a restorer for a site. The function receives
|
|
57
|
-
* `{ snapshot, action_name, params_hash, meta }` and must return either
|
|
58
|
-
* `{ ok:true }` or `{ ok:false, error }`.
|
|
59
|
-
*/
|
|
60
|
-
function setRestorer(siteId, fn) {
|
|
61
|
-
if (typeof fn !== 'function') throw new TypeError('restorer must be a function');
|
|
62
|
-
_restorers.set(String(siteId), fn);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function _getRestorer(siteId) {
|
|
66
|
-
return _restorers.get(String(siteId)) || null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ─── helpers ─────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
function _hashParams(params) {
|
|
72
|
-
const canon = JSON.stringify(_canonicalize(params || {}));
|
|
73
|
-
return crypto.createHash('sha256').update(canon).digest('hex').slice(0, 24);
|
|
74
|
-
}
|
|
75
|
-
function _canonicalize(value) {
|
|
76
|
-
if (value === null || typeof value !== 'object') return value;
|
|
77
|
-
if (Array.isArray(value)) return value.map(_canonicalize);
|
|
78
|
-
const out = {};
|
|
79
|
-
for (const k of Object.keys(value).sort()) out[k] = _canonicalize(value[k]);
|
|
80
|
-
return out;
|
|
81
|
-
}
|
|
82
|
-
function _fingerprint(token) {
|
|
83
|
-
if (!token) return null;
|
|
84
|
-
return crypto.createHash('sha256').update(String(token)).digest('hex').slice(0, 16);
|
|
85
|
-
}
|
|
86
|
-
function _genId() { return 'wabs_' + crypto.randomBytes(16).toString('hex'); }
|
|
87
|
-
|
|
88
|
-
// ─── public API ──────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Record a snapshot just BEFORE the destructive action runs.
|
|
92
|
-
*
|
|
93
|
-
* @param {object} ctx { siteId, actionName, actorId, sessionToken, params }
|
|
94
|
-
* @param {object} payload { snapshot, meta?, reversible?, ttlMs? }
|
|
95
|
-
* @returns { snapshot_id, expires_at }
|
|
96
|
-
*/
|
|
97
|
-
function recordSnapshot(ctx, payload) {
|
|
98
|
-
_ensureSchema();
|
|
99
|
-
const id = _genId();
|
|
100
|
-
const now = Date.now();
|
|
101
|
-
const ttlMs = Math.max(0, payload.ttlMs || 30 * 24 * 60 * 60 * 1000); // 30 days default
|
|
102
|
-
const expiresAt = ttlMs > 0 ? new Date(now + ttlMs).toISOString() : null;
|
|
103
|
-
|
|
104
|
-
db.prepare(`
|
|
105
|
-
INSERT INTO wab_snapshots
|
|
106
|
-
(id, site_id, action_name, actor_id, actor_type, session_fingerprint, params_hash,
|
|
107
|
-
snapshot, meta, reversible, status, expires_at)
|
|
108
|
-
VALUES (?, ?, ?, ?, 'agent', ?, ?, ?, ?, ?, 'recorded', ?)
|
|
109
|
-
`).run(
|
|
110
|
-
id,
|
|
111
|
-
String(ctx.siteId),
|
|
112
|
-
String(ctx.actionName),
|
|
113
|
-
ctx.actorId || null,
|
|
114
|
-
_fingerprint(ctx.sessionToken),
|
|
115
|
-
_hashParams(ctx.params),
|
|
116
|
-
JSON.stringify(payload.snapshot ?? null),
|
|
117
|
-
JSON.stringify(payload.meta || {}),
|
|
118
|
-
payload.reversible === false ? 0 : 1,
|
|
119
|
-
expiresAt
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
return { snapshot_id: id, expires_at: expiresAt };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function getSnapshot(snapshotId) {
|
|
126
|
-
_ensureSchema();
|
|
127
|
-
const row = db.prepare(`SELECT * FROM wab_snapshots WHERE id = ?`).get(snapshotId);
|
|
128
|
-
if (!row) return null;
|
|
129
|
-
return _hydrate(row);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function listSnapshots(siteId, opts = {}) {
|
|
133
|
-
_ensureSchema();
|
|
134
|
-
const limit = Math.min(Math.max(opts.limit || 50, 1), 500);
|
|
135
|
-
const status = opts.status;
|
|
136
|
-
const rows = status
|
|
137
|
-
? db.prepare(`SELECT * FROM wab_snapshots WHERE site_id=? AND status=? ORDER BY created_at DESC, rowid DESC LIMIT ?`).all(siteId, status, limit)
|
|
138
|
-
: db.prepare(`SELECT * FROM wab_snapshots WHERE site_id=? ORDER BY created_at DESC, rowid DESC LIMIT ?`).all(siteId, limit);
|
|
139
|
-
return rows.map(_hydrate);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function _hydrate(row) {
|
|
143
|
-
let snapshot = null;
|
|
144
|
-
let meta = {};
|
|
145
|
-
try { snapshot = JSON.parse(row.snapshot); } catch (_) {}
|
|
146
|
-
try { meta = JSON.parse(row.meta || '{}'); } catch (_) {}
|
|
147
|
-
return {
|
|
148
|
-
id: row.id,
|
|
149
|
-
site_id: row.site_id,
|
|
150
|
-
action_name: row.action_name,
|
|
151
|
-
actor_id: row.actor_id,
|
|
152
|
-
actor_type: row.actor_type,
|
|
153
|
-
session_fingerprint: row.session_fingerprint,
|
|
154
|
-
params_hash: row.params_hash,
|
|
155
|
-
snapshot,
|
|
156
|
-
meta,
|
|
157
|
-
reversible: !!row.reversible,
|
|
158
|
-
status: row.status,
|
|
159
|
-
created_at: row.created_at,
|
|
160
|
-
restored_at: row.restored_at,
|
|
161
|
-
expires_at: row.expires_at,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Restore a snapshot. Returns { ok:true } on success, or
|
|
167
|
-
* { ok:false, code, message }. Restoration is single-use: a snapshot
|
|
168
|
-
* already in `restored` cannot be replayed.
|
|
169
|
-
*/
|
|
170
|
-
async function restoreSnapshot(snapshotId, opts = {}) {
|
|
171
|
-
_ensureSchema();
|
|
172
|
-
const row = db.prepare(`SELECT * FROM wab_snapshots WHERE id = ?`).get(snapshotId);
|
|
173
|
-
if (!row) return { ok: false, code: 'SNAPSHOT_NOT_FOUND' };
|
|
174
|
-
if (row.status === 'restored') return { ok: false, code: 'SNAPSHOT_ALREADY_RESTORED' };
|
|
175
|
-
if (row.status === 'expired') return { ok: false, code: 'SNAPSHOT_EXPIRED' };
|
|
176
|
-
if (!row.reversible) return { ok: false, code: 'SNAPSHOT_IRREVERSIBLE' };
|
|
177
|
-
if (row.expires_at && new Date(row.expires_at).getTime() < Date.now()) {
|
|
178
|
-
db.prepare(`UPDATE wab_snapshots SET status='expired' WHERE id=?`).run(snapshotId);
|
|
179
|
-
return { ok: false, code: 'SNAPSHOT_EXPIRED' };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const restorer = opts.restorer || _getRestorer(row.site_id);
|
|
183
|
-
if (!restorer) {
|
|
184
|
-
return { ok: false, code: 'NO_RESTORER', message: `no restorer registered for site ${row.site_id}` };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
let result;
|
|
188
|
-
try {
|
|
189
|
-
result = await restorer({
|
|
190
|
-
snapshot_id: row.id,
|
|
191
|
-
site_id: row.site_id,
|
|
192
|
-
action_name: row.action_name,
|
|
193
|
-
params_hash: row.params_hash,
|
|
194
|
-
snapshot: JSON.parse(row.snapshot),
|
|
195
|
-
meta: JSON.parse(row.meta || '{}'),
|
|
196
|
-
});
|
|
197
|
-
} catch (err) {
|
|
198
|
-
db.prepare(`UPDATE wab_snapshots SET status='failed' WHERE id=?`).run(snapshotId);
|
|
199
|
-
return { ok: false, code: 'RESTORER_THREW', message: err.message };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (!result || result.ok !== true) {
|
|
203
|
-
db.prepare(`UPDATE wab_snapshots SET status='failed' WHERE id=?`).run(snapshotId);
|
|
204
|
-
return { ok: false, code: 'RESTORER_FAILED', message: result?.error || 'restorer reported failure' };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
db.prepare(`UPDATE wab_snapshots SET status='restored', restored_at=datetime('now') WHERE id=?`).run(snapshotId);
|
|
208
|
-
return { ok: true, snapshot_id: snapshotId };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Expire snapshots past their TTL. Returns the count expired.
|
|
213
|
-
*/
|
|
214
|
-
function expireOld() {
|
|
215
|
-
_ensureSchema();
|
|
216
|
-
const r = db.prepare(`
|
|
217
|
-
UPDATE wab_snapshots SET status='expired'
|
|
218
|
-
WHERE status='recorded' AND expires_at IS NOT NULL AND expires_at < datetime('now')
|
|
219
|
-
`).run();
|
|
220
|
-
return r.changes || 0;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function _resetForTests() {
|
|
224
|
-
_ensureSchema();
|
|
225
|
-
db.prepare(`DELETE FROM wab_snapshots`).run();
|
|
226
|
-
_restorers.clear();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
module.exports = {
|
|
230
|
-
recordSnapshot,
|
|
231
|
-
getSnapshot,
|
|
232
|
-
listSnapshots,
|
|
233
|
-
restoreSnapshot,
|
|
234
|
-
expireOld,
|
|
235
|
-
setRestorer,
|
|
236
|
-
// test helpers
|
|
237
|
-
_resetForTests,
|
|
238
|
-
_hashParams,
|
|
239
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Safety Shield — Snapshot & Rollback Store (SPEC §8.13)
|
|
5
|
+
*
|
|
6
|
+
* Persists a "before-image" snapshot for every destructive action that
|
|
7
|
+
* site adapters opt into, plus a forward audit linking the snapshot to
|
|
8
|
+
* the agent action that created it. Operators (or the site admin UI)
|
|
9
|
+
* can then `restore` a snapshot to undo agent damage.
|
|
10
|
+
*
|
|
11
|
+
* The snapshot payload is opaque JSON provided by the site adapter
|
|
12
|
+
* (e.g. a serialized DB row, a tombstoned file URL, an S3 version-id).
|
|
13
|
+
* This module owns ONLY the durable index + lifecycle; it does not
|
|
14
|
+
* know how to actually restore site data — that contract is delegated
|
|
15
|
+
* to a per-site `restorer` callable registered via `setRestorer`.
|
|
16
|
+
*
|
|
17
|
+
* Tier: Enterprise.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const { db } = require('../models/db');
|
|
22
|
+
|
|
23
|
+
// Schema is created lazily so this module can be required even on
|
|
24
|
+
// installations that do not enable rollback.
|
|
25
|
+
let _initialized = false;
|
|
26
|
+
function _ensureSchema() {
|
|
27
|
+
if (_initialized) return;
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS wab_snapshots (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
site_id TEXT NOT NULL,
|
|
32
|
+
action_name TEXT NOT NULL,
|
|
33
|
+
actor_id TEXT,
|
|
34
|
+
actor_type TEXT NOT NULL DEFAULT 'agent',
|
|
35
|
+
session_fingerprint TEXT,
|
|
36
|
+
params_hash TEXT,
|
|
37
|
+
snapshot TEXT NOT NULL,
|
|
38
|
+
meta TEXT DEFAULT '{}',
|
|
39
|
+
reversible INTEGER NOT NULL DEFAULT 1,
|
|
40
|
+
status TEXT NOT NULL DEFAULT 'recorded'
|
|
41
|
+
CHECK(status IN ('recorded','restored','expired','failed')),
|
|
42
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
43
|
+
restored_at TEXT,
|
|
44
|
+
expires_at TEXT
|
|
45
|
+
);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_wab_snapshots_site ON wab_snapshots (site_id, created_at DESC);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_wab_snapshots_status ON wab_snapshots (status);
|
|
48
|
+
`);
|
|
49
|
+
_initialized = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── per-site restorers ──────────────────────────────────────────────
|
|
53
|
+
const _restorers = new Map();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Register a restorer for a site. The function receives
|
|
57
|
+
* `{ snapshot, action_name, params_hash, meta }` and must return either
|
|
58
|
+
* `{ ok:true }` or `{ ok:false, error }`.
|
|
59
|
+
*/
|
|
60
|
+
function setRestorer(siteId, fn) {
|
|
61
|
+
if (typeof fn !== 'function') throw new TypeError('restorer must be a function');
|
|
62
|
+
_restorers.set(String(siteId), fn);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _getRestorer(siteId) {
|
|
66
|
+
return _restorers.get(String(siteId)) || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── helpers ─────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function _hashParams(params) {
|
|
72
|
+
const canon = JSON.stringify(_canonicalize(params || {}));
|
|
73
|
+
return crypto.createHash('sha256').update(canon).digest('hex').slice(0, 24);
|
|
74
|
+
}
|
|
75
|
+
function _canonicalize(value) {
|
|
76
|
+
if (value === null || typeof value !== 'object') return value;
|
|
77
|
+
if (Array.isArray(value)) return value.map(_canonicalize);
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const k of Object.keys(value).sort()) out[k] = _canonicalize(value[k]);
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function _fingerprint(token) {
|
|
83
|
+
if (!token) return null;
|
|
84
|
+
return crypto.createHash('sha256').update(String(token)).digest('hex').slice(0, 16);
|
|
85
|
+
}
|
|
86
|
+
function _genId() { return 'wabs_' + crypto.randomBytes(16).toString('hex'); }
|
|
87
|
+
|
|
88
|
+
// ─── public API ──────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Record a snapshot just BEFORE the destructive action runs.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} ctx { siteId, actionName, actorId, sessionToken, params }
|
|
94
|
+
* @param {object} payload { snapshot, meta?, reversible?, ttlMs? }
|
|
95
|
+
* @returns { snapshot_id, expires_at }
|
|
96
|
+
*/
|
|
97
|
+
function recordSnapshot(ctx, payload) {
|
|
98
|
+
_ensureSchema();
|
|
99
|
+
const id = _genId();
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const ttlMs = Math.max(0, payload.ttlMs || 30 * 24 * 60 * 60 * 1000); // 30 days default
|
|
102
|
+
const expiresAt = ttlMs > 0 ? new Date(now + ttlMs).toISOString() : null;
|
|
103
|
+
|
|
104
|
+
db.prepare(`
|
|
105
|
+
INSERT INTO wab_snapshots
|
|
106
|
+
(id, site_id, action_name, actor_id, actor_type, session_fingerprint, params_hash,
|
|
107
|
+
snapshot, meta, reversible, status, expires_at)
|
|
108
|
+
VALUES (?, ?, ?, ?, 'agent', ?, ?, ?, ?, ?, 'recorded', ?)
|
|
109
|
+
`).run(
|
|
110
|
+
id,
|
|
111
|
+
String(ctx.siteId),
|
|
112
|
+
String(ctx.actionName),
|
|
113
|
+
ctx.actorId || null,
|
|
114
|
+
_fingerprint(ctx.sessionToken),
|
|
115
|
+
_hashParams(ctx.params),
|
|
116
|
+
JSON.stringify(payload.snapshot ?? null),
|
|
117
|
+
JSON.stringify(payload.meta || {}),
|
|
118
|
+
payload.reversible === false ? 0 : 1,
|
|
119
|
+
expiresAt
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return { snapshot_id: id, expires_at: expiresAt };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getSnapshot(snapshotId) {
|
|
126
|
+
_ensureSchema();
|
|
127
|
+
const row = db.prepare(`SELECT * FROM wab_snapshots WHERE id = ?`).get(snapshotId);
|
|
128
|
+
if (!row) return null;
|
|
129
|
+
return _hydrate(row);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function listSnapshots(siteId, opts = {}) {
|
|
133
|
+
_ensureSchema();
|
|
134
|
+
const limit = Math.min(Math.max(opts.limit || 50, 1), 500);
|
|
135
|
+
const status = opts.status;
|
|
136
|
+
const rows = status
|
|
137
|
+
? db.prepare(`SELECT * FROM wab_snapshots WHERE site_id=? AND status=? ORDER BY created_at DESC, rowid DESC LIMIT ?`).all(siteId, status, limit)
|
|
138
|
+
: db.prepare(`SELECT * FROM wab_snapshots WHERE site_id=? ORDER BY created_at DESC, rowid DESC LIMIT ?`).all(siteId, limit);
|
|
139
|
+
return rows.map(_hydrate);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _hydrate(row) {
|
|
143
|
+
let snapshot = null;
|
|
144
|
+
let meta = {};
|
|
145
|
+
try { snapshot = JSON.parse(row.snapshot); } catch (_) {}
|
|
146
|
+
try { meta = JSON.parse(row.meta || '{}'); } catch (_) {}
|
|
147
|
+
return {
|
|
148
|
+
id: row.id,
|
|
149
|
+
site_id: row.site_id,
|
|
150
|
+
action_name: row.action_name,
|
|
151
|
+
actor_id: row.actor_id,
|
|
152
|
+
actor_type: row.actor_type,
|
|
153
|
+
session_fingerprint: row.session_fingerprint,
|
|
154
|
+
params_hash: row.params_hash,
|
|
155
|
+
snapshot,
|
|
156
|
+
meta,
|
|
157
|
+
reversible: !!row.reversible,
|
|
158
|
+
status: row.status,
|
|
159
|
+
created_at: row.created_at,
|
|
160
|
+
restored_at: row.restored_at,
|
|
161
|
+
expires_at: row.expires_at,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Restore a snapshot. Returns { ok:true } on success, or
|
|
167
|
+
* { ok:false, code, message }. Restoration is single-use: a snapshot
|
|
168
|
+
* already in `restored` cannot be replayed.
|
|
169
|
+
*/
|
|
170
|
+
async function restoreSnapshot(snapshotId, opts = {}) {
|
|
171
|
+
_ensureSchema();
|
|
172
|
+
const row = db.prepare(`SELECT * FROM wab_snapshots WHERE id = ?`).get(snapshotId);
|
|
173
|
+
if (!row) return { ok: false, code: 'SNAPSHOT_NOT_FOUND' };
|
|
174
|
+
if (row.status === 'restored') return { ok: false, code: 'SNAPSHOT_ALREADY_RESTORED' };
|
|
175
|
+
if (row.status === 'expired') return { ok: false, code: 'SNAPSHOT_EXPIRED' };
|
|
176
|
+
if (!row.reversible) return { ok: false, code: 'SNAPSHOT_IRREVERSIBLE' };
|
|
177
|
+
if (row.expires_at && new Date(row.expires_at).getTime() < Date.now()) {
|
|
178
|
+
db.prepare(`UPDATE wab_snapshots SET status='expired' WHERE id=?`).run(snapshotId);
|
|
179
|
+
return { ok: false, code: 'SNAPSHOT_EXPIRED' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const restorer = opts.restorer || _getRestorer(row.site_id);
|
|
183
|
+
if (!restorer) {
|
|
184
|
+
return { ok: false, code: 'NO_RESTORER', message: `no restorer registered for site ${row.site_id}` };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let result;
|
|
188
|
+
try {
|
|
189
|
+
result = await restorer({
|
|
190
|
+
snapshot_id: row.id,
|
|
191
|
+
site_id: row.site_id,
|
|
192
|
+
action_name: row.action_name,
|
|
193
|
+
params_hash: row.params_hash,
|
|
194
|
+
snapshot: JSON.parse(row.snapshot),
|
|
195
|
+
meta: JSON.parse(row.meta || '{}'),
|
|
196
|
+
});
|
|
197
|
+
} catch (err) {
|
|
198
|
+
db.prepare(`UPDATE wab_snapshots SET status='failed' WHERE id=?`).run(snapshotId);
|
|
199
|
+
return { ok: false, code: 'RESTORER_THREW', message: err.message };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!result || result.ok !== true) {
|
|
203
|
+
db.prepare(`UPDATE wab_snapshots SET status='failed' WHERE id=?`).run(snapshotId);
|
|
204
|
+
return { ok: false, code: 'RESTORER_FAILED', message: result?.error || 'restorer reported failure' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
db.prepare(`UPDATE wab_snapshots SET status='restored', restored_at=datetime('now') WHERE id=?`).run(snapshotId);
|
|
208
|
+
return { ok: true, snapshot_id: snapshotId };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Expire snapshots past their TTL. Returns the count expired.
|
|
213
|
+
*/
|
|
214
|
+
function expireOld() {
|
|
215
|
+
_ensureSchema();
|
|
216
|
+
const r = db.prepare(`
|
|
217
|
+
UPDATE wab_snapshots SET status='expired'
|
|
218
|
+
WHERE status='recorded' AND expires_at IS NOT NULL AND expires_at < datetime('now')
|
|
219
|
+
`).run();
|
|
220
|
+
return r.changes || 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function _resetForTests() {
|
|
224
|
+
_ensureSchema();
|
|
225
|
+
db.prepare(`DELETE FROM wab_snapshots`).run();
|
|
226
|
+
_restorers.clear();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
recordSnapshot,
|
|
231
|
+
getSnapshot,
|
|
232
|
+
listSnapshots,
|
|
233
|
+
restoreSnapshot,
|
|
234
|
+
expireOld,
|
|
235
|
+
setRestorer,
|
|
236
|
+
// test helpers
|
|
237
|
+
_resetForTests,
|
|
238
|
+
_hashParams,
|
|
239
|
+
};
|