@integraledger/agent-guard 0.9.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 (80) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +15 -0
  4. package/README.md +154 -0
  5. package/dist/decision.d.ts +26 -0
  6. package/dist/decision.d.ts.map +1 -0
  7. package/dist/decision.js +28 -0
  8. package/dist/decision.js.map +1 -0
  9. package/dist/evaluate.d.ts +15 -0
  10. package/dist/evaluate.d.ts.map +1 -0
  11. package/dist/evaluate.js +112 -0
  12. package/dist/evaluate.js.map +1 -0
  13. package/dist/fetch.d.ts +61 -0
  14. package/dist/fetch.d.ts.map +1 -0
  15. package/dist/fetch.js +126 -0
  16. package/dist/fetch.js.map +1 -0
  17. package/dist/fingerprint.d.ts +14 -0
  18. package/dist/fingerprint.d.ts.map +1 -0
  19. package/dist/fingerprint.js +15 -0
  20. package/dist/fingerprint.js.map +1 -0
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +15 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/log.d.ts +32 -0
  26. package/dist/log.d.ts.map +1 -0
  27. package/dist/log.js +8 -0
  28. package/dist/log.js.map +1 -0
  29. package/dist/mechanical.d.ts +30 -0
  30. package/dist/mechanical.d.ts.map +1 -0
  31. package/dist/mechanical.js +35 -0
  32. package/dist/mechanical.js.map +1 -0
  33. package/dist/policy.d.ts +36 -0
  34. package/dist/policy.d.ts.map +1 -0
  35. package/dist/policy.js +96 -0
  36. package/dist/policy.js.map +1 -0
  37. package/dist/proposal-ack.d.ts +111 -0
  38. package/dist/proposal-ack.d.ts.map +1 -0
  39. package/dist/proposal-ack.js +114 -0
  40. package/dist/proposal-ack.js.map +1 -0
  41. package/dist/proposal-acp.d.ts +18 -0
  42. package/dist/proposal-acp.d.ts.map +1 -0
  43. package/dist/proposal-acp.js +72 -0
  44. package/dist/proposal-acp.js.map +1 -0
  45. package/dist/proposal-ap2.d.ts +121 -0
  46. package/dist/proposal-ap2.d.ts.map +1 -0
  47. package/dist/proposal-ap2.js +112 -0
  48. package/dist/proposal-ap2.js.map +1 -0
  49. package/dist/proposal-mpp.d.ts +44 -0
  50. package/dist/proposal-mpp.d.ts.map +1 -0
  51. package/dist/proposal-mpp.js +106 -0
  52. package/dist/proposal-mpp.js.map +1 -0
  53. package/dist/proposal-universal.d.ts +239 -0
  54. package/dist/proposal-universal.d.ts.map +1 -0
  55. package/dist/proposal-universal.js +470 -0
  56. package/dist/proposal-universal.js.map +1 -0
  57. package/dist/proposal.d.ts +33 -0
  58. package/dist/proposal.d.ts.map +1 -0
  59. package/dist/proposal.js +107 -0
  60. package/dist/proposal.js.map +1 -0
  61. package/dist/transact.d.ts +24 -0
  62. package/dist/transact.d.ts.map +1 -0
  63. package/dist/transact.js +11 -0
  64. package/dist/transact.js.map +1 -0
  65. package/package.json +81 -0
  66. package/src/decision.ts +54 -0
  67. package/src/evaluate.ts +176 -0
  68. package/src/fetch.ts +169 -0
  69. package/src/fingerprint.ts +27 -0
  70. package/src/index.ts +68 -0
  71. package/src/log.ts +34 -0
  72. package/src/mechanical.ts +71 -0
  73. package/src/policy.ts +135 -0
  74. package/src/proposal-ack.ts +216 -0
  75. package/src/proposal-acp.ts +89 -0
  76. package/src/proposal-ap2.ts +195 -0
  77. package/src/proposal-mpp.ts +125 -0
  78. package/src/proposal-universal.ts +671 -0
  79. package/src/proposal.ts +170 -0
  80. package/src/transact.ts +34 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The ACK buyer surface: read an ACK-Pay `PaymentReceiptCredential` and say which rung of the record each
3
+ * half of it feeds.
4
+ *
5
+ * **There is deliberately no `GateProposal` here, and that is a finding about ACK rather than a gap.** Every
6
+ * other parser in this package turns a PROPOSAL into the typed inputs a buyer decides on. ACK has no
7
+ * proposal-time carrier to turn: read live at `agentcommercekit/ack@main` on 2026-07-30,
8
+ * `packages/ack-pay/src/schemas/valibot.ts` declares `paymentRequestSchema` as
9
+ * `{ id, description?, serviceCallback?, expiresAt?, paymentOptions }` — no `metadata`, no open field, no
10
+ * extension point anywhere on the request. The open map (`v.optional(v.record(v.string(), v.unknown()))`)
11
+ * exists on `paymentReceiptClaimSchema` ALONE. So an LCP reference reaches an ACK document only at receipt
12
+ * time, and manufacturing a `GateProposal` from a receipt would name a decision the buyer no longer has:
13
+ * the payment has already settled. What the receipt genuinely supports is post-settlement verification, and
14
+ * that is what this module produces.
15
+ *
16
+ * An ACK buyer that wants a decision BEFORE paying gets it the same way any Level 1–2 buyer does: from the
17
+ * seller's `/.well-known/legal-context.json` (LCP §2), which is out of band from ACK entirely.
18
+ */
19
+ import type { Artifact } from "@integraledger/lcp-evidence";
20
+ /**
21
+ * The DID methods ACK's own resolver registers — `packages/did/src/did-resolvers/get-did-resolver.ts`
22
+ * composes exactly four (`key-did-resolver`, `./web-did-resolver`, `jwks-did-resolver`,
23
+ * `./pkh-did-resolver`), read live 2026-07-30. A receipt naming any other method is one ACK's own chain
24
+ * could not have verified, so it is refused here rather than carried into a record as an identity nobody
25
+ * can resolve. `did:jwks` was still pending when earlier surveys of ACK were written and has since landed,
26
+ * which is why this list is cut from ACK's own resolver code rather than from any survey of it.
27
+ */
28
+ export declare const ACK_DID_METHODS: readonly string[];
29
+ /** What an issued ACK receipt yields, before anything is decided about it. */
30
+ export interface AckReceiptFacts {
31
+ /** The reference recovered from `credentialSubject.metadata`, through the placement's own manifest. */
32
+ readonly atrHash: `0x${string}`;
33
+ /** ACK's join key: the signed payment request this receipt attests, and no other. */
34
+ readonly paymentRequestToken: string;
35
+ /** Which of the request's `paymentOptions` was taken. */
36
+ readonly paymentOptionId: string;
37
+ /** `credentialSubject.id` — the payer DID the receipt attributes the payment to. */
38
+ readonly payerDid: string;
39
+ /** The payer DID's method, narrowed to `ACK_DID_METHODS`. */
40
+ readonly payerDidMethod: string;
41
+ /** `issuer.id` — the receipt service that signed. Recorded; never mistaken for the payer. */
42
+ readonly issuerDid: string;
43
+ /**
44
+ * The other keys ACK's own audit trail put in the same map, in wire order — `policyRef`, `mandateRef`,
45
+ * `executionRef`, `executionReceiptHash`, `settlementNetwork`, `settlementReference` and whatever else a
46
+ * deployment added. Reported because a record composer wants to know what else this receipt
47
+ * cross-references, and because naming them is the honest form of "we are a guest in this map": we read
48
+ * exactly one key and own none of the rest.
49
+ */
50
+ readonly siblingRefKeys: readonly string[];
51
+ }
52
+ /**
53
+ * Parse an ISSUED `PaymentReceiptCredential` into its facts. Fail-fast on every departure from what ACK's
54
+ * own code emits — a receipt this cannot read is one ACK's verification chain could not have produced.
55
+ *
56
+ * **The unissued receipt is refused, and it is the mirror of the seller's rule.**
57
+ * `placement-ack.place` refuses a credential that ALREADY carries a `proof`, because a field added after
58
+ * issuance either breaks the embedded signature or falls outside the JWT payload ACK treats as
59
+ * authoritative. This refuses one that does NOT YET carry one, because a credential nobody signed attests
60
+ * nothing and must not reach a record as evidence of a payment. Between the two rules the reference can
61
+ * only ever ride a receipt the issuer signed over. The predicate is ACK's own — `isDecodedCredential`
62
+ * accepts a value iff `"proof" in value && value.proof != null` — mirrored, so a `null` proof is unissued
63
+ * by the host's rule and is unissued here.
64
+ */
65
+ export declare function parseAckReceipt(credential: unknown): AckReceiptFacts;
66
+ /**
67
+ * The receipt's contribution to a post-settlement `verify` call, with each field naming the rung it feeds.
68
+ *
69
+ * The first of ACK's two hard edges, answered in code rather than in prose: the `PaymentReceiptCredential` is
70
+ * **both** an evidence artifact and an identity input, and the split is here.
71
+ */
72
+ export interface AckReceiptContribution {
73
+ /** Feeds `VerifyInput.placement` — the reference recovered from the host protocol's OWN document. */
74
+ readonly placement: {
75
+ readonly extracted: unknown;
76
+ };
77
+ /**
78
+ * RCS-4 → one `evidence.Artifact` under the `settlement` role.
79
+ *
80
+ * `settlement` rather than `attestation`: an ACK receipt IS ACK's settlement artifact — the credential
81
+ * that attests the payment happened, carrying the `paymentRequestToken` it settled and, by convention,
82
+ * ACK's own `settlementNetwork`/`settlementReference`. `settlement` is one of `RCS4_REQUIRED_ROLES`, so
83
+ * filing it correctly is what lets an ACK record's evidence package be complete; filing it under
84
+ * `attestation` would leave the settlement role empty while double-filling one that is already occupied
85
+ * by the identity attestation.
86
+ */
87
+ readonly artifact: Artifact;
88
+ /** IDN-1 → the payer `resolve-party` attributes the payment to. */
89
+ readonly payerDid: string;
90
+ /**
91
+ * The payer DID's method — and NOT an assurance level.
92
+ *
93
+ * ACK's second hard edge: `did:web` / `did:jwks` resolution reuses the existing identity path rather than
94
+ * minting a second one, and that path authors identity SELLER-side — the record's IDN content is
95
+ * seller-authored. A buyer verifying a record reads the assurance the record already states; restating it
96
+ * here would be a second authority on the same fact. The mapping from ACK's four DID methods to an
97
+ * `authority.Assurance` therefore lives once, on the seller side, and this surface stops at the method.
98
+ */
99
+ readonly payerDidMethod: string;
100
+ /** The receipt service that signed. Recorded on the record; never the payer. */
101
+ readonly issuerDid: string;
102
+ /** ACK's join key — the guard against reading a receipt against a payment request it never attested. */
103
+ readonly paymentRequestToken: string;
104
+ }
105
+ /**
106
+ * Split an issued receipt into its two rungs. `receiptBytes` are the retained credential exactly as it was
107
+ * received — the artifact is the bytes, not a re-serialization of the parse, because what the evidence
108
+ * package must hold is what the issuer signed.
109
+ */
110
+ export declare function ackReceiptContribution(credential: unknown, receiptBytes: Uint8Array): AckReceiptContribution;
111
+ //# sourceMappingURL=proposal-ack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-ack.d.ts","sourceRoot":"","sources":["../src/proposal-ack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAG5D;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAAkC,CAAC;AAwBhF,8EAA8E;AAC9E,MAAM,WAAW,eAAe;IAC9B,uGAAuG;IACvG,QAAQ,CAAC,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;IAChC,qFAAqF;IACrF,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,yDAAyD;IACzD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5C;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,OAAO,GAAG,eAAe,CAiEpE;AAED;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,qGAAqG;IACrG,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IACpD;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,gFAAgF;IAChF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wGAAwG;IACxG,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACtC;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,OAAO,EACnB,YAAY,EAAE,UAAU,GACvB,sBAAsB,CAUxB"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The ACK buyer surface: read an ACK-Pay `PaymentReceiptCredential` and say which rung of the record each
3
+ * half of it feeds.
4
+ *
5
+ * **There is deliberately no `GateProposal` here, and that is a finding about ACK rather than a gap.** Every
6
+ * other parser in this package turns a PROPOSAL into the typed inputs a buyer decides on. ACK has no
7
+ * proposal-time carrier to turn: read live at `agentcommercekit/ack@main` on 2026-07-30,
8
+ * `packages/ack-pay/src/schemas/valibot.ts` declares `paymentRequestSchema` as
9
+ * `{ id, description?, serviceCallback?, expiresAt?, paymentOptions }` — no `metadata`, no open field, no
10
+ * extension point anywhere on the request. The open map (`v.optional(v.record(v.string(), v.unknown()))`)
11
+ * exists on `paymentReceiptClaimSchema` ALONE. So an LCP reference reaches an ACK document only at receipt
12
+ * time, and manufacturing a `GateProposal` from a receipt would name a decision the buyer no longer has:
13
+ * the payment has already settled. What the receipt genuinely supports is post-settlement verification, and
14
+ * that is what this module produces.
15
+ *
16
+ * An ACK buyer that wants a decision BEFORE paying gets it the same way any Level 1–2 buyer does: from the
17
+ * seller's `/.well-known/legal-context.json` (LCP §2), which is out of band from ACK entirely.
18
+ */
19
+ import { readAtPath } from "@integraledger/lcp-binding-core";
20
+ import { ACK_PLACEMENT, ackPlacement } from "@integraledger/lcp-placement-ack";
21
+ /**
22
+ * The DID methods ACK's own resolver registers — `packages/did/src/did-resolvers/get-did-resolver.ts`
23
+ * composes exactly four (`key-did-resolver`, `./web-did-resolver`, `jwks-did-resolver`,
24
+ * `./pkh-did-resolver`), read live 2026-07-30. A receipt naming any other method is one ACK's own chain
25
+ * could not have verified, so it is refused here rather than carried into a record as an identity nobody
26
+ * can resolve. `did:jwks` was still pending when earlier surveys of ACK were written and has since landed,
27
+ * which is why this list is cut from ACK's own resolver code rather than from any survey of it.
28
+ */
29
+ export const ACK_DID_METHODS = ["key", "web", "jwks", "pkh"];
30
+ /**
31
+ * The leaf key names the LCP placement owns — DERIVED from the manifest so the two cannot drift.
32
+ *
33
+ * `readAlso` is OPTIONAL on `PlacementManifest` and manifests legitimately omit it, so `[]` is the CORRECT
34
+ * value for "this placement owns no alias keys" — under it `siblingRefKeys` rightly reports the un-owned key
35
+ * as one of ACK's own. Total handling of an optional field, not a fallback path: there is nothing here to
36
+ * fail loudly about. `ACK_PLACEMENT` declares an alias today, which is why the arm is unreached.
37
+ */
38
+ function lcpKeys() {
39
+ const leaf = (path) => path.slice(path.lastIndexOf(".") + 1);
40
+ return [
41
+ leaf(ACK_PLACEMENT.field),
42
+ ...(ACK_PLACEMENT.readAlso ?? []).map((alias) => leaf(alias.path)),
43
+ ];
44
+ }
45
+ function requireString(value, what) {
46
+ if (typeof value !== "string" || value === "")
47
+ throw new Error(`ACK receipt: ${what} is absent or not a non-empty string`);
48
+ return value;
49
+ }
50
+ /**
51
+ * Parse an ISSUED `PaymentReceiptCredential` into its facts. Fail-fast on every departure from what ACK's
52
+ * own code emits — a receipt this cannot read is one ACK's verification chain could not have produced.
53
+ *
54
+ * **The unissued receipt is refused, and it is the mirror of the seller's rule.**
55
+ * `placement-ack.place` refuses a credential that ALREADY carries a `proof`, because a field added after
56
+ * issuance either breaks the embedded signature or falls outside the JWT payload ACK treats as
57
+ * authoritative. This refuses one that does NOT YET carry one, because a credential nobody signed attests
58
+ * nothing and must not reach a record as evidence of a payment. Between the two rules the reference can
59
+ * only ever ride a receipt the issuer signed over. The predicate is ACK's own — `isDecodedCredential`
60
+ * accepts a value iff `"proof" in value && value.proof != null` — mirrored, so a `null` proof is unissued
61
+ * by the host's rule and is unissued here.
62
+ */
63
+ export function parseAckReceipt(credential) {
64
+ const proof = readAtPath(credential, "proof");
65
+ if (proof === undefined || proof === null)
66
+ throw new Error("ack/receipt-not-issued: this credential carries no proof — a receipt nobody signed attests nothing, " +
67
+ "and the reference it carries is outside anything ACK's verification chain would return");
68
+ const issuerDid = requireString(readAtPath(credential, "issuer.id"), "issuer.id (ACK's own createCredential emits `issuer: { id: issuer }`, never a bare string)");
69
+ const payerDid = requireString(readAtPath(credential, "credentialSubject.id"), "credentialSubject.id");
70
+ const [scheme, method] = payerDid.split(":");
71
+ if (scheme !== "did" ||
72
+ method === undefined ||
73
+ !ACK_DID_METHODS.includes(method))
74
+ throw new Error(`ACK receipt: "${payerDid}" is not a did uri in a did method ACK resolves (${ACK_DID_METHODS.join(", ")})`);
75
+ const paymentRequestToken = requireString(readAtPath(credential, "credentialSubject.paymentRequestToken"), "credentialSubject.paymentRequestToken");
76
+ const paymentOptionId = requireString(readAtPath(credential, "credentialSubject.paymentOptionId"), "credentialSubject.paymentOptionId");
77
+ const extracted = ackPlacement.extract(credential);
78
+ if (!("ok" in extracted))
79
+ throw new Error(`ACK receipt carries no readable LCP reference (${extracted.haltClass}/${extracted.code})`);
80
+ if (extracted.value.type !== "sha256")
81
+ throw new Error(`ACK legalContext must be a sha256 carrier, got "${extracted.value.type}" — a locator commits to nothing`);
82
+ const owned = lcpKeys();
83
+ // `extract` above read the LCP key out of THIS map, so the map is an object and the narrowing cannot
84
+ // fail. Asserted rather than branched: a defensive `typeof` ternary here would ship an else-arm no input
85
+ // can reach, and its silent `[]` would under-report ACK's own refs on a receipt that has them.
86
+ const metadata = readAtPath(credential, "credentialSubject.metadata");
87
+ const siblingRefKeys = Object.keys(metadata).filter((k) => !owned.includes(k));
88
+ return {
89
+ atrHash: extracted.value.value,
90
+ paymentRequestToken,
91
+ paymentOptionId,
92
+ payerDid,
93
+ payerDidMethod: method,
94
+ issuerDid,
95
+ siblingRefKeys,
96
+ };
97
+ }
98
+ /**
99
+ * Split an issued receipt into its two rungs. `receiptBytes` are the retained credential exactly as it was
100
+ * received — the artifact is the bytes, not a re-serialization of the parse, because what the evidence
101
+ * package must hold is what the issuer signed.
102
+ */
103
+ export function ackReceiptContribution(credential, receiptBytes) {
104
+ const facts = parseAckReceipt(credential);
105
+ return {
106
+ placement: { extracted: { type: "sha256", value: facts.atrHash } },
107
+ artifact: { role: "settlement", bytes: receiptBytes },
108
+ payerDid: facts.payerDid,
109
+ payerDidMethod: facts.payerDidMethod,
110
+ issuerDid: facts.issuerDid,
111
+ paymentRequestToken: facts.paymentRequestToken,
112
+ };
113
+ }
114
+ //# sourceMappingURL=proposal-ack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-ack.js","sourceRoot":"","sources":["../src/proposal-ack.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { GateProposal, ProposalContext } from "./proposal.js";
2
+ /**
3
+ * Parse an ACP checkout session into the SAME typed `GateProposal` the x402 parser produces (the LCP §12.7
4
+ * boundary — no prose field on the type). Fail-fast (throws) on a malformed session, a reference that is not
5
+ * a canonical `lcp:sha256:0x…` 32-byte carrier, a non-HTTPS terms URL, a missing `total` row, or a
6
+ * non-integer minor-unit amount.
7
+ *
8
+ * `legal_context_url` is required, and the ACP placement manifest DECLARES it as
9
+ * `termsUrlField: "metadata.legal_context_url"` — the field this parser demands is a field the placement
10
+ * names, which is what makes the round-trip compose. It is deliberately NOT read from ACP's native
11
+ * `links[type=terms_of_use]`: that link is the merchant's standing policy page, while this names the ATR
12
+ * terms document for this transaction, and falling back to it would substitute one for the other.
13
+ *
14
+ * `GateProposal` and `ProposalContext` are IMPORTED, never redefined — one type for every wire is the whole
15
+ * point of a single typed proposal, and a second copy would let the two drift.
16
+ */
17
+ export declare function parseProposalFromAcpCheckout(session: unknown, ctx: ProposalContext): GateProposal;
18
+ //# sourceMappingURL=proposal-acp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-acp.d.ts","sourceRoot":"","sources":["../src/proposal-acp.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAgBnE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,eAAe,GACnB,YAAY,CAoDd"}
@@ -0,0 +1,72 @@
1
+ import { decodeLegalContextString } from "@integraledger/lcp-binding-core";
2
+ import { z } from "zod";
3
+ // Module-internal structural view of the ACP checkout session (NOT the seller's type — buyer ≠ seller).
4
+ // The session object is TOP-LEVEL and `totals` is an ARRAY (stable 2026-04-17); there is no `checkout`
5
+ // wrapper and no `total` string. z.object strips unknown keys, so a real session's many other fields are
6
+ // harmlessly ignored. Kept internal and not z.infer-exported (isolatedDeclarations), exactly as
7
+ // X402ChallengeSchema is in proposal.ts.
8
+ const AcpSessionSchema = z.object({
9
+ currency: z.string(),
10
+ totals: z.array(z.object({ type: z.string(), amount: z.number() })),
11
+ metadata: z.object({
12
+ legal_context: z.string(),
13
+ legal_context_url: z.string(),
14
+ }),
15
+ });
16
+ /**
17
+ * Parse an ACP checkout session into the SAME typed `GateProposal` the x402 parser produces (the LCP §12.7
18
+ * boundary — no prose field on the type). Fail-fast (throws) on a malformed session, a reference that is not
19
+ * a canonical `lcp:sha256:0x…` 32-byte carrier, a non-HTTPS terms URL, a missing `total` row, or a
20
+ * non-integer minor-unit amount.
21
+ *
22
+ * `legal_context_url` is required, and the ACP placement manifest DECLARES it as
23
+ * `termsUrlField: "metadata.legal_context_url"` — the field this parser demands is a field the placement
24
+ * names, which is what makes the round-trip compose. It is deliberately NOT read from ACP's native
25
+ * `links[type=terms_of_use]`: that link is the merchant's standing policy page, while this names the ATR
26
+ * terms document for this transaction, and falling back to it would substitute one for the other.
27
+ *
28
+ * `GateProposal` and `ProposalContext` are IMPORTED, never redefined — one type for every wire is the whole
29
+ * point of a single typed proposal, and a second copy would let the two drift.
30
+ */
31
+ export function parseProposalFromAcpCheckout(session, ctx) {
32
+ const parsed = AcpSessionSchema.parse(session);
33
+ const { legal_context, legal_context_url } = parsed.metadata;
34
+ // Decoded through binding-core's codec, never by slicing a prefix. The codec enforces the canonical
35
+ // sha256 carrier form (0x-prefixed 32-byte hex) — a bare-digits value throws here rather than being
36
+ // silently re-prefixed into something that looks valid. An earlier draft did `0x${slice(...)}`, which
37
+ // turned a CONFORMANT 0x-carrying reference into `0x0x…` and accepted the non-canonical form instead.
38
+ const ref = decodeLegalContextString(legal_context);
39
+ if (ref === undefined)
40
+ throw new Error(`ACP legal_context is not a parseable lcp: reference: ${legal_context}`);
41
+ if (ref.type !== "sha256")
42
+ throw new Error(`ACP legal_context must be an lcp:sha256: reference, got lcp:${ref.type}:`);
43
+ // No 0x-32-byte re-check here. `decodeLegalContextString` runs `assertValidValue`, which for `sha256`
44
+ // IS `kernel.isAtrHash` — the identical `/^0x[0-9a-fA-F]{64}$/`. A second copy of that regex could not
45
+ // reject anything the decode admitted: it would be a branch no input reaches, permanently unkillable by
46
+ // any test, and it would tell a reader there is a case here that there is not. The x402 parser carries
47
+ // its own check because it reads a RAW `extra.atrHash` string that no codec has validated; this one
48
+ // does not, and that asymmetry is the point rather than an oversight.
49
+ if (!legal_context_url.startsWith("https://"))
50
+ throw new Error(`legalContextUrl must be HTTPS: ${legal_context_url}`);
51
+ // The `total` ROW, never the first row and never a sum. ACP's `totals` carries several typed rows
52
+ // (`items_base_amount`, `tax`, `fee`, `discount`, …) and only the one typed `total` is the amount the
53
+ // buyer is being asked to authorize. There is deliberately no fallback: a session with no `total` row is
54
+ // malformed, and picking another row would gate the buyer against a number nobody quoted.
55
+ const total = parsed.totals.find((t) => t.type === "total");
56
+ if (total === undefined)
57
+ throw new Error("ACP session carries no row typed `total` — nothing to authorize against");
58
+ if (!Number.isSafeInteger(total.amount) || total.amount < 0)
59
+ throw new Error(`offer amount must be a non-negative minor-unit integer: ${total.amount}`);
60
+ return {
61
+ advertisedAtrHash: ref.value,
62
+ legalContextUrl: legal_context_url,
63
+ level: ctx.level,
64
+ offer: {
65
+ amount: String(total.amount),
66
+ // ACP settles in a fiat currency code, not a network:asset pair — the unit string says which.
67
+ unit: parsed.currency,
68
+ },
69
+ sellerAssurance: ctx.sellerAssurance,
70
+ };
71
+ }
72
+ //# sourceMappingURL=proposal-acp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-acp.js","sourceRoot":"","sources":["../src/proposal-acp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,wGAAwG;AACxG,uGAAuG;AACvG,yGAAyG;AACzG,gGAAgG;AAChG,yCAAyC;AACzC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACnE,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC;QACjB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE;QACzB,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE;KAC9B,CAAC;CACH,CAAC,CAAC;AAEH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,4BAA4B,CAC1C,OAAgB,EAChB,GAAoB;IAEpB,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/C,MAAM,EAAE,aAAa,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC;IAE7D,oGAAoG;IACpG,oGAAoG;IACpG,sGAAsG;IACtG,sGAAsG;IACtG,MAAM,GAAG,GAAG,wBAAwB,CAAC,aAAa,CAAC,CAAC;IACpD,IAAI,GAAG,KAAK,SAAS;QACnB,MAAM,IAAI,KAAK,CACb,wDAAwD,aAAa,EAAE,CACxE,CAAC;IACJ,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;QACvB,MAAM,IAAI,KAAK,CACb,+DAA+D,GAAG,CAAC,IAAI,GAAG,CAC3E,CAAC;IACJ,sGAAsG;IACtG,uGAAuG;IACvG,wGAAwG;IACxG,uGAAuG;IACvG,oGAAoG;IACpG,sEAAsE;IAEtE,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,UAAU,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,kCAAkC,iBAAiB,EAAE,CAAC,CAAC;IAEzE,kGAAkG;IAClG,sGAAsG;IACtG,yGAAyG;IACzG,0FAA0F;IAC1F,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC;IAC5D,IAAI,KAAK,KAAK,SAAS;QACrB,MAAM,IAAI,KAAK,CACb,yEAAyE,CAC1E,CAAC;IACJ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QACzD,MAAM,IAAI,KAAK,CACb,2DAA2D,KAAK,CAAC,MAAM,EAAE,CAC1E,CAAC;IAEJ,OAAO;QACL,iBAAiB,EAAE,GAAG,CAAC,KAAsB;QAC7C,eAAe,EAAE,iBAAiB;QAClC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,KAAK,EAAE;YACL,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;YAC5B,8FAA8F;YAC9F,IAAI,EAAE,MAAM,CAAC,QAAQ;SACtB;QACD,eAAe,EAAE,GAAG,CAAC,eAAe;KACrC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The AP2 buyer surface: read the LCP reference off the transport envelope that carries an AP2 mandate, and
3
+ * name the moment "verify before sign" is measured against.
4
+ *
5
+ * AP2 is the one wire in this set where the sign moment is genuinely ambiguous. x402 has one (the EIP-3009
6
+ * authorization), ACP has one (the checkout confirmation). AP2 has FOUR mandates — Checkout and Payment,
7
+ * each in an Open and a Closed stage — signed by different keys in different modes. A gate that says "verify
8
+ * before sign" without saying WHICH sign is a gate that cannot be audited, so `AP2_HALT_POINT` names it, and
9
+ * `parseProposalFromAp2Envelope` refuses to run at or after it.
10
+ */
11
+ import type { GateProposal, ProposalContext } from "./proposal.js";
12
+ /**
13
+ * The steps of AP2 v0.2's authorization flow, in the order a Shopping Agent passes through them. CLOSED —
14
+ * an unknown token is a wire this parser has no halt point for, and inventing one would be the whole defect
15
+ * this type exists to prevent.
16
+ *
17
+ * `agent-key-signing` occurs in Autonomous mode only; `key-binding-presentation` is the `kb+sd-jwt` binding
18
+ * made when the closed mandate is PRESENTED to the merchant, which is why it is a separate step from the
19
+ * signature that created the mandate.
20
+ */
21
+ export type Ap2Step = "checkout-jwt-received" | "mandate-content-built" | "trusted-surface-authorization" | "agent-key-signing" | "key-binding-presentation";
22
+ /** The named sign moment, its defence, and the last step at which gating is still meaningful. */
23
+ export interface Ap2HaltPoint {
24
+ /** The last step at which no signing key has been invoked — where the gate must have finished. */
25
+ readonly lastSafeStep: Ap2Step;
26
+ /** The first step at which a signing key IS invoked. The gate halts strictly before this. */
27
+ readonly haltBefore: Ap2Step;
28
+ /** Every step at which some key signs, in flow order. `haltBefore` is the first of them. */
29
+ readonly signingSteps: readonly Ap2Step[];
30
+ /** Why this step and not another — the argument, not a restatement of the field. */
31
+ readonly rationale: string;
32
+ /** The host protocol's own text this is defended against — AP2 decides its flow, not LCP's appendix. */
33
+ readonly specRef: string;
34
+ }
35
+ /**
36
+ * AP2's halt point: **immediately before the Trusted Surface is invoked.**
37
+ *
38
+ * The defence, from AP2 v0.2's own specification rather than from LCP's Appendix C:
39
+ *
40
+ * - **Human Present (Direct).** "When a Shopping Agent has a Checkout JWT for the closed Checkout from the
41
+ * Merchant, they construct the Checkout and Payment Mandate Content and pass it to a Trusted Surface for
42
+ * display to the user and signing." The user's key signs at the Trusted Surface. Nothing signs before it.
43
+ * - **Human Not Present (Autonomous).** "When a Shopping Agent needs to operate autonomously, it will create
44
+ * open Checkout and Payment Mandate Content and have these authorized by the Trusted Surface. These MUST
45
+ * include the agent's public key as a `cnf` claim." The Trusted Surface is invoked FIRST here too; the
46
+ * Shopping Agent's own Agent Key signs the closed mandates only afterwards.
47
+ *
48
+ * So both modes place the Trusted Surface first, and that makes it the halt point for both — the gate does
49
+ * not need to know which mode it is in, which is the property that makes this auditable. `agent-key-signing`
50
+ * is a later signing step, not an earlier one, and gating there would already be too late.
51
+ *
52
+ * **And the window closes hard.** The closed Checkout Mandate carries `checkout_hash`, "a base64url-encoded
53
+ * hash of the merchant-signed `checkout_jwt`", the Payment Mandate "is bound to a particular Checkout using
54
+ * the cryptographic hash of the Checkout JWT", and the closed mandate is a `kb+sd-jwt` whose key-binding
55
+ * signature is constructed over `sd_hash` **at presentation**. Once the Trusted Surface has signed, nothing
56
+ * inside either mandate is mutable and no later verification can be turned back into a decision.
57
+ *
58
+ * **This is a HALT POINT, not a placement constraint.** The reference rides the transport envelope's
59
+ * metadata, which AP2 never signs and never reads — so the SELLER may place it at any time, under no
60
+ * ordering precondition at all. Contrast ACK, where placement must happen BEFORE the issuer signs: a field
61
+ * added after issuance either breaks the embedded signature or falls outside the payload the host treats as
62
+ * authoritative. What is time-critical here is the BUYER's decision, because after this step the buyer has
63
+ * no decision left to make.
64
+ */
65
+ export declare const AP2_HALT_POINT: Ap2HaltPoint;
66
+ /** Does this step invoke a signing key? Membership in `AP2_HALT_POINT.signingSteps`, as a function. */
67
+ export declare function isAp2SigningStep(step: Ap2Step): boolean;
68
+ /**
69
+ * Throw unless the client is still strictly before the halt point.
70
+ *
71
+ * Fail-fast rather than a returned flag: a caller that gates at `agent-key-signing` has already handed the
72
+ * user's key to the Trusted Surface, and there is no honest outcome to report at that point — the decision
73
+ * the gate exists to make has already been made by someone else.
74
+ */
75
+ export declare function assertBeforeAp2HaltPoint(step: Ap2Step): void;
76
+ /**
77
+ * The context AP2's wire does NOT carry, which the buyer's client establishes — and it is more than the
78
+ * other parsers need, for two reasons that are both properties of AP2 rather than gaps here.
79
+ */
80
+ export interface Ap2ProposalContext extends ProposalContext {
81
+ /**
82
+ * The terms document's locator.
83
+ *
84
+ * AP2's placement is INTEGRITY-ONLY: `AP2_PLACEMENT` declares no `termsUrlField`, because the A2A
85
+ * `Message.metadata` map holds the reference and AP2 models no terms-URL field anywhere. So unlike ACP
86
+ * (`metadata.legal_context_url`) and x402 (`extra.legalContextUrl`), the locator cannot come off the wire.
87
+ * It comes from LCP §2 discovery — the seller's `/.well-known/legal-context.json` — resolved by the
88
+ * client before it calls this parser. HTTPS is enforced here exactly as it is on every other wire.
89
+ */
90
+ readonly legalContextUrl: string;
91
+ /**
92
+ * The offer the client is about to authorize.
93
+ *
94
+ * AP2 v0.2 carries the price inside the merchant-signed Checkout JWT, referenced from the closed mandate
95
+ * only by `checkout_hash`. The transport carries mandates as OPAQUE SD-JWT compact strings; this parser
96
+ * does not decode one, and decoding one would mean trusting an unverified payload to produce the number
97
+ * the gate compares against a policy cap. The client already holds the checkout it built its Mandate
98
+ * Content from, so it supplies the amount it is about to commit — the honest source.
99
+ */
100
+ readonly offer: {
101
+ readonly amount: string;
102
+ readonly unit: string;
103
+ };
104
+ /** Where the client is in AP2's flow. At or after the halt point this parser refuses. */
105
+ readonly step: Ap2Step;
106
+ }
107
+ /**
108
+ * Parse an AP2 transport envelope into the SAME typed `GateProposal` every other wire produces.
109
+ *
110
+ * The reference is read through `@integraledger/lcp-placement-ap2`, never through a hand-rolled path. The
111
+ * manifest declares both the canonical `metadata.legalContext` and the snake_case alias
112
+ * `metadata.legal_context`; a second reader here would be a second spelling of the manifest, free to drift
113
+ * from the one the seller writes through. It also inherits the placement's deliberate blindness to the Tier
114
+ * B shape — a reference embedded INSIDE the mandate is not read, so a buyer cannot be walked into blessing
115
+ * a placement no shipped package can emit, because LCP's appendix illustrates and the host protocol binds.
116
+ *
117
+ * Fail-fast (throws) on: a step at or after `AP2_HALT_POINT`, an envelope carrying no reference, a carrier
118
+ * that is not `sha256`, a non-HTTPS terms URL, or a non-base-unit-integer amount.
119
+ */
120
+ export declare function parseProposalFromAp2Envelope(envelope: unknown, ctx: Ap2ProposalContext): GateProposal;
121
+ //# sourceMappingURL=proposal-ap2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-ap2.d.ts","sourceRoot":"","sources":["../src/proposal-ap2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEnE;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,GACf,uBAAuB,GACvB,uBAAuB,GACvB,+BAA+B,GAC/B,mBAAmB,GACnB,0BAA0B,CAAC;AAE/B,iGAAiG;AACjG,MAAM,WAAW,YAAY;IAC3B,kGAAkG;IAClG,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,6FAA6F;IAC7F,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,4FAA4F;IAC5F,QAAQ,CAAC,YAAY,EAAE,SAAS,OAAO,EAAE,CAAC;IAC1C,oFAAoF;IACpF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wGAAwG;IACxG,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,cAAc,EAAE,YAY5B,CAAC;AAEF,uGAAuG;AACvG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAEvD;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAM5D;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD;;;;;;;;OAQG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,yFAAyF;IACzF,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,OAAO,EACjB,GAAG,EAAE,kBAAkB,GACtB,YAAY,CAoCd"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The AP2 buyer surface: read the LCP reference off the transport envelope that carries an AP2 mandate, and
3
+ * name the moment "verify before sign" is measured against.
4
+ *
5
+ * AP2 is the one wire in this set where the sign moment is genuinely ambiguous. x402 has one (the EIP-3009
6
+ * authorization), ACP has one (the checkout confirmation). AP2 has FOUR mandates — Checkout and Payment,
7
+ * each in an Open and a Closed stage — signed by different keys in different modes. A gate that says "verify
8
+ * before sign" without saying WHICH sign is a gate that cannot be audited, so `AP2_HALT_POINT` names it, and
9
+ * `parseProposalFromAp2Envelope` refuses to run at or after it.
10
+ */
11
+ import { ap2Placement } from "@integraledger/lcp-placement-ap2";
12
+ /**
13
+ * AP2's halt point: **immediately before the Trusted Surface is invoked.**
14
+ *
15
+ * The defence, from AP2 v0.2's own specification rather than from LCP's Appendix C:
16
+ *
17
+ * - **Human Present (Direct).** "When a Shopping Agent has a Checkout JWT for the closed Checkout from the
18
+ * Merchant, they construct the Checkout and Payment Mandate Content and pass it to a Trusted Surface for
19
+ * display to the user and signing." The user's key signs at the Trusted Surface. Nothing signs before it.
20
+ * - **Human Not Present (Autonomous).** "When a Shopping Agent needs to operate autonomously, it will create
21
+ * open Checkout and Payment Mandate Content and have these authorized by the Trusted Surface. These MUST
22
+ * include the agent's public key as a `cnf` claim." The Trusted Surface is invoked FIRST here too; the
23
+ * Shopping Agent's own Agent Key signs the closed mandates only afterwards.
24
+ *
25
+ * So both modes place the Trusted Surface first, and that makes it the halt point for both — the gate does
26
+ * not need to know which mode it is in, which is the property that makes this auditable. `agent-key-signing`
27
+ * is a later signing step, not an earlier one, and gating there would already be too late.
28
+ *
29
+ * **And the window closes hard.** The closed Checkout Mandate carries `checkout_hash`, "a base64url-encoded
30
+ * hash of the merchant-signed `checkout_jwt`", the Payment Mandate "is bound to a particular Checkout using
31
+ * the cryptographic hash of the Checkout JWT", and the closed mandate is a `kb+sd-jwt` whose key-binding
32
+ * signature is constructed over `sd_hash` **at presentation**. Once the Trusted Surface has signed, nothing
33
+ * inside either mandate is mutable and no later verification can be turned back into a decision.
34
+ *
35
+ * **This is a HALT POINT, not a placement constraint.** The reference rides the transport envelope's
36
+ * metadata, which AP2 never signs and never reads — so the SELLER may place it at any time, under no
37
+ * ordering precondition at all. Contrast ACK, where placement must happen BEFORE the issuer signs: a field
38
+ * added after issuance either breaks the embedded signature or falls outside the payload the host treats as
39
+ * authoritative. What is time-critical here is the BUYER's decision, because after this step the buyer has
40
+ * no decision left to make.
41
+ */
42
+ export const AP2_HALT_POINT = {
43
+ lastSafeStep: "mandate-content-built",
44
+ haltBefore: "trusted-surface-authorization",
45
+ signingSteps: [
46
+ "trusted-surface-authorization",
47
+ "agent-key-signing",
48
+ "key-binding-presentation",
49
+ ],
50
+ rationale: "Both AP2 modes invoke the Trusted Surface before any other key: in Direct mode the Shopping Agent passes the Mandate Content to it for user signing, and in Autonomous mode it authorizes the OPEN mandates before the Agent Key signs the closed ones. It is therefore the first signing step in either mode, and the closed mandate's checkout_hash plus its presentation-time key binding leave nothing mutable afterwards.",
51
+ specRef: "AP2 v0.2 (google-agentic-commerce/AP2 @ main, release 0.2.0 dated 2026-04-28) docs/ap2/specification.md §Human Present / §Human Not Present, and docs/ap2/checkout_mandate.md (vct mandate.checkout.1, checkout_hash, kb+sd-jwt over sd_hash at presentation); read 2026-07-30",
52
+ };
53
+ /** Does this step invoke a signing key? Membership in `AP2_HALT_POINT.signingSteps`, as a function. */
54
+ export function isAp2SigningStep(step) {
55
+ return AP2_HALT_POINT.signingSteps.includes(step);
56
+ }
57
+ /**
58
+ * Throw unless the client is still strictly before the halt point.
59
+ *
60
+ * Fail-fast rather than a returned flag: a caller that gates at `agent-key-signing` has already handed the
61
+ * user's key to the Trusted Surface, and there is no honest outcome to report at that point — the decision
62
+ * the gate exists to make has already been made by someone else.
63
+ */
64
+ export function assertBeforeAp2HaltPoint(step) {
65
+ if (isAp2SigningStep(step))
66
+ throw new Error(`AP2 gate refused at "${step}": that is at or after the halt point (${AP2_HALT_POINT.haltBefore}). ` +
67
+ `Verify before sign means gating no later than "${AP2_HALT_POINT.lastSafeStep}". ${AP2_HALT_POINT.rationale}`);
68
+ }
69
+ const BASE_UNIT_INT = /^[0-9]+$/; // decimal base-unit integer — no sign, no decimal point, non-empty
70
+ /**
71
+ * Parse an AP2 transport envelope into the SAME typed `GateProposal` every other wire produces.
72
+ *
73
+ * The reference is read through `@integraledger/lcp-placement-ap2`, never through a hand-rolled path. The
74
+ * manifest declares both the canonical `metadata.legalContext` and the snake_case alias
75
+ * `metadata.legal_context`; a second reader here would be a second spelling of the manifest, free to drift
76
+ * from the one the seller writes through. It also inherits the placement's deliberate blindness to the Tier
77
+ * B shape — a reference embedded INSIDE the mandate is not read, so a buyer cannot be walked into blessing
78
+ * a placement no shipped package can emit, because LCP's appendix illustrates and the host protocol binds.
79
+ *
80
+ * Fail-fast (throws) on: a step at or after `AP2_HALT_POINT`, an envelope carrying no reference, a carrier
81
+ * that is not `sha256`, a non-HTTPS terms URL, or a non-base-unit-integer amount.
82
+ */
83
+ export function parseProposalFromAp2Envelope(envelope, ctx) {
84
+ // FIRST, before any reading. A gate that parses and then discovers it was too late has already spent the
85
+ // buyer's time pretending a decision was available.
86
+ assertBeforeAp2HaltPoint(ctx.step);
87
+ const extracted = ap2Placement.extract(envelope);
88
+ if (!("ok" in extracted))
89
+ throw new Error(`AP2 envelope carries no readable LCP reference (${extracted.haltClass}/${extracted.code})`);
90
+ const ref = extracted.value;
91
+ if (ref.type !== "sha256")
92
+ throw new Error(`AP2 legalContext must be a sha256 carrier, got "${ref.type}" — the gate compares the advertised ` +
93
+ "hash against a recomputed one, and a locator commits to nothing");
94
+ if (!ctx.legalContextUrl.startsWith("https://"))
95
+ throw new Error(`legalContextUrl must be HTTPS: ${ctx.legalContextUrl}`);
96
+ if (!BASE_UNIT_INT.test(ctx.offer.amount))
97
+ throw new Error(`offer amount must be a base-unit integer string: "${ctx.offer.amount}"`);
98
+ // No 0x-32-byte re-check: `extract` decodes through binding-core's codec, whose `sha256` validity rule IS
99
+ // `kernel.isAtrHash`. A second copy of that regex could reject nothing the decode admitted — the same
100
+ // asymmetry `proposal-acp.ts` documents, and for the same reason.
101
+ return {
102
+ advertisedAtrHash: ref.value,
103
+ legalContextUrl: ctx.legalContextUrl,
104
+ level: ctx.level,
105
+ offer: {
106
+ amount: ctx.offer.amount,
107
+ unit: ctx.offer.unit,
108
+ },
109
+ sellerAssurance: ctx.sellerAssurance,
110
+ };
111
+ }
112
+ //# sourceMappingURL=proposal-ap2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-ap2.js","sourceRoot":"","sources":["../src/proposal-ap2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAiChE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,cAAc,GAAiB;IAC1C,YAAY,EAAE,uBAAuB;IACrC,UAAU,EAAE,+BAA+B;IAC3C,YAAY,EAAE;QACZ,+BAA+B;QAC/B,mBAAmB;QACnB,0BAA0B;KAC3B;IACD,SAAS,EACP,gaAAga;IACla,OAAO,EACL,gRAAgR;CACnR,CAAC;AAEF,uGAAuG;AACvG,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,OAAO,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAa;IACpD,IAAI,gBAAgB,CAAC,IAAI,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,wBAAwB,IAAI,0CAA0C,cAAc,CAAC,UAAU,KAAK;YAClG,kDAAkD,cAAc,CAAC,YAAY,MAAM,cAAc,CAAC,SAAS,EAAE,CAChH,CAAC;AACN,CAAC;AAkCD,MAAM,aAAa,GAAG,UAAU,CAAC,CAAC,mEAAmE;AAErG;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAiB,EACjB,GAAuB;IAEvB,yGAAyG;IACzG,oDAAoD;IACpD,wBAAwB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAEnC,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,CAAC,CAAC,IAAI,IAAI,SAAS,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,mDAAmD,SAAS,CAAC,SAAS,IAAI,SAAS,CAAC,IAAI,GAAG,CAC5F,CAAC;IACJ,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC;IAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;QACvB,MAAM,IAAI,KAAK,CACb,mDAAmD,GAAG,CAAC,IAAI,uCAAuC;YAChG,iEAAiE,CACpE,CAAC;IACJ,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,UAAU,CAAC,UAAU,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,kCAAkC,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC;IAC3E,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;QACvC,MAAM,IAAI,KAAK,CACb,qDAAqD,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CACzE,CAAC;IAEJ,0GAA0G;IAC1G,sGAAsG;IACtG,kEAAkE;IAClE,OAAO;QACL,iBAAiB,EAAE,GAAG,CAAC,KAAsB;QAC7C,eAAe,EAAE,GAAG,CAAC,eAAe;QACpC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,KAAK,EAAE;YACL,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM;YACxB,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI;SACrB;QACD,eAAe,EAAE,GAAG,CAAC,eAAe;KACrC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,44 @@
1
+ import type { GateProposal, ProposalContext } from "./proposal.js";
2
+ /**
3
+ * Parse an MPP `request` body into the SAME typed `GateProposal` the x402 and ACP parsers produce (the
4
+ * LCP §12.7 boundary — no prose field on the type). Fail-fast (throws) on a malformed body, an absent or
5
+ * non-0x-32-byte `atrHash`, a non-HTTPS terms URL, or a non-base-unit-integer amount.
6
+ *
7
+ * **The document is the `request` body, not the challenge.** MPP's identity lives one layer out, in the
8
+ * `WWW-Authenticate: Payment` challenge's auth-params; the body this parses is the base64url(JCS(JSON))
9
+ * payload that challenge's `request` auth-param carries. A caller holding a decoded body is expected to
10
+ * have decoded it from there.
11
+ *
12
+ * **BY NAME, and deliberately not in `PROPOSAL_PARSERS`.** MPP's discriminant is recorded as
13
+ * `kind: "undiscriminable"`: the body's members are `amount` and `currency` plus optionals, and an
14
+ * amount/currency pair is the shape of almost every payment document there is. Nothing in the body names
15
+ * MPP. So a caller must name `mpp` and reach this function directly — the same by-name route
16
+ * `parseProposalFromAp2Envelope` takes, and for a sibling reason. Adding an `mpp` row to the universal
17
+ * dispatch map would not make it reachable; it would make the map claim a discrimination it cannot perform.
18
+ *
19
+ * **The carrier is `bare-value`, so no codec has validated it.** `placement-mpp` declares
20
+ * `field: "methodDetails.atrHash"` with `encoding: "bare-value"` and `carrierTypes: ["sha256"]` — the wire
21
+ * carries the raw hash rather than an `lcp:sha256:0x…` string. That is why this validates the hash itself
22
+ * through the kernel's `isAtrHash`, as the x402 parser validates its raw `extra.atrHash`, and why it does
23
+ * NOT call `decodeLegalContextString` as the ACP parser does. Using the kernel's own predicate rather than
24
+ * restating its regex keeps one definition of what an ATR hash is.
25
+ *
26
+ * **`legalContextUrl` is the field the placement names.** `placement-mpp` declares
27
+ * `termsUrlField: "methodDetails.legalContextUrl"`, so the field this parser demands is the field the
28
+ * manifest names — which is what makes the seller's write and this buyer's read compose. There is no
29
+ * fallback to any other member: MPP defines none that means "these terms", and inventing one would gate the
30
+ * buyer against a document nobody pointed at.
31
+ *
32
+ * **What the carrier is worth is weaker than Tier A alone suggests, and the buyer should know it.** MPP
33
+ * binds the challenge `id` to the challenge parameters, so a client cannot alter the advertised values and
34
+ * still be accepted — but the binding key is a server secret the specification requires implementations to
35
+ * keep server-side, so **the buyer cannot verify that MAC.** What this reference gets is tamper-evidence,
36
+ * not a buyer-verifiable seller commitment. The gate treats it as an advertised value to be recomputed
37
+ * against fetched bytes, exactly as it treats every other protocol's, and that recomputation is what the
38
+ * guarantee actually rests on.
39
+ *
40
+ * `GateProposal` and `ProposalContext` are IMPORTED, never redefined — one type for every wire is the whole
41
+ * point of a single typed proposal, and a second copy would let the two drift.
42
+ */
43
+ export declare function parseProposalFromMppRequest(request: unknown, ctx: ProposalContext): GateProposal;
44
+ //# sourceMappingURL=proposal-mpp.d.ts.map