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.
- package/README.md +122 -93
- package/README.nl.md +205 -0
- package/dist/bench-helpers.d.ts +12 -0
- package/dist/bench-helpers.d.ts.map +1 -0
- package/dist/bench-helpers.js +51 -0
- package/dist/bench-helpers.js.map +1 -0
- package/dist/discovery.bench.d.ts +2 -0
- package/dist/discovery.bench.d.ts.map +1 -0
- package/dist/discovery.bench.js +30 -0
- package/dist/discovery.bench.js.map +1 -0
- package/dist/discovery.d.ts +2 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +57 -2
- package/dist/discovery.js.map +1 -1
- package/dist/eas.bench.d.ts +2 -0
- package/dist/eas.bench.d.ts.map +1 -0
- package/dist/eas.bench.js +40 -0
- package/dist/eas.bench.js.map +1 -0
- package/dist/eas.d.ts +89 -7
- package/dist/eas.d.ts.map +1 -1
- package/dist/eas.js +151 -34
- package/dist/eas.js.map +1 -1
- package/dist/erc8004-reputation.d.ts +1 -0
- package/dist/erc8004-reputation.d.ts.map +1 -1
- package/dist/http-source.bench.d.ts +2 -0
- package/dist/http-source.bench.d.ts.map +1 -0
- package/dist/http-source.bench.js +65 -0
- package/dist/http-source.bench.js.map +1 -0
- package/dist/http-source.d.ts +28 -0
- package/dist/http-source.d.ts.map +1 -0
- package/dist/http-source.js +154 -0
- package/dist/http-source.js.map +1 -0
- package/dist/receipt-authorization.d.ts +77 -0
- package/dist/receipt-authorization.d.ts.map +1 -0
- package/dist/receipt-authorization.js +128 -0
- package/dist/receipt-authorization.js.map +1 -0
- package/dist/receipt-check.d.ts +58 -0
- package/dist/receipt-check.d.ts.map +1 -0
- package/dist/receipt-check.js +88 -0
- package/dist/receipt-check.js.map +1 -0
- package/dist/receipt-issue.d.ts +181 -0
- package/dist/receipt-issue.d.ts.map +1 -0
- package/dist/receipt-issue.js +177 -0
- package/dist/receipt-issue.js.map +1 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +55 -1
- package/dist/schema.js.map +1 -1
- 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":"AAAA,sEAAsE;AACtE,0EAA0E;AAC1E,oEAAoE;AACpE,2EAA2E;AAC3E,4EAA4E;AAC5E,6EAA6E;AAC7E,yEAAyE;AACzE,+DAA+D;AAC/D,EAAE;AACF,mEAAmE;AACnE,2EAA2E;AAC3E,qEAAqE;AACrE,wEAAwE;AACxE,yEAAyE;AACzE,4EAA4E;AAC5E,6EAA6E;AAC7E,2EAA2E;AAC3E,qDAAqD;AACrD,EAAE;AACF,0EAA0E;AAC1E,yEAAyE;AACzE,iCAAiC;AAGjC,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAEjE,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,6EAA6E;AAC7E,2EAA2E;AAC3E,wEAAwE;AACxE,wEAAwE;AACxE,0EAA0E;AAC1E,6EAA6E;AAC7E,0EAA0E;AAC1E,yEAAyE;AACzE,wDAAwD;AACxD,MAAM,0BAA0B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEpD,wEAAwE;AACxE,gFAAgF;AAChF,2EAA2E;AAC3E,6BAA6B;AAC7B,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,+DAA+D;AAC/D,4EAA4E;AAC5E,2EAA2E;AAC3E,0EAA0E;AAC1E,0EAA0E;AAC1E,uEAAuE;AACvE,sEAAsE;AACtE,wEAAwE;AACxE,mEAAmE;AACnE,MAAM,aAAa,GAAG,cAAc,CAAC;AAYrC;;;;GAIG;AACH,KAAK,UAAU,WAAW,CAAC,QAAkB,EAAE,QAAgB;IAC7D,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC3B,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;IAChC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,SAAS,CAAC;QACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QAC5C,IAAI,IAAI;YAAE,MAAM;QAChB,IAAI,KAAK,EAAE,CAAC;YACV,KAAK,IAAI,KAAK,CAAC,UAAU,CAAC;YAC1B,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;gBACrB,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBACtC,MAAM,IAAI,eAAe,CAAC,qBAAqB,QAAQ,gCAAgC,CAAC,CAAC;YAC3F,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC5E,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,OAA+B,EAAE;IACpF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC;IACjC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC;IACvD,MAAM,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,0BAA0B,CAAC;IAC7E,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IAC1C,OAAO;QACL,IAAI;QACJ,cAAc,EAAE,KAAK,EAAE,aAAqB,EAA4B,EAAE;YACxE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;gBACvC,MAAM,IAAI,eAAe,CAAC,2DAA2D,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;YACxH,CAAC;YACD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;YACjC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;YAE9C,IAAI,QAAkB,CAAC;YACvB,IAAI,CAAC;gBACH,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YAC9E,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,IAAI,eAAe,CAAC,cAAc,GAAG,YAAa,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACjF,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,eAAe,CAAC,GAAG,GAAG,cAAc,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;YAC1F,CAAC;YAED,sEAAsE;YACtE,mEAAmE;YACnE,uEAAuE;YACvE,sEAAsE;YACtE,sEAAsE;YACtE,kEAAkE;YAClE,6DAA6D;YAC7D,IAAI,IAAY,CAAC;YACjB,IAAI,CAAC;gBACH,IAAI,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;YACvD,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,CAAC,YAAY,eAAe;oBAAE,MAAM,CAAC,CAAC,CAAC,8CAA8C;gBACzF,MAAM,IAAI,eAAe,CAAC,8BAA8B,GAAG,YAAa,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACjG,CAAC;YACD,IAAI,MAAe,CAAC;YACpB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,IAAI,eAAe,CAAC,iBAAiB,GAAG,wBAAyB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YAChG,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,eAAe,CAAC,iBAAiB,GAAG,kCAAkC,CAAC,CAAC;YACpF,CAAC;YACD,wEAAwE;YACxE,mEAAmE;YACnE,gEAAgE;YAChE,qEAAqE;YACrE,qEAAqE;YACrE,mEAAmE;YACnE,OAAO,MAAyB,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -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":"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"}
|