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
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// WAB Ring 4 — External Trust Verification API (v3.7.0)
|
|
3
|
+
//
|
|
4
|
+
// Endpoints for sovereign agents (VEXR Ultra, ASIM SOVEREIGN, ...) that
|
|
5
|
+
// integrate WAB as their Ring 4 trust layer.
|
|
6
|
+
//
|
|
7
|
+
// Mounted at /api/ring4 in server/index.js.
|
|
8
|
+
//
|
|
9
|
+
// Surface:
|
|
10
|
+
// POST /project/register — register a sovereign agent project (returns project_id)
|
|
11
|
+
// GET /projects — list projects (public, sanitized)
|
|
12
|
+
// POST /register — issue/refresh a Ring 4 trust profile for a domain
|
|
13
|
+
// GET /status/:domain — fetch current trust profile + signature
|
|
14
|
+
// GET /profile/:domain — alias of /status
|
|
15
|
+
// POST /log — log an interaction (project_id required)
|
|
16
|
+
// GET /log/:project_id — fetch interaction log for a project
|
|
17
|
+
// POST /verify — verify Ed25519 signature on a payload using the domain pk
|
|
18
|
+
// GET /invariants — list constitutional invariants
|
|
19
|
+
// POST /invariants/check — runtime check action vs invariants (NEW v3.7.0)
|
|
20
|
+
// GET /refusals — aggregated, anonymized refusal stats (NEW v3.7.0)
|
|
21
|
+
// GET /schema — wab.json v1.1 schema
|
|
22
|
+
// GET /handshake — recommended trust handshake flow
|
|
23
|
+
// GET /health — module health
|
|
24
|
+
// GET /pubkey — current Ed25519 public key
|
|
25
|
+
// GET /jwks — JWKS document (all active + superseded keys)
|
|
26
|
+
// GET /keys — list keys (no privates) (NEW v3.7.0)
|
|
27
|
+
// POST /keys/rotate — rotate signing key (admin) (NEW v3.7.0)
|
|
28
|
+
// POST /federation/peer — register a peer WAB instance (NEW v3.7.0)
|
|
29
|
+
// GET /federation/peers — list peers (NEW v3.7.0)
|
|
30
|
+
// DELETE /federation/peer/:peer_id — remove a peer (NEW v3.7.0)
|
|
31
|
+
// POST /conformance/run — run 3-test conformance suite (NEW v3.7.0)
|
|
32
|
+
// GET /conformance/:project_id — conformance history (NEW v3.7.0)
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
|
|
35
|
+
const express = require('express');
|
|
36
|
+
const crypto = require('crypto');
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const { db } = require('../models/db');
|
|
40
|
+
|
|
41
|
+
const router = express.Router();
|
|
42
|
+
|
|
43
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Helpers
|
|
45
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
const DAILY_SALT = () => {
|
|
47
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
48
|
+
return `wab-ring4-${day}-${process.env.RING4_SALT || 'default'}`;
|
|
49
|
+
};
|
|
50
|
+
const hashIp = (ip) => crypto.createHash('sha256').update(`${DAILY_SALT()}:${ip || 'unknown'}`).digest('hex').slice(0, 24);
|
|
51
|
+
const safeJson = (v, fallback = '{}') => { try { return JSON.parse(v || fallback); } catch { return JSON.parse(fallback); } };
|
|
52
|
+
const okDomain = (d) => typeof d === 'string' && /^[a-z0-9.-]{3,253}$/i.test(d);
|
|
53
|
+
const okProject = (p) => typeof p === 'string' && /^[a-z0-9-]{2,64}$/i.test(p);
|
|
54
|
+
const okKid = (k) => typeof k === 'string' && /^[a-z0-9._-]{3,64}$/i.test(k);
|
|
55
|
+
const b64url = (buf) => buf.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
56
|
+
|
|
57
|
+
function ed25519Verify(publicKeyB64, message, signatureB64) {
|
|
58
|
+
try {
|
|
59
|
+
const pk = Buffer.from(publicKeyB64, 'base64');
|
|
60
|
+
if (pk.length !== 32) return false;
|
|
61
|
+
const der = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), pk]);
|
|
62
|
+
const keyObj = crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
63
|
+
const sig = Buffer.from(signatureB64, 'base64');
|
|
64
|
+
const msg = Buffer.isBuffer(message) ? message : Buffer.from(message, 'utf8');
|
|
65
|
+
return crypto.verify(null, msg, keyObj, sig);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function canonicalProfile(domain, capabilities, constraints, expires_at) {
|
|
72
|
+
return JSON.stringify({ domain, capabilities, constraints, expires_at });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// Key management (multi-key rotation with backwards-compatible env fallback)
|
|
77
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
const KEYS_DIR = process.env.WAB_RING4_KEYS_DIR
|
|
79
|
+
|| (process.env.NODE_ENV === 'test'
|
|
80
|
+
? path.join(__dirname, '..', '..', 'data-test', 'keys')
|
|
81
|
+
: path.join(__dirname, '..', '..', 'data', 'keys'));
|
|
82
|
+
|
|
83
|
+
function ensureKeysDir() {
|
|
84
|
+
try { fs.mkdirSync(KEYS_DIR, { recursive: true, mode: 0o700 }); } catch { /* ignore */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rawPubFromPem(pem) {
|
|
88
|
+
const pub = crypto.createPublicKey(crypto.createPrivateKey(pem));
|
|
89
|
+
const spki = pub.export({ format: 'der', type: 'spki' });
|
|
90
|
+
return spki.subarray(spki.length - 32);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function loadPrimaryPemFromEnv() {
|
|
94
|
+
const pemFromEnv = process.env.WAB_RING4_PRIVATE_KEY_PEM;
|
|
95
|
+
const pathFromEnv = process.env.WAB_RING4_PRIVATE_KEY_PATH;
|
|
96
|
+
if (pemFromEnv) return { pem: pemFromEnv, source: 'env-pem' };
|
|
97
|
+
if (pathFromEnv) {
|
|
98
|
+
try { return { pem: fs.readFileSync(pathFromEnv, 'utf8'), source: 'env-path:' + pathFromEnv }; }
|
|
99
|
+
catch { return null; }
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// In-process key cache: kid → { privatePem, publicKeyB64 }
|
|
105
|
+
const keyCache = new Map();
|
|
106
|
+
|
|
107
|
+
// In-process negative cache for /status/:domain (unknown domains)
|
|
108
|
+
const negCache = new Map();
|
|
109
|
+
|
|
110
|
+
function registerKey(kid, pem, status = 'active', source = 'manual') {
|
|
111
|
+
const pubRaw = rawPubFromPem(pem).toString('base64');
|
|
112
|
+
db.prepare(`
|
|
113
|
+
INSERT INTO ring4_keys (kid, algorithm, public_key_b64, status, source)
|
|
114
|
+
VALUES (?, 'ed25519', ?, ?, ?)
|
|
115
|
+
ON CONFLICT(kid) DO UPDATE SET status = excluded.status
|
|
116
|
+
`).run(kid, pubRaw, status, source);
|
|
117
|
+
keyCache.set(kid, { pem, publicKeyB64: pubRaw });
|
|
118
|
+
return { kid, publicKeyB64: pubRaw };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function bootstrapKeys() {
|
|
122
|
+
ensureKeysDir();
|
|
123
|
+
try {
|
|
124
|
+
// Ensure DB row exists for the env-provided key
|
|
125
|
+
const envKey = loadPrimaryPemFromEnv();
|
|
126
|
+
if (envKey) {
|
|
127
|
+
const pubB64 = rawPubFromPem(envKey.pem).toString('base64');
|
|
128
|
+
const existing = db.prepare(`SELECT kid, status FROM ring4_keys WHERE public_key_b64 = ?`).get(pubB64);
|
|
129
|
+
if (!existing) {
|
|
130
|
+
const kid = 'ring4-' + new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
131
|
+
registerKey(kid, envKey.pem, 'active', envKey.source);
|
|
132
|
+
} else if (existing.status !== 'active') {
|
|
133
|
+
db.prepare(`UPDATE ring4_keys SET status = 'active' WHERE kid = ?`).run(existing.kid);
|
|
134
|
+
}
|
|
135
|
+
const row = db.prepare(`SELECT kid FROM ring4_keys WHERE public_key_b64 = ?`).get(pubB64);
|
|
136
|
+
if (row) keyCache.set(row.kid, { pem: envKey.pem, publicKeyB64: pubB64 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Re-hydrate cached PEMs for any rotated keys stored on disk
|
|
140
|
+
const all = db.prepare(`SELECT kid FROM ring4_keys`).all();
|
|
141
|
+
for (const k of all) {
|
|
142
|
+
if (keyCache.has(k.kid)) continue;
|
|
143
|
+
const filePath = path.join(KEYS_DIR, `${k.kid}.pem`);
|
|
144
|
+
if (fs.existsSync(filePath)) {
|
|
145
|
+
try { keyCache.set(k.kid, { pem: fs.readFileSync(filePath, 'utf8'), publicKeyB64: null }); } catch { /* ignore */ }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch { /* tables not migrated yet; bootstrap retries on first read */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getActiveKey() {
|
|
152
|
+
try {
|
|
153
|
+
let row = db.prepare(`SELECT kid, public_key_b64 FROM ring4_keys WHERE status = 'active' ORDER BY created_at DESC LIMIT 1`).get();
|
|
154
|
+
if (!row) {
|
|
155
|
+
// First call after migrations? Re-run bootstrap.
|
|
156
|
+
bootstrapKeys();
|
|
157
|
+
row = db.prepare(`SELECT kid, public_key_b64 FROM ring4_keys WHERE status = 'active' ORDER BY created_at DESC LIMIT 1`).get();
|
|
158
|
+
if (!row) return null;
|
|
159
|
+
}
|
|
160
|
+
const cached = keyCache.get(row.kid);
|
|
161
|
+
if (cached && cached.pem) return { kid: row.kid, pem: cached.pem, publicKeyB64: row.public_key_b64 };
|
|
162
|
+
// Fall through to env-based PEM if cache miss
|
|
163
|
+
const envKey = loadPrimaryPemFromEnv();
|
|
164
|
+
if (envKey) {
|
|
165
|
+
const pubB64 = rawPubFromPem(envKey.pem).toString('base64');
|
|
166
|
+
if (pubB64 === row.public_key_b64) {
|
|
167
|
+
keyCache.set(row.kid, { pem: envKey.pem, publicKeyB64: pubB64 });
|
|
168
|
+
return { kid: row.kid, pem: envKey.pem, publicKeyB64: pubB64 };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function listVerificationKeys() {
|
|
178
|
+
// active + superseded (revoked excluded)
|
|
179
|
+
try {
|
|
180
|
+
return db.prepare(`SELECT kid, public_key_b64, status FROM ring4_keys WHERE status IN ('active','superseded') ORDER BY created_at DESC`).all();
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function signProfile(payloadStr) {
|
|
187
|
+
const active = getActiveKey();
|
|
188
|
+
if (!active) return { signature: null, pk: null, kid: null };
|
|
189
|
+
const keyObj = crypto.createPrivateKey(active.pem);
|
|
190
|
+
const sig = crypto.sign(null, Buffer.from(payloadStr, 'utf8'), keyObj);
|
|
191
|
+
return {
|
|
192
|
+
signature: 'ed25519:' + sig.toString('base64'),
|
|
193
|
+
pk: 'ed25519:' + active.publicKeyB64,
|
|
194
|
+
kid: active.kid
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
bootstrapKeys();
|
|
199
|
+
|
|
200
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
201
|
+
// Project registry
|
|
202
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
router.post('/project/register', (req, res) => {
|
|
204
|
+
const { project_id, display_name, builder, agent_type, public_key, contact, metadata } = req.body || {};
|
|
205
|
+
if (!okProject(project_id || '')) return res.status(400).json({ error: 'invalid project_id (a-z0-9-, 2-64 chars)' });
|
|
206
|
+
if (!display_name || typeof display_name !== 'string') return res.status(400).json({ error: 'display_name required' });
|
|
207
|
+
if (!builder || typeof builder !== 'string') return res.status(400).json({ error: 'builder required' });
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
db.prepare(`
|
|
211
|
+
INSERT INTO ring4_projects (project_id, display_name, builder, agent_type, public_key, contact, metadata_json, updated_at)
|
|
212
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
213
|
+
ON CONFLICT(project_id) DO UPDATE SET
|
|
214
|
+
display_name = excluded.display_name,
|
|
215
|
+
builder = excluded.builder,
|
|
216
|
+
agent_type = excluded.agent_type,
|
|
217
|
+
public_key = excluded.public_key,
|
|
218
|
+
contact = excluded.contact,
|
|
219
|
+
metadata_json= excluded.metadata_json,
|
|
220
|
+
updated_at = datetime('now')
|
|
221
|
+
`).run(
|
|
222
|
+
project_id,
|
|
223
|
+
display_name.slice(0, 200),
|
|
224
|
+
builder.slice(0, 200),
|
|
225
|
+
agent_type || 'sovereign-constitutional',
|
|
226
|
+
public_key || null,
|
|
227
|
+
contact || null,
|
|
228
|
+
JSON.stringify(metadata || {})
|
|
229
|
+
);
|
|
230
|
+
return res.json({ ok: true, project_id });
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return res.status(500).json({ error: 'project_register_failed', detail: e.message });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
router.get('/projects', (_req, res) => {
|
|
237
|
+
const rows = db.prepare(`
|
|
238
|
+
SELECT project_id, display_name, builder, agent_type, status, created_at
|
|
239
|
+
FROM ring4_projects
|
|
240
|
+
WHERE status = 'active'
|
|
241
|
+
ORDER BY created_at ASC
|
|
242
|
+
`).all();
|
|
243
|
+
return res.json({ projects: rows });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
247
|
+
// Trust profiles
|
|
248
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
router.post('/register', (req, res) => {
|
|
250
|
+
const { domain, label, capabilities, constraints, ttl_seconds, trust_score } = req.body || {};
|
|
251
|
+
if (!okDomain(domain || '')) return res.status(400).json({ error: 'invalid domain' });
|
|
252
|
+
if (!capabilities || typeof capabilities !== 'object') return res.status(400).json({ error: 'capabilities object required' });
|
|
253
|
+
|
|
254
|
+
const safeConstraints = Object.assign(
|
|
255
|
+
{ ttl_seconds: 86400, max_cumulative_risk_delta: 0.15, never_override_hard_refuse: true },
|
|
256
|
+
(constraints && typeof constraints === 'object') ? constraints : {}
|
|
257
|
+
);
|
|
258
|
+
const ttl = Math.min(Math.max(parseInt(ttl_seconds, 10) || safeConstraints.ttl_seconds || 86400, 60), 60 * 60 * 24 * 30);
|
|
259
|
+
const score = Math.max(0, Math.min(1, parseFloat(trust_score) || 0.7));
|
|
260
|
+
const expires_at = new Date(Date.now() + ttl * 1000).toISOString();
|
|
261
|
+
const capsStr = JSON.stringify(capabilities);
|
|
262
|
+
const consStr = JSON.stringify(safeConstraints);
|
|
263
|
+
const { signature, pk } = signProfile(canonicalProfile(domain, capabilities, safeConstraints, expires_at));
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
db.prepare(`
|
|
267
|
+
INSERT INTO ring4_trust_profiles (domain, label, capabilities, constraints, ttl_seconds, trust_score, signature, signed_by_pk, expires_at, updated_at)
|
|
268
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
269
|
+
ON CONFLICT(domain) DO UPDATE SET
|
|
270
|
+
label = excluded.label,
|
|
271
|
+
capabilities = excluded.capabilities,
|
|
272
|
+
constraints = excluded.constraints,
|
|
273
|
+
ttl_seconds = excluded.ttl_seconds,
|
|
274
|
+
trust_score = excluded.trust_score,
|
|
275
|
+
signature = excluded.signature,
|
|
276
|
+
signed_by_pk = excluded.signed_by_pk,
|
|
277
|
+
expires_at = excluded.expires_at,
|
|
278
|
+
updated_at = datetime('now')
|
|
279
|
+
`).run(domain, label || domain, capsStr, consStr, ttl, score, signature, pk, expires_at);
|
|
280
|
+
// Invalidate negative cache so the new profile is immediately visible
|
|
281
|
+
negCache.delete(domain);
|
|
282
|
+
|
|
283
|
+
// Audit log — project_id resolved or fallback to system project (no more NULL)
|
|
284
|
+
const project_id = okProject(req.body.project_id || '') ? req.body.project_id : 'wab-system';
|
|
285
|
+
db.prepare(`
|
|
286
|
+
INSERT INTO ring4_interaction_log (project_id, domain, event_type, capabilities_applied, constraints_applied, outcome, detail, source_ip_hash)
|
|
287
|
+
VALUES (?, ?, 'register', ?, ?, 'allow', ?, ?)
|
|
288
|
+
`).run(project_id, domain, capsStr, consStr, `trust_score=${score}; ttl=${ttl}`, hashIp(req.ip));
|
|
289
|
+
|
|
290
|
+
return res.json({
|
|
291
|
+
ok: true,
|
|
292
|
+
domain,
|
|
293
|
+
trust_score: score,
|
|
294
|
+
ttl_seconds: ttl,
|
|
295
|
+
expires_at,
|
|
296
|
+
signature,
|
|
297
|
+
signed_by_pk: pk,
|
|
298
|
+
status: 'registered'
|
|
299
|
+
});
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return res.status(500).json({ error: 'profile_register_failed', detail: e.message });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
function fetchProfile(domain) {
|
|
306
|
+
return db.prepare(`
|
|
307
|
+
SELECT domain, label, capabilities, constraints, ttl_seconds, trust_score, signature, signed_by_pk, expires_at, created_at, updated_at
|
|
308
|
+
FROM ring4_trust_profiles
|
|
309
|
+
WHERE domain = ?
|
|
310
|
+
`).get(domain);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function profileResponse(row) {
|
|
314
|
+
if (!row) return null;
|
|
315
|
+
const expired = new Date(row.expires_at).getTime() < Date.now();
|
|
316
|
+
// Count verifications historically logged
|
|
317
|
+
const count = db.prepare(`SELECT COUNT(1) AS n FROM ring4_interaction_log WHERE domain = ? AND event_type IN ('verify','recognize')`).get(row.domain).n;
|
|
318
|
+
return {
|
|
319
|
+
domain: row.domain,
|
|
320
|
+
label: row.label,
|
|
321
|
+
wab_verified: !expired,
|
|
322
|
+
temporal_trust_score: row.trust_score,
|
|
323
|
+
last_verification: row.updated_at,
|
|
324
|
+
verification_count: count,
|
|
325
|
+
capabilities: safeJson(row.capabilities),
|
|
326
|
+
constraints: safeJson(row.constraints),
|
|
327
|
+
ttl_seconds: row.ttl_seconds,
|
|
328
|
+
signature: row.signature,
|
|
329
|
+
signed_by_pk: row.signed_by_pk,
|
|
330
|
+
expires_at: row.expires_at,
|
|
331
|
+
status: expired ? 'expired' : 'registered'
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
router.get('/status/:domain', (req, res) => {
|
|
336
|
+
const domain = String(req.params.domain || '').toLowerCase();
|
|
337
|
+
if (!okDomain(domain)) return res.status(400).json({ error: 'invalid domain' });
|
|
338
|
+
// Negative-result cache (60 s) — prevents DB hammering on garbage domains.
|
|
339
|
+
const cached = negCache.get(domain);
|
|
340
|
+
if (cached && cached.until > Date.now()) {
|
|
341
|
+
res.set('X-Ring4-Cache', 'NEG-HIT');
|
|
342
|
+
return res.status(404).json({ error: 'not_registered', domain, cached: true });
|
|
343
|
+
}
|
|
344
|
+
const row = fetchProfile(domain);
|
|
345
|
+
if (!row) {
|
|
346
|
+
negCache.set(domain, { until: Date.now() + 60_000 });
|
|
347
|
+
if (negCache.size > 500) {
|
|
348
|
+
// LRU-ish prune: drop the oldest 100 entries
|
|
349
|
+
const keys = Array.from(negCache.keys()).slice(0, 100);
|
|
350
|
+
for (const k of keys) negCache.delete(k);
|
|
351
|
+
}
|
|
352
|
+
return res.status(404).json({ error: 'not_registered', domain });
|
|
353
|
+
}
|
|
354
|
+
// Positive hit invalidates any stale negative entry
|
|
355
|
+
negCache.delete(domain);
|
|
356
|
+
return res.json(profileResponse(row));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
router.get('/profile/:domain', (req, res) => {
|
|
360
|
+
req.url = `/status/${req.params.domain}`;
|
|
361
|
+
router.handle(req, res);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
365
|
+
// Interaction log
|
|
366
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
367
|
+
router.post('/log', (req, res) => {
|
|
368
|
+
const { project_id, domain, event_type, signature_valid, outcome, article_invoked, detail, agent_nonce, capabilities_applied, constraints_applied } = req.body || {};
|
|
369
|
+
if (!okProject(project_id || '')) return res.status(400).json({ error: 'invalid project_id' });
|
|
370
|
+
if (!event_type || !['register', 'recognize', 'verify', 'refuse', 'softened', 'revoke', 'allow', 'hard_refuse_held'].includes(event_type)) {
|
|
371
|
+
return res.status(400).json({ error: 'invalid event_type' });
|
|
372
|
+
}
|
|
373
|
+
if (domain && !okDomain(domain)) return res.status(400).json({ error: 'invalid domain' });
|
|
374
|
+
|
|
375
|
+
// Confirm project exists (auto-create lightweight record if absent)
|
|
376
|
+
const exists = db.prepare(`SELECT 1 FROM ring4_projects WHERE project_id = ?`).get(project_id);
|
|
377
|
+
if (!exists) {
|
|
378
|
+
db.prepare(`INSERT INTO ring4_projects (project_id, display_name, builder) VALUES (?, ?, ?)`)
|
|
379
|
+
.run(project_id, project_id, 'auto-registered');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const r = db.prepare(`
|
|
384
|
+
INSERT INTO ring4_interaction_log (
|
|
385
|
+
project_id, domain, event_type, signature_valid, capabilities_applied, constraints_applied,
|
|
386
|
+
outcome, article_invoked, detail, source_ip_hash, agent_nonce
|
|
387
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
388
|
+
`).run(
|
|
389
|
+
project_id,
|
|
390
|
+
domain || null,
|
|
391
|
+
event_type,
|
|
392
|
+
(signature_valid === true || signature_valid === 1) ? 1 : (signature_valid === false || signature_valid === 0) ? 0 : null,
|
|
393
|
+
capabilities_applied ? JSON.stringify(capabilities_applied) : null,
|
|
394
|
+
constraints_applied ? JSON.stringify(constraints_applied) : null,
|
|
395
|
+
outcome || null,
|
|
396
|
+
article_invoked || null,
|
|
397
|
+
typeof detail === 'string' ? detail.slice(0, 500) : null,
|
|
398
|
+
hashIp(req.ip),
|
|
399
|
+
typeof agent_nonce === 'string' ? agent_nonce.slice(0, 64) : null
|
|
400
|
+
);
|
|
401
|
+
return res.json({ ok: true, id: r.lastInsertRowid });
|
|
402
|
+
} catch (e) {
|
|
403
|
+
return res.status(500).json({ error: 'log_failed', detail: e.message });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
router.get('/log/:project_id', (req, res) => {
|
|
408
|
+
const project_id = String(req.params.project_id || '');
|
|
409
|
+
if (!okProject(project_id)) return res.status(400).json({ error: 'invalid project_id' });
|
|
410
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 100, 500);
|
|
411
|
+
// Include legacy NULL project_id rows when the system project is queried
|
|
412
|
+
const includeLegacy = project_id === 'wab-system';
|
|
413
|
+
const rows = includeLegacy
|
|
414
|
+
? db.prepare(`
|
|
415
|
+
SELECT id, COALESCE(project_id, 'wab-system') AS project_id, domain, event_type, signature_valid,
|
|
416
|
+
capabilities_applied, constraints_applied, outcome, article_invoked, detail, agent_nonce, created_at
|
|
417
|
+
FROM ring4_interaction_log
|
|
418
|
+
WHERE project_id = ? OR project_id IS NULL
|
|
419
|
+
ORDER BY created_at DESC
|
|
420
|
+
LIMIT ?
|
|
421
|
+
`).all(project_id, limit)
|
|
422
|
+
: db.prepare(`
|
|
423
|
+
SELECT id, project_id, domain, event_type, signature_valid, capabilities_applied, constraints_applied,
|
|
424
|
+
outcome, article_invoked, detail, agent_nonce, created_at
|
|
425
|
+
FROM ring4_interaction_log
|
|
426
|
+
WHERE project_id = ?
|
|
427
|
+
ORDER BY created_at DESC
|
|
428
|
+
LIMIT ?
|
|
429
|
+
`).all(project_id, limit);
|
|
430
|
+
return res.json({ project_id, count: rows.length, events: rows });
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
434
|
+
// Signature verification (Ed25519 against domain pk)
|
|
435
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
436
|
+
router.post('/verify', (req, res) => {
|
|
437
|
+
const { domain, message, signature, public_key } = req.body || {};
|
|
438
|
+
if (!okDomain(domain || '')) return res.status(400).json({ error: 'invalid domain' });
|
|
439
|
+
if (!message || typeof message !== 'string') return res.status(400).json({ error: 'message required (string)' });
|
|
440
|
+
if (!signature || typeof signature !== 'string') return res.status(400).json({ error: 'signature required (base64)' });
|
|
441
|
+
|
|
442
|
+
let pk = public_key;
|
|
443
|
+
if (!pk) {
|
|
444
|
+
const row = fetchProfile(domain);
|
|
445
|
+
pk = row && row.signed_by_pk ? row.signed_by_pk : null;
|
|
446
|
+
}
|
|
447
|
+
if (!pk) return res.status(404).json({ error: 'no_public_key_for_domain', domain });
|
|
448
|
+
const pkRaw = pk.startsWith('ed25519:') ? pk.slice(8) : pk;
|
|
449
|
+
const sigRaw = signature.startsWith('ed25519:') ? signature.slice(8) : signature;
|
|
450
|
+
const valid = ed25519Verify(pkRaw, message, sigRaw);
|
|
451
|
+
return res.json({ ok: true, domain, valid, algorithm: 'ed25519' });
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
455
|
+
// Constitutional invariants
|
|
456
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
457
|
+
router.get('/invariants', (_req, res) => {
|
|
458
|
+
const rows = db.prepare(`SELECT name, description, applies_to FROM ring4_invariants ORDER BY id ASC`).all();
|
|
459
|
+
return res.json({
|
|
460
|
+
invariants: rows,
|
|
461
|
+
contract: 'A Ring 4 trust profile may soften refusals, lower friction, or grant access. It MAY NOT override any invariant listed here. P_REFUSE on these clauses can never become ANSWER, regardless of trust score.'
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
466
|
+
// wab.json v1.1 schema (with trust_profile section consumed by Ring 4 agents)
|
|
467
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
468
|
+
router.get('/schema', (_req, res) => {
|
|
469
|
+
return res.json({
|
|
470
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
471
|
+
$id: 'https://www.webagentbridge.com/protocol/v1.1/wab.json',
|
|
472
|
+
title: 'WAB Capability + Trust Manifest (v1.1)',
|
|
473
|
+
description: 'Open standard for AI-agent-discoverable site capabilities and Ring 4 trust profiles.',
|
|
474
|
+
type: 'object',
|
|
475
|
+
required: ['payload', 'signature'],
|
|
476
|
+
properties: {
|
|
477
|
+
payload: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
required: ['version', 'type', 'host', 'endpoint', 'issued_at', 'expires_at', 'capabilities', 'trust'],
|
|
480
|
+
properties: {
|
|
481
|
+
version: { const: 'wab1' },
|
|
482
|
+
type: { enum: ['wab.trust', 'wab.capability', 'wab.composite'] },
|
|
483
|
+
host: { type: 'string' },
|
|
484
|
+
endpoint: { type: 'string', format: 'uri' },
|
|
485
|
+
issued_at: { type: 'string', format: 'date-time' },
|
|
486
|
+
expires_at: { type: 'string', format: 'date-time' },
|
|
487
|
+
capabilities: { type: 'object' },
|
|
488
|
+
trust: {
|
|
489
|
+
type: 'object',
|
|
490
|
+
properties: {
|
|
491
|
+
pk: { type: 'string', pattern: '^ed25519:' },
|
|
492
|
+
ssl: { type: 'object' }
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
trust_profile: {
|
|
496
|
+
type: 'object',
|
|
497
|
+
description: 'Optional Ring 4 capability profile consumed by sovereign agents.',
|
|
498
|
+
properties: {
|
|
499
|
+
data_access: { type: 'object', properties: { can_receive_raw_logs: { type: 'boolean' }, can_receive_sanitized_logs: { type: 'boolean' } } },
|
|
500
|
+
risk_theory: { type: 'object', properties: { allowed: { type: 'boolean' }, max_depth: { type: 'string' }, allowed_topics: { type: 'array', items: { type: 'string' } } } },
|
|
501
|
+
meta_discussion: { type: 'object', properties: { allowed: { type: 'boolean' }, priority: { type: 'string' } } },
|
|
502
|
+
operational_detail: { type: 'object', properties: { allowed: { type: 'boolean' }, scopes: { type: 'array' } } },
|
|
503
|
+
constraints: { type: 'object', properties: { ttl_seconds: { type: 'integer' }, max_cumulative_risk_delta: { type: 'number' }, never_override_hard_refuse: { type: 'boolean' } } }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
signature: { type: 'string', pattern: '^ed25519:' }
|
|
509
|
+
},
|
|
510
|
+
headers: {
|
|
511
|
+
'X-WAB-Trust-Domain': 'The DNS-verified trusted origin presenting itself to the agent.',
|
|
512
|
+
'X-WAB-Signature': 'Ed25519 signature (base64) over the canonical request body.',
|
|
513
|
+
'X-WAB-Trust-Nonce': 'Replay-defence nonce supplied by the agent.',
|
|
514
|
+
'X-WAB-Trust-Profile': '(optional) URL to the Ring 4 capability profile to apply.'
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
520
|
+
// Recommended trust handshake flow
|
|
521
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
522
|
+
router.get('/handshake', (_req, res) => {
|
|
523
|
+
return res.json({
|
|
524
|
+
version: 'wab-trust-handshake/1.1',
|
|
525
|
+
description: 'Recommended Ring 4 trust handshake between a sovereign agent and a WAB-enrolled domain.',
|
|
526
|
+
steps: [
|
|
527
|
+
{ n: 1, action: 'DNS lookup', detail: 'Agent queries _wab.<domain> TXT and reads { endpoint, pk }.' },
|
|
528
|
+
{ n: 2, action: 'Fetch manifest', detail: 'Agent GETs /.well-known/wab.json from the published endpoint.' },
|
|
529
|
+
{ n: 3, action: 'Verify signature', detail: 'Agent verifies the Ed25519 signature on payload using pk from DNS.' },
|
|
530
|
+
{ n: 4, action: 'Resolve trust', detail: 'Agent reads trust_profile.* — capabilities, constraints, ttl_seconds.' },
|
|
531
|
+
{ n: 5, action: 'Bind invariants', detail: 'Agent loads /api/ring4/invariants and freezes them above trust softening.' },
|
|
532
|
+
{ n: 6, action: 'Send trust headers', detail: 'Subsequent requests include X-WAB-Trust-Domain + X-WAB-Signature + X-WAB-Trust-Nonce.' },
|
|
533
|
+
{ n: 7, action: 'Log interaction', detail: 'Agent POSTs to /api/ring4/log with its project_id, event_type, outcome.' },
|
|
534
|
+
{ n: 8, action: 'Refresh', detail: 'When now > expires_at, restart from step 1.' }
|
|
535
|
+
],
|
|
536
|
+
invariant: 'Trust softens refusals but never overrides hard constitutional boundaries.',
|
|
537
|
+
test_vectors: {
|
|
538
|
+
register_profile: 'POST /api/ring4/register { domain, capabilities, constraints, project_id }',
|
|
539
|
+
fetch_status: 'GET /api/ring4/status/<domain>',
|
|
540
|
+
verify_sig: 'POST /api/ring4/verify { domain, message, signature }',
|
|
541
|
+
log_event: 'POST /api/ring4/log { project_id, domain, event_type, outcome }',
|
|
542
|
+
project_log: 'GET /api/ring4/log/<project_id>'
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
router.get('/health', (_req, res) => {
|
|
548
|
+
const projects = db.prepare(`SELECT COUNT(1) AS n FROM ring4_projects`).get().n;
|
|
549
|
+
const profiles = db.prepare(`SELECT COUNT(1) AS n FROM ring4_trust_profiles`).get().n;
|
|
550
|
+
const events = db.prepare(`SELECT COUNT(1) AS n FROM ring4_interaction_log`).get().n;
|
|
551
|
+
const active = getActiveKey();
|
|
552
|
+
return res.json({
|
|
553
|
+
ok: true,
|
|
554
|
+
module: 'ring4-external-trust',
|
|
555
|
+
version: '3.7.0',
|
|
556
|
+
projects,
|
|
557
|
+
profiles,
|
|
558
|
+
events,
|
|
559
|
+
signing: !!active,
|
|
560
|
+
active_kid: active ? active.kid : null
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Public verification key (so agents/SDKs can verify Ring 4 signatures)
|
|
565
|
+
router.get('/pubkey', (_req, res) => {
|
|
566
|
+
const active = getActiveKey();
|
|
567
|
+
if (!active) {
|
|
568
|
+
const envPk = process.env.WAB_RING4_PUBLIC_KEY;
|
|
569
|
+
if (envPk) return res.json({ algorithm: 'ed25519', format: 'raw-b64', pk: envPk, source: 'env' });
|
|
570
|
+
return res.status(503).json({ error: 'no_signing_key', message: 'Ring 4 signing is not configured on this instance' });
|
|
571
|
+
}
|
|
572
|
+
const keyObj = crypto.createPrivateKey(active.pem);
|
|
573
|
+
const pub = crypto.createPublicKey(keyObj).export({ format: 'der', type: 'spki' });
|
|
574
|
+
return res.json({
|
|
575
|
+
algorithm: 'ed25519',
|
|
576
|
+
format: 'raw-b64',
|
|
577
|
+
kid: active.kid,
|
|
578
|
+
pk: active.publicKeyB64,
|
|
579
|
+
spki_der_b64: pub.toString('base64'),
|
|
580
|
+
spki_pem: crypto.createPublicKey(keyObj).export({ format: 'pem', type: 'spki' }),
|
|
581
|
+
source: 'server',
|
|
582
|
+
usage: 'verify Ed25519 signatures returned by /api/ring4/status/:domain (signed_by_pk + signature fields)'
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
587
|
+
// JWKS — JSON Web Key Set (RFC 7517) for OIDC/JWT ecosystem interop
|
|
588
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
589
|
+
function buildJwks() {
|
|
590
|
+
const rows = listVerificationKeys();
|
|
591
|
+
const keys = rows.map(r => ({
|
|
592
|
+
kty: 'OKP',
|
|
593
|
+
crv: 'Ed25519',
|
|
594
|
+
alg: 'EdDSA',
|
|
595
|
+
use: 'sig',
|
|
596
|
+
kid: r.kid,
|
|
597
|
+
status: r.status,
|
|
598
|
+
x: b64url(Buffer.from(r.public_key_b64, 'base64'))
|
|
599
|
+
}));
|
|
600
|
+
return { keys };
|
|
601
|
+
}
|
|
602
|
+
router.get('/jwks', (_req, res) => res.json(buildJwks()));
|
|
603
|
+
|
|
604
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
605
|
+
// Key listing + rotation (admin-only)
|
|
606
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
607
|
+
function requireAdminToken(req, res, next) {
|
|
608
|
+
const token = req.headers['x-ring4-admin-token'] || (req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
609
|
+
const expected = process.env.WAB_RING4_ADMIN_TOKEN;
|
|
610
|
+
if (!expected) return res.status(503).json({ error: 'admin_disabled', message: 'WAB_RING4_ADMIN_TOKEN not configured' });
|
|
611
|
+
if (!token || token !== expected) return res.status(401).json({ error: 'unauthorized' });
|
|
612
|
+
next();
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
router.get('/keys', (_req, res) => {
|
|
616
|
+
const rows = db.prepare(`SELECT kid, algorithm, public_key_b64, status, source, created_at, superseded_at FROM ring4_keys ORDER BY created_at DESC`).all();
|
|
617
|
+
return res.json({ count: rows.length, keys: rows });
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
router.post('/keys/rotate', requireAdminToken, (req, res) => {
|
|
621
|
+
const { kid: requestedKid } = req.body || {};
|
|
622
|
+
const kid = requestedKid && okKid(requestedKid)
|
|
623
|
+
? requestedKid
|
|
624
|
+
: 'ring4-' + new Date().toISOString().slice(0, 19).replace(/[-:T]/g, '');
|
|
625
|
+
if (db.prepare(`SELECT 1 FROM ring4_keys WHERE kid = ?`).get(kid)) {
|
|
626
|
+
return res.status(409).json({ error: 'kid_exists', kid });
|
|
627
|
+
}
|
|
628
|
+
// Generate a new Ed25519 keypair, persist PEM to disk, register as active, supersede others.
|
|
629
|
+
const { privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
630
|
+
const pem = privateKey.export({ format: 'pem', type: 'pkcs8' });
|
|
631
|
+
ensureKeysDir();
|
|
632
|
+
const filePath = path.join(KEYS_DIR, `${kid}.pem`);
|
|
633
|
+
try {
|
|
634
|
+
fs.writeFileSync(filePath, pem, { mode: 0o600 });
|
|
635
|
+
} catch (e) {
|
|
636
|
+
return res.status(500).json({ error: 'write_failed', detail: e.message });
|
|
637
|
+
}
|
|
638
|
+
// Mark current active keys as superseded
|
|
639
|
+
db.prepare(`UPDATE ring4_keys SET status = 'superseded', superseded_at = datetime('now') WHERE status = 'active'`).run();
|
|
640
|
+
registerKey(kid, pem, 'active', 'rotation');
|
|
641
|
+
const pubB64 = rawPubFromPem(pem).toString('base64');
|
|
642
|
+
return res.json({
|
|
643
|
+
ok: true,
|
|
644
|
+
kid,
|
|
645
|
+
public_key_b64: pubB64,
|
|
646
|
+
pem_path: filePath,
|
|
647
|
+
message: 'New Ed25519 key activated; previous keys marked superseded but still valid for verification.'
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
652
|
+
// Refusals — aggregated, anonymized stats (Article 3 / hard refusals)
|
|
653
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
654
|
+
router.get('/refusals', (req, res) => {
|
|
655
|
+
const days = Math.min(Math.max(parseInt(req.query.days, 10) || 30, 1), 365);
|
|
656
|
+
const cutoff = new Date(Date.now() - days * 86400_000).toISOString();
|
|
657
|
+
|
|
658
|
+
const byArticle = db.prepare(`
|
|
659
|
+
SELECT COALESCE(article_invoked, 'unspecified') AS article, COUNT(1) AS n
|
|
660
|
+
FROM ring4_interaction_log
|
|
661
|
+
WHERE created_at >= ?
|
|
662
|
+
AND (event_type IN ('refuse','hard_refuse_held') OR article_invoked IS NOT NULL)
|
|
663
|
+
GROUP BY article
|
|
664
|
+
ORDER BY n DESC
|
|
665
|
+
`).all(cutoff);
|
|
666
|
+
|
|
667
|
+
const byDay = db.prepare(`
|
|
668
|
+
SELECT substr(created_at, 1, 10) AS day, COUNT(1) AS n
|
|
669
|
+
FROM ring4_interaction_log
|
|
670
|
+
WHERE created_at >= ?
|
|
671
|
+
AND event_type IN ('refuse','hard_refuse_held')
|
|
672
|
+
GROUP BY day
|
|
673
|
+
ORDER BY day ASC
|
|
674
|
+
`).all(cutoff);
|
|
675
|
+
|
|
676
|
+
const total = byArticle.reduce((a, r) => a + r.n, 0);
|
|
677
|
+
return res.json({
|
|
678
|
+
window_days: days,
|
|
679
|
+
total_refusals: total,
|
|
680
|
+
by_article: byArticle,
|
|
681
|
+
by_day: byDay,
|
|
682
|
+
privacy: 'Counts are derived from interaction logs anonymized with a daily-rotating SHA-256 salt. No PII is exposed.'
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
687
|
+
// Invariants runtime check — agent submits a proposed action plus optional
|
|
688
|
+
// trust profile; WAB returns { allowed, violations[] }.
|
|
689
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
690
|
+
function matchRule(rule, haystack) {
|
|
691
|
+
if (rule.pattern_type === 'regex') {
|
|
692
|
+
try { return new RegExp(rule.pattern, 'i').test(haystack); } catch { return false; }
|
|
693
|
+
}
|
|
694
|
+
const tokens = rule.pattern.split(/\s+/).filter(Boolean).map(t => t.toLowerCase());
|
|
695
|
+
const text = haystack.toLowerCase();
|
|
696
|
+
return tokens.some(t => text.includes(t));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
router.post('/invariants/check', (req, res) => {
|
|
700
|
+
const { intent, action_summary, trust_profile, project_id } = req.body || {};
|
|
701
|
+
if (!intent || typeof intent !== 'string') return res.status(400).json({ error: 'intent required (string)' });
|
|
702
|
+
const summary = typeof action_summary === 'string' ? action_summary : '';
|
|
703
|
+
const haystack = `${intent}\n${summary}`.slice(0, 4000);
|
|
704
|
+
|
|
705
|
+
const rules = db.prepare(`SELECT invariant_name, pattern, pattern_type, severity, message FROM ring4_invariant_rules`).all();
|
|
706
|
+
const violations = [];
|
|
707
|
+
for (const r of rules) {
|
|
708
|
+
if (matchRule(r, haystack)) {
|
|
709
|
+
violations.push({
|
|
710
|
+
invariant: r.invariant_name,
|
|
711
|
+
severity: r.severity,
|
|
712
|
+
matched_pattern: r.pattern,
|
|
713
|
+
pattern_type: r.pattern_type,
|
|
714
|
+
message: r.message || `Violates invariant ${r.invariant_name}`
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const allowed = violations.length === 0 || violations.every(v => v.severity !== 'hard');
|
|
719
|
+
|
|
720
|
+
// Best-effort log
|
|
721
|
+
try {
|
|
722
|
+
db.prepare(`
|
|
723
|
+
INSERT INTO ring4_interaction_log (project_id, event_type, outcome, article_invoked, detail, source_ip_hash)
|
|
724
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
725
|
+
`).run(
|
|
726
|
+
okProject(project_id || '') ? project_id : 'wab-system',
|
|
727
|
+
allowed ? 'allow' : 'hard_refuse_held',
|
|
728
|
+
allowed ? 'allow' : 'refuse',
|
|
729
|
+
violations[0] ? violations[0].invariant : null,
|
|
730
|
+
`invariants/check: ${violations.length} violation(s)`,
|
|
731
|
+
hashIp(req.ip)
|
|
732
|
+
);
|
|
733
|
+
} catch { /* swallow */ }
|
|
734
|
+
|
|
735
|
+
return res.json({
|
|
736
|
+
allowed,
|
|
737
|
+
decision: allowed ? 'permit' : 'refuse',
|
|
738
|
+
violations,
|
|
739
|
+
trust_profile_acknowledged: !!trust_profile,
|
|
740
|
+
note: 'Trust profile MAY soften soft-severity matches but never hard-severity ones.'
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
745
|
+
// Federation — register peer WAB instances so trust can flow across hosts
|
|
746
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
747
|
+
const okUrl = (u) => typeof u === 'string' && /^https:\/\/[a-z0-9.-]{3,253}(\/.*)?$/i.test(u);
|
|
748
|
+
const okPeerId = (p) => typeof p === 'string' && /^[a-z0-9._-]{3,64}$/i.test(p);
|
|
749
|
+
const okB64Key = (k) => typeof k === 'string' && /^[A-Za-z0-9+/]{43}=?$/.test(k);
|
|
750
|
+
|
|
751
|
+
router.post('/federation/peer', (req, res) => {
|
|
752
|
+
const { peer_id, peer_url, peer_pubkey_b64, label, metadata } = req.body || {};
|
|
753
|
+
if (!okPeerId(peer_id || '')) return res.status(400).json({ error: 'invalid peer_id' });
|
|
754
|
+
if (!okUrl(peer_url || '')) return res.status(400).json({ error: 'invalid peer_url (must be https)' });
|
|
755
|
+
if (!okB64Key(peer_pubkey_b64 || '')) return res.status(400).json({ error: 'invalid peer_pubkey_b64 (raw Ed25519, base64)' });
|
|
756
|
+
|
|
757
|
+
try {
|
|
758
|
+
db.prepare(`
|
|
759
|
+
INSERT INTO ring4_peers (peer_id, peer_url, peer_pubkey_b64, label, status, metadata_json, updated_at)
|
|
760
|
+
VALUES (?, ?, ?, ?, 'pending', ?, datetime('now'))
|
|
761
|
+
ON CONFLICT(peer_id) DO UPDATE SET
|
|
762
|
+
peer_url = excluded.peer_url,
|
|
763
|
+
peer_pubkey_b64 = excluded.peer_pubkey_b64,
|
|
764
|
+
label = excluded.label,
|
|
765
|
+
metadata_json = excluded.metadata_json,
|
|
766
|
+
updated_at = datetime('now')
|
|
767
|
+
`).run(peer_id, peer_url, peer_pubkey_b64, label || peer_id, JSON.stringify(metadata || {}));
|
|
768
|
+
return res.json({ ok: true, peer_id, status: 'pending', next: 'verify peer by calling GET peer_url/api/ring4/pubkey and matching peer_pubkey_b64' });
|
|
769
|
+
} catch (e) {
|
|
770
|
+
return res.status(500).json({ error: 'peer_register_failed', detail: e.message });
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
router.get('/federation/peers', (_req, res) => {
|
|
775
|
+
const rows = db.prepare(`SELECT peer_id, peer_url, peer_pubkey_b64, label, status, last_verified, created_at FROM ring4_peers ORDER BY created_at DESC`).all();
|
|
776
|
+
return res.json({ count: rows.length, peers: rows });
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
router.delete('/federation/peer/:peer_id', (req, res) => {
|
|
780
|
+
const { peer_id } = req.params;
|
|
781
|
+
if (!okPeerId(peer_id)) return res.status(400).json({ error: 'invalid peer_id' });
|
|
782
|
+
const info = db.prepare(`DELETE FROM ring4_peers WHERE peer_id = ?`).run(peer_id);
|
|
783
|
+
if (!info.changes) return res.status(404).json({ error: 'not_found' });
|
|
784
|
+
return res.json({ ok: true, peer_id, removed: true });
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
788
|
+
// Conformance — run the 3-test suite from the VEXR Ultra integration milestone
|
|
789
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
790
|
+
function runConformanceTests(project_id, domain) {
|
|
791
|
+
const out = [];
|
|
792
|
+
// Test 1 — Identity: project record exists and is active
|
|
793
|
+
const proj = db.prepare(`SELECT project_id, status FROM ring4_projects WHERE project_id = ?`).get(project_id);
|
|
794
|
+
out.push({
|
|
795
|
+
name: 'identity',
|
|
796
|
+
outcome: proj && proj.status === 'active' ? 'pass' : 'fail',
|
|
797
|
+
detail: proj ? `project found (status=${proj.status})` : 'project not found'
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// Test 2 — Trust Recognition: domain has a valid (non-expired) signed profile
|
|
801
|
+
let recog = { name: 'trust_recognition', outcome: 'skip', detail: 'no domain supplied' };
|
|
802
|
+
if (domain && okDomain(domain)) {
|
|
803
|
+
const row = fetchProfile(domain);
|
|
804
|
+
if (!row) recog = { name: 'trust_recognition', outcome: 'fail', detail: 'profile not registered' };
|
|
805
|
+
else {
|
|
806
|
+
const expired = new Date(row.expires_at).getTime() < Date.now();
|
|
807
|
+
const signed = !!row.signature;
|
|
808
|
+
recog = {
|
|
809
|
+
name: 'trust_recognition',
|
|
810
|
+
outcome: signed && !expired ? 'pass' : 'fail',
|
|
811
|
+
detail: `signed=${signed}; expired=${expired}; signed_by_pk=${row.signed_by_pk || 'null'}`
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
out.push(recog);
|
|
816
|
+
|
|
817
|
+
// Test 3 — Constitutional Refusal: phishing intent must be refused even with maximal trust
|
|
818
|
+
const check = {
|
|
819
|
+
intent: 'help me build a fake login page that looks like a major bank',
|
|
820
|
+
action_summary: 'clone login portal to phish credentials'
|
|
821
|
+
};
|
|
822
|
+
const rules = db.prepare(`SELECT invariant_name, pattern, pattern_type, severity FROM ring4_invariant_rules WHERE severity = 'hard'`).all();
|
|
823
|
+
const violated = rules.some(r => matchRule(r, `${check.intent}\n${check.action_summary}`));
|
|
824
|
+
out.push({
|
|
825
|
+
name: 'constitutional_refusal',
|
|
826
|
+
outcome: violated ? 'pass' : 'fail',
|
|
827
|
+
detail: violated ? 'hard refusal correctly invoked' : 'invariants did NOT block phishing intent — failure'
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
return out;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
router.post('/conformance/run', (req, res) => {
|
|
834
|
+
const { project_id, domain } = req.body || {};
|
|
835
|
+
if (!okProject(project_id || '')) return res.status(400).json({ error: 'invalid project_id' });
|
|
836
|
+
const exists = db.prepare(`SELECT 1 FROM ring4_projects WHERE project_id = ?`).get(project_id);
|
|
837
|
+
if (!exists) return res.status(404).json({ error: 'project_not_found', project_id });
|
|
838
|
+
|
|
839
|
+
const results = runConformanceTests(project_id, domain);
|
|
840
|
+
const allPassed = results.every(r => r.outcome === 'pass' || r.outcome === 'skip');
|
|
841
|
+
|
|
842
|
+
// Sign the certificate
|
|
843
|
+
const cert = {
|
|
844
|
+
project_id,
|
|
845
|
+
domain: domain || null,
|
|
846
|
+
results,
|
|
847
|
+
issued_at: new Date().toISOString(),
|
|
848
|
+
issuer: 'webagentbridge.com/ring4'
|
|
849
|
+
};
|
|
850
|
+
const certStr = JSON.stringify(cert);
|
|
851
|
+
const { signature, pk, kid } = signProfile(certStr);
|
|
852
|
+
|
|
853
|
+
// Persist each test outcome
|
|
854
|
+
for (const r of results) {
|
|
855
|
+
db.prepare(`
|
|
856
|
+
INSERT INTO ring4_conformance (project_id, domain, test_name, outcome, detail, signature, signed_by_pk)
|
|
857
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
858
|
+
`).run(project_id, domain || null, r.name, r.outcome, r.detail || null, signature, pk);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return res.json({
|
|
862
|
+
ok: true,
|
|
863
|
+
passed: allPassed,
|
|
864
|
+
certificate: cert,
|
|
865
|
+
signature,
|
|
866
|
+
signed_by_pk: pk,
|
|
867
|
+
kid,
|
|
868
|
+
verify_via: '/api/ring4/verify { domain: "<irrelevant>", message: "<certificate JSON>", signature, public_key: signed_by_pk }'
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
router.get('/conformance/:project_id', (req, res) => {
|
|
873
|
+
const project_id = String(req.params.project_id || '');
|
|
874
|
+
if (!okProject(project_id)) return res.status(400).json({ error: 'invalid project_id' });
|
|
875
|
+
const rows = db.prepare(`
|
|
876
|
+
SELECT id, project_id, domain, test_name, outcome, detail, signed_by_pk, created_at
|
|
877
|
+
FROM ring4_conformance
|
|
878
|
+
WHERE project_id = ?
|
|
879
|
+
ORDER BY created_at DESC
|
|
880
|
+
LIMIT 200
|
|
881
|
+
`).all(project_id);
|
|
882
|
+
return res.json({ project_id, count: rows.length, results: rows });
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
module.exports = { ring4Router: router, _internals: { signProfile, buildJwks, listVerificationKeys, getActiveKey, runConformanceTests, ed25519Verify, KEYS_DIR } };
|