@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 @@
1
+ {"version":3,"file":"proposal-mpp.d.ts","sourceRoot":"","sources":["../src/proposal-mpp.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAgCnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,eAAe,GACnB,YAAY,CA8Cd"}
@@ -0,0 +1,106 @@
1
+ import { isAtrHash } from "@integraledger/lcp-kernel";
2
+ import { z } from "zod";
3
+ // Module-internal structural view of the MPP `request` body (NOT the seller's type — buyer ≠ seller).
4
+ // `z.object` strips unknown keys, so the optional members this parser has no use for — `description`,
5
+ // `expires`, `externalId`, `recipient` — are harmlessly ignored. Kept internal and not `z.infer`-exported
6
+ // (isolatedDeclarations), exactly as `X402ChallengeSchema` and `AcpSessionSchema` are.
7
+ //
8
+ // `methodDetails` is OPTIONAL in the charge intent (draft-payment-intent-charge-00 §5.1.2, Table 3), which
9
+ // is why it is optional here and its absence is a refusal below rather than a schema error: a request body
10
+ // with no `methodDetails` is a perfectly conformant MPP document that simply advertises no LCP reference,
11
+ // and saying so is more useful than a Zod path error.
12
+ const MppRequestSchema = z.object({
13
+ amount: z.string(),
14
+ currency: z.string(),
15
+ methodDetails: z
16
+ .object({
17
+ atrHash: z.string().optional(),
18
+ legalContextUrl: z.string().optional(),
19
+ })
20
+ .optional(),
21
+ });
22
+ // Decimal base-unit integer — no sign, no decimal point, non-empty. Identical in shape to the x402 parser's
23
+ // check and required for the same reason: `policy.ts` compares the offer with `BigInt(offer.amount)`, and
24
+ // `BigInt("10.50")` THROWS rather than returning, which would escape `evaluate`'s contract to RETURN a
25
+ // `GateDecision`. The grammar is the host's, not ours: draft-payment-intent-charge-00 Table 2 defines
26
+ // `amount` as "Payment amount in base units (smallest denomination)", and §3 defines base units as "the
27
+ // smallest denomination of a currency or asset. For USD, this is cents (1/100)." Every example in the
28
+ // specification agrees — "5000"/usd, "1000000"/token, "100000"/sat — so a decimal amount is malformed MPP
29
+ // and is refused here rather than thrown from inside the gate.
30
+ const BASE_UNIT_INT = /^[0-9]+$/;
31
+ /**
32
+ * Parse an MPP `request` body into the SAME typed `GateProposal` the x402 and ACP parsers produce (the
33
+ * LCP §12.7 boundary — no prose field on the type). Fail-fast (throws) on a malformed body, an absent or
34
+ * non-0x-32-byte `atrHash`, a non-HTTPS terms URL, or a non-base-unit-integer amount.
35
+ *
36
+ * **The document is the `request` body, not the challenge.** MPP's identity lives one layer out, in the
37
+ * `WWW-Authenticate: Payment` challenge's auth-params; the body this parses is the base64url(JCS(JSON))
38
+ * payload that challenge's `request` auth-param carries. A caller holding a decoded body is expected to
39
+ * have decoded it from there.
40
+ *
41
+ * **BY NAME, and deliberately not in `PROPOSAL_PARSERS`.** MPP's discriminant is recorded as
42
+ * `kind: "undiscriminable"`: the body's members are `amount` and `currency` plus optionals, and an
43
+ * amount/currency pair is the shape of almost every payment document there is. Nothing in the body names
44
+ * MPP. So a caller must name `mpp` and reach this function directly — the same by-name route
45
+ * `parseProposalFromAp2Envelope` takes, and for a sibling reason. Adding an `mpp` row to the universal
46
+ * dispatch map would not make it reachable; it would make the map claim a discrimination it cannot perform.
47
+ *
48
+ * **The carrier is `bare-value`, so no codec has validated it.** `placement-mpp` declares
49
+ * `field: "methodDetails.atrHash"` with `encoding: "bare-value"` and `carrierTypes: ["sha256"]` — the wire
50
+ * carries the raw hash rather than an `lcp:sha256:0x…` string. That is why this validates the hash itself
51
+ * through the kernel's `isAtrHash`, as the x402 parser validates its raw `extra.atrHash`, and why it does
52
+ * NOT call `decodeLegalContextString` as the ACP parser does. Using the kernel's own predicate rather than
53
+ * restating its regex keeps one definition of what an ATR hash is.
54
+ *
55
+ * **`legalContextUrl` is the field the placement names.** `placement-mpp` declares
56
+ * `termsUrlField: "methodDetails.legalContextUrl"`, so the field this parser demands is the field the
57
+ * manifest names — which is what makes the seller's write and this buyer's read compose. There is no
58
+ * fallback to any other member: MPP defines none that means "these terms", and inventing one would gate the
59
+ * buyer against a document nobody pointed at.
60
+ *
61
+ * **What the carrier is worth is weaker than Tier A alone suggests, and the buyer should know it.** MPP
62
+ * binds the challenge `id` to the challenge parameters, so a client cannot alter the advertised values and
63
+ * still be accepted — but the binding key is a server secret the specification requires implementations to
64
+ * keep server-side, so **the buyer cannot verify that MAC.** What this reference gets is tamper-evidence,
65
+ * not a buyer-verifiable seller commitment. The gate treats it as an advertised value to be recomputed
66
+ * against fetched bytes, exactly as it treats every other protocol's, and that recomputation is what the
67
+ * guarantee actually rests on.
68
+ *
69
+ * `GateProposal` and `ProposalContext` are IMPORTED, never redefined — one type for every wire is the whole
70
+ * point of a single typed proposal, and a second copy would let the two drift.
71
+ */
72
+ export function parseProposalFromMppRequest(request, ctx) {
73
+ const parsed = MppRequestSchema.parse(request);
74
+ // Absence is a refusal, not a permission. A conformant MPP body may omit `methodDetails` entirely, and
75
+ // may carry one that names no LCP reference; neither is a document this gate can bind terms from, and
76
+ // both are named rather than collapsed into one message so a seller reading the error knows which it is.
77
+ const details = parsed.methodDetails;
78
+ if (details === undefined)
79
+ throw new Error("MPP request body carries no `methodDetails` — nothing advertises an LCP reference");
80
+ const { atrHash, legalContextUrl } = details;
81
+ if (atrHash === undefined)
82
+ throw new Error("MPP request body advertises no `methodDetails.atrHash` — the placement's declared carrier is absent");
83
+ if (legalContextUrl === undefined)
84
+ throw new Error("MPP request body advertises no `methodDetails.legalContextUrl` — the placement's declared terms-URL field is absent");
85
+ if (!isAtrHash(atrHash))
86
+ throw new Error(`advertised atrHash is not a 0x-prefixed 32-byte hex: ${atrHash}`);
87
+ if (!legalContextUrl.startsWith("https://"))
88
+ throw new Error(`legalContextUrl must be HTTPS: ${legalContextUrl}`);
89
+ if (!BASE_UNIT_INT.test(parsed.amount))
90
+ throw new Error(`offer amount must be a base-unit integer string: "${parsed.amount}"`);
91
+ return {
92
+ advertisedAtrHash: atrHash,
93
+ legalContextUrl,
94
+ level: ctx.level,
95
+ offer: {
96
+ amount: parsed.amount,
97
+ // MPP's `currency` is a "Currency or asset identifier" (Table 2) and spans the whole family — an ISO
98
+ // 4217 code for `card`/`stripe`, a token address for `evm`, `sat` for `lightning`. It is carried
99
+ // verbatim as the unit string, exactly as ACP's fiat code and x402's `network:asset` pair are: the
100
+ // gate compares units for equality against the buyer's cap and never interprets them.
101
+ unit: parsed.currency,
102
+ },
103
+ sellerAssurance: ctx.sellerAssurance,
104
+ };
105
+ }
106
+ //# sourceMappingURL=proposal-mpp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-mpp.js","sourceRoot":"","sources":["../src/proposal-mpp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,sGAAsG;AACtG,sGAAsG;AACtG,0GAA0G;AAC1G,uFAAuF;AACvF,EAAE;AACF,2GAA2G;AAC3G,2GAA2G;AAC3G,0GAA0G;AAC1G,sDAAsD;AACtD,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,aAAa,EAAE,CAAC;SACb,MAAM,CAAC;QACN,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC9B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KACvC,CAAC;SACD,QAAQ,EAAE;CACd,CAAC,CAAC;AAEH,4GAA4G;AAC5G,0GAA0G;AAC1G,uGAAuG;AACvG,sGAAsG;AACtG,wGAAwG;AACxG,sGAAsG;AACtG,0GAA0G;AAC1G,+DAA+D;AAC/D,MAAM,aAAa,GAAG,UAAU,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,UAAU,2BAA2B,CACzC,OAAgB,EAChB,GAAoB;IAEpB,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAE/C,uGAAuG;IACvG,sGAAsG;IACtG,yGAAyG;IACzG,MAAM,OAAO,GAAG,MAAM,CAAC,aAAa,CAAC;IACrC,IAAI,OAAO,KAAK,SAAS;QACvB,MAAM,IAAI,KAAK,CACb,mFAAmF,CACpF,CAAC;IACJ,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC;IAC7C,IAAI,OAAO,KAAK,SAAS;QACvB,MAAM,IAAI,KAAK,CACb,qGAAqG,CACtG,CAAC;IACJ,IAAI,eAAe,KAAK,SAAS;QAC/B,MAAM,IAAI,KAAK,CACb,qHAAqH,CACtH,CAAC;IAEJ,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,wDAAwD,OAAO,EAAE,CAClE,CAAC;IACJ,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,UAAU,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,kCAAkC,eAAe,EAAE,CAAC,CAAC;IACvE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,qDAAqD,MAAM,CAAC,MAAM,GAAG,CACtE,CAAC;IAEJ,OAAO;QACL,iBAAiB,EAAE,OAAO;QAC1B,eAAe;QACf,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,KAAK,EAAE;YACL,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,qGAAqG;YACrG,iGAAiG;YACjG,mGAAmG;YACnG,sFAAsF;YACtF,IAAI,EAAE,MAAM,CAAC,QAAQ;SACtB;QACD,eAAe,EAAE,GAAG,CAAC,eAAe;KACrC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,239 @@
1
+ import { type ProtocolId } from "@integraledger/lcp-binding-core";
2
+ import { type PlacementDeployment } from "@integraledger/lcp-placements";
3
+ import { type GateProposal, type ProposalContext } from "./proposal.js";
4
+ /**
5
+ * ACP's `CheckoutSession.status` — the closed eleven-value enum, and the discriminant that separates an ACP
6
+ * session from every other checkout-shaped document in the set.
7
+ *
8
+ * DERIVED, NOT AUTHORED — from the live ACP schema, which `placement-acp` enumerated protocol-side in a
9
+ * write condition it no longer declares (checked 2026-07-30).
10
+ *
11
+ * ⚠️ THIS COPY NO LONGER HAS A DRIFT GUARD. It was pinned equal to the manifest's write condition through
12
+ * the registry, but the protocol line declares no `writeCondition` on any ACP manifest — its `readAlso` aliases
13
+ * carry `{path, encoding}` only — so the enum is not published anywhere to pin against. The pin test was
14
+ * deleted rather than loosened (an assertion over an absent field guards nothing). Re-derive this list
15
+ * against the live ACP schema when touching it; the discriminant tests only prove it matches our fixtures.
16
+ */
17
+ export declare const ACP_SESSION_STATUS: readonly string[];
18
+ /**
19
+ * The two Verifiable Intent Autonomous-mode credential types, derived from Mastercard's own open-mandate
20
+ * definitions. Formerly pinned against `placement-mastercard-vi`'s write condition; that placement is now
21
+ * declaration-only (LCP v1.38 §C.7 — an unregistered constraint type gets the WHOLE mandate rejected by a
22
+ * stock verifier), so it declares no write condition and there is nothing to pin against. Same standing as
23
+ * {@link ACP_SESSION_STATUS}: hand-kept, re-derive against the host when touching it.
24
+ */
25
+ export declare const VI_OPEN_MANDATE_VCT: readonly string[];
26
+ /**
27
+ * How ONE protocol's document is recognized — or the recorded fact that it cannot be.
28
+ *
29
+ * A keyed union rather than an optional `matches`, because "no rule" and "a rule that never fires" are
30
+ * different claims and only one of them is honest about a protocol whose document carries nothing to
31
+ * recognize. An `undiscriminable` row is a finding with a citation, not a hole: it says the live specification
32
+ * was read and no identifying member exists, which is the answer a future unit needs in order not to redo the
33
+ * reading, and it keeps the table TOTAL over the placement registry so a newly registered protocol cannot slip
34
+ * in unnoticed.
35
+ *
36
+ * `cite` is data rather than a comment because a claim about somebody else's protocol is exactly the kind that
37
+ * gets stale, and an audit trail that only a reader of the source can see is not one a deployment can act on.
38
+ */
39
+ export type ProtocolDiscriminant = {
40
+ readonly protocol: ProtocolId;
41
+ readonly kind: "structural";
42
+ /** Does this document belong to `protocol`? TOTAL — never throws, on any shape a wire can present. */
43
+ readonly matches: (doc: unknown) => boolean;
44
+ /** The host protocol's own source the rule was read from, and when. */
45
+ readonly cite: string;
46
+ } | {
47
+ readonly protocol: ProtocolId;
48
+ readonly kind: "undiscriminable";
49
+ /** Why no rule exists, and where the protocol's identity actually lives instead. */
50
+ readonly reason: string;
51
+ readonly cite: string;
52
+ };
53
+ /**
54
+ * Every supported protocol's document discriminant, one row each.
55
+ *
56
+ * DETECTION IS BY DISCRIMINANT, NEVER BY "does the field I want happen to be present": a document is x402
57
+ * because it carries `x402Version`, not because it has an atrHash somewhere. Detecting on the LCP field would
58
+ * make every protocol look alike the moment it carried a reference — which is exactly the situation this table
59
+ * exists to disambiguate.
60
+ *
61
+ * EVERY RULE IS POSITIVE. No row says "and not the other protocol's marker". Documents CAN satisfy two rows,
62
+ * and where they do the answer is AMBIGUITY, which {@link parseProposalUniversal} refuses. A negative term
63
+ * would convert that refusal into a silent choice, and a wrong negative term (one protocol adding a field
64
+ * another already had) converts it into a silent WRONG choice. Refusing is never the wrong answer; guessing
65
+ * sometimes is.
66
+ *
67
+ * THE TWO OVERLAPS ARE NOT THE SAME STRENGTH, and the difference is load-bearing:
68
+ *
69
+ * - ACP/UCP is CONTINGENT. A UCP checkout response that also carries `currency`, `totals` and `line_items`
70
+ * satisfies both rows; one that does not carries only `ucp`, `id`, `status` and `links` and is
71
+ * unambiguously UCP. Either protocol's documents remain individually reachable.
72
+ * - AP2/A2A is TOTAL. `ap2` matches `carriesAp2Mandate(doc) && isA2aMessage(doc)`, and `a2a` matches
73
+ * `isA2aTask(doc) || isA2aMessage(doc)`, so the `ap2` predicate is a strict logical SUBSET of the `a2a`
74
+ * one: every document the AP2 row fires on fires the A2A row too, without exception and by construction.
75
+ * So NO AP2 document is reachable through {@link parseProposalUniversal} — the ambiguity refusal always
76
+ * triggers first, and registering an `ap2` entry in {@link PROPOSAL_PARSERS} would not make one parseable.
77
+ * A caller holding an AP2 envelope names `ap2` and calls its parser directly. That is a consequence of AP2
78
+ * defining no transport of its own (v0.2 rides A2A), not a defect to engineer away: a negative term on the
79
+ * A2A row would be this package asserting that a valid A2A message is not one, which is A2A's call to
80
+ * make rather than ours — the host protocol defines its own documents.
81
+ *
82
+ * ORDERED as `KNOWN_PROTOCOL_IDS` orders the closed set, so `matchProtocols` reports in a stable order rather
83
+ * than in the order units happened to land.
84
+ */
85
+ export declare const PROTOCOL_DISCRIMINANTS: readonly ProtocolDiscriminant[];
86
+ /**
87
+ * Every protocol whose discriminant this document satisfies, in the table's order.
88
+ *
89
+ * ALL matches, never the first. A first-match detector cannot tell a document that belongs to one protocol
90
+ * from a document that belongs to two, and the second case is the one that costs a buyer money: an AP2
91
+ * envelope IS an A2A message, so both rows fire on it, and answering "a2a" because it came first would be a
92
+ * guess wearing an answer's clothes.
93
+ */
94
+ export declare function matchProtocols(wire: unknown): readonly ProtocolId[];
95
+ /**
96
+ * Identify which commerce protocol a wire document belongs to, structurally.
97
+ *
98
+ * Returns `undefined` unless EXACTLY ONE discriminant fires — so both "nothing matched" and "several matched"
99
+ * answer `undefined`, because a single-valued return cannot honestly distinguish them and inventing a
100
+ * preference is the failure this function exists to prevent. A caller that needs to tell the two apart calls
101
+ * {@link matchProtocols} and reads the length; {@link parseProposalUniversal} does exactly that, and refuses
102
+ * each case with its own message.
103
+ *
104
+ * This function never guesses and never throws.
105
+ */
106
+ export declare function detectProtocol(wire: unknown): ProtocolId | undefined;
107
+ /**
108
+ * The terms URL a document advertises at the path its manifest declares — or the reason there is no answer,
109
+ * stated rather than folded into an `undefined`.
110
+ *
111
+ * A UNION, because `string | undefined` conflated three facts and only two of them are absences. The third is
112
+ * x402 today: `PlacementManifest.termsUrlField` is SINGULAR, and x402's names a path INSIDE the
113
+ * `extensions.legalContext.info` carrier — while the challenge LCP v1.38 §C.4 illustrates carries both the
114
+ * reference and the URL in `accepts[].extra` instead, which the manifest declares as a `readAlso` alias. Read
115
+ * such a challenge and the hash answers from the alias while the declared terms path holds nothing. That is
116
+ * not "this seller advertised no terms"; it is "the manifest says nothing about where a terms URL rides on the
117
+ * carrier that answered". Returning `undefined` there would assert the seller's silence — and
118
+ * `parseProposalFromChallenge`, reading the same bytes in the same package, would contradict it.
119
+ *
120
+ * The gap is owed back to the manifest: a per-alias terms-URL declaration is the fix and it is a protocol-repo
121
+ * change. Until it lands this type REPORTS the gap, because a reader that cannot tell two facts apart must say
122
+ * which one it cannot tell.
123
+ */
124
+ export type AdvertisedTermsUrl =
125
+ /** Read from the field the manifest declares. */
126
+ {
127
+ readonly kind: "read";
128
+ readonly url: string;
129
+ }
130
+ /** The manifest declares no terms-URL field at all: the protocol has no room for one. */
131
+ | {
132
+ readonly kind: "no-field-declared";
133
+ }
134
+ /** The manifest declares one, its declaration reaches the answering carrier, and the document leaves it empty. */
135
+ | {
136
+ readonly kind: "declared-field-empty";
137
+ readonly field: string;
138
+ }
139
+ /**
140
+ * The manifest's terms-URL path lies INSIDE the canonical carrier's own object, and the reference answered
141
+ * from a declared alias instead — so the declaration never reached the carrier this document used. Whether
142
+ * the document advertises a terms URL is UNKNOWN to this reader, not answered.
143
+ */
144
+ | {
145
+ readonly kind: "undeclared-at-answering-carrier";
146
+ readonly field: string;
147
+ readonly answeredAt: string;
148
+ };
149
+ /** What a protocol document advertises about the terms governing it. */
150
+ export type AdvertisedTerms = {
151
+ /** The protocol whose manifest the carriers were read through. */
152
+ readonly protocol: ProtocolId;
153
+ /** The advertised ATR hash, reconciled across every integrity-bearing carrier the manifest declares. */
154
+ readonly advertisedAtrHash: `0x${string}`;
155
+ /** What the document says about where its terms live — read, absent, or not answerable from here. */
156
+ readonly legalContextUrl: AdvertisedTermsUrl;
157
+ };
158
+ /**
159
+ * Read what ANY supported protocol document advertises: its ATR hash, and its terms URL where the protocol
160
+ * has room for one.
161
+ *
162
+ * UNIVERSAL BY CONSTRUCTION. The carriers are not listed here — they are read out of the protocol's own
163
+ * `PlacementManifest` through `@integraledger/lcp-placements`, so a protocol this function supports is precisely
164
+ * one the build can also place a reference INTO, and adding a protocol product-side is nothing at all. There
165
+ * is no second place protocols are listed and no path by which a reader and a writer can drift.
166
+ *
167
+ * THE PROTOCOL IS AN ARGUMENT, NOT A DETECTION. Detection is the only part of this seam that can be wrong, so
168
+ * a caller that knows its protocol — which is the ordinary case, since a buyer agent knows which counterparty
169
+ * it dialled — never pays for it. {@link detectProtocol} is available for the caller that genuinely does not.
170
+ *
171
+ * FAIL-FAST, four ways, all loud:
172
+ *
173
+ * - a protocol with no registered placement (`mcp` is the only one, and LCP v1.38 §C.9 makes that terminal:
174
+ * it describes an LCP-aware MCP *server*, which has no document field for a reference to ride in);
175
+ * - a document advertising nothing at any declared carrier;
176
+ * - DISAGREEMENT between two declared carriers. This is the generalization of the x402 two-carrier rule to
177
+ * every protocol in the set, and it is deliberately stricter than the placement adapter's own `extract`,
178
+ * which answers with the canonical field and says nothing (`readDeclaredPaths` returns the first hit, not
179
+ * the set). A placement is structural and does not adjudicate a host's document; a BUYER must, because two
180
+ * different values on one document would let a seller advertise different terms to different readers of it
181
+ * and then disown whichever one it lost by. Preference is not an answer here — refusal is;
182
+ * - a reference that is not a `sha256` carrier. `carrierTypes` permits `url` on several manifests and that is
183
+ * correct for a placement, but the gate compares the advertised value against a RECOMPUTED record hash, and
184
+ * nothing but a hash can be compared to a hash.
185
+ *
186
+ * `deployment` is required only for a protocol whose placement is namespaced — Mastercard VI, whose constraint
187
+ * type is minted under the deployment's own reverse-domain namespace and has no default. Omitting it there
188
+ * throws from the registry rather than answering about some invented namespace.
189
+ */
190
+ export declare function readAdvertisedTerms(protocol: ProtocolId, wire: unknown, deployment?: PlacementDeployment): AdvertisedTerms;
191
+ /** A parser from one protocol's wire document to the one `GateProposal` every parser produces. */
192
+ export type ProposalParser = (wire: unknown, ctx: ProposalContext) => GateProposal;
193
+ /**
194
+ * Every protocol this build can turn into a complete `GateProposal`, keyed by `ProtocolId`.
195
+ *
196
+ * SMALLER THAN THE PLACEMENT REGISTRY, and the difference is a fact about the protocols rather than a gap in
197
+ * this package. A `GateProposal` carries an OFFER — an amount and a unit — and an offer is protocol-native
198
+ * economics that no `PlacementManifest` declares and LCP does not standardize: x402 quotes it in
199
+ * `accepts[].amount` with a `network:asset` unit, ACP in the row of `totals` typed `total` with an ISO 4217
200
+ * currency, and the remaining seven each differently again. Inventing an offer-locator axis product-side would
201
+ * put protocol knowledge in a second place and put it there UNGATED, which is the one thing the placement seam
202
+ * was built to stop. {@link readAdvertisedTerms} is universal because the reference is; this is not, because
203
+ * the offer is not.
204
+ *
205
+ * ONE PROTOCOL COULD NOT BE DISPATCHED HERE EVEN IF ITS OFFER WERE READABLE. `ap2`'s discriminant is a strict
206
+ * subset of `a2a`'s, so an AP2 envelope always matches two rows and {@link parseProposalUniversal} refuses it
207
+ * as ambiguous before any lookup in this map. An `ap2` entry added here would be unreachable through the
208
+ * universal door; the parser a future unit writes must be exported and called by name.
209
+ *
210
+ * `Object.freeze` for the same reason `PLACEMENTS` is frozen: a published package is consumed as JavaScript,
211
+ * where the type alone does not stop a consumer swapping the parser that decides what a buyer is agreeing to.
212
+ */
213
+ export declare const PROPOSAL_PARSERS: Readonly<Partial<Record<ProtocolId, ProposalParser>>>;
214
+ /** Every protocol this build can parse a complete proposal from, in registration order. */
215
+ export declare function parseableProtocols(): readonly ProtocolId[];
216
+ /**
217
+ * Parse ANY supported wire into the one `GateProposal` every parser produces.
218
+ *
219
+ * The universal entry point: a buyer no longer has to know which protocol it is on to gate a
220
+ * transaction. Every named parser stays exported — a caller that DOES know its protocol should keep calling
221
+ * the specific one, because a known protocol needs no detection and detection is the only part that can be
222
+ * wrong.
223
+ *
224
+ * Fail-fast on four conditions, all loud: an unidentifiable wire, an AMBIGUOUS wire matching more than one
225
+ * protocol's discriminant, a protocol whose offer this build cannot read, and any refusal from the protocol
226
+ * parser it routes to. There is no "try them all and take the first that works" path — that is a fallback
227
+ * chain, and it would let a malformed document of one protocol be silently reinterpreted as a valid document
228
+ * of another.
229
+ *
230
+ * The named parsers are DELEGATED to rather than reimplemented over the placement manifests, and one of them
231
+ * proves why that is not merely convenient: x402's wire carries `legalContextUrl` in BOTH `accepts[].extra`
232
+ * and `extensions.legalContext.info` — LCP v1.38 §C.4's own illustration uses the first — while
233
+ * `PlacementManifest.termsUrlField` is singular and declares only the second. Routing x402 through the
234
+ * manifest would drop the terms URL from a spec-legal challenge. The reference reconcile generalizes; the
235
+ * terms-URL locator does not yet, so this entry point delegates and {@link readAdvertisedTerms} REPORTS the
236
+ * shortfall as `undeclared-at-answering-carrier` rather than answering an absence it cannot vouch for.
237
+ */
238
+ export declare function parseProposalUniversal(wire: unknown, ctx: ProposalContext): GateProposal;
239
+ //# sourceMappingURL=proposal-universal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-universal.d.ts","sourceRoot":"","sources":["../src/proposal-universal.ts"],"names":[],"mappings":"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"}