agent-receipt-verify 0.0.0-stage → 0.2.2

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 (40) hide show
  1. package/CONFORMANCE.md +110 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +39 -0
  4. package/README.md +273 -2
  5. package/bench/eip3009-beleid.mjs +113 -0
  6. package/bench/robustness.mjs +264 -0
  7. package/bin/cli.mjs +95 -0
  8. package/examples/receipt-live-base-sepolia-2.json +21 -0
  9. package/examples/receipt-live-base-sepolia-3.json +21 -0
  10. package/examples/receipt-live-base-sepolia.json +21 -0
  11. package/package.json +61 -4
  12. package/profiles/profile.schema.json +115 -0
  13. package/profiles/x402-deployed-ladder-v1.json +178 -0
  14. package/src/eip3009-autorisatie.mjs +228 -0
  15. package/src/inam-countersigned.mjs +162 -0
  16. package/src/mcp-server.mjs +262 -0
  17. package/src/profiel.mjs +145 -0
  18. package/src/x402-conformance-check.mjs +194 -0
  19. package/src/x402-conformance-treden.mjs +246 -0
  20. package/src/x402-discovery-census.mjs +247 -0
  21. package/src/x402-index-liveness.mjs +201 -0
  22. package/src/x402-link-onchain.mjs +280 -0
  23. package/src/x402-metingen-bundel.mjs +96 -0
  24. package/src/x402-offer-receipt.mjs +84 -0
  25. package/src/x402-poortwachters.mjs +241 -0
  26. package/tools/bundel-bouw.mjs +126 -0
  27. package/tools/pack-inhoud.mjs +96 -0
  28. package/vectors/inam/BRON-README.md +38 -0
  29. package/vectors/inam/canonical-vectors.json +122 -0
  30. package/vectors/inam/vectors.json +96 -0
  31. package/vectors/kanon/amount-insufficient-001.json +52 -0
  32. package/vectors/kanon/cross-chain-replay-001.json +54 -0
  33. package/vectors/kanon/cross-contract-replay-001.json +54 -0
  34. package/vectors/kanon/domain-name-mismatch-001.json +53 -0
  35. package/vectors/kanon/expired-001.json +51 -0
  36. package/vectors/kanon/network-mismatch-001.json +52 -0
  37. package/vectors/kanon/nonce-replay-001.json +56 -0
  38. package/vectors/kanon/not-yet-valid-001.json +51 -0
  39. package/vectors/kanon/sig-malleable-high-s-001.json +54 -0
  40. package/vectors/kanon/valid-baseline-001.json +52 -0
@@ -0,0 +1,115 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/holistis/agent-receipt-verify/blob/main/profiles/profile.schema.json",
4
+ "title": "Deployed-endpoint conformance profile",
5
+ "description": "A versioned, declarative description of what can be checked on someone else's deployed endpoint from the response it already returns. A profile names the clause each check comes from, the input the check needs, and what to report when the check cannot decide. It does not describe how to run the check; that is the runner's job, and the evaluator field is the name that binds the two.",
6
+ "type": "object",
7
+ "required": ["profile", "version", "published", "intent", "outcomes", "specs", "checks"],
8
+ "additionalProperties": true,
9
+ "properties": {
10
+ "profile": { "type": "string", "description": "Stable name of the profile, independent of its version." },
11
+ "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
12
+ "published": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", "description": "A profile without a date cannot be told apart from a later one, and then a published number cannot be traced back to the ladder it was measured on." },
13
+ "intent": { "type": "string", "minLength": 20 },
14
+ "notMeasured": { "type": "string", "description": "What this profile deliberately says nothing about. A profile that only lists what it checks invites the reader to assume the rest was fine." },
15
+ "outcomes": {
16
+ "type": "object",
17
+ "description": "Exactly four, and each one described. Three is the common mistake: it collapses 'we did not try' into 'we could not decide', and a reader cannot then tell a gap in the runner from a defect in the endpoint.",
18
+ "required": ["pass", "fail", "skip", "inconclusive"],
19
+ "additionalProperties": false,
20
+ "properties": {
21
+ "pass": { "type": "string", "minLength": 10 },
22
+ "fail": { "type": "string", "minLength": 10 },
23
+ "skip": { "type": "string", "minLength": 10 },
24
+ "inconclusive": { "type": "string", "minLength": 10 }
25
+ }
26
+ },
27
+ "wireStates": { "type": "object", "description": "Optional. How a particular runner's own state names map onto the four outcomes, so a consumer does not have to read that runner's source." },
28
+ "specs": {
29
+ "type": "object",
30
+ "description": "Every specification this profile cites, pinned. A clause reference without a pinned version is a reference to whatever the document says today.",
31
+ "minProperties": 1,
32
+ "additionalProperties": {
33
+ "type": "object",
34
+ "required": ["repo", "path", "commit", "state"],
35
+ "additionalProperties": true,
36
+ "properties": {
37
+ "repo": { "type": "string" },
38
+ "path": { "type": "string" },
39
+ "commit": { "type": "string", "minLength": 7 },
40
+ "state": { "type": "string", "enum": ["merged on main", "proposed, not merged"] },
41
+ "pullRequest": { "type": "integer" },
42
+ "retrieved": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
43
+ "warning": { "type": "string" }
44
+ }
45
+ }
46
+ },
47
+ "relatedWork": { "type": "object", "description": "Optional. Other vector or profile sets this one references instead of restating." },
48
+ "checks": {
49
+ "type": "array",
50
+ "minItems": 1,
51
+ "items": {
52
+ "type": "object",
53
+ "required": ["id", "key", "level", "name", "requirement", "clause", "input", "assertion", "normative", "whenNotObservable"],
54
+ "additionalProperties": true,
55
+ "properties": {
56
+ "id": { "type": "string", "pattern": "^[A-Z]+-[A-Za-z0-9]+$" },
57
+ "key": { "type": "string", "description": "Short key a runner uses in its own output. Kept separate from id so a runner's existing output does not have to change when a profile is adopted." },
58
+ "level": { "type": "integer", "minimum": 0 },
59
+ "name": { "type": "string", "minLength": 5 },
60
+ "requirement": { "type": "string", "minLength": 20 },
61
+ "clause": {
62
+ "type": "array",
63
+ "minItems": 1,
64
+ "description": "Where the requirement comes from. Each entry names a key of specs, so the pin travels with the reference.",
65
+ "items": {
66
+ "type": "object",
67
+ "required": ["spec", "section"],
68
+ "additionalProperties": true,
69
+ "properties": {
70
+ "spec": { "type": "string" },
71
+ "section": { "type": "string", "minLength": 3 },
72
+ "quote": { "type": "string" }
73
+ }
74
+ }
75
+ },
76
+ "input": {
77
+ "type": "object",
78
+ "required": ["needs", "pays"],
79
+ "additionalProperties": true,
80
+ "properties": {
81
+ "needs": { "type": "array", "minItems": 1, "items": { "type": "string" } },
82
+ "requests": { "type": "integer", "minimum": 0 },
83
+ "pays": { "type": "boolean", "description": "Whether the check needs a real settlement. A passive profile must skip these rather than fail them." }
84
+ }
85
+ },
86
+ "assertion": {
87
+ "type": "object",
88
+ "required": ["evaluator", "describes"],
89
+ "additionalProperties": false,
90
+ "properties": {
91
+ "evaluator": { "type": "string", "description": "Name of the routine that decides this check. Deliberately a name and not an expression language: the two checks that matter here involve signature recovery and a chain read, and pretending those fit in a data expression would mean either a dishonest profile or an expression language nobody else implements." },
92
+ "describes": { "type": "string", "minLength": 20, "description": "What is actually asserted, in plain words, so a second implementation can be written from the profile alone." }
93
+ }
94
+ },
95
+ "normative": { "type": "string", "enum": ["MUST", "SHOULD"] },
96
+ "whenNotObservable": { "type": "string", "enum": ["skip", "inconclusive"] },
97
+ "inconclusiveWhen": { "type": "array", "items": { "type": "string" } },
98
+ "relatedVectors": {
99
+ "type": "array",
100
+ "items": {
101
+ "type": "object",
102
+ "required": ["set", "id"],
103
+ "additionalProperties": true,
104
+ "properties": {
105
+ "set": { "type": "string" },
106
+ "id": { "type": "string" },
107
+ "subject": { "type": "string" }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,178 @@
1
+ {
2
+ "$schema": "./profile.schema.json",
3
+ "profile": "x402-deployed-ladder",
4
+ "version": "1.0.0",
5
+ "published": "2026-10-17",
6
+ "intent": "Passive audit of a deployed third-party x402 endpoint from the response it already returns, without cooperation from its operator and without paying.",
7
+ "notMeasured": "Whether the party behind the endpoint is honest, whether the service does what it advertises, whether a price is fair, and anything that needs a settlement. See CONFORMANCE.md for the full list.",
8
+ "outcomes": {
9
+ "pass": "The check ran and the response satisfies it.",
10
+ "fail": "The check ran and the response does not satisfy it.",
11
+ "skip": "The check was deliberately not attempted, because this profile is passive and the check needs something a passive run will not do, such as paying.",
12
+ "inconclusive": "The check was attempted and could not decide, for example because a signature algorithm or a key location is outside what the runner supports. Never report this as fail: a runner's own gap is not a defect of the endpoint."
13
+ },
14
+ "wireStates": {
15
+ "note": "The reference runner in this repository emits three states for backward compatibility. This table maps them onto the four outcomes above, so a consumer of this profile can translate without reading our code.",
16
+ "HAALT": "pass",
17
+ "HAALT_NIET": "fail",
18
+ "ONGEMETEN": "skip when the check declares whenNotObservable skip, inconclusive otherwise"
19
+ },
20
+ "specs": {
21
+ "x402-http-v2": {
22
+ "repo": "x402-foundation/x402",
23
+ "path": "specs/transports-v2/http.md",
24
+ "commit": "faa1c694c66a",
25
+ "state": "merged on main",
26
+ "retrieved": "2026-10-17"
27
+ },
28
+ "offer-receipt": {
29
+ "repo": "x402-foundation/x402",
30
+ "path": "specs/extensions/extension-offer-and-receipt.md",
31
+ "commit": "faa1c694c66a",
32
+ "state": "merged on main",
33
+ "retrieved": "2026-10-17"
34
+ },
35
+ "offer-receipt-commitment": {
36
+ "repo": "x402-foundation/x402",
37
+ "path": "specs/extensions/extension-offer-and-receipt.md",
38
+ "commit": "0e492d6",
39
+ "state": "proposed, not merged",
40
+ "pullRequest": 3827,
41
+ "retrieved": "2026-10-17",
42
+ "warning": "Sections 5.6 through 5.6.5 do not exist in the version on main. They exist only on this pull request, which is open. The pinned commit is four commits ahead of main and is fetchable, but the section numbers can still move before it merges. Any check referencing this spec entry is measuring against a proposal, and a result carrying it should say so."
43
+ }
44
+ },
45
+ "relatedWork": {
46
+ "x402-core-vectors": {
47
+ "repo": "x402-foundation/x402",
48
+ "path": "conformance/vectors/x402-core/",
49
+ "pullRequest": 2776,
50
+ "state": "proposed, not merged",
51
+ "note": "That set names a requirement and the request that exercises it, for an implementer proving their own implementation. Its expected field is the same sentence on every vector, so it carries no machine-checkable expectation. This profile references its ids where the subject overlaps rather than restating the requirement, and adds the three things a passive run needs: which input the check requires, what is actually asserted, and what to report when the check cannot decide. Raised as a question in issue 3396."
52
+ }
53
+ },
54
+ "checks": [
55
+ {
56
+ "id": "ARV-L0",
57
+ "key": "t0",
58
+ "level": 0,
59
+ "name": "asks for payment the way x402 says",
60
+ "requirement": "HTTP 402 carrying accepts[] with scheme, network, payTo and an amount, in the body or in the Payment-Required header",
61
+ "clause": [
62
+ { "spec": "x402-http-v2", "section": "Payment Required Signaling" },
63
+ { "spec": "x402-http-v2", "section": "Header Summary" }
64
+ ],
65
+ "input": { "needs": ["response status", "response headers", "response body"], "requests": 1, "pays": false },
66
+ "assertion": {
67
+ "evaluator": "statusAndAccepts",
68
+ "describes": "status is 402, and an accepts entry is reachable either in the JSON body or base64 in the Payment-Required header, carrying all four fields"
69
+ },
70
+ "normative": "MUST",
71
+ "whenNotObservable": "inconclusive",
72
+ "relatedVectors": [
73
+ { "set": "x402-core-vectors", "id": "X4-001", "subject": "402 payment challenge headers present" }
74
+ ]
75
+ },
76
+ {
77
+ "id": "ARV-L1",
78
+ "key": "t1",
79
+ "level": 1,
80
+ "name": "declares the offer-receipt extension",
81
+ "requirement": "the 402 carries an offer-receipt entry in extensions",
82
+ "clause": [
83
+ { "spec": "offer-receipt", "section": "4.1 Placement", "quote": "extensions[\"offer-receipt\"].info.offers[]" }
84
+ ],
85
+ "input": { "needs": ["the same 402 as ARV-L0"], "requests": 0, "pays": false },
86
+ "assertion": {
87
+ "evaluator": "extensionDeclared",
88
+ "describes": "extensions carries an offer-receipt key, counted from either the object form or the array form"
89
+ },
90
+ "normative": "SHOULD",
91
+ "whenNotObservable": "inconclusive",
92
+ "relatedVectors": []
93
+ },
94
+ {
95
+ "id": "ARV-L2",
96
+ "key": "t2",
97
+ "level": 2,
98
+ "name": "signs its offer, and the signature verifies",
99
+ "requirement": "extensions[\"offer-receipt\"].info.offers[0] is a signed artifact whose signature verifies: either an eip712 artifact recovering to an address, or a jws with an EdDSA signature over the offer payload whose key resolves from its kid",
100
+ "clause": [
101
+ { "spec": "offer-receipt", "section": "3.1 Common Object Shape" },
102
+ { "spec": "offer-receipt", "section": "3.3 JWS Header Requirements" },
103
+ { "spec": "offer-receipt", "section": "4.3 EIP-712 Types for Offer (Normative Schema)" },
104
+ { "spec": "offer-receipt", "section": "4.5 Offer Verification" },
105
+ { "spec": "offer-receipt", "section": "4.5.1 Signer Authorization" }
106
+ ],
107
+ "input": { "needs": ["the same 402 as ARV-L0", "for a jws offer with a did:web kid: one https fetch of that did document"], "requests": 0, "pays": false },
108
+ "assertion": {
109
+ "evaluator": "offerSignatureVerifies",
110
+ "describes": "the artifact's format is recognised and its signature verifies over the offer payload"
111
+ },
112
+ "normative": "SHOULD",
113
+ "whenNotObservable": "inconclusive",
114
+ "inconclusiveWhen": [
115
+ "the signature algorithm is not one this runner implements",
116
+ "the key cannot be resolved, for example a did:web document that does not answer or whose publicKeyMultibase is not multibase over multicodec 0xed01",
117
+ "the optional ethers dependency is absent, which is what an eip712 recovery needs"
118
+ ],
119
+ "relatedVectors": []
120
+ },
121
+ {
122
+ "id": "ARV-L3",
123
+ "key": "t3",
124
+ "level": 3,
125
+ "name": "emits a receipt that verifies",
126
+ "requirement": "after payment, the settlement response carries a receipt whose signature recovers to the same signer",
127
+ "clause": [
128
+ { "spec": "offer-receipt", "section": "5.1 Placement", "quote": "extensions[\"offer-receipt\"].info.receipt" },
129
+ { "spec": "offer-receipt", "section": "5.5 Receipt Verification" }
130
+ ],
131
+ "input": { "needs": ["a settlement response from a paid request"], "requests": 1, "pays": true },
132
+ "assertion": {
133
+ "evaluator": "receiptSignatureVerifies",
134
+ "describes": "a receipt is present and its signature recovers to the signer that signed the offer"
135
+ },
136
+ "normative": "SHOULD",
137
+ "whenNotObservable": "skip",
138
+ "relatedVectors": []
139
+ },
140
+ {
141
+ "id": "ARV-L4",
142
+ "key": "t4",
143
+ "level": 4,
144
+ "name": "receipt is version 2 and the salt arrives where 5.6.3 says",
145
+ "requirement": "payload.version is 2 with both commitment fields, and the salt is at extensions[\"offer-receipt\"].info.authorizationSalt",
146
+ "clause": [
147
+ { "spec": "offer-receipt-commitment", "section": "5.6 Authorization Commitment (receipt version 2)" },
148
+ { "spec": "offer-receipt-commitment", "section": "5.6.3 Salt" }
149
+ ],
150
+ "input": { "needs": ["a settlement response from a paid request"], "requests": 0, "pays": true },
151
+ "assertion": {
152
+ "evaluator": "receiptVersionTwoAndSaltPlacement",
153
+ "describes": "payload.version is 2, both commitment fields are present, and the salt sits outside the signed payload at the declared location"
154
+ },
155
+ "normative": "MUST",
156
+ "whenNotObservable": "skip",
157
+ "relatedVectors": []
158
+ },
159
+ {
160
+ "id": "ARV-L5",
161
+ "key": "t5",
162
+ "level": 5,
163
+ "name": "the commitment opens against the chain to exactly one settlement",
164
+ "requirement": "section 5.6.4 run against live chain state resolves to exactly one AuthorizationUsed nonce",
165
+ "clause": [
166
+ { "spec": "offer-receipt-commitment", "section": "5.6.4 Linking a receipt to its settlement (eip3009)" }
167
+ ],
168
+ "input": { "needs": ["a receipt", "its authorization salt", "the asset that was paid", "a chain read"], "requests": 0, "pays": true },
169
+ "assertion": {
170
+ "evaluator": "commitmentOpensOnExactlyOneNonce",
171
+ "describes": "recomputing the commitment over candidate AuthorizationUsed nonces opens on exactly one; zero and more than one both fail closed"
172
+ },
173
+ "normative": "MUST",
174
+ "whenNotObservable": "skip",
175
+ "relatedVectors": []
176
+ }
177
+ ]
178
+ }
@@ -0,0 +1,228 @@
1
+ // Een onafhankelijke verifieerder voor EIP-3009 betaalautorisaties, geschreven tegen
2
+ // de vectoren van iamonuwa/kanon en niet tegen zijn code.
3
+ //
4
+ // WAAROM DIT HIER STAAT, want het is de ANDERE kant van wat deze repo tot nu toe deed.
5
+ // Alles hier ging over de VERKOPER: zijn ondertekende aanbod, zijn bonnetje, en de
6
+ // vraag of dat bonnetje op precies een afrekening op de keten opent. Een EIP-3009
7
+ // autorisatie is de KOPER-kant: de handtekening waarmee een agent zijn geld vrijgeeft,
8
+ // voor er iets afgerekend is. Samen zijn dat de twee uiteinden van dezelfde transactie,
9
+ // en ze overlappen nergens. Dat is precies waarom een tweede implementatie hier iets
10
+ // toevoegt in plaats van iets te verdubbelen.
11
+ //
12
+ // DE REGEL DIE DEZE HELE REPO DRAAGT, ook hier: geschreven tegen de GEPUBLICEERDE
13
+ // vectoren en de specificaties (EIP-3009, EIP-712), nooit tegen de broncode van de
14
+ // referentie-implementatie. Twee implementaties die elkaars code niet gezien hebben en
15
+ // toch op dezelfde uitslagen komen, dat is interop. Een implementatie die met zichzelf
16
+ // instemt bewijst niets.
17
+ //
18
+ // DE VOLGORDE VAN DE CONTROLES IS ZELF EEN UITSPRAAK, en zij volgt uit zijn eigen
19
+ // beschrijvingen bij de vectoren:
20
+ // 1. netwerk (platte tekst, voor de handtekening; zijn tekst: "the plaintext mismatch")
21
+ // 2. kneedbaarheid van de handtekening (voor het herstellen, anders herstel je een
22
+ // tweede geldige vorm van dezelfde handtekening)
23
+ // 3. geldigheidsvenster
24
+ // 4. herstellen en de ondertekenaar vergelijken
25
+ // 5. bedrag (zijn tekst: "caught only after the signature verifies")
26
+ // 6. nonce al verbruikt
27
+ // Een andere volgorde geeft bij een vector met twee gebreken een andere redencode. Waar
28
+ // wij van hem afwijken hoort dat GEMELD te worden en niet weggepoetst tot het groen is.
29
+ //
30
+ // ethers is optioneel in dit pakket. Zonder dat pakket kan stap 4 niet en komt de
31
+ // uitslag terug als ONGEMETEN, nooit als afgekeurd: een gat in onze meter is geen
32
+ // gebrek van de betaling.
33
+
34
+ import { laadEthers } from './x402-conformance-treden.mjs'
35
+
36
+ /** De helft van de secp256k1-groepsorde. Daarboven is een handtekening kneedbaar. */
37
+ const HALVE_ORDE = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0n
38
+
39
+ export const REDENEN = [
40
+ 'VALID',
41
+ 'NETWORK_MISMATCH',
42
+ 'SIG_MALLEABLE',
43
+ 'SIGNER_MISMATCH',
44
+ 'NOT_YET_VALID',
45
+ 'EXPIRED',
46
+ 'NONCE_REPLAY',
47
+ 'AMOUNT_INSUFFICIENT',
48
+ ]
49
+
50
+ /** De EIP-712-types van EIP-3009, normatief en niet over de lijn verzonden. */
51
+ export const TRANSFER_TYPES = {
52
+ TransferWithAuthorization: [
53
+ { name: 'from', type: 'address' },
54
+ { name: 'to', type: 'address' },
55
+ { name: 'value', type: 'uint256' },
56
+ { name: 'validAfter', type: 'uint256' },
57
+ { name: 'validBefore', type: 'uint256' },
58
+ { name: 'nonce', type: 'bytes32' },
59
+ ],
60
+ }
61
+
62
+ /** `eip155:84532` naar 84532. Geeft null bij een vorm die wij niet kennen. */
63
+ export function chainIdUitNetwerk(netwerk) {
64
+ const m = /^eip155:(\d+)$/.exec(String(netwerk ?? ''))
65
+ return m ? Number(m[1]) : null
66
+ }
67
+
68
+ /**
69
+ * Het domein waaronder deze autorisatie hoort te zijn ondertekend.
70
+ *
71
+ * Alle drie de replay-vectoren draaien hierom: wie onder een ander domein tekent
72
+ * (andere keten, ander contract, andere naam) levert een handtekening die op een ANDER
73
+ * adres herstelt. Daarom is er geen aparte controle voor cross-chain of cross-contract
74
+ * nodig: het domein doet dat werk, en de uitslag heet dan terecht SIGNER_MISMATCH.
75
+ */
76
+ export function domeinUit(accepted) {
77
+ return {
78
+ name: accepted?.extra?.name,
79
+ version: accepted?.extra?.version,
80
+ chainId: chainIdUitNetwerk(accepted?.network),
81
+ verifyingContract: accepted?.asset,
82
+ }
83
+ }
84
+
85
+ /** Is de s-waarde van deze handtekening boven de halve groepsorde. */
86
+ export function isKneedbaar(signature) {
87
+ const h = String(signature ?? '').replace(/^0x/i, '')
88
+ if (h.length < 130) return false
89
+ const s = BigInt('0x' + h.slice(64, 128))
90
+ return s > HALVE_ORDE
91
+ }
92
+
93
+ /**
94
+ * Beoordeelt een enkele autorisatie.
95
+ *
96
+ * Geeft { valid, reason_code } terug in exact de vorm die kanon se vectoren in hun
97
+ * `expected` gebruiken, plus `ongemeten` met een reden als de controle niet KON
98
+ * draaien. Die derde mogelijkheid staat niet in zijn schema, en dat is precies het
99
+ * punt dat wij in x402#3396 open hebben gelegd: een gat in de verifieerder is iets
100
+ * anders dan een afgekeurde betaling.
101
+ */
102
+ export async function beoordeelAutorisatie(input, context = {}) {
103
+ const doelNetwerk = context.doelNetwerk ?? input?.accepted?.network
104
+ const accepted = input?.accepted ?? {}
105
+ const auth = input?.payload?.authorization ?? {}
106
+ const signature = input?.payload?.signature
107
+ const nu = Number(context.verification_time ?? Math.floor(Date.now() / 1000))
108
+
109
+ // 1. Netwerk, in platte tekst, voor er iets met de handtekening gebeurt.
110
+ if (String(doelNetwerk) !== String(accepted.network)) {
111
+ return { valid: false, reason_code: 'NETWORK_MISMATCH' }
112
+ }
113
+
114
+ // 2. Kneedbaarheid, voor het herstellen. Een hoge s is een tweede geldige vorm van
115
+ // dezelfde handtekening, en wie eerst herstelt accepteert die vorm stilletjes.
116
+ if (isKneedbaar(signature)) {
117
+ return { valid: false, reason_code: 'SIG_MALLEABLE' }
118
+ }
119
+
120
+ // 3. Het geldigheidsvenster.
121
+ if (nu < Number(auth.validAfter)) return { valid: false, reason_code: 'NOT_YET_VALID' }
122
+ if (nu > Number(auth.validBefore)) return { valid: false, reason_code: 'EXPIRED' }
123
+
124
+ // 4. Herstellen en de ondertekenaar vergelijken. Zonder ethers kan dit niet, en dan
125
+ // is de eerlijke uitslag ONGEMETEN.
126
+ const ethers = await laadEthers()
127
+ if (!ethers) {
128
+ return { valid: false, reason_code: null, ongemeten: true, reden: 'ethers ontbreekt, dus de handtekening is niet te herstellen' }
129
+ }
130
+ let hersteld
131
+ try {
132
+ hersteld = ethers.verifyTypedData(domeinUit(accepted), TRANSFER_TYPES, {
133
+ from: auth.from, to: auth.to, value: auth.value,
134
+ validAfter: auth.validAfter, validBefore: auth.validBefore, nonce: auth.nonce,
135
+ }, signature)
136
+ } catch (e) {
137
+ // Een handtekening die niet eens te herstellen is, is geen geldige ondertekenaar.
138
+ return { valid: false, reason_code: 'SIGNER_MISMATCH', detail: String(e?.message ?? e).slice(0, 120) }
139
+ }
140
+ if (String(hersteld).toLowerCase() !== String(auth.from).toLowerCase()) {
141
+ return { valid: false, reason_code: 'SIGNER_MISMATCH', hersteld }
142
+ }
143
+
144
+ // 5. Het bedrag, pas NA de handtekening: een authentiek ondertekende afwijking.
145
+ //
146
+ // DE STANDAARD NEEMT HIER GEEN POSITIE IN, DUS WIJ MAKEN DE KEUZE ZICHTBAAR. Twee
147
+ // referentie-implementaties eisen GELIJKHEID en wijzen dus ook te veel betalen af:
148
+ // go/mechanisms/evm/exact/facilitator/eip3009.go regel 65 (`Cmp(requiredValue) != 0`,
149
+ // fout ErrAuthorizationValueMismatch) en python/x402/mechanisms/evm/exact/
150
+ // facilitator.py regel 235 (`!=`), met op regel 168 de eigen zin "Amount exactly
151
+ // matches requirements.amount". Onze eerste versie liet te veel betalen toe en zou
152
+ // dus op dat ene geval van hen afwijken, terwijl geen enkele gepubliceerde vector
153
+ // het dekt. Daarom staat 'exact' nu als standaard, is 'at-least' een bewuste keuze,
154
+ // en draagt de uitslag ALTIJD welke van de twee gebruikt is.
155
+ const beleid = context.amountPolicy === 'at-least' ? 'at-least' : 'exact'
156
+ const betaald = BigInt(auth.value ?? 0)
157
+ const gevraagd = BigInt(accepted.amount ?? 0)
158
+ const bedragFout = beleid === 'exact' ? betaald !== gevraagd : betaald < gevraagd
159
+ if (bedragFout) {
160
+ return { valid: false, reason_code: 'AMOUNT_INSUFFICIENT', amountPolicy: beleid }
161
+ }
162
+
163
+ // 6. Is deze nonce al verbruikt, PER BETALER en niet over iedereen heen.
164
+ //
165
+ // EIP-3009 houdt dit zelf per adres bij: `mapping(address => mapping(bytes32 =>
166
+ // bool))`, en de leesfunctie heet `authorizationState(address authorizer, bytes32
167
+ // nonce)`. De spec zegt er bovendien bij dat nonces willekeurige 32 bytes zijn,
168
+ // uniek per adres, en juist geen volgnummers. Een verifieerder die EEN lijst
169
+ // bijhoudt zonder onderscheid naar betaler is dus STRENGER dan de keten zelf: hij
170
+ // zou een geldige betaling van een andere partij weigeren die toevallig dezelfde
171
+ // 32 bytes koos. Onze eerste versie deed precies dat.
172
+ //
173
+ // Twee vormen worden aanvaard. Een platte lijst hoort bij DEZE betaler, want zo
174
+ // gebruiken de gepubliceerde vectoren hem. Een object met het adres als sleutel is
175
+ // de nauwkeurige vorm en komt overeen met wat het token-contract doet.
176
+ const betaler = String(auth.from ?? '').toLowerCase()
177
+ const bron = context.seen_nonces
178
+ let gezien = []
179
+ if (Array.isArray(bron)) gezien = bron
180
+ else if (bron && typeof bron === 'object') {
181
+ for (const [adres, lijst] of Object.entries(bron)) {
182
+ if (String(adres).toLowerCase() === betaler) gezien = lijst ?? []
183
+ }
184
+ }
185
+ if (gezien.map((n) => String(n).toLowerCase()).includes(String(auth.nonce).toLowerCase())) {
186
+ return { valid: false, reason_code: 'NONCE_REPLAY' }
187
+ }
188
+
189
+ return { valid: true, reason_code: 'VALID', ondertekenaar: hersteld, amountPolicy: beleid }
190
+ }
191
+
192
+ /** Een hele vector beoordelen, met zijn eigen context en doelnetwerk. */
193
+ export async function beoordeelVector(vector) {
194
+ return beoordeelAutorisatie(vector.input, { ...(vector.context ?? {}), doelNetwerk: vector.network })
195
+ }
196
+
197
+ // Een commandoregel, zodat een vreemde de kruiscontrole in EEN regel kan narekenen in
198
+ // plaats van ons op ons woord te geloven. Dat is het hele punt van deze repo.
199
+ if (process.argv[1]?.endsWith('eip3009-autorisatie.mjs')) {
200
+ const fs = await import('node:fs')
201
+ const path = await import('node:path')
202
+ const { fileURLToPath } = await import('node:url')
203
+ const map = path.join(fileURLToPath(new URL('..', import.meta.url)), 'vectors', 'kanon')
204
+ if (!fs.existsSync(map)) {
205
+ console.log('geen vectoren gevonden in ' + map)
206
+ process.exitCode = 2
207
+ } else {
208
+ const bestanden = fs.readdirSync(map).filter((f) => f.endsWith('.json')).sort()
209
+ let eens = 0, oneens = 0, ongemeten = 0
210
+ console.log('Cross-check against iamonuwa/kanon, EIP-3009 payment authorizations')
211
+ console.log('Vectors pinned at commit 0d5a7461e996, Apache-2.0, redistributed unmodified.')
212
+ console.log('This implementation was written against those vectors and against EIP-3009')
213
+ console.log('and EIP-712, never against that project\'s source.')
214
+ console.log('')
215
+ for (const f of bestanden) {
216
+ const v = JSON.parse(fs.readFileSync(path.join(map, f), 'utf8'))
217
+ const u = await beoordeelVector(v)
218
+ if (u.ongemeten) { ongemeten++; console.log(' not measured ' + v.id + ' ' + u.reden); continue }
219
+ const zelfde = u.valid === v.expected.valid && u.reason_code === v.expected.reason_code
220
+ if (zelfde) { eens++; console.log(' agree ' + v.id.padEnd(42) + ' ' + u.reason_code) }
221
+ else { oneens++; console.log(' DISAGREE ' + v.id.padEnd(42) + ' ours ' + u.reason_code + ', theirs ' + v.expected.reason_code) }
222
+ }
223
+ console.log('')
224
+ console.log('agree on ' + eens + ' of ' + bestanden.length + ', disagree ' + oneens + ', not measured ' + ongemeten)
225
+ if (ongemeten) console.log('Install the optional ethers dependency to run the signature recovery.')
226
+ process.exitCode = oneens === 0 ? 0 : 1
227
+ }
228
+ }