thumbgate 1.34.3 → 1.37.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/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +373 -5
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +653 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +21 -2
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +10 -1
- package/config/model-candidates.json +382 -24
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +401 -147
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/auto-promote-gates.js +178 -27
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-progress.js +111 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard-limits.js +27 -0
- package/scripts/dashboard.js +279 -9
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gate-stats.js +2 -2
- package/scripts/gates-engine.js +859 -58
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +802 -185
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
package/scripts/pr-manager.js
CHANGED
|
@@ -28,6 +28,10 @@ const PENDING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.pendingBuckets || []).map(
|
|
|
28
28
|
const FAILING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.failingBuckets || []).map((value) => String(value || '').toLowerCase()));
|
|
29
29
|
|
|
30
30
|
const SELF_REFERENTIAL_CHECKS = new Set(MERGE_QUALITY_CHECKS.selfReferentialChecks || []);
|
|
31
|
+
// Optional checks are ignored by summarizeChecks. Vercel is optional because it
|
|
32
|
+
// is not in requiredStatusCheckContexts / classic branch protection; free-tier
|
|
33
|
+
// deploy rate-limits must not stall Trunk when required checks are green.
|
|
34
|
+
const OPTIONAL_CHECKS = new Set(MERGE_QUALITY_CHECKS.optionalChecks || []);
|
|
31
35
|
|
|
32
36
|
function assertSafeGhArgs(args) {
|
|
33
37
|
if (!Array.isArray(args) || args.length === 0) {
|
|
@@ -176,6 +180,7 @@ function summarizeChecks(checks = []) {
|
|
|
176
180
|
const name = check.name || 'unknown-check';
|
|
177
181
|
|
|
178
182
|
if (SELF_REFERENTIAL_CHECKS.has(name)) continue;
|
|
183
|
+
if (OPTIONAL_CHECKS.has(name)) continue;
|
|
179
184
|
|
|
180
185
|
const bucket = String(check.bucket || '').toLowerCase();
|
|
181
186
|
if (bucket) {
|
|
@@ -220,6 +225,43 @@ function sleep(ms) {
|
|
|
220
225
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
221
226
|
}
|
|
222
227
|
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Read-only repository ruleset inventory for diagnosis.
|
|
231
|
+
* Never mutates rulesets or bypass actors.
|
|
232
|
+
*/
|
|
233
|
+
function inspectRepositoryRulesets(repo = '', runner = runGh) {
|
|
234
|
+
const normalizedRepo = String(repo || '').trim();
|
|
235
|
+
const args = normalizedRepo
|
|
236
|
+
? ['api', `repos/${normalizedRepo}/rulesets`, '-H', 'Accept: application/vnd.github+json']
|
|
237
|
+
: ['api', 'repos/{owner}/{repo}/rulesets', '-H', 'Accept: application/vnd.github+json'];
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const result = runner(args);
|
|
241
|
+
if (result && result.status === 0 && result.stdout) {
|
|
242
|
+
const rulesets = JSON.parse(result.stdout);
|
|
243
|
+
if (Array.isArray(rulesets)) {
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
count: rulesets.length,
|
|
247
|
+
activeRulesets: rulesets
|
|
248
|
+
.filter((rs) => String(rs?.enforcement || '').toLowerCase() === 'active')
|
|
249
|
+
.map((rs) => ({
|
|
250
|
+
id: rs.id,
|
|
251
|
+
name: rs.name,
|
|
252
|
+
target: rs.target || 'branch',
|
|
253
|
+
enforcement: rs.enforcement,
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// Diagnostic only — never block merges on inventory failure.
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { ok: false, count: 0, activeRulesets: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
223
265
|
async function resolveBlockers(pr, runner = runGh) {
|
|
224
266
|
const title = pr.title || 'Untitled PR';
|
|
225
267
|
const mergeState = pr.mergeStateStatus || 'UNKNOWN';
|
|
@@ -228,6 +270,25 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
228
270
|
console.log(`[PR Manager] Diagnosing PR #${pr.number}: "${title}"`);
|
|
229
271
|
console.log(`[PR Manager] Merge State: ${mergeState} | Mergeable: ${mergeable}`);
|
|
230
272
|
|
|
273
|
+
// Live-default runner only: injected test runners must not have their mock
|
|
274
|
+
// queues consumed by optional ruleset inventory.
|
|
275
|
+
if (runner === runGh) {
|
|
276
|
+
try {
|
|
277
|
+
const rulesetInventory = inspectRepositoryRulesets('', runner);
|
|
278
|
+
if (rulesetInventory.ok) {
|
|
279
|
+
const names = rulesetInventory.activeRulesets.map((rs) => rs.name).join(', ') || '(none)';
|
|
280
|
+
console.log(
|
|
281
|
+
`[PR Manager] Repository rulesets: ${rulesetInventory.count} total, `
|
|
282
|
+
+ `${rulesetInventory.activeRulesets.length} active [${names}]`,
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
console.log('[PR Manager] Repository rulesets: unavailable (diagnostic only)');
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
console.log('[PR Manager] Repository rulesets: unavailable (diagnostic only)');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
231
292
|
if (pr.isDraft) {
|
|
232
293
|
console.log('[PR Manager] PR is a draft. Skipping.');
|
|
233
294
|
return { status: 'skipped', reason: 'draft' };
|
|
@@ -282,7 +343,7 @@ async function resolveBlockers(pr, runner = runGh) {
|
|
|
282
343
|
}
|
|
283
344
|
|
|
284
345
|
if (
|
|
285
|
-
(pr.mergeStateStatus === 'CLEAN' || pr.mergeStateStatus === 'UNSTABLE')
|
|
346
|
+
(pr.mergeStateStatus === 'CLEAN' || pr.mergeStateStatus === 'UNSTABLE' || pr.mergeStateStatus === 'BLOCKED')
|
|
286
347
|
&& pr.mergeable === 'MERGEABLE'
|
|
287
348
|
) {
|
|
288
349
|
console.log('[PR Manager] SUCCESS: PR is ready for protected autonomous merge.');
|
|
@@ -446,4 +507,5 @@ module.exports = {
|
|
|
446
507
|
performMerge,
|
|
447
508
|
managePrs,
|
|
448
509
|
summarizeChecks,
|
|
510
|
+
inspectRepositoryRulesets,
|
|
449
511
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* prove-herdr-adapter.js — Prove Herdr terminal multiplexer governance plugin manifest.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
|
|
11
|
+
const MANIFEST_PATH = path.join(__dirname, '..', 'adapters', 'herdr', 'herdr-plugin.toml');
|
|
12
|
+
|
|
13
|
+
function verifyHerdrPlugin() {
|
|
14
|
+
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
15
|
+
return { ok: false, error: `manifest missing at ${MANIFEST_PATH}` };
|
|
16
|
+
}
|
|
17
|
+
const content = fs.readFileSync(MANIFEST_PATH, 'utf8');
|
|
18
|
+
|
|
19
|
+
const idMatch = content.match(/^id\s*=\s*"(.*?)"/m);
|
|
20
|
+
const nameMatch = content.match(/^name\s*=\s*"(.*?)"/m);
|
|
21
|
+
const versionMatch = content.match(/^version\s*=\s*"(.*?)"/m);
|
|
22
|
+
const minHerdrMatch = content.match(/^minHerdrVersion\s*=\s*"(.*?)"/m);
|
|
23
|
+
const categoryMatch = content.match(/^category\s*=\s*"(.*?)"/m);
|
|
24
|
+
const commandMatch = content.match(/^command\s*=\s*"(.*?)"/m);
|
|
25
|
+
|
|
26
|
+
if (!idMatch || !nameMatch || !versionMatch || !minHerdrMatch || !categoryMatch || !commandMatch) {
|
|
27
|
+
return { ok: false, error: 'missing required fields in herdr-plugin.toml' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pluginId = idMatch[1];
|
|
31
|
+
if (pluginId !== 'thumbgate-approvals') {
|
|
32
|
+
return { ok: false, error: `expected plugin id thumbgate-approvals, got ${pluginId}` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
pluginId,
|
|
38
|
+
pluginName: nameMatch[1],
|
|
39
|
+
version: versionMatch[1],
|
|
40
|
+
category: categoryMatch[1],
|
|
41
|
+
mcpCommand: commandMatch[1],
|
|
42
|
+
mcpArgs: ['--yes', '--package', `thumbgate@${versionMatch[1]}`, 'thumbgate', 'serve'],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.argv[1] && require('node:path').resolve(process.argv[1]) === require('node:path').resolve(__filename)) {
|
|
47
|
+
const res = verifyHerdrPlugin();
|
|
48
|
+
process.stdout.write(`${JSON.stringify(res, null, 2)}\n`);
|
|
49
|
+
process.exit(res.ok ? 0 : 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { verifyHerdrPlugin, MANIFEST_PATH };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const {
|
|
6
|
+
PYRAMID_LAYERS,
|
|
7
|
+
classifyPyramidLayer,
|
|
8
|
+
distillMemoryPyramid,
|
|
9
|
+
} = require('./agent-memory-lifecycle');
|
|
10
|
+
const {
|
|
11
|
+
renderSymbolicTaskCanvas,
|
|
12
|
+
compactSymbolicTaskCanvas,
|
|
13
|
+
buildContextFootprintReport,
|
|
14
|
+
} = require('./context-footprint');
|
|
15
|
+
|
|
16
|
+
function proveMemoryPyramidAndSymbolicCanvas() {
|
|
17
|
+
console.log('--- Proving High-ROI Memory Pyramid & Symbolic Task Canvas ---');
|
|
18
|
+
|
|
19
|
+
// 1. Prove Semantic Memory Pyramid Classification & Distillation
|
|
20
|
+
const sampleMemories = [
|
|
21
|
+
{
|
|
22
|
+
type: 'preference',
|
|
23
|
+
tags: ['sop', 'guardrail'],
|
|
24
|
+
content: 'NEVER approve PR or satisfy branch protection without human review',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'procedural',
|
|
28
|
+
tags: ['workflow'],
|
|
29
|
+
content: 'Multi-agent parallel execution sequence and lock coordination',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'semantic',
|
|
33
|
+
whatWentWrong: 'Passed raw 500KB transcript to prompt',
|
|
34
|
+
whatWorked: 'Compacted with Symbolic Task Canvas',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: 'working',
|
|
38
|
+
content: 'Raw subagent output chunk #14',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const pyramidReport = distillMemoryPyramid(sampleMemories);
|
|
43
|
+
assert.strictEqual(pyramidReport.kind, 'semantic-memory-pyramid');
|
|
44
|
+
assert.strictEqual(pyramidReport.totalMemories, 4);
|
|
45
|
+
assert.strictEqual(pyramidReport.layers.L3_PERSONA_SOP.count, 1);
|
|
46
|
+
assert.strictEqual(pyramidReport.layers.L2_SCENARIO.count, 1);
|
|
47
|
+
assert.strictEqual(pyramidReport.layers.L1_ATOM.count, 1);
|
|
48
|
+
assert.strictEqual(pyramidReport.layers.L0_CONVERSATION.count, 1);
|
|
49
|
+
console.log('✔ Semantic Memory Pyramid (L0-L3) classification & distillation verified');
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// 2. Prove Symbolic Task Canvas Generation
|
|
53
|
+
const canvas = renderSymbolicTaskCanvas({
|
|
54
|
+
activeTask: 'Refactor Context Engine',
|
|
55
|
+
milestones: [
|
|
56
|
+
{ name: 'Implement L0-L3 Pyramid Classifier', status: 'completed' },
|
|
57
|
+
{ name: 'Wire Symbolic Task Canvas Generator', status: 'completed' },
|
|
58
|
+
{ name: 'Verify Token Savings', status: 'in_progress' },
|
|
59
|
+
],
|
|
60
|
+
blockers: [],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.strictEqual(canvas.milestoneCount, 3);
|
|
64
|
+
assert.match(canvas.mermaidDiagram, /graph TD/);
|
|
65
|
+
|
|
66
|
+
assert.match(canvas.mermaidDiagram, /Implement L0-L3 Pyramid Classifier \[DONE\]/);
|
|
67
|
+
console.log('✔ Symbolic Task Canvas (Mermaid state graph) generation verified');
|
|
68
|
+
|
|
69
|
+
// 3. Prove Token Footprint Compaction & Report Integration
|
|
70
|
+
const verboseTrace = Array.from({ length: 15 }, (_, i) => ({
|
|
71
|
+
step: i,
|
|
72
|
+
rawLog: `Verbose interaction step ${i} with heavy transcript chatter and raw tool calls`,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const footprintReport = buildContextFootprintReport({
|
|
76
|
+
entries: verboseTrace,
|
|
77
|
+
symbolicCanvas: true,
|
|
78
|
+
activeTask: 'Proof Task',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.ok(footprintReport.symbolicTaskCanvas);
|
|
82
|
+
assert.ok(footprintReport.symbolicTaskCanvas.footprint.savings.estimatedTokens > 0);
|
|
83
|
+
console.log(
|
|
84
|
+
`✔ Context Footprint Compaction verified: Saved ${footprintReport.symbolicTaskCanvas.footprint.savings.estimatedTokens} tokens (${footprintReport.symbolicTaskCanvas.footprint.savings.reductionPercent}% reduction)`,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
console.log('--- ALL HIGH-ROI PROOFS PASSED CLEANLY ---');
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (process.argv[1] && require('node:path').resolve(process.argv[1]) === require('node:path').resolve(__filename)) {
|
|
92
|
+
proveMemoryPyramidAndSymbolicCanvas();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { proveMemoryPyramidAndSymbolicCanvas };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* prove-workos.js — evidence that WorkOS production auth + MCP OAuth hierarchy
|
|
6
|
+
* still satisfy ThumbGate's enterprise readiness contract ($10/mo AuthKit cap,
|
|
7
|
+
* production client, scope implication for MCP tools).
|
|
8
|
+
*
|
|
9
|
+
* Usage: node scripts/prove-workos.js [--skip-live]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const skipLive = process.argv.includes('--skip-live');
|
|
16
|
+
const root = path.resolve(__dirname, '..');
|
|
17
|
+
|
|
18
|
+
function run(cmd, args, opts = {}) {
|
|
19
|
+
const res = spawnSync(cmd, args, {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
cwd: root,
|
|
22
|
+
timeout: opts.timeout || 120000,
|
|
23
|
+
env: process.env,
|
|
24
|
+
});
|
|
25
|
+
return res;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function main() {
|
|
29
|
+
const failures = [];
|
|
30
|
+
// Unit tests: OAuth hierarchy + guard constants
|
|
31
|
+
const unit = run(process.execPath, [
|
|
32
|
+
'--test',
|
|
33
|
+
'tests/mcp-oauth.test.js',
|
|
34
|
+
'tests/workos-production-guard.test.js',
|
|
35
|
+
]);
|
|
36
|
+
if (unit.status !== 0) {
|
|
37
|
+
failures.push(`unit tests failed:\n${unit.stdout || ''}\n${unit.stderr || ''}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!skipLive) {
|
|
41
|
+
const live = run(process.execPath, ['scripts/workos-production-guard.js', '--json'], { timeout: 60000 });
|
|
42
|
+
let report = null;
|
|
43
|
+
try {
|
|
44
|
+
report = JSON.parse(live.stdout || '{}');
|
|
45
|
+
} catch {
|
|
46
|
+
failures.push(`live guard did not emit JSON: ${live.stdout || live.stderr}`);
|
|
47
|
+
}
|
|
48
|
+
if (report && report.ok !== true) {
|
|
49
|
+
failures.push(`live WorkOS guard failed: ${JSON.stringify(report.failures || report)}`);
|
|
50
|
+
}
|
|
51
|
+
if (report && report.ok) {
|
|
52
|
+
console.log(
|
|
53
|
+
`LIVE ok client=${report.clientId} host=${report.finalHost} methods=${(report.methodsFound || []).join(',')}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
console.log('LIVE skipped (--skip-live)');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (failures.length) {
|
|
61
|
+
console.error('prove:workos FAILED');
|
|
62
|
+
for (const f of failures) console.error(f);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
console.log('prove:workos OK');
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
|
|
70
|
+
main();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { main };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { validateToolContract } = require('./tool-contract-validator');
|
|
8
|
+
|
|
9
|
+
const SCHEMA_VERSION = 'thumbgate-provider-execution-attestation-v1';
|
|
10
|
+
const SCHEMA_PATH = path.join(__dirname, '..', 'config', 'schemas', 'provider-execution-attestation-v1.schema.json');
|
|
11
|
+
const DEFAULT_VECTORS_PATH = path.join(__dirname, '..', 'conformance', 'provider-attestation', 'vectors.json');
|
|
12
|
+
const SCHEMA = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
13
|
+
|
|
14
|
+
// Strict RFC 3339 / ISO-8601 date-time with required timezone (Z or ±HH:MM).
|
|
15
|
+
const RFC3339_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
16
|
+
const BASE64_STRICT = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
17
|
+
const ED25519_SIG_BYTES = 64;
|
|
18
|
+
|
|
19
|
+
function hasUnpairedSurrogate(text) {
|
|
20
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
21
|
+
const code = text.charCodeAt(i);
|
|
22
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
23
|
+
const next = text.charCodeAt(i + 1);
|
|
24
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return true;
|
|
25
|
+
i += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (code >= 0xdc00 && code <= 0xdfff) return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function jcsCanonicalize(value) {
|
|
34
|
+
if (value === null) return 'null';
|
|
35
|
+
if (typeof value === 'boolean') return JSON.stringify(value);
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
if (hasUnpairedSurrogate(value)) {
|
|
38
|
+
throw new TypeError('JCS rejects unpaired UTF-16 surrogates');
|
|
39
|
+
}
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'number') {
|
|
43
|
+
if (!Number.isFinite(value)) throw new TypeError('JCS rejects non-finite numbers');
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value)) return `[${value.map(jcsCanonicalize).join(',')}]`;
|
|
47
|
+
if (typeof value !== 'object') throw new TypeError(`JCS cannot serialize ${typeof value}`);
|
|
48
|
+
return `{${Object.keys(value)
|
|
49
|
+
.sort()
|
|
50
|
+
.map((key) => {
|
|
51
|
+
if (hasUnpairedSurrogate(key)) {
|
|
52
|
+
throw new TypeError('JCS rejects unpaired UTF-16 surrogates in property names');
|
|
53
|
+
}
|
|
54
|
+
return `${JSON.stringify(key)}:${jcsCanonicalize(value[key])}`;
|
|
55
|
+
})
|
|
56
|
+
.join(',')}}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unsignedPayload(attestation) {
|
|
60
|
+
const payload = { ...attestation };
|
|
61
|
+
delete payload.signature;
|
|
62
|
+
return payload;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sha256Hex(value) {
|
|
66
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseRfc3339Ms(value) {
|
|
70
|
+
if (typeof value !== 'string' || !RFC3339_DATE_TIME.test(value)) return Number.NaN;
|
|
71
|
+
const ms = Date.parse(value);
|
|
72
|
+
return Number.isFinite(ms) ? ms : Number.NaN;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isStrictBase64(value) {
|
|
76
|
+
if (typeof value !== 'string' || value.length === 0 || value.length % 4 !== 0) return false;
|
|
77
|
+
if (!BASE64_STRICT.test(value)) return false;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function semanticErrors(attestation) {
|
|
82
|
+
const errors = [];
|
|
83
|
+
const evidence = attestation.providerEvidence || {};
|
|
84
|
+
const policy = attestation.reconciliationPolicy || {};
|
|
85
|
+
const observedAt = parseRfc3339Ms(evidence.observedAt);
|
|
86
|
+
const startedAt = parseRfc3339Ms(evidence.windowStartedAt);
|
|
87
|
+
const closesAt = parseRfc3339Ms(evidence.windowClosesAt);
|
|
88
|
+
const issuedAt = parseRfc3339Ms(attestation.issuedAt);
|
|
89
|
+
|
|
90
|
+
if (!Number.isFinite(observedAt)) errors.push('observedAt_not_rfc3339');
|
|
91
|
+
if (!Number.isFinite(startedAt)) errors.push('windowStartedAt_not_rfc3339');
|
|
92
|
+
if (!Number.isFinite(closesAt)) errors.push('windowClosesAt_not_rfc3339');
|
|
93
|
+
if (!Number.isFinite(issuedAt)) errors.push('issuedAt_not_rfc3339');
|
|
94
|
+
|
|
95
|
+
if (attestation.provider?.id !== policy.provider) errors.push('policy_provider_mismatch');
|
|
96
|
+
if (policy.declaredWindowMs > policy.maxWindowMs) errors.push('holder_window_exceeds_gate_maximum');
|
|
97
|
+
if (Number.isFinite(startedAt) && Number.isFinite(closesAt) && closesAt - startedAt !== policy.declaredWindowMs) {
|
|
98
|
+
errors.push('declared_window_duration_mismatch');
|
|
99
|
+
}
|
|
100
|
+
if (attestation.outcome === 'not-yet-visible' && Number.isFinite(observedAt) && Number.isFinite(closesAt) && observedAt >= closesAt) {
|
|
101
|
+
errors.push('not_yet_visible_after_window');
|
|
102
|
+
}
|
|
103
|
+
if (attestation.outcome === 'absent-after-window' && Number.isFinite(observedAt) && Number.isFinite(closesAt) && observedAt < closesAt) {
|
|
104
|
+
errors.push('absence_claimed_before_window_closed');
|
|
105
|
+
}
|
|
106
|
+
if ((attestation.outcome === 'matched' || attestation.outcome === 'mismatched') && !evidence.eventId) {
|
|
107
|
+
errors.push('provider_event_required_for_comparison');
|
|
108
|
+
}
|
|
109
|
+
if ((attestation.outcome === 'not-yet-visible' || attestation.outcome === 'absent-after-window') && evidence.eventId !== null) {
|
|
110
|
+
errors.push('provider_event_must_be_null_when_unobserved');
|
|
111
|
+
}
|
|
112
|
+
return errors;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function verifyAttestation(attestation, publicKeyPem) {
|
|
116
|
+
const schema = validateToolContract(SCHEMA, attestation);
|
|
117
|
+
const errors = schema.valid ? [] : schema.errors.map((error) => `schema:${error}`);
|
|
118
|
+
errors.push(...semanticErrors(attestation));
|
|
119
|
+
if (errors.length > 0) return { valid: false, errors };
|
|
120
|
+
|
|
121
|
+
let canonicalPayload;
|
|
122
|
+
let payloadHash;
|
|
123
|
+
try {
|
|
124
|
+
canonicalPayload = jcsCanonicalize(unsignedPayload(attestation));
|
|
125
|
+
payloadHash = sha256Hex(canonicalPayload);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return {
|
|
128
|
+
valid: false,
|
|
129
|
+
errors: [`jcs_canonicalization_failed:${error.message}`],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (payloadHash !== attestation.signature.payloadHash) errors.push('payload_hash_mismatch');
|
|
134
|
+
|
|
135
|
+
const signatureValue = attestation.signature?.value;
|
|
136
|
+
if (!isStrictBase64(signatureValue)) {
|
|
137
|
+
errors.push('signature_not_strict_base64');
|
|
138
|
+
} else {
|
|
139
|
+
try {
|
|
140
|
+
const signatureBytes = Buffer.from(signatureValue, 'base64');
|
|
141
|
+
if (signatureBytes.length !== ED25519_SIG_BYTES) {
|
|
142
|
+
errors.push('signature_length_invalid');
|
|
143
|
+
} else {
|
|
144
|
+
const verified = crypto.verify(
|
|
145
|
+
null,
|
|
146
|
+
Buffer.from(payloadHash, 'utf8'),
|
|
147
|
+
crypto.createPublicKey(publicKeyPem),
|
|
148
|
+
signatureBytes,
|
|
149
|
+
);
|
|
150
|
+
if (!verified) errors.push('signature_invalid');
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
errors.push('public_key_or_signature_unusable');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { valid: errors.length === 0, errors, canonicalPayload, payloadHash };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runConformance(vectorsPath = DEFAULT_VECTORS_PATH) {
|
|
161
|
+
const suite = JSON.parse(fs.readFileSync(vectorsPath, 'utf8'));
|
|
162
|
+
const results = suite.vectors.map((vector) => {
|
|
163
|
+
const result = verifyAttestation(vector.attestation, suite.publicKeyPem);
|
|
164
|
+
const passed = result.valid === vector.expectedValid
|
|
165
|
+
&& (!vector.expectedError || result.errors.includes(vector.expectedError))
|
|
166
|
+
&& (!vector.expectedCanonicalPayload || result.canonicalPayload === vector.expectedCanonicalPayload);
|
|
167
|
+
return { id: vector.id, passed, expectedValid: vector.expectedValid, actualValid: result.valid, errors: result.errors };
|
|
168
|
+
});
|
|
169
|
+
return { passed: results.every((result) => result.passed), count: results.length, results };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function runCli(argv = process.argv.slice(2)) {
|
|
173
|
+
const result = runConformance(argv[0] ? path.resolve(argv[0]) : DEFAULT_VECTORS_PATH);
|
|
174
|
+
console.log(JSON.stringify(result, null, 2));
|
|
175
|
+
return result.passed ? 0 : 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) process.exitCode = runCli();
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
SCHEMA,
|
|
182
|
+
SCHEMA_VERSION,
|
|
183
|
+
jcsCanonicalize,
|
|
184
|
+
runConformance,
|
|
185
|
+
semanticErrors,
|
|
186
|
+
sha256Hex,
|
|
187
|
+
unsignedPayload,
|
|
188
|
+
verifyAttestation,
|
|
189
|
+
parseRfc3339Ms,
|
|
190
|
+
isStrictBase64,
|
|
191
|
+
hasUnpairedSurrogate,
|
|
192
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Partner-Neutral Broker Execution Receipt Contract Schema:
|
|
8
|
+
* - schemaVersion: '1.0'
|
|
9
|
+
* - receiptId: string (UUID / unique receipt identifier)
|
|
10
|
+
* - idempotencyKey: string (deterministic action key)
|
|
11
|
+
* - principal: string (agent or user identity)
|
|
12
|
+
* - target: string (target resource, URL, or API endpoint)
|
|
13
|
+
* - decision: 'allow' | 'block' | 'warn'
|
|
14
|
+
* - toolName: string
|
|
15
|
+
* - canonicalDigest: string (SHA-256 over canonical request payload)
|
|
16
|
+
* - providerEventId: string (external event ID from provider/broker)
|
|
17
|
+
* - brokerId: string (identifier of the out-of-boundary credential holder, e.g. 'aigate')
|
|
18
|
+
* - signature: string (HMAC-SHA256 or RSA/ECDSA signature emitted by broker)
|
|
19
|
+
* - reconciliation: { status: 'pending'|'reconciled'|'failed', timestamp: string }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const RECEIPT_SCHEMA_VERSION = '1.0';
|
|
23
|
+
|
|
24
|
+
function canonicalStringify(value) {
|
|
25
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
26
|
+
if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(',')}]`;
|
|
27
|
+
return `{${Object.keys(value).sort((left, right) => left.localeCompare(right, 'en')).map((key) => `${JSON.stringify(key)}:${canonicalStringify(value[key])}`).join(',')}}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function computeCanonicalRequestDigest({ toolName, toolInput, target, idempotencyKey, recordedAt }) {
|
|
31
|
+
const parts = [
|
|
32
|
+
String(toolName || ''),
|
|
33
|
+
typeof toolInput === 'object' ? canonicalStringify(toolInput || {}) : String(toolInput || ''),
|
|
34
|
+
String(target || ''),
|
|
35
|
+
String(idempotencyKey || ''),
|
|
36
|
+
String(recordedAt || ''),
|
|
37
|
+
];
|
|
38
|
+
return crypto.createHash('sha256').update(parts.join('|')).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateProviderReceiptSchema(receipt) {
|
|
42
|
+
if (!receipt || typeof receipt !== 'object') {
|
|
43
|
+
return { ok: false, errors: ['Receipt must be an object.'] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const errors = [];
|
|
47
|
+
for (const field of ['schemaVersion', 'receiptId', 'toolName', 'providerEventId']) {
|
|
48
|
+
if (!receipt[field] || typeof receipt[field] !== 'string') {
|
|
49
|
+
errors.push(`Missing or invalid ${field}.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!receipt.canonicalDigest || typeof receipt.canonicalDigest !== 'string') {
|
|
53
|
+
errors.push('Missing or invalid canonicalDigest.');
|
|
54
|
+
}
|
|
55
|
+
if (!receipt.principal || typeof receipt.principal !== 'string') {
|
|
56
|
+
errors.push('Missing or invalid principal.');
|
|
57
|
+
}
|
|
58
|
+
if (!receipt.target || typeof receipt.target !== 'string') {
|
|
59
|
+
errors.push('Missing or invalid target.');
|
|
60
|
+
}
|
|
61
|
+
if (!['allow', 'block', 'warn'].includes(receipt.decision)) {
|
|
62
|
+
errors.push('Missing or invalid decision (must be allow, block, or warn).');
|
|
63
|
+
}
|
|
64
|
+
if (!receipt.idempotencyKey || typeof receipt.idempotencyKey !== 'string') {
|
|
65
|
+
errors.push('Missing or invalid idempotencyKey.');
|
|
66
|
+
}
|
|
67
|
+
if (!receipt.brokerId || typeof receipt.brokerId !== 'string') {
|
|
68
|
+
errors.push('Missing or invalid brokerId.');
|
|
69
|
+
}
|
|
70
|
+
if (!receipt.signature || typeof receipt.signature !== 'string') {
|
|
71
|
+
errors.push('Missing or invalid broker signature.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { ok: errors.length === 0, errors };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function verifyBrokerSignature(receipt, brokerSecretKey) {
|
|
78
|
+
const schemaValidation = validateProviderReceiptSchema(receipt);
|
|
79
|
+
if (!schemaValidation.ok) {
|
|
80
|
+
return { verified: false, reason: 'schema_validation_failed', errors: schemaValidation.errors };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!brokerSecretKey) {
|
|
84
|
+
return { verified: false, reason: 'missing_broker_key' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const signedClaims = { ...receipt };
|
|
88
|
+
delete signedClaims.signature;
|
|
89
|
+
delete signedClaims.reconciliation;
|
|
90
|
+
const expectedSignature = crypto
|
|
91
|
+
.createHmac('sha256', brokerSecretKey)
|
|
92
|
+
.update(canonicalStringify(signedClaims))
|
|
93
|
+
.digest('hex');
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const verified = crypto.timingSafeEqual(
|
|
97
|
+
Buffer.from(receipt.signature, 'hex'),
|
|
98
|
+
Buffer.from(expectedSignature, 'hex')
|
|
99
|
+
);
|
|
100
|
+
return { verified, reason: verified ? 'valid_broker_signature' : 'signature_mismatch' };
|
|
101
|
+
} catch {
|
|
102
|
+
return { verified: false, reason: 'crypto_error' };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function reconcileProviderEvent(receipt, providerEventResponse = {}) {
|
|
107
|
+
const schemaCheck = validateProviderReceiptSchema(receipt);
|
|
108
|
+
if (!schemaCheck.ok) {
|
|
109
|
+
return { reconciled: false, reason: 'invalid_receipt_schema' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!providerEventResponse.eventId || typeof providerEventResponse.eventId !== 'string') {
|
|
113
|
+
return { reconciled: false, reason: 'missing_provider_event_id' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const matchesEventId = providerEventResponse.eventId === receipt.providerEventId;
|
|
117
|
+
const matchesTarget = !providerEventResponse.target || providerEventResponse.target === receipt.target;
|
|
118
|
+
|
|
119
|
+
const reconciled = Boolean(matchesEventId && matchesTarget);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
reconciled,
|
|
123
|
+
reconciliationTimestamp: new Date().toISOString(),
|
|
124
|
+
providerEventId: receipt.providerEventId,
|
|
125
|
+
reason: reconciled ? 'event_reconciled' : 'event_mismatch',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
RECEIPT_SCHEMA_VERSION,
|
|
131
|
+
canonicalStringify,
|
|
132
|
+
computeCanonicalRequestDigest,
|
|
133
|
+
validateProviderReceiptSchema,
|
|
134
|
+
verifyBrokerSignature,
|
|
135
|
+
reconcileProviderEvent,
|
|
136
|
+
};
|