capacity-attest 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +122 -93
  2. package/README.nl.md +205 -0
  3. package/dist/bench-helpers.d.ts +12 -0
  4. package/dist/bench-helpers.d.ts.map +1 -0
  5. package/dist/bench-helpers.js +51 -0
  6. package/dist/bench-helpers.js.map +1 -0
  7. package/dist/discovery.bench.d.ts +2 -0
  8. package/dist/discovery.bench.d.ts.map +1 -0
  9. package/dist/discovery.bench.js +30 -0
  10. package/dist/discovery.bench.js.map +1 -0
  11. package/dist/discovery.d.ts +2 -0
  12. package/dist/discovery.d.ts.map +1 -1
  13. package/dist/discovery.js +57 -2
  14. package/dist/discovery.js.map +1 -1
  15. package/dist/eas.bench.d.ts +2 -0
  16. package/dist/eas.bench.d.ts.map +1 -0
  17. package/dist/eas.bench.js +40 -0
  18. package/dist/eas.bench.js.map +1 -0
  19. package/dist/eas.d.ts +89 -7
  20. package/dist/eas.d.ts.map +1 -1
  21. package/dist/eas.js +151 -34
  22. package/dist/eas.js.map +1 -1
  23. package/dist/erc8004-reputation.d.ts +1 -0
  24. package/dist/erc8004-reputation.d.ts.map +1 -1
  25. package/dist/http-source.bench.d.ts +2 -0
  26. package/dist/http-source.bench.d.ts.map +1 -0
  27. package/dist/http-source.bench.js +65 -0
  28. package/dist/http-source.bench.js.map +1 -0
  29. package/dist/http-source.d.ts +28 -0
  30. package/dist/http-source.d.ts.map +1 -0
  31. package/dist/http-source.js +154 -0
  32. package/dist/http-source.js.map +1 -0
  33. package/dist/receipt-authorization.d.ts +77 -0
  34. package/dist/receipt-authorization.d.ts.map +1 -0
  35. package/dist/receipt-authorization.js +128 -0
  36. package/dist/receipt-authorization.js.map +1 -0
  37. package/dist/receipt-check.d.ts +58 -0
  38. package/dist/receipt-check.d.ts.map +1 -0
  39. package/dist/receipt-check.js +88 -0
  40. package/dist/receipt-check.js.map +1 -0
  41. package/dist/receipt-issue.d.ts +181 -0
  42. package/dist/receipt-issue.d.ts.map +1 -0
  43. package/dist/receipt-issue.js +177 -0
  44. package/dist/receipt-issue.js.map +1 -0
  45. package/dist/schema.d.ts +7 -0
  46. package/dist/schema.d.ts.map +1 -1
  47. package/dist/schema.js +55 -1
  48. package/dist/schema.js.map +1 -1
  49. package/package.json +4 -1
@@ -0,0 +1,154 @@
1
+ // http-source.ts — a ClaimSource over a plain HTTP endpoint: "another
2
+ // installation's public endpoint", one of the substrate examples named in
3
+ // discovery.ts's own header comment. This is WP1's second, non-EAS,
4
+ // production-grade source: it proves discoverDeliveryHistory's aggregation
5
+ // logic against a genuinely different SOURCE TYPE (plain HTTP, no chain, no
6
+ // ethers), not just a second EAS deployment. Like every ClaimSource, this is
7
+ // UNTRUSTED: discoverDeliveryHistory re-verifies every claim it returns,
8
+ // regardless of what this module does or does not check first.
9
+ //
10
+ // Convention (deliberately the simplest defensible one, not a full
11
+ // paginated/authenticated protocol): GET `${endpointUrl}?seller=<address>`
12
+ // returns a JSON array of claims for that seller. DECISIONS.md D-005
13
+ // explicitly defers designing a full decentralized-findability PROTOCOL
14
+ // until a real second installation exists to standardize with; inventing
15
+ // cursor-based pagination or auth here now would be protocol surface nobody
16
+ // has agreed to yet. A single bounded JSON response, capped the same way any
17
+ // other source is capped downstream, is the right-sized amount of protocol
18
+ // for a substrate that has no live second party yet.
19
+ //
20
+ // Native fetch only (Node >=20 ships it as a global) — no new dependency,
21
+ // same minimal-dependency stance eas.ts states for itself ("plain ethers
22
+ // only, no eas-sdk dependency").
23
+ import { ETH_ADDRESS_RE } from "./schema.js";
24
+ export class HttpSourceError extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = "HttpSourceError";
28
+ }
29
+ }
30
+ // discoverDeliveryHistory's own MAX_CLAIMS_PER_SOURCE only bounds the claims
31
+ // ARRAY, after it is already fully parsed into memory. A raw response body
32
+ // far larger than any real claim set (an oversized or hostile response)
33
+ // would pay the cost of buffering and JSON.parse BEFORE that cap gets a
34
+ // chance to help. This bounds the raw bytes read, before parsing, closing
35
+ // that gap specifically for this transport (EAS does not have this exposure:
36
+ // its equivalent bound is the block-range windowing in eas.ts). 16 MiB is
37
+ // generous for thousands of real claims (each a few hundred bytes) while
38
+ // still refusing a source that tries to exhaust memory.
39
+ const DEFAULT_MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
40
+ // Independent of discoverDeliveryHistory's own sourceTimeoutMs (default
41
+ // 30s): this source is also usable standalone, outside discoverDeliveryHistory,
42
+ // so it carries its own, tighter default rather than relying entirely on a
43
+ // caller that may not exist.
44
+ const DEFAULT_TIMEOUT_MS = 10_000;
45
+ // Reuses schema.ts's own canonical address shape (the same one
46
+ // DeliveryClaimSchema enforces on sellerAddress/buyerAddress) rather than a
47
+ // separately invented, looser check. sellerAddress is caller-supplied here
48
+ // (same trust level as every other source's argument), but validating its
49
+ // shape before string-building a URL from it is nearly free, and matching
50
+ // the one canonical format means a malformed address fails fast, right
51
+ // here, with a clear message, instead of surfacing later as an opaque
52
+ // low-level exception from whatever eventually tries to use it (e.g. an
53
+ // ethers "invalid BytesLike value" a couple of layers downstream).
54
+ const ADDRESS_SHAPE = ETH_ADDRESS_RE;
55
+ /**
56
+ * Read a Response body up to maxBytes, throwing HttpSourceError the moment
57
+ * the limit is crossed instead of buffering the rest. Cancels the underlying
58
+ * stream on overflow so the connection is not left dangling.
59
+ */
60
+ async function readBounded(response, maxBytes) {
61
+ const body = response.body;
62
+ if (!body)
63
+ return "";
64
+ const reader = body.getReader();
65
+ const chunks = [];
66
+ let total = 0;
67
+ for (;;) {
68
+ const { done, value } = await reader.read();
69
+ if (done)
70
+ break;
71
+ if (value) {
72
+ total += value.byteLength;
73
+ if (total > maxBytes) {
74
+ await reader.cancel().catch(() => { });
75
+ throw new HttpSourceError(`response exceeded ${maxBytes} bytes, refused before parsing`);
76
+ }
77
+ chunks.push(value);
78
+ }
79
+ }
80
+ return Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf-8");
81
+ }
82
+ /**
83
+ * A ClaimSource over a plain HTTP endpoint. GETs
84
+ * `${endpointUrl}?seller=<sellerAddress>` and expects a JSON array back.
85
+ * Deliberately does NO verification and NO per-entry shape checking itself
86
+ * (same division of responsibility as staticSource/easSource):
87
+ * discoverDeliveryHistory re-verifies every claim from every source
88
+ * uniformly. A malformed sellerAddress, a non-2xx status, a request that
89
+ * times out or fails at the network level, a body over the size cap, or a
90
+ * body that is not JSON or not a JSON array, all become a thrown
91
+ * HttpSourceError — which discoverDeliveryHistory's existing per-source
92
+ * try/catch already turns into a SourceReport.error, never a crash of the
93
+ * whole aggregation.
94
+ */
95
+ export function httpClaimSource(endpointUrl, opts = {}) {
96
+ const name = opts.name ?? "http";
97
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
98
+ const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
99
+ const fetchImpl = opts.fetchImpl ?? fetch;
100
+ return {
101
+ name,
102
+ fetchForSeller: async (sellerAddress) => {
103
+ if (!ADDRESS_SHAPE.test(sellerAddress)) {
104
+ throw new HttpSourceError(`refusing to build a URL from a malformed sellerAddress: ${JSON.stringify(sellerAddress)}`);
105
+ }
106
+ const url = new URL(endpointUrl);
107
+ url.searchParams.set("seller", sellerAddress);
108
+ let response;
109
+ try {
110
+ response = await fetchImpl(url, { signal: AbortSignal.timeout(timeoutMs) });
111
+ }
112
+ catch (e) {
113
+ throw new HttpSourceError(`request to ${url} failed: ${e.message}`);
114
+ }
115
+ if (!response.ok) {
116
+ throw new HttpSourceError(`${url} responded ${response.status} ${response.statusText}`);
117
+ }
118
+ // A stall DURING body streaming (headers already arrived, response.ok
119
+ // already true, but the body never finishes within timeoutMs) is a
120
+ // SEPARATE failure moment from the connect-phase catch above: the same
121
+ // AbortSignal also governs the body read, so it rejects too, but with
122
+ // a raw DOMException (name "TimeoutError"), not HttpSourceError. Wrap
123
+ // it here so every documented failure mode really does surface as
124
+ // HttpSourceError, matching this function's own doc comment.
125
+ let text;
126
+ try {
127
+ text = await readBounded(response, maxResponseBytes);
128
+ }
129
+ catch (e) {
130
+ if (e instanceof HttpSourceError)
131
+ throw e; // already correct: the byte-cap-exceeded case
132
+ throw new HttpSourceError(`reading response body from ${url} failed: ${e.message}`);
133
+ }
134
+ let parsed;
135
+ try {
136
+ parsed = JSON.parse(text);
137
+ }
138
+ catch (e) {
139
+ throw new HttpSourceError(`response from ${url} was not valid JSON: ${e.message}`);
140
+ }
141
+ if (!Array.isArray(parsed)) {
142
+ throw new HttpSourceError(`response from ${url} was valid JSON but not an array`);
143
+ }
144
+ // Untyped cast is deliberate: every entry is untrusted input regardless
145
+ // of shape, exactly like staticSource's fixed list and easSource's
146
+ // decoded attestations. discoverDeliveryHistory's own per-entry
147
+ // computeClaimId/verifyClaim/DeliveryClaimSchema.parse chain is what
148
+ // actually validates each one; duplicating that here would just be a
149
+ // second, redundant copy of logic that already lives in one place.
150
+ return parsed;
151
+ },
152
+ };
153
+ }
154
+ //# sourceMappingURL=http-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-source.js","sourceRoot":"","sources":["../src/http-source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * §5.6.2's table. Each method fixes the byte length of `asset` and `authorizer`;
3
+ * `identifier` and `salt` are 32 bytes for every method. Fixed lengths are what
4
+ * make the preimage unambiguous without any separators, so they are enforced
5
+ * rather than assumed: a field of the wrong length would hash to a commitment
6
+ * that opens to nothing, and would do so silently.
7
+ */
8
+ export declare const AUTHORIZATION_METHODS: {
9
+ readonly eip3009: {
10
+ readonly asset: 20;
11
+ readonly authorizer: 20;
12
+ };
13
+ };
14
+ export type AuthorizationMethod = keyof typeof AUTHORIZATION_METHODS;
15
+ export interface CommitmentInput {
16
+ /** Currently only "eip3009". Equals the receipt's `authorizationMethod`. */
17
+ method: AuthorizationMethod;
18
+ /** Token contract of the accepts entry that was paid. Not carried by the receipt. */
19
+ asset: string;
20
+ /** `from` of the authorization. §5.6.4 step 1 takes this from the receipt's signed payer. */
21
+ authorizer: string;
22
+ /** The ERC-3009 nonce, 32 bytes. */
23
+ identifier: string;
24
+ /** 32 bytes of CSPRNG output, per payment. Never signed, never in the receipt. */
25
+ salt: string;
26
+ }
27
+ /** §5.6.3: 32 bytes from a CSPRNG, independent per payment, never derived from anything. */
28
+ export declare function generateAuthorizationSalt(): string;
29
+ /** §5.6.2, the whole construction. Hex inputs are case-insensitive; bytes are hashed. */
30
+ export declare function computeAuthorizationCommitment(input: CommitmentInput): string;
31
+ export type OpenResult = {
32
+ linked: true;
33
+ identifier: string;
34
+ } | {
35
+ linked: false;
36
+ reason: string;
37
+ };
38
+ export interface OpenInput {
39
+ /** The signed receipt payload. `payer` is the authorizer; nothing else is trusted. */
40
+ payload: {
41
+ payer: string;
42
+ authorizationMethod?: string;
43
+ authorizationCommitment?: string;
44
+ };
45
+ salt: string;
46
+ /** The asset of the accepts entry that was paid. §5.6.4: salt alone cannot link. */
47
+ asset: string;
48
+ /** Nonces from AuthorizationUsed(authorizer, nonce) logs on that asset. */
49
+ candidateIdentifiers: readonly string[];
50
+ }
51
+ /**
52
+ * §5.6.4 steps 1 and 3. Fails closed on both nothing and ambiguity.
53
+ *
54
+ * Two matches is not "probably the first one": if an identifier can repeat across
55
+ * settlements the commitment genuinely opens to several, and the spec says that
56
+ * case does not link. Reporting a guess there would turn a fail-closed design into
57
+ * a false link, which is the one outcome the whole construction exists to avoid.
58
+ */
59
+ export declare function openAuthorizationCommitment(input: OpenInput): OpenResult;
60
+ /**
61
+ * The vector published with the spec, kept here so our own test has an external
62
+ * anchor. The independent verifier in agent-receipt-verify asserts the same
63
+ * constant from its own implementation: two implementations anchored to the same
64
+ * published number cannot drift silently, two anchored to each other can.
65
+ */
66
+ export declare const SPEC_VECTOR: {
67
+ readonly source: "x402-foundation/x402 PR #3827, specs/extensions/extension-offer-and-receipt.md §5.6.2, commit 0e492d6730bef14b9c722428386c114f3475a914";
68
+ readonly input: {
69
+ readonly method: "eip3009";
70
+ readonly asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
71
+ readonly authorizer: "0x857b06519E91e3A54538791bDbb0E22373e36b66";
72
+ readonly identifier: "0x5135a00b531830a13a751f3c48944d7a5fdfde7a3b87f73493591cd77efdef4f";
73
+ readonly salt: "0x10d2ae5f0c8536e82d882bf3d2c31a663597e890ca6c3fede0d278fd2a78f655";
74
+ };
75
+ readonly expected: "0x5d905bbf08a97e04ab551d276d8e94b1abaff7d0737324d360e983c2796af23b";
76
+ };
77
+ //# sourceMappingURL=receipt-authorization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt-authorization.d.ts","sourceRoot":"","sources":["../src/receipt-authorization.ts"],"names":[],"mappings":"AA0BA;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;CAExB,CAAC;AAEX,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,qBAAqB,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,MAAM,EAAE,mBAAmB,CAAC;IAC5B,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,6FAA6F;IAC7F,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;CACd;AAaD,4FAA4F;AAC5F,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED,yFAAyF;AACzF,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAc7E;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,MAAM,WAAW,SAAS;IACxB,sFAAsF;IACtF,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAAC,uBAAuB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAgCxE;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;CAWd,CAAC"}
@@ -0,0 +1,128 @@
1
+ // receipt-authorization.ts — the salted commitment that ties a receipt to one settlement.
2
+ //
3
+ // Written against specs/extensions/extension-offer-and-receipt.md §5.6 at commit
4
+ // 0e492d6730bef14b9c722428386c114f3475a914 of the receipt-authorization-commitment
5
+ // branch. That PR is open, so the preimage can still change: the commit is pinned
6
+ // here on purpose, and the test asserts the vector published with it. If the spec
7
+ // moves, the test fails rather than the signature quietly changing meaning.
8
+ //
9
+ // WHY A COMMITMENT AND NOT THE TRANSACTION HASH. A receipt with an empty
10
+ // `transaction` can be matched to a settlement only by payer, payee, amount and
11
+ // time. Two payments with the same terms in the same block cannot be ordered at
12
+ // all. The ERC-3009 authorization already carries a nonce that is unique per
13
+ // settlement and that the server holds before settlement runs, so a salted hash of
14
+ // it lets whoever holds the salt recompute the link to exactly one settlement, and
15
+ // tells everyone else nothing.
16
+ //
17
+ // AND WHY IT MATTERS TO US SPECIFICALLY: §5.6.5 says a consumer that counts
18
+ // settlements MUST count opened commitments only. A receipt whose commitment
19
+ // nobody can open does not count. That is the category our own receipts were in.
20
+ import { createHash, randomBytes } from "node:crypto";
21
+ /** §5.6.2. ASCII, and the trailing /0 is the preimage version, not the payload version. */
22
+ const DOMAIN_TAG = Buffer.from("x402-receipt-authorization/0", "ascii");
23
+ const NUL = Buffer.from([0]);
24
+ /**
25
+ * §5.6.2's table. Each method fixes the byte length of `asset` and `authorizer`;
26
+ * `identifier` and `salt` are 32 bytes for every method. Fixed lengths are what
27
+ * make the preimage unambiguous without any separators, so they are enforced
28
+ * rather than assumed: a field of the wrong length would hash to a commitment
29
+ * that opens to nothing, and would do so silently.
30
+ */
31
+ export const AUTHORIZATION_METHODS = {
32
+ eip3009: { asset: 20, authorizer: 20 },
33
+ };
34
+ function fixedBytes(value, name, expected) {
35
+ if (typeof value !== "string")
36
+ throw new Error(`${name} must be a hex string`);
37
+ const hex = value.startsWith("0x") || value.startsWith("0X") ? value.slice(2) : value;
38
+ if (!/^[0-9a-fA-F]*$/.test(hex))
39
+ throw new Error(`${name} is not hex: ${JSON.stringify(value)}`);
40
+ const buf = Buffer.from(hex, "hex");
41
+ if (buf.length !== expected) {
42
+ throw new Error(`${name} must be ${expected} bytes per section 5.6.2, got ${buf.length}`);
43
+ }
44
+ return buf;
45
+ }
46
+ /** §5.6.3: 32 bytes from a CSPRNG, independent per payment, never derived from anything. */
47
+ export function generateAuthorizationSalt() {
48
+ return "0x" + randomBytes(32).toString("hex");
49
+ }
50
+ /** §5.6.2, the whole construction. Hex inputs are case-insensitive; bytes are hashed. */
51
+ export function computeAuthorizationCommitment(input) {
52
+ const lengths = AUTHORIZATION_METHODS[input.method];
53
+ if (!lengths)
54
+ throw new Error(`unknown authorizationMethod: ${String(input.method)}`);
55
+ const preimage = Buffer.concat([
56
+ DOMAIN_TAG,
57
+ NUL,
58
+ Buffer.from(input.method, "ascii"),
59
+ NUL,
60
+ fixedBytes(input.asset, "asset", lengths.asset),
61
+ fixedBytes(input.authorizer, "authorizer", lengths.authorizer),
62
+ fixedBytes(input.identifier, "identifier", 32),
63
+ fixedBytes(input.salt, "salt", 32),
64
+ ]);
65
+ return "0x" + createHash("sha256").update(preimage).digest("hex");
66
+ }
67
+ /**
68
+ * §5.6.4 steps 1 and 3. Fails closed on both nothing and ambiguity.
69
+ *
70
+ * Two matches is not "probably the first one": if an identifier can repeat across
71
+ * settlements the commitment genuinely opens to several, and the spec says that
72
+ * case does not link. Reporting a guess there would turn a fail-closed design into
73
+ * a false link, which is the one outcome the whole construction exists to avoid.
74
+ */
75
+ export function openAuthorizationCommitment(input) {
76
+ const { payload, salt, asset, candidateIdentifiers } = input;
77
+ const method = payload?.authorizationMethod;
78
+ const commitment = payload?.authorizationCommitment;
79
+ if (!method || !commitment)
80
+ return { linked: false, reason: "receipt carries no commitment" };
81
+ if (!(method in AUTHORIZATION_METHODS)) {
82
+ return { linked: false, reason: `unknown authorizationMethod: ${method}` };
83
+ }
84
+ const hits = [];
85
+ for (const identifier of candidateIdentifiers ?? []) {
86
+ let computed;
87
+ try {
88
+ computed = computeAuthorizationCommitment({
89
+ method: method,
90
+ asset,
91
+ // §5.6.4 step 1: the authorizer comes from the SIGNED payer field, never
92
+ // from a value handed over next to the salt. Otherwise a commitment can be
93
+ // opened against someone else's settlement.
94
+ authorizer: payload.payer,
95
+ identifier,
96
+ salt,
97
+ });
98
+ }
99
+ catch {
100
+ continue;
101
+ }
102
+ if (computed.toLowerCase() === String(commitment).toLowerCase())
103
+ hits.push(identifier);
104
+ }
105
+ if (hits.length === 1)
106
+ return { linked: true, identifier: hits[0] };
107
+ if (hits.length === 0)
108
+ return { linked: false, reason: "no candidate opens this commitment" };
109
+ return { linked: false, reason: `${hits.length} candidates open the same commitment, ambiguous` };
110
+ }
111
+ /**
112
+ * The vector published with the spec, kept here so our own test has an external
113
+ * anchor. The independent verifier in agent-receipt-verify asserts the same
114
+ * constant from its own implementation: two implementations anchored to the same
115
+ * published number cannot drift silently, two anchored to each other can.
116
+ */
117
+ export const SPEC_VECTOR = {
118
+ source: "x402-foundation/x402 PR #3827, specs/extensions/extension-offer-and-receipt.md §5.6.2, commit 0e492d6730bef14b9c722428386c114f3475a914",
119
+ input: {
120
+ method: "eip3009",
121
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
122
+ authorizer: "0x857b06519E91e3A54538791bDbb0E22373e36b66",
123
+ identifier: "0x5135a00b531830a13a751f3c48944d7a5fdfde7a3b87f73493591cd77efdef4f",
124
+ salt: "0x10d2ae5f0c8536e82d882bf3d2c31a663597e890ca6c3fede0d278fd2a78f655",
125
+ },
126
+ expected: "0x5d905bbf08a97e04ab551d276d8e94b1abaff7d0737324d360e983c2796af23b",
127
+ };
128
+ //# sourceMappingURL=receipt-authorization.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt-authorization.js","sourceRoot":"","sources":["../src/receipt-authorization.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /** Fields the caller extracted from a receipt payload. Every one is optional: a comparison is skipped rather than guessed. */
2
+ export interface ReceiptFields {
3
+ /** The paying party, per the receipt. Compared case-insensitively against the claim's buyerAddress. */
4
+ payer?: string;
5
+ /** The settlement this receipt is about. Compared case-insensitively against the claim's settlementRef. */
6
+ transaction?: string;
7
+ /**
8
+ * When the seller issued the receipt. The x402 offer-and-receipt extension defines this as a
9
+ * unix timestamp in SECONDS (§5.2), which is why a number is accepted and treated as such.
10
+ * A string is accepted too, parsed as a date, because a caller may already have normalised it.
11
+ * A receipt issued after the buyer's claim is a disagreement about ordering.
12
+ */
13
+ issuedAt?: string | number;
14
+ }
15
+ export interface ClaimSide {
16
+ buyerAddress: string;
17
+ settlementRef: string;
18
+ timestamp: string;
19
+ offerReceiptHash?: string | undefined;
20
+ }
21
+ export type BindingResult = {
22
+ bound: true;
23
+ digest: string;
24
+ } | {
25
+ bound: false;
26
+ reason: "claim_names_no_receipt";
27
+ digest: string;
28
+ } | {
29
+ bound: false;
30
+ reason: "digest_mismatch";
31
+ digest: string;
32
+ named: string;
33
+ };
34
+ /** sha256 of the artifact exactly as given, lower-case hex, no prefix. No normalisation of any kind. */
35
+ export declare function receiptDigest(artifact: string | Uint8Array): string;
36
+ /** Does this claim actually name this artifact? Recomputes the digest rather than trusting the field. */
37
+ export declare function checkReceiptBinding(claim: ClaimSide, artifact: string | Uint8Array): BindingResult;
38
+ export interface Disagreement {
39
+ field: "payer" | "transaction" | "issuedAt";
40
+ claim: string;
41
+ receipt: string;
42
+ note: string;
43
+ }
44
+ export interface ComparisonResult {
45
+ /** True only when at least one field was comparable and none disagreed. */
46
+ agrees: boolean;
47
+ /** Fields present on both sides that tell different stories. */
48
+ disagreements: Disagreement[];
49
+ /** Fields that could not be compared because the receipt side was absent. Never counted as agreement. */
50
+ notCompared: Array<"payer" | "transaction" | "issuedAt">;
51
+ }
52
+ /**
53
+ * Compare the two accounts field by field. Only fields both sides describe are
54
+ * compared; a missing receipt field is reported as not compared, never as
55
+ * agreement.
56
+ */
57
+ export declare function compareClaimToReceipt(claim: ClaimSide, receipt: ReceiptFields): ComparisonResult;
58
+ //# sourceMappingURL=receipt-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt-check.d.ts","sourceRoot":"","sources":["../src/receipt-check.ts"],"names":[],"mappings":"AAwBA,8HAA8H;AAC9H,MAAM,WAAW,aAAa;IAC5B,uGAAuG;IACvG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2GAA2G;IAC3G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,wBAAwB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAEnE;AAED,yGAAyG;AACzG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,CAOlG;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,UAAU,CAAC;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,MAAM,EAAE,OAAO,CAAC;IAChB,gEAAgE;IAChE,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,yGAAyG;IACzG,WAAW,EAAE,KAAK,CAAC,OAAO,GAAG,aAAa,GAAG,UAAU,CAAC,CAAC;CAC1D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,GAAG,gBAAgB,CA6ChG"}
@@ -0,0 +1,88 @@
1
+ // receipt-check.ts — compute whether a buyer's claim and the seller's receipt
2
+ // it names actually agree.
3
+ //
4
+ // The claim already carries `offerReceiptHash`, the sha256 of the seller's
5
+ // x402 offer-receipt artifact exactly as transmitted. That binds the two halves
6
+ // by name. It does not say whether they tell the same story: nothing here or
7
+ // anywhere else recomputed the digest against a real artifact, or compared the
8
+ // fields the two sides both describe. Until it does, a contradiction between a
9
+ // buyer's account and the seller's own receipt stays a matter of reading rather
10
+ // than of arithmetic.
11
+ //
12
+ // Deliberately NOT in here: any verdict about who is right. A disagreement is
13
+ // reported as a disagreement. Adjudication belongs to whatever protocol the
14
+ // claim's `externalRefs.disputeContext` names, exactly as that field already
15
+ // states.
16
+ //
17
+ // The receipt is never re-canonicalised, parsed, or re-serialised by us. The
18
+ // caller passes the raw artifact for the digest, and separately passes whatever
19
+ // fields it extracted with its own x402 tooling. That split is the point: this
20
+ // module cannot silently disagree with the spec about wire shape, because it
21
+ // never interprets the wire shape.
22
+ import { createHash } from "node:crypto";
23
+ /** sha256 of the artifact exactly as given, lower-case hex, no prefix. No normalisation of any kind. */
24
+ export function receiptDigest(artifact) {
25
+ return createHash("sha256").update(artifact).digest("hex");
26
+ }
27
+ /** Does this claim actually name this artifact? Recomputes the digest rather than trusting the field. */
28
+ export function checkReceiptBinding(claim, artifact) {
29
+ const digest = receiptDigest(artifact);
30
+ if (claim.offerReceiptHash === undefined)
31
+ return { bound: false, reason: "claim_names_no_receipt", digest };
32
+ if (claim.offerReceiptHash !== digest) {
33
+ return { bound: false, reason: "digest_mismatch", digest, named: claim.offerReceiptHash };
34
+ }
35
+ return { bound: true, digest };
36
+ }
37
+ /**
38
+ * Compare the two accounts field by field. Only fields both sides describe are
39
+ * compared; a missing receipt field is reported as not compared, never as
40
+ * agreement.
41
+ */
42
+ export function compareClaimToReceipt(claim, receipt) {
43
+ const disagreements = [];
44
+ const notCompared = [];
45
+ if (receipt.payer === undefined) {
46
+ notCompared.push("payer");
47
+ }
48
+ else if (receipt.payer.toLowerCase() !== claim.buyerAddress.toLowerCase()) {
49
+ disagreements.push({
50
+ field: "payer",
51
+ claim: claim.buyerAddress,
52
+ receipt: receipt.payer,
53
+ note: "the receipt names a different paying party than the claim was signed for",
54
+ });
55
+ }
56
+ if (receipt.transaction === undefined) {
57
+ notCompared.push("transaction");
58
+ }
59
+ else if (receipt.transaction.toLowerCase() !== claim.settlementRef.toLowerCase()) {
60
+ disagreements.push({
61
+ field: "transaction",
62
+ claim: claim.settlementRef,
63
+ receipt: receipt.transaction,
64
+ note: "the two halves are about different settlements",
65
+ });
66
+ }
67
+ if (receipt.issuedAt === undefined) {
68
+ notCompared.push("issuedAt");
69
+ }
70
+ else {
71
+ const r = typeof receipt.issuedAt === "number" ? receipt.issuedAt * 1000 : Date.parse(receipt.issuedAt);
72
+ const c = Date.parse(claim.timestamp);
73
+ if (!Number.isFinite(r) || Number.isNaN(c)) {
74
+ notCompared.push("issuedAt");
75
+ }
76
+ else if (r > c) {
77
+ disagreements.push({
78
+ field: "issuedAt",
79
+ claim: claim.timestamp,
80
+ receipt: String(receipt.issuedAt),
81
+ note: "the seller issued the receipt after the buyer signed the claim about it",
82
+ });
83
+ }
84
+ }
85
+ const compared = 3 - notCompared.length;
86
+ return { agrees: compared > 0 && disagreements.length === 0, disagreements, notCompared };
87
+ }
88
+ //# sourceMappingURL=receipt-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt-check.js","sourceRoot":"","sources":["../src/receipt-check.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2BAA2B;AAC3B,EAAE;AACF,2EAA2E;AAC3E,gFAAgF;AAChF,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,sBAAsB;AACtB,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,6EAA6E;AAC7E,UAAU;AACV,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,mCAAmC;AAEnC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA6BzC,wGAAwG;AACxG,MAAM,UAAU,aAAa,CAAC,QAA6B;IACzD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,mBAAmB,CAAC,KAAgB,EAAE,QAA6B;IACjF,MAAM,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,KAAK,CAAC,gBAAgB,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,wBAAwB,EAAE,MAAM,EAAE,CAAC;IAC5G,IAAI,KAAK,CAAC,gBAAgB,KAAK,MAAM,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,gBAAgB,EAAE,CAAC;IAC5F,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACjC,CAAC;AAkBD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAgB,EAAE,OAAsB;IAC5E,MAAM,aAAa,GAAmB,EAAE,CAAC;IACzC,MAAM,WAAW,GAAoC,EAAE,CAAC;IAExD,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5B,CAAC;SAAM,IAAI,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,EAAE,CAAC;QAC5E,aAAa,CAAC,IAAI,CAAC;YACjB,KAAK,EAAE,OAAO;YACd,KAAK,EAAE,KAAK,CAAC,YAAY;YACzB,OAAO,EAAE,OAAO,CAAC,KAAK;YACtB,IAAI,EAAE,0EAA0E;SACjF,CAAC,CAAC;IACL,CAAC;IAED,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACtC,WAAW,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAClC,CAAC;SAAM,IAAI,OAAO,CAAC,WAAW,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,aAAa,CAAC,WAAW,EAAE,EAAE,CAAC;QACnF,aAAa,CAAC,IAAI,CAAC;YACjB,KAAK,EAAE,aAAa;YACpB,KAAK,EAAE,KAAK,CAAC,aAAa;YAC1B,OAAO,EAAE,OAAO,CAAC,WAAW;YAC5B,IAAI,EAAE,gDAAgD;SACvD,CAAC,CAAC;IACL,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC/B,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,GAAG,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACxG,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/B,CAAC;aAAM,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACjB,aAAa,CAAC,IAAI,CAAC;gBACjB,KAAK,EAAE,UAAU;gBACjB,KAAK,EAAE,KAAK,CAAC,SAAS;gBACtB,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;gBACjC,IAAI,EAAE,yEAAyE;aAChF,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC;IACxC,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAG,CAAC,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC;AAC5F,CAAC"}