@rubric-protocol/decision-verify 1.0.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/dist/bazaar.d.ts +56 -0
- package/dist/bazaar.d.ts.map +1 -0
- package/dist/bazaar.js +48 -0
- package/dist/bazaar.js.map +1 -0
- package/dist/chain.d.ts +10 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +39 -0
- package/dist/chain.js.map +1 -0
- package/dist/gate.d.ts +24 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +32 -0
- package/dist/gate.js.map +1 -0
- package/dist/http.d.ts +23 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +30 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/merkle.d.ts +22 -0
- package/dist/merkle.d.ts.map +1 -0
- package/dist/merkle.js +58 -0
- package/dist/merkle.js.map +1 -0
- package/dist/ports.d.ts +109 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/route.d.ts +15 -0
- package/dist/route.d.ts.map +1 -0
- package/dist/route.js +41 -0
- package/dist/route.js.map +1 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +55 -0
- package/dist/verify.js.map +1 -0
- package/package.json +32 -0
- package/src/bazaar.ts +88 -0
- package/src/chain.ts +56 -0
- package/src/gate.ts +55 -0
- package/src/http.ts +53 -0
- package/src/index.ts +38 -0
- package/src/merkle.ts +63 -0
- package/src/ports.ts +123 -0
- package/src/route.ts +55 -0
- package/src/verify.ts +63 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.js","sourceRoot":"","sources":["../src/route.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,IAAI,EAAwB,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,IAAI,EAAyD,MAAM,WAAW,CAAC;AAIxF,MAAM,CAAC,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AAU/D,iEAAiE;AACjE,SAAS,cAAc,CAAC,GAAkB,EAAE,IAAqB;IAC/D,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC;IAE9D,IAAI,IAAI,KAAK,OAAO,IAAI,OAAO,EAAE,CAAC;QAChC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAC,CAAC;QACjF,OAAO,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,GAAG,GAAG,UAAU;QACpB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC;QACrC,CAAC,CAAC,YAAY;YACZ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,YAAY,CAAC;YACzC,CAAC,CAAC,SAAS,CAAC;IAEhB,IAAI,CAAC,UAAU,IAAI,CAAC,YAAY,EAAE,CAAC;QACjC,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,wCAAwC,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,oBAAoB,EAAE,CAAC,CAAC;IAE5D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,2BAA2B,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;IAElG,OAAO,IAAI,CAAC,GAAG,EAAE,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,yBAAyB,CAAC,IAAqB;IAC7D,MAAM,OAAO,GAAY,CAAC,GAAG,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;AACnF,CAAC"}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,kBAAkB,EAAgB,MAAM,YAAY,CAAC;AAmBxG,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,EACxB,eAAe,EAAE,iBAAiB,GACjC,kBAAkB,CA2BpB"}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble a decision-verify result from a stored bundle (tasks/P4.md).
|
|
3
|
+
*
|
|
4
|
+
* Scope (be honest about what this proves): this checks the bundle's INTERNAL
|
|
5
|
+
* consistency — the decision still hashes to its committed decisionHash, the
|
|
6
|
+
* recomputed leaf is the one the proof carries, the proof folds to the root the
|
|
7
|
+
* bundle claims, and the (caller-supplied) signature verifies. It does NOT
|
|
8
|
+
* contact HCS to confirm the root was actually anchored, nor does it establish
|
|
9
|
+
* that the signing key is trusted. `consistencyVerified` reflects only the
|
|
10
|
+
* former; on-chain anchoring is the job of the (out-of-scope) attestation service.
|
|
11
|
+
*/
|
|
12
|
+
import { DAR_VERSION, decisionHashOf, leafHash } from "@rubric-protocol/attest-decision";
|
|
13
|
+
import { verifyMerkleProof } from "./merkle.js";
|
|
14
|
+
const LEAF_TYPES = new Set(["decision", "schema-change", "checkpoint"]);
|
|
15
|
+
const KNOWN = parseVersion(DAR_VERSION); // { major: 0, minor: 1 }
|
|
16
|
+
function parseVersion(v) {
|
|
17
|
+
const m = /^DAR\/(\d+)\.(\d+)$/.exec(v);
|
|
18
|
+
return m ? { major: Number(m[1]), minor: Number(m[2]) } : null;
|
|
19
|
+
}
|
|
20
|
+
/** Spec §5.1 version gate: reject unknown major / bad leafType; needs-upgrade for a newer minor. */
|
|
21
|
+
function versionStatus(v, leafType) {
|
|
22
|
+
const parsed = parseVersion(v);
|
|
23
|
+
if (!parsed || parsed.major !== KNOWN.major)
|
|
24
|
+
return "rejected"; // unknown major/format
|
|
25
|
+
if (!LEAF_TYPES.has(leafType))
|
|
26
|
+
return "rejected";
|
|
27
|
+
if (parsed.minor > KNOWN.minor)
|
|
28
|
+
return "needs-upgrade"; // MUST NOT strip-and-rehash a newer minor
|
|
29
|
+
return "ok";
|
|
30
|
+
}
|
|
31
|
+
export function assembleVerification(bundle, verifySignature) {
|
|
32
|
+
const { dar, merkleProof, anchorRef } = bundle;
|
|
33
|
+
const status = versionStatus(dar.v, dar.leafType);
|
|
34
|
+
const sig = verifySignature(bundle.signature, anchorRef.root);
|
|
35
|
+
const base = {
|
|
36
|
+
decisionId: dar.decisionId,
|
|
37
|
+
dar,
|
|
38
|
+
merkleProof,
|
|
39
|
+
anchorRef,
|
|
40
|
+
signature: { verified: sig.verified, keyRef: sig.keyRef },
|
|
41
|
+
};
|
|
42
|
+
// Do not claim (or compute) leaf verification for a version we can't fully
|
|
43
|
+
// understand — spec §5.1 forbids strip-and-rehash of a newer minor.
|
|
44
|
+
if (status !== "ok") {
|
|
45
|
+
return { ...base, drift: false, status, consistencyVerified: false };
|
|
46
|
+
}
|
|
47
|
+
const decisionHashOk = decisionHashOf(dar.schemaHash, dar.inputHash, dar.outputHash) === dar.decisionHash;
|
|
48
|
+
const leaf = leafHash(dar);
|
|
49
|
+
const leafMatches = leaf === merkleProof.leaf;
|
|
50
|
+
const proofFolds = verifyMerkleProof(leaf, merkleProof.steps, merkleProof.root);
|
|
51
|
+
const rootMatchesAnchor = merkleProof.root === anchorRef.root;
|
|
52
|
+
const drift = !decisionHashOk || !leafMatches || !proofFolds || !rootMatchesAnchor;
|
|
53
|
+
return { ...base, drift, status, consistencyVerified: !drift && sig.verified };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AACzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGhD,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC;AAC7F,MAAM,KAAK,GAAG,YAAY,CAAC,WAAW,CAAE,CAAC,CAAC,yBAAyB;AAEnE,SAAS,YAAY,CAAC,CAAS;IAC7B,MAAM,CAAC,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACjE,CAAC;AAED,oGAAoG;AACpG,SAAS,aAAa,CAAC,CAAS,EAAE,QAAgB;IAChD,MAAM,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK;QAAE,OAAO,UAAU,CAAC,CAAC,uBAAuB;IACvF,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;QAAE,OAAO,UAAU,CAAC;IACjD,IAAI,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK;QAAE,OAAO,eAAe,CAAC,CAAC,0CAA0C;IAClG,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,MAAwB,EACxB,eAAkC;IAElC,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAC/C,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,eAAe,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAE9D,MAAM,IAAI,GAAG;QACX,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,GAAG;QACH,WAAW;QACX,SAAS;QACT,SAAS,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;KAC1D,CAAC;IAEF,2EAA2E;IAC3E,oEAAoE;IACpE,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,CAAC;IACvE,CAAC;IAED,MAAM,cAAc,GAAG,cAAc,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,YAAY,CAAC;IAC1G,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,WAAW,GAAG,IAAI,KAAK,WAAW,CAAC,IAAI,CAAC;IAC9C,MAAM,UAAU,GAAG,iBAAiB,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IAChF,MAAM,iBAAiB,GAAG,WAAW,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC;IAC9D,MAAM,KAAK,GAAG,CAAC,cAAc,IAAI,CAAC,WAAW,IAAI,CAAC,UAAU,IAAI,CAAC,iBAAiB,CAAC;IAEnF,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC,KAAK,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;AACjF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rubric-protocol/decision-verify",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Rubric decision-verify route: x402 gate, Merkle/anchor/signature verification, chain-check",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/0xsims/rubric-attest.git",
|
|
10
|
+
"directory": "packages/verify"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"development": "./src/index.ts",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist", "src"],
|
|
22
|
+
"publishConfig": { "access": "public" },
|
|
23
|
+
"scripts": {
|
|
24
|
+
"prepublishOnly": "npm run build",
|
|
25
|
+
"build": "tsc -p tsconfig.build.json",
|
|
26
|
+
"test": "vitest run"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@rubric-protocol/attest-decision": "^1.0.1",
|
|
30
|
+
"@rubric-protocol/attest-index": "^1.0.1"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/bazaar.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 discovery + settlement (tasks/P4.md). An unpaid request is answered with
|
|
3
|
+
* `svcSend402`: HTTP 402 carrying a "bazaar blob" — the discoverable payment
|
|
4
|
+
* requirements — computed with zero downstream (index/store) work. A settled
|
|
5
|
+
* (paid) response carries `extensions.bazaar`.
|
|
6
|
+
*/
|
|
7
|
+
import { json, type VerifyResponse } from "./http.js";
|
|
8
|
+
|
|
9
|
+
export interface BazaarConfig {
|
|
10
|
+
resource: string; // e.g. "/v1/x402/decision-verify"
|
|
11
|
+
network: string; // e.g. "hedera-testnet"
|
|
12
|
+
asset: string; // token/asset identifier
|
|
13
|
+
maxAmountRequired: string; // price, atomic units, as a string
|
|
14
|
+
payTo: string; // recipient address
|
|
15
|
+
description?: string;
|
|
16
|
+
discoveryUrl?: string; // bazaar listing URL
|
|
17
|
+
category?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface X402Accept {
|
|
21
|
+
scheme: "exact";
|
|
22
|
+
network: string;
|
|
23
|
+
maxAmountRequired: string;
|
|
24
|
+
resource: string;
|
|
25
|
+
description: string;
|
|
26
|
+
payTo: string;
|
|
27
|
+
asset: string;
|
|
28
|
+
mimeType: "application/json";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BazaarBlob {
|
|
32
|
+
x402Version: 1;
|
|
33
|
+
accepts: X402Accept[];
|
|
34
|
+
bazaar: { discoverable: true; resource: string; category: string; url?: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The discoverable 402 challenge body. */
|
|
38
|
+
export function buildBazaarBlob(cfg: BazaarConfig): BazaarBlob {
|
|
39
|
+
const accept: X402Accept = {
|
|
40
|
+
scheme: "exact",
|
|
41
|
+
network: cfg.network,
|
|
42
|
+
maxAmountRequired: cfg.maxAmountRequired,
|
|
43
|
+
resource: cfg.resource,
|
|
44
|
+
description: cfg.description ?? "Rubric decision-verify",
|
|
45
|
+
payTo: cfg.payTo,
|
|
46
|
+
asset: cfg.asset,
|
|
47
|
+
mimeType: "application/json",
|
|
48
|
+
};
|
|
49
|
+
const bazaar: BazaarBlob["bazaar"] = {
|
|
50
|
+
discoverable: true,
|
|
51
|
+
resource: cfg.resource,
|
|
52
|
+
category: cfg.category ?? "attestation.verify",
|
|
53
|
+
};
|
|
54
|
+
if (cfg.discoveryUrl !== undefined) bazaar.url = cfg.discoveryUrl;
|
|
55
|
+
return { x402Version: 1, accepts: [accept], bazaar };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Instant 402 (no downstream work): the discovery response. */
|
|
59
|
+
export function svcSend402(cfg: BazaarConfig): VerifyResponse {
|
|
60
|
+
return json(402, buildBazaarBlob(cfg), { "www-authenticate": `x402 resource="${cfg.resource}"` });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SettlementBlob {
|
|
64
|
+
settled: true;
|
|
65
|
+
resource: string;
|
|
66
|
+
network: string;
|
|
67
|
+
asset: string;
|
|
68
|
+
amount: string;
|
|
69
|
+
payer?: string;
|
|
70
|
+
settlementId?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The `extensions.bazaar` payload attached to a settled response. */
|
|
74
|
+
export function settlementBlob(
|
|
75
|
+
cfg: BazaarConfig,
|
|
76
|
+
payment: { payer?: string; settlementId?: string },
|
|
77
|
+
): SettlementBlob {
|
|
78
|
+
const blob: SettlementBlob = {
|
|
79
|
+
settled: true,
|
|
80
|
+
resource: cfg.resource,
|
|
81
|
+
network: cfg.network,
|
|
82
|
+
asset: cfg.asset,
|
|
83
|
+
amount: cfg.maxAmountRequired,
|
|
84
|
+
};
|
|
85
|
+
if (payment.payer !== undefined) blob.payer = payment.payer;
|
|
86
|
+
if (payment.settlementId !== undefined) blob.settlementId = payment.settlementId;
|
|
87
|
+
return blob;
|
|
88
|
+
}
|
package/src/chain.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain-check: build a continuity report for an agent's per-agent `prev` chain
|
|
3
|
+
* (tasks/P4.md). Forks (a decision with >1 child) are reported as branches, and
|
|
4
|
+
* are explicitly NOT treated as tampering — a legitimately branched history is
|
|
5
|
+
* still authentic. Gaps (a `prev` pointing at an absent decision) are surfaced
|
|
6
|
+
* too. Actual tampering is detected per-record via the drift flag (see verify).
|
|
7
|
+
*/
|
|
8
|
+
import type { ContinuityReport, IndexRow } from "./ports.js";
|
|
9
|
+
|
|
10
|
+
const byId = (rows: IndexRow[]) => new Map(rows.map((r) => [r.decisionId, r]));
|
|
11
|
+
|
|
12
|
+
export function chainCheck(agentId: string, rows: IndexRow[]): ContinuityReport {
|
|
13
|
+
const present = byId(rows);
|
|
14
|
+
const childrenOf = new Map<string, string[]>();
|
|
15
|
+
const referenced = new Set<string>();
|
|
16
|
+
|
|
17
|
+
for (const r of rows) {
|
|
18
|
+
if (r.prev !== null) {
|
|
19
|
+
referenced.add(r.prev);
|
|
20
|
+
(childrenOf.get(r.prev) ?? childrenOf.set(r.prev, []).get(r.prev)!).push(r.decisionId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sorted = (xs: string[]) => xs.slice().sort();
|
|
25
|
+
|
|
26
|
+
const genesis = sorted(rows.filter((r) => r.prev === null).map((r) => r.decisionId));
|
|
27
|
+
const heads = sorted(rows.filter((r) => !referenced.has(r.decisionId)).map((r) => r.decisionId));
|
|
28
|
+
|
|
29
|
+
const branches = [...childrenOf.entries()]
|
|
30
|
+
.filter(([, kids]) => kids.length > 1)
|
|
31
|
+
.map(([parent, kids]) => ({ parent, children: sorted(kids) }))
|
|
32
|
+
.sort((a, b) => (a.parent < b.parent ? -1 : 1));
|
|
33
|
+
|
|
34
|
+
const gaps = rows
|
|
35
|
+
.filter((r) => r.prev !== null && !present.has(r.prev))
|
|
36
|
+
.map((r) => ({ decisionId: r.decisionId, missingPrev: r.prev! }))
|
|
37
|
+
.sort((a, b) => (a.decisionId < b.decisionId ? -1 : 1));
|
|
38
|
+
|
|
39
|
+
const linear =
|
|
40
|
+
rows.length > 0 &&
|
|
41
|
+
genesis.length === 1 &&
|
|
42
|
+
heads.length === 1 &&
|
|
43
|
+
branches.length === 0 &&
|
|
44
|
+
gaps.length === 0;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
agentId,
|
|
48
|
+
count: rows.length,
|
|
49
|
+
genesis,
|
|
50
|
+
heads,
|
|
51
|
+
branches,
|
|
52
|
+
gaps,
|
|
53
|
+
linear,
|
|
54
|
+
tampering: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/gate.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402 payment gate (tasks/P4.md). Fronts a handler: an unpaid request is
|
|
3
|
+
* answered instantly with `svcSend402` (the bazaar discovery blob) — the wrapped
|
|
4
|
+
* handler, which does the index/store work, is NEVER invoked until payment is
|
|
5
|
+
* present, so discovery does zero DB work. A settled response carries
|
|
6
|
+
* `extensions.bazaar` (and an `x-payment-response` header).
|
|
7
|
+
*/
|
|
8
|
+
import { svcSend402, settlementBlob, type BazaarConfig } from "./bazaar.js";
|
|
9
|
+
import type { Handler, VerifyRequest, VerifyResponse } from "./http.js";
|
|
10
|
+
|
|
11
|
+
export const X402_PAYMENT_HEADER = "x-payment";
|
|
12
|
+
|
|
13
|
+
export interface PaymentResult {
|
|
14
|
+
paid: boolean;
|
|
15
|
+
payer?: string;
|
|
16
|
+
settlementId?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Verifies a payment header. Injected + mockable; MUST NOT do DB work. */
|
|
20
|
+
export type PaymentVerifier = (
|
|
21
|
+
paymentHeader: string | undefined,
|
|
22
|
+
req: VerifyRequest,
|
|
23
|
+
) => PaymentResult;
|
|
24
|
+
|
|
25
|
+
export interface GateOptions {
|
|
26
|
+
bazaar: BazaarConfig;
|
|
27
|
+
verifyPayment: PaymentVerifier;
|
|
28
|
+
paymentHeader?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
32
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function gate(handler: Handler, opts: GateOptions): Handler {
|
|
36
|
+
const headerName = (opts.paymentHeader ?? X402_PAYMENT_HEADER).toLowerCase();
|
|
37
|
+
return async (req: VerifyRequest): Promise<VerifyResponse> => {
|
|
38
|
+
const payment = opts.verifyPayment(req.headers[headerName], req);
|
|
39
|
+
if (!payment.paid) {
|
|
40
|
+
// Discovery: instant 402 with the bazaar blob, before any downstream work.
|
|
41
|
+
return svcSend402(opts.bazaar);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const res = await handler(req);
|
|
45
|
+
|
|
46
|
+
// Settle path: attach extensions.bazaar to a successful JSON body.
|
|
47
|
+
if (res.status >= 200 && res.status < 300 && isObject(res.body)) {
|
|
48
|
+
const settle = settlementBlob(opts.bazaar, payment);
|
|
49
|
+
const existingExt = isObject(res.body.extensions) ? res.body.extensions : {};
|
|
50
|
+
res.body = { ...res.body, extensions: { ...existingExt, bazaar: settle } };
|
|
51
|
+
res.headers["x-payment-response"] = Buffer.from(JSON.stringify(settle), "utf8").toString("base64");
|
|
52
|
+
}
|
|
53
|
+
return res;
|
|
54
|
+
};
|
|
55
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic request/response shapes for the route, plus a thin Node
|
|
3
|
+
* `http` adapter. The route is a pure `Handler` so tests drive it directly; the
|
|
4
|
+
* adapter is for running it as a real server.
|
|
5
|
+
*/
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
|
+
|
|
8
|
+
export interface VerifyRequest {
|
|
9
|
+
method: string;
|
|
10
|
+
path: string;
|
|
11
|
+
query: Record<string, string | undefined>;
|
|
12
|
+
headers: Record<string, string | undefined>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VerifyResponse {
|
|
16
|
+
status: number;
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
body: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Handler = (req: VerifyRequest) => VerifyResponse | Promise<VerifyResponse>;
|
|
22
|
+
|
|
23
|
+
/** Build a JSON response. */
|
|
24
|
+
export function json(status: number, body: unknown, headers: Record<string, string> = {}): VerifyResponse {
|
|
25
|
+
return { status, headers: { "content-type": "application/json", ...headers }, body };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function firstHeader(v: string | string[] | undefined): string | undefined {
|
|
29
|
+
return Array.isArray(v) ? v[0] : v;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Adapt a Handler to a Node `http` request listener. */
|
|
33
|
+
export function toNodeListener(handler: Handler) {
|
|
34
|
+
return (req: IncomingMessage, res: ServerResponse): void => {
|
|
35
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
36
|
+
const query: Record<string, string | undefined> = {};
|
|
37
|
+
for (const [k, val] of url.searchParams) query[k] = val;
|
|
38
|
+
const headers: Record<string, string | undefined> = {};
|
|
39
|
+
for (const [k, val] of Object.entries(req.headers)) headers[k.toLowerCase()] = firstHeader(val);
|
|
40
|
+
|
|
41
|
+
const request: VerifyRequest = { method: req.method ?? "GET", path: url.pathname, query, headers };
|
|
42
|
+
|
|
43
|
+
Promise.resolve(handler(request))
|
|
44
|
+
.then((r) => {
|
|
45
|
+
res.writeHead(r.status, r.headers);
|
|
46
|
+
res.end(typeof r.body === "string" ? r.body : JSON.stringify(r.body));
|
|
47
|
+
})
|
|
48
|
+
.catch(() => {
|
|
49
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
50
|
+
res.end(JSON.stringify({ error: "internal error" }));
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @rubric-protocol/decision-verify — the x402-gated decision-verify route (tasks/P4.md).
|
|
3
|
+
*
|
|
4
|
+
* A framework-agnostic Handler that, once payment is present, verifies a DAR
|
|
5
|
+
* against its Merkle inclusion proof, HCS anchor ref, and service signature
|
|
6
|
+
* (surfacing a drift flag), or produces a chain-check continuity report.
|
|
7
|
+
*/
|
|
8
|
+
export { createDecisionVerifyRoute, DECISION_VERIFY_PATH, type VerifyRouteDeps } from "./route.js";
|
|
9
|
+
export { gate, X402_PAYMENT_HEADER, type GateOptions, type PaymentVerifier, type PaymentResult } from "./gate.js";
|
|
10
|
+
export {
|
|
11
|
+
svcSend402,
|
|
12
|
+
buildBazaarBlob,
|
|
13
|
+
settlementBlob,
|
|
14
|
+
type BazaarConfig,
|
|
15
|
+
type BazaarBlob,
|
|
16
|
+
type SettlementBlob,
|
|
17
|
+
} from "./bazaar.js";
|
|
18
|
+
export { assembleVerification } from "./verify.js";
|
|
19
|
+
export { chainCheck } from "./chain.js";
|
|
20
|
+
export { verifyMerkleProof, buildMerkleTree, nodeHash } from "./merkle.js";
|
|
21
|
+
export { json, toNodeListener, type VerifyRequest, type VerifyResponse, type Handler } from "./http.js";
|
|
22
|
+
export type {
|
|
23
|
+
IndexPort,
|
|
24
|
+
StorePort,
|
|
25
|
+
SignatureVerifier,
|
|
26
|
+
SignatureResult,
|
|
27
|
+
VerifiableBundle,
|
|
28
|
+
VerificationResult,
|
|
29
|
+
VerifyStatus,
|
|
30
|
+
MerkleProof,
|
|
31
|
+
MerkleProofStep,
|
|
32
|
+
AnchorRef,
|
|
33
|
+
BatchSignature,
|
|
34
|
+
ContinuityReport,
|
|
35
|
+
ChainBranch,
|
|
36
|
+
ChainGap,
|
|
37
|
+
IndexRow,
|
|
38
|
+
} from "./ports.js";
|
package/src/merkle.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merkle inclusion proofs over SHA3-256 (tasks/P4.md; interior-node hashing is
|
|
3
|
+
* defined by the attestation service per spec/dar-0.1.md §4.3). Leaves are DAR
|
|
4
|
+
* leaf hashes; interior nodes are domain-separated to prevent second-preimage
|
|
5
|
+
* ambiguity between leaves and internal nodes.
|
|
6
|
+
*/
|
|
7
|
+
import { sha3_256, type HashString } from "@rubric-protocol/attest-decision";
|
|
8
|
+
import type { MerkleProofStep } from "./ports.js";
|
|
9
|
+
|
|
10
|
+
const NODE_DOMAIN = "rubric-merkle-node/1\n";
|
|
11
|
+
|
|
12
|
+
/** Hash an interior node from its ordered children. */
|
|
13
|
+
export function nodeHash(left: HashString, right: HashString): HashString {
|
|
14
|
+
return sha3_256(`${NODE_DOMAIN}${left}\n${right}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Fold a leaf up through its proof steps and check it reproduces `root`. */
|
|
18
|
+
export function verifyMerkleProof(leaf: HashString, steps: MerkleProofStep[], root: HashString): boolean {
|
|
19
|
+
let acc = leaf;
|
|
20
|
+
for (const step of steps) {
|
|
21
|
+
acc = step.position === "left" ? nodeHash(step.sibling, acc) : nodeHash(acc, step.sibling);
|
|
22
|
+
}
|
|
23
|
+
return acc === root;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build a Merkle root and an inclusion proof for every leaf. Odd levels promote
|
|
28
|
+
* the last node by pairing it with itself. Utility for constructing anchored
|
|
29
|
+
* bundles (and test fixtures); the route only needs verifyMerkleProof.
|
|
30
|
+
*/
|
|
31
|
+
export function buildMerkleTree(leaves: HashString[]): { root: HashString; proofs: MerkleProofStep[][] } {
|
|
32
|
+
if (leaves.length === 0) throw new Error("merkle: cannot build a tree over zero leaves");
|
|
33
|
+
|
|
34
|
+
const proofs: MerkleProofStep[][] = leaves.map(() => []);
|
|
35
|
+
// indexAtLevel[i] tracks where original leaf i currently sits in `level`.
|
|
36
|
+
let level = leaves.slice();
|
|
37
|
+
let positions = leaves.map((_, i) => i);
|
|
38
|
+
|
|
39
|
+
while (level.length > 1) {
|
|
40
|
+
const next: HashString[] = [];
|
|
41
|
+
const nextPositions = positions.map(() => 0);
|
|
42
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
43
|
+
const left = level[i]!;
|
|
44
|
+
const right = i + 1 < level.length ? level[i + 1]! : level[i]!; // promote odd tail
|
|
45
|
+
const parent = nodeHash(left, right);
|
|
46
|
+
const parentIdx = next.length;
|
|
47
|
+
next.push(parent);
|
|
48
|
+
for (let li = 0; li < positions.length; li++) {
|
|
49
|
+
if (positions[li] === i) {
|
|
50
|
+
proofs[li]!.push({ sibling: right, position: "right" });
|
|
51
|
+
nextPositions[li] = parentIdx;
|
|
52
|
+
} else if (positions[li] === i + 1) {
|
|
53
|
+
proofs[li]!.push({ sibling: left, position: "left" });
|
|
54
|
+
nextPositions[li] = parentIdx;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
level = next;
|
|
59
|
+
positions = nextPositions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { root: level[0]!, proofs };
|
|
63
|
+
}
|
package/src/ports.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ports and wire types for the decision-verify route (tasks/P4.md).
|
|
3
|
+
*
|
|
4
|
+
* The route depends only on these interfaces, so tests inject in-memory mocks
|
|
5
|
+
* for the index and store ("route tests green on mocked index+store").
|
|
6
|
+
*/
|
|
7
|
+
import type { DarCore, HashString } from "@rubric-protocol/attest-decision";
|
|
8
|
+
import type { IndexRow } from "@rubric-protocol/attest-index";
|
|
9
|
+
|
|
10
|
+
export type { IndexRow };
|
|
11
|
+
|
|
12
|
+
/** A Merkle inclusion proof step: a sibling hash and which side it sits on. */
|
|
13
|
+
export interface MerkleProofStep {
|
|
14
|
+
sibling: HashString;
|
|
15
|
+
position: "left" | "right";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Inclusion proof from a leaf up to a published root. */
|
|
19
|
+
export interface MerkleProof {
|
|
20
|
+
leaf: HashString;
|
|
21
|
+
steps: MerkleProofStep[];
|
|
22
|
+
root: HashString;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Reference to the on-chain (HCS) anchor that committed a Merkle root. */
|
|
26
|
+
export interface AnchorRef {
|
|
27
|
+
network: string;
|
|
28
|
+
topicId: string;
|
|
29
|
+
sequenceNumber: number;
|
|
30
|
+
root: HashString;
|
|
31
|
+
consensusTimestamp?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Service signature over an anchored root. */
|
|
35
|
+
export interface BatchSignature {
|
|
36
|
+
alg: string;
|
|
37
|
+
keyRef: string;
|
|
38
|
+
signature: string;
|
|
39
|
+
over: HashString; // the root the signature covers
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A stored bundle: the DAR core plus the attestation envelope (spec §6). */
|
|
43
|
+
export interface VerifiableBundle {
|
|
44
|
+
attestationId: string;
|
|
45
|
+
dar: DarCore;
|
|
46
|
+
merkleProof: MerkleProof;
|
|
47
|
+
anchorRef: AnchorRef;
|
|
48
|
+
signature: BatchSignature;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Index port — row lookups by decisionId/decisionHash and per-agent chains. */
|
|
52
|
+
export interface IndexPort {
|
|
53
|
+
byDecisionId(decisionId: string): IndexRow | undefined;
|
|
54
|
+
byDecisionHash(decisionHash: string): IndexRow | undefined;
|
|
55
|
+
agentChain(agentId: string): IndexRow[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Store port — load a full bundle by its path. */
|
|
59
|
+
export interface StorePort {
|
|
60
|
+
get(bundlePath: string): VerifiableBundle | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SignatureResult {
|
|
64
|
+
verified: boolean;
|
|
65
|
+
keyRef: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Verifies a service signature over an anchored root. Injected (mockable). */
|
|
69
|
+
export type SignatureVerifier = (signature: BatchSignature, root: HashString) => SignatureResult;
|
|
70
|
+
|
|
71
|
+
/** Version gate outcome (spec §5.1). */
|
|
72
|
+
export type VerifyStatus = "ok" | "needs-upgrade" | "rejected";
|
|
73
|
+
|
|
74
|
+
/** The decision-verify result body. */
|
|
75
|
+
export interface VerificationResult {
|
|
76
|
+
decisionId: string;
|
|
77
|
+
dar: DarCore;
|
|
78
|
+
merkleProof: MerkleProof;
|
|
79
|
+
anchorRef: AnchorRef;
|
|
80
|
+
signature: SignatureResult;
|
|
81
|
+
/** True when the record no longer matches the proof/root carried in its bundle (tampering). */
|
|
82
|
+
drift: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Version gate per spec §5.1: `rejected` (unknown DAR major or bad leafType),
|
|
85
|
+
* `needs-upgrade` (known major but a newer minor this verifier can't fully
|
|
86
|
+
* check), or `ok` (this verifier fully understands the record's version).
|
|
87
|
+
*/
|
|
88
|
+
status: VerifyStatus;
|
|
89
|
+
/**
|
|
90
|
+
* INTERNAL CONSISTENCY ONLY. True iff status is `ok`, there is no drift, and
|
|
91
|
+
* the supplied signature verified. This attests that the bundle is internally
|
|
92
|
+
* self-consistent — it is NOT proof that the Merkle root was anchored on HCS,
|
|
93
|
+
* nor that the signing key is trusted. Those require the (out-of-scope)
|
|
94
|
+
* attestation service; this route does not contact any ledger.
|
|
95
|
+
*/
|
|
96
|
+
consistencyVerified: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A fork point: a decision with more than one child in the chain. */
|
|
100
|
+
export interface ChainBranch {
|
|
101
|
+
parent: string;
|
|
102
|
+
children: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A break where a record's `prev` points at a decision absent from the index. */
|
|
106
|
+
export interface ChainGap {
|
|
107
|
+
decisionId: string;
|
|
108
|
+
missingPrev: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Continuity report for an agent's chain (tasks/P4.md). */
|
|
112
|
+
export interface ContinuityReport {
|
|
113
|
+
agentId: string;
|
|
114
|
+
count: number;
|
|
115
|
+
genesis: string[]; // decisions with prev === null
|
|
116
|
+
heads: string[]; // tips: not referenced as anyone's prev
|
|
117
|
+
branches: ChainBranch[]; // forks, represented as branches (NOT tampering)
|
|
118
|
+
gaps: ChainGap[];
|
|
119
|
+
/** True iff a single unbroken linear chain (one genesis, one head, no forks/gaps). */
|
|
120
|
+
linear: boolean;
|
|
121
|
+
/** Forks and gaps are structural, not tampering — always false here. */
|
|
122
|
+
tampering: false;
|
|
123
|
+
}
|
package/src/route.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate()-fronted /v1/x402/decision-verify route (tasks/P4.md).
|
|
3
|
+
*
|
|
4
|
+
* Query:
|
|
5
|
+
* ?decisionId=... | ?decisionHash=... -> single-record verification
|
|
6
|
+
* ?agentId=... (or ?mode=chain) -> chain-check continuity report
|
|
7
|
+
*/
|
|
8
|
+
import { assembleVerification } from "./verify.js";
|
|
9
|
+
import { chainCheck } from "./chain.js";
|
|
10
|
+
import { gate, type PaymentVerifier } from "./gate.js";
|
|
11
|
+
import { json, type Handler, type VerifyRequest, type VerifyResponse } from "./http.js";
|
|
12
|
+
import type { BazaarConfig } from "./bazaar.js";
|
|
13
|
+
import type { IndexPort, SignatureVerifier, StorePort } from "./ports.js";
|
|
14
|
+
|
|
15
|
+
export const DECISION_VERIFY_PATH = "/v1/x402/decision-verify";
|
|
16
|
+
|
|
17
|
+
export interface VerifyRouteDeps {
|
|
18
|
+
index: IndexPort;
|
|
19
|
+
store: StorePort;
|
|
20
|
+
verifySignature: SignatureVerifier;
|
|
21
|
+
bazaar: BazaarConfig;
|
|
22
|
+
verifyPayment: PaymentVerifier;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The paid handler — this is where index/store work happens. */
|
|
26
|
+
function decisionVerify(req: VerifyRequest, deps: VerifyRouteDeps): VerifyResponse {
|
|
27
|
+
const { decisionId, decisionHash, agentId, mode } = req.query;
|
|
28
|
+
|
|
29
|
+
if (mode === "chain" || agentId) {
|
|
30
|
+
if (!agentId) return json(400, { error: "agentId is required for chain-check" });
|
|
31
|
+
return json(200, chainCheck(agentId, deps.index.agentChain(agentId)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const row = decisionId
|
|
35
|
+
? deps.index.byDecisionId(decisionId)
|
|
36
|
+
: decisionHash
|
|
37
|
+
? deps.index.byDecisionHash(decisionHash)
|
|
38
|
+
: undefined;
|
|
39
|
+
|
|
40
|
+
if (!decisionId && !decisionHash) {
|
|
41
|
+
return json(400, { error: "decisionId or decisionHash is required" });
|
|
42
|
+
}
|
|
43
|
+
if (!row) return json(404, { error: "decision not found" });
|
|
44
|
+
|
|
45
|
+
const bundle = deps.store.get(row.bundlePath);
|
|
46
|
+
if (!bundle) return json(502, { error: "bundle missing from store", bundlePath: row.bundlePath });
|
|
47
|
+
|
|
48
|
+
return json(200, assembleVerification(bundle, deps.verifySignature));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Build the gate()-fronted route handler. */
|
|
52
|
+
export function createDecisionVerifyRoute(deps: VerifyRouteDeps): Handler {
|
|
53
|
+
const handler: Handler = (req) => decisionVerify(req, deps);
|
|
54
|
+
return gate(handler, { bazaar: deps.bazaar, verifyPayment: deps.verifyPayment });
|
|
55
|
+
}
|