@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,470 @@
1
+ import { decodeDeclaredRead, readAtPath, readDeclaredPaths, readFromContainer, } from "@integraledger/lcp-binding-core";
2
+ import { placementFor, } from "@integraledger/lcp-placements";
3
+ import { parseProposalFromChallenge, } from "./proposal.js";
4
+ import { parseProposalFromAcpCheckout } from "./proposal-acp.js";
5
+ // ---------------------------------------------------------------------------------------------------------
6
+ // Structural predicates. Every read goes through `readAtPath`, never through `doc.x` or `doc["x"]`, so the
7
+ // own-property and array-traversal rules a hostile wire is read under are binding-core's audited ones rather
8
+ // than a second set invented here. A prototype key answering as though it were a declared field is exactly
9
+ // how a detector gets talked into naming the wrong protocol.
10
+ // ---------------------------------------------------------------------------------------------------------
11
+ function isRecord(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+ function isStringAt(doc, path) {
15
+ return typeof readAtPath(doc, path) === "string";
16
+ }
17
+ function isArrayAt(doc, path) {
18
+ return Array.isArray(readAtPath(doc, path));
19
+ }
20
+ function stringAtIsOneOf(doc, path, permitted) {
21
+ const value = readAtPath(doc, path);
22
+ // `some` rather than a `typeof` guard plus `includes`: the guard would be a branch no input can take, since
23
+ // a list of strings never contains a non-string, and a branch nothing reaches is one no test can constrain.
24
+ return permitted.some((token) => token === value);
25
+ }
26
+ /**
27
+ * ACP's `CheckoutSession.status` — the closed eleven-value enum, and the discriminant that separates an ACP
28
+ * session from every other checkout-shaped document in the set.
29
+ *
30
+ * DERIVED, NOT AUTHORED — from the live ACP schema, which `placement-acp` enumerated protocol-side in a
31
+ * write condition it no longer declares (checked 2026-07-30).
32
+ *
33
+ * ⚠️ THIS COPY NO LONGER HAS A DRIFT GUARD. It was pinned equal to the manifest's write condition through
34
+ * the registry, but the protocol line declares no `writeCondition` on any ACP manifest — its `readAlso` aliases
35
+ * carry `{path, encoding}` only — so the enum is not published anywhere to pin against. The pin test was
36
+ * deleted rather than loosened (an assertion over an absent field guards nothing). Re-derive this list
37
+ * against the live ACP schema when touching it; the discriminant tests only prove it matches our fixtures.
38
+ */
39
+ export const ACP_SESSION_STATUS = [
40
+ "incomplete",
41
+ "not_ready_for_payment",
42
+ "requires_escalation",
43
+ "authentication_required",
44
+ "ready_for_payment",
45
+ "pending_approval",
46
+ "complete_in_progress",
47
+ "completed",
48
+ "canceled",
49
+ "in_progress",
50
+ "expired",
51
+ ];
52
+ /**
53
+ * The two Verifiable Intent Autonomous-mode credential types, derived from Mastercard's own open-mandate
54
+ * definitions. Formerly pinned against `placement-mastercard-vi`'s write condition; that placement is now
55
+ * declaration-only (LCP v1.38 §C.7 — an unregistered constraint type gets the WHOLE mandate rejected by a
56
+ * stock verifier), so it declares no write condition and there is nothing to pin against. Same standing as
57
+ * {@link ACP_SESSION_STATUS}: hand-kept, re-derive against the host when touching it.
58
+ */
59
+ export const VI_OPEN_MANDATE_VCT = [
60
+ "mandate.checkout.open.1",
61
+ "mandate.payment.open.1",
62
+ ];
63
+ /** The two AP2 v0.2 mandate DataPart keys. An envelope carrying either is carrying an AP2 mandate. */
64
+ const AP2_MANDATE_DATA_KEYS = [
65
+ "ap2.mandates.CheckoutMandateSdJwt",
66
+ "ap2.mandates.PaymentMandateSdJwt",
67
+ ];
68
+ /** Visa TAP's two agent-recognition signature tags, as they appear quoted in an RFC 9421 `Signature-Input`. */
69
+ const TAP_SIGNATURE_TAGS = [
70
+ "agent-browser-auth",
71
+ "agent-payer-auth",
72
+ ];
73
+ /** An A2A `Task`: `id` and `status` are the two REQUIRED members (a2a.proto lines 178-190). */
74
+ function isA2aTask(doc) {
75
+ return isStringAt(doc, "id") && isStringAt(doc, "status.state");
76
+ }
77
+ /** An A2A `Message`: `messageId`, `role` and `parts` are its three REQUIRED members (a2a.proto 267-280). */
78
+ function isA2aMessage(doc) {
79
+ return (isStringAt(doc, "messageId") &&
80
+ isStringAt(doc, "role") &&
81
+ isArrayAt(doc, "parts"));
82
+ }
83
+ /**
84
+ * Does this document carry an AP2 mandate DataPart?
85
+ *
86
+ * Reads `parts` itself rather than trusting a caller to have checked it, because the AP2 rule asks this
87
+ * question FIRST — the mandate part is AP2's own half of the discriminant, and the A2A envelope check is a
88
+ * second, weaker fact about it.
89
+ */
90
+ function carriesAp2Mandate(doc) {
91
+ const parts = readAtPath(doc, "parts");
92
+ if (!Array.isArray(parts))
93
+ return false;
94
+ return parts.some((_, index) => {
95
+ const data = readAtPath(doc, `parts.${index}.data`);
96
+ return (isRecord(data) &&
97
+ AP2_MANDATE_DATA_KEYS.some((key) => Object.hasOwn(data, key)));
98
+ });
99
+ }
100
+ /**
101
+ * Every supported protocol's document discriminant, one row each.
102
+ *
103
+ * DETECTION IS BY DISCRIMINANT, NEVER BY "does the field I want happen to be present": a document is x402
104
+ * because it carries `x402Version`, not because it has an atrHash somewhere. Detecting on the LCP field would
105
+ * make every protocol look alike the moment it carried a reference — which is exactly the situation this table
106
+ * exists to disambiguate.
107
+ *
108
+ * EVERY RULE IS POSITIVE. No row says "and not the other protocol's marker". Documents CAN satisfy two rows,
109
+ * and where they do the answer is AMBIGUITY, which {@link parseProposalUniversal} refuses. A negative term
110
+ * would convert that refusal into a silent choice, and a wrong negative term (one protocol adding a field
111
+ * another already had) converts it into a silent WRONG choice. Refusing is never the wrong answer; guessing
112
+ * sometimes is.
113
+ *
114
+ * THE TWO OVERLAPS ARE NOT THE SAME STRENGTH, and the difference is load-bearing:
115
+ *
116
+ * - ACP/UCP is CONTINGENT. A UCP checkout response that also carries `currency`, `totals` and `line_items`
117
+ * satisfies both rows; one that does not carries only `ucp`, `id`, `status` and `links` and is
118
+ * unambiguously UCP. Either protocol's documents remain individually reachable.
119
+ * - AP2/A2A is TOTAL. `ap2` matches `carriesAp2Mandate(doc) && isA2aMessage(doc)`, and `a2a` matches
120
+ * `isA2aTask(doc) || isA2aMessage(doc)`, so the `ap2` predicate is a strict logical SUBSET of the `a2a`
121
+ * one: every document the AP2 row fires on fires the A2A row too, without exception and by construction.
122
+ * So NO AP2 document is reachable through {@link parseProposalUniversal} — the ambiguity refusal always
123
+ * triggers first, and registering an `ap2` entry in {@link PROPOSAL_PARSERS} would not make one parseable.
124
+ * A caller holding an AP2 envelope names `ap2` and calls its parser directly. That is a consequence of AP2
125
+ * defining no transport of its own (v0.2 rides A2A), not a defect to engineer away: a negative term on the
126
+ * A2A row would be this package asserting that a valid A2A message is not one, which is A2A's call to
127
+ * make rather than ours — the host protocol defines its own documents.
128
+ *
129
+ * ORDERED as `KNOWN_PROTOCOL_IDS` orders the closed set, so `matchProtocols` reports in a stable order rather
130
+ * than in the order units happened to land.
131
+ */
132
+ export const PROTOCOL_DISCRIMINANTS = [
133
+ {
134
+ protocol: "x402",
135
+ kind: "structural",
136
+ matches: (doc) => typeof readAtPath(doc, "x402Version") === "number" &&
137
+ isArrayAt(doc, "accepts"),
138
+ cite: "x402 v2 PaymentRequired — `x402Version` plus the `accepts` requirement array (coinbase/x402 specs/x402-specification-v2.md §5.1.2, read 2026-07-30)",
139
+ },
140
+ {
141
+ protocol: "mpp",
142
+ kind: "undiscriminable",
143
+ reason: "The document `placement-mpp` operates on is the DECODED `request` auth-param body, whose members are `amount` and `currency` (both REQUIRED strings, §5.1.1) plus optional `recipient`, `description`, `externalId` and `methodDetails` (§5.1.2). Not one of them names MPP, and an amount/currency pair is the shape of almost every payment document there is. MPP's identity lives one layer OUT, in the `WWW-Authenticate: Payment` challenge's auth-params (`realm`, `method`, `id`, `request`, `expires`, `digest`, `opaque`) — which is not the document the placement reads, so a caller holding a decoded request body must name `mpp` itself. Note `expires` is an auth-param ONLY: §5.1.2 states that expiry is conveyed in the challenge and that request objects MUST NOT duplicate it, so a body carrying one is malformed rather than discriminating.",
144
+ cite: "MPP charge intent draft-payment-intent-charge-00 §5.1.1 Table 2 (required: amount, currency) and §5.1.2 Table 3 (optional: recipient, description, externalId, methodDetails) (mpp.dev/intents/charge, read 2026-07-30)",
145
+ },
146
+ {
147
+ protocol: "ap2",
148
+ kind: "structural",
149
+ // AP2's own half first, A2A's second: the mandate DataPart is what makes this AP2 rather than any other
150
+ // A2A message, and asking it first is also what keeps the `parts`-is-an-array guard a reachable branch
151
+ // instead of one another predicate has already made true.
152
+ //
153
+ // This conjunction is a strict subset of the `a2a` row's disjunction, so the two ALWAYS co-fire — see the
154
+ // table docblock. `ap2` is therefore a detect-and-name row, never a dispatchable one.
155
+ matches: (doc) => carriesAp2Mandate(doc) && isA2aMessage(doc),
156
+ cite: "AP2 v0.2 defines no transport; its reference samples carry mandates as A2A DataParts keyed `ap2.mandates.CheckoutMandateSdJwt` / `ap2.mandates.PaymentMandateSdJwt`, and the reference rides the A2A envelope's `metadata` beside them (placement-ap2 specification gate discharged 2026-07-30)",
157
+ },
158
+ {
159
+ protocol: "ack",
160
+ kind: "structural",
161
+ matches: (doc) => {
162
+ const types = readAtPath(doc, "type");
163
+ return (Array.isArray(types) &&
164
+ types.includes("PaymentReceiptCredential") &&
165
+ isRecord(readAtPath(doc, "credentialSubject")));
166
+ },
167
+ cite: 'ACK-Pay `createPaymentReceipt` mints a W3C credential typed ["VerifiableCredential", "PaymentReceiptCredential"] with a `credentialSubject` (agentcommercekit/ack packages/ack-pay/src/create-payment-receipt.ts + packages/vc/src/create-credential.ts, read 2026-07-30)',
168
+ },
169
+ {
170
+ protocol: "acp",
171
+ kind: "structural",
172
+ matches: (doc) => stringAtIsOneOf(doc, "status", ACP_SESSION_STATUS) &&
173
+ isStringAt(doc, "currency") &&
174
+ isArrayAt(doc, "totals") &&
175
+ isArrayAt(doc, "line_items"),
176
+ cite: "ACP agentic checkout (stable 2026-04-17) — `status` is REQUIRED on `CheckoutSessionBase` and is a closed eleven-value enum absent from all three request schemas; `currency`, `totals` and `line_items` are its siblings (enum enumerated live 2026-07-30, see placement-acp's write condition)",
177
+ },
178
+ {
179
+ protocol: "ucp",
180
+ kind: "structural",
181
+ matches: (doc) => isRecord(readAtPath(doc, "ucp")) &&
182
+ isStringAt(doc, "id") &&
183
+ isStringAt(doc, "status") &&
184
+ isArrayAt(doc, "links"),
185
+ cite: "UCP checkout response — `ucp` (the UCP metadata object), `id`, `status` and `links` are all REQUIRED, and `ucp` is the member no other protocol in the set carries (ucp.dev/latest/specification/checkout, read 2026-07-30)",
186
+ },
187
+ {
188
+ protocol: "visa-tap",
189
+ kind: "structural",
190
+ matches: (doc) => {
191
+ const signatureInput = readFromContainer(doc, { kind: "header-map" }, "headers.signature-input");
192
+ return (typeof signatureInput === "string" &&
193
+ TAP_SIGNATURE_TAGS.some((tag) => signatureInput.includes(`"${tag}"`)));
194
+ },
195
+ cite: "Visa TAP agent recognition signature — an RFC 9421 `Signature-Input` whose `tag` parameter is `agent-browser-auth` or `agent-payer-auth` (placement-visa-tap specification gate discharged 2026-07-30)",
196
+ },
197
+ {
198
+ protocol: "mastercard-vi",
199
+ kind: "structural",
200
+ matches: (doc) => stringAtIsOneOf(doc, "vct", VI_OPEN_MANDATE_VCT) &&
201
+ isArrayAt(doc, "constraints"),
202
+ cite: "Verifiable Intent — `constraints` appears in Autonomous-mode open mandates only, whose `vct` is `mandate.checkout.open.1` or `mandate.payment.open.1` (placement-mastercard-vi specification gate discharged 2026-07-30)",
203
+ },
204
+ {
205
+ protocol: "a2a",
206
+ kind: "structural",
207
+ matches: (doc) => isA2aTask(doc) || isA2aMessage(doc),
208
+ cite: "A2A — `Task` REQUIRES `id` and `status`, `Message` REQUIRES `messageId`, `role` and `parts`; both carry the free-form `metadata` map this placement writes (a2aproject/A2A specification/a2a.proto @ main, read 2026-07-30)",
209
+ },
210
+ ];
211
+ /**
212
+ * Every protocol whose discriminant this document satisfies, in the table's order.
213
+ *
214
+ * ALL matches, never the first. A first-match detector cannot tell a document that belongs to one protocol
215
+ * from a document that belongs to two, and the second case is the one that costs a buyer money: an AP2
216
+ * envelope IS an A2A message, so both rows fire on it, and answering "a2a" because it came first would be a
217
+ * guess wearing an answer's clothes.
218
+ */
219
+ export function matchProtocols(wire) {
220
+ const matched = [];
221
+ for (const row of PROTOCOL_DISCRIMINANTS) {
222
+ if (row.kind === "structural" && row.matches(wire))
223
+ matched.push(row.protocol);
224
+ }
225
+ return matched;
226
+ }
227
+ /**
228
+ * Identify which commerce protocol a wire document belongs to, structurally.
229
+ *
230
+ * Returns `undefined` unless EXACTLY ONE discriminant fires — so both "nothing matched" and "several matched"
231
+ * answer `undefined`, because a single-valued return cannot honestly distinguish them and inventing a
232
+ * preference is the failure this function exists to prevent. A caller that needs to tell the two apart calls
233
+ * {@link matchProtocols} and reads the length; {@link parseProposalUniversal} does exactly that, and refuses
234
+ * each case with its own message.
235
+ *
236
+ * This function never guesses and never throws.
237
+ */
238
+ export function detectProtocol(wire) {
239
+ const matched = matchProtocols(wire);
240
+ return matched.length === 1 ? matched[0] : undefined;
241
+ }
242
+ /**
243
+ * The canonical field plus every declared alias, each flattened into a one-field manifest of its own.
244
+ *
245
+ * A SLOT IS A MANIFEST, so the actual reading is `readDeclaredPaths` — the same function, once per declared
246
+ * path, rather than a second decoder that has to be kept honest by review. Only the INHERITANCE rules are
247
+ * restated (an alias takes the manifest's container and encoding unless it declares its own, and is
248
+ * `integrity` unless it says otherwise), because binding-core resolves those inside the first-hit-wins loop a
249
+ * buyer cannot use. Everything downstream of a resolved slot — the `bare-value` type coming from
250
+ * `carrierTypes[0]`, the wrapping into the §8.1 codec, the corrupt-value throw — stays where it was written.
251
+ *
252
+ * A declared `bareType` becomes the slot's WHOLE `carrierTypes`, which is the same thing said in the shape
253
+ * this call takes: a bare value carries no type tag, so its type is whatever its one-field contract fixes.
254
+ */
255
+ function carrierSlots(manifest) {
256
+ const canonical = {
257
+ path: manifest.field,
258
+ container: manifest.container,
259
+ encoding: manifest.encoding,
260
+ carrierClass: "integrity",
261
+ carrierTypes: manifest.carrierTypes,
262
+ };
263
+ return [
264
+ canonical,
265
+ ...(manifest.readAlso ?? []).map((alias) => ({
266
+ path: alias.path,
267
+ container: alias.container ?? manifest.container,
268
+ encoding: alias.encoding ?? manifest.encoding,
269
+ carrierClass: alias.carrierClass ?? "integrity",
270
+ carrierTypes: alias.bareType === undefined
271
+ ? manifest.carrierTypes
272
+ : [alias.bareType],
273
+ })),
274
+ ];
275
+ }
276
+ /**
277
+ * The reference each declared INTEGRITY carrier actually holds, with the path it was read from.
278
+ *
279
+ * Discovery-class slots are skipped before they are read, not after: UCP's `links[type=terms_of_service].url`
280
+ * locates the terms without attesting to them — LCP v1.38 §C.3 files UCP's `links` under "discovery without
281
+ * integrity" and says a standing policy page "is not a per-transaction terms record and carries no hash",
282
+ * which is exactly why one cannot stand in for the other. Reading
283
+ * it and discarding it later would also mean decoding a URL under the capability field's `sha256` contract,
284
+ * which is a corrupt carrier and throws.
285
+ */
286
+ function integrityHits(doc, manifest) {
287
+ const hits = [];
288
+ for (const slot of carrierSlots(manifest)) {
289
+ if (slot.carrierClass !== "integrity")
290
+ continue;
291
+ const read = readDeclaredPaths(doc, {
292
+ field: slot.path,
293
+ container: slot.container,
294
+ encoding: slot.encoding,
295
+ carrierTypes: slot.carrierTypes,
296
+ });
297
+ if (read === undefined)
298
+ continue;
299
+ const ref = decodeDeclaredRead(read);
300
+ if (ref === undefined)
301
+ continue;
302
+ hits.push({ path: slot.path, ref });
303
+ }
304
+ return hits;
305
+ }
306
+ /** Two references are the same reference iff type and value agree — hex compared case-insensitively. */
307
+ function carrierKey(ref) {
308
+ return ref.type === "sha256"
309
+ ? `sha256:${ref.value.toLowerCase()}`
310
+ : `${ref.type}:${ref.value}`;
311
+ }
312
+ /**
313
+ * Read what ANY supported protocol document advertises: its ATR hash, and its terms URL where the protocol
314
+ * has room for one.
315
+ *
316
+ * UNIVERSAL BY CONSTRUCTION. The carriers are not listed here — they are read out of the protocol's own
317
+ * `PlacementManifest` through `@integraledger/lcp-placements`, so a protocol this function supports is precisely
318
+ * one the build can also place a reference INTO, and adding a protocol product-side is nothing at all. There
319
+ * is no second place protocols are listed and no path by which a reader and a writer can drift.
320
+ *
321
+ * THE PROTOCOL IS AN ARGUMENT, NOT A DETECTION. Detection is the only part of this seam that can be wrong, so
322
+ * a caller that knows its protocol — which is the ordinary case, since a buyer agent knows which counterparty
323
+ * it dialled — never pays for it. {@link detectProtocol} is available for the caller that genuinely does not.
324
+ *
325
+ * FAIL-FAST, four ways, all loud:
326
+ *
327
+ * - a protocol with no registered placement (`mcp` is the only one, and LCP v1.38 §C.9 makes that terminal:
328
+ * it describes an LCP-aware MCP *server*, which has no document field for a reference to ride in);
329
+ * - a document advertising nothing at any declared carrier;
330
+ * - DISAGREEMENT between two declared carriers. This is the generalization of the x402 two-carrier rule to
331
+ * every protocol in the set, and it is deliberately stricter than the placement adapter's own `extract`,
332
+ * which answers with the canonical field and says nothing (`readDeclaredPaths` returns the first hit, not
333
+ * the set). A placement is structural and does not adjudicate a host's document; a BUYER must, because two
334
+ * different values on one document would let a seller advertise different terms to different readers of it
335
+ * and then disown whichever one it lost by. Preference is not an answer here — refusal is;
336
+ * - a reference that is not a `sha256` carrier. `carrierTypes` permits `url` on several manifests and that is
337
+ * correct for a placement, but the gate compares the advertised value against a RECOMPUTED record hash, and
338
+ * nothing but a hash can be compared to a hash.
339
+ *
340
+ * `deployment` is required only for a protocol whose placement is namespaced — Mastercard VI, whose constraint
341
+ * type is minted under the deployment's own reverse-domain namespace and has no default. Omitting it there
342
+ * throws from the registry rather than answering about some invented namespace.
343
+ */
344
+ export function readAdvertisedTerms(protocol, wire, deployment) {
345
+ const placement = placementFor(protocol, deployment);
346
+ if (placement === undefined)
347
+ throw new Error(`no placement is registered for ${protocol} — this build cannot read a reference out of its documents`);
348
+ const manifest = placement.manifest;
349
+ const hits = integrityHits(wire, manifest);
350
+ const sole = hits[0];
351
+ if (sole === undefined) {
352
+ const looked = carrierSlots(manifest)
353
+ .filter((slot) => slot.carrierClass === "integrity")
354
+ .map((slot) => slot.path)
355
+ .join(", ");
356
+ throw new Error(`${protocol} document advertises no LCP reference at any declared integrity carrier (${looked})`);
357
+ }
358
+ const distinct = new Set(hits.map((hit) => carrierKey(hit.ref)));
359
+ if (distinct.size > 1)
360
+ throw new Error(`${protocol} carriers disagree — ${hits
361
+ .map((hit) => `${hit.path} advertises lcp:${hit.ref.type}:${hit.ref.value}`)
362
+ .join(", ")}`);
363
+ const ref = sole.ref;
364
+ if (ref.type !== "sha256")
365
+ throw new Error(`${protocol} advertises an lcp:${ref.type}: reference; the gate compares the advertised value against a recomputed record hash, which only a sha256 carrier can be`);
366
+ return {
367
+ protocol,
368
+ advertisedAtrHash: ref.value,
369
+ legalContextUrl: readTermsUrl(protocol, wire, manifest, sole.path),
370
+ };
371
+ }
372
+ /**
373
+ * The terms URL at the field the manifest DECLARES — or which of the three absences this is.
374
+ *
375
+ * `termsUrlField` is a plain dotted path on the manifest: it declares no container of its own, so it is read
376
+ * with `readAtPath` rather than through the reference carrier's container. A manifest that omits it (A2A, ACK,
377
+ * Visa TAP, Mastercard VI) is stating that its protocol has no room for one — a fact, not a gap.
378
+ *
379
+ * NOTHING AT THE DECLARED PATH IS TWO DIFFERENT FACTS, and which one it is turns on where the reference
380
+ * answered from. `termsUrlField` is declared once, beside the canonical `field`; a `readAlso` alias carries no
381
+ * terms-URL declaration of its own. So when the declared path is nested INSIDE the canonical field — x402's
382
+ * `extensions.legalContext.info.legalContextUrl` inside `extensions.legalContext.info` — and the reference
383
+ * answered from an alias instead, the manifest's declaration was scoped to a carrier this document did not
384
+ * use, and its emptiness says nothing about the document. When the declared path is a SIBLING of the canonical
385
+ * field (ACP's `metadata.legal_context_url`, MPP's `methodDetails.legalContextUrl`), it is reachable whichever
386
+ * carrier answered, and empty means empty.
387
+ *
388
+ * `answeredAt` is the path the reconciled reference was read from. `carrierSlots` puts the canonical field
389
+ * first and `integrityHits` preserves that order, so it differs from `manifest.field` exactly when the
390
+ * canonical carrier did not answer.
391
+ */
392
+ function readTermsUrl(protocol, doc, manifest, answeredAt) {
393
+ const field = manifest.termsUrlField;
394
+ if (field === undefined)
395
+ return { kind: "no-field-declared" };
396
+ const raw = readAtPath(doc, field);
397
+ if (raw === undefined)
398
+ return answeredAt !== manifest.field &&
399
+ field.startsWith(`${manifest.field}.`)
400
+ ? { kind: "undeclared-at-answering-carrier", field, answeredAt }
401
+ : { kind: "declared-field-empty", field };
402
+ if (typeof raw !== "string")
403
+ throw new Error(`${protocol} ${field} is not a string: ${JSON.stringify(raw)}`);
404
+ if (!raw.startsWith("https://"))
405
+ throw new Error(`legalContextUrl must be HTTPS: ${raw}`);
406
+ return { kind: "read", url: raw };
407
+ }
408
+ /**
409
+ * Every protocol this build can turn into a complete `GateProposal`, keyed by `ProtocolId`.
410
+ *
411
+ * SMALLER THAN THE PLACEMENT REGISTRY, and the difference is a fact about the protocols rather than a gap in
412
+ * this package. A `GateProposal` carries an OFFER — an amount and a unit — and an offer is protocol-native
413
+ * economics that no `PlacementManifest` declares and LCP does not standardize: x402 quotes it in
414
+ * `accepts[].amount` with a `network:asset` unit, ACP in the row of `totals` typed `total` with an ISO 4217
415
+ * currency, and the remaining seven each differently again. Inventing an offer-locator axis product-side would
416
+ * put protocol knowledge in a second place and put it there UNGATED, which is the one thing the placement seam
417
+ * was built to stop. {@link readAdvertisedTerms} is universal because the reference is; this is not, because
418
+ * the offer is not.
419
+ *
420
+ * ONE PROTOCOL COULD NOT BE DISPATCHED HERE EVEN IF ITS OFFER WERE READABLE. `ap2`'s discriminant is a strict
421
+ * subset of `a2a`'s, so an AP2 envelope always matches two rows and {@link parseProposalUniversal} refuses it
422
+ * as ambiguous before any lookup in this map. An `ap2` entry added here would be unreachable through the
423
+ * universal door; the parser a future unit writes must be exported and called by name.
424
+ *
425
+ * `Object.freeze` for the same reason `PLACEMENTS` is frozen: a published package is consumed as JavaScript,
426
+ * where the type alone does not stop a consumer swapping the parser that decides what a buyer is agreeing to.
427
+ */
428
+ export const PROPOSAL_PARSERS = Object.freeze({
429
+ x402: parseProposalFromChallenge,
430
+ acp: parseProposalFromAcpCheckout,
431
+ });
432
+ /** Every protocol this build can parse a complete proposal from, in registration order. */
433
+ export function parseableProtocols() {
434
+ return Object.keys(PROPOSAL_PARSERS);
435
+ }
436
+ /**
437
+ * Parse ANY supported wire into the one `GateProposal` every parser produces.
438
+ *
439
+ * The universal entry point: a buyer no longer has to know which protocol it is on to gate a
440
+ * transaction. Every named parser stays exported — a caller that DOES know its protocol should keep calling
441
+ * the specific one, because a known protocol needs no detection and detection is the only part that can be
442
+ * wrong.
443
+ *
444
+ * Fail-fast on four conditions, all loud: an unidentifiable wire, an AMBIGUOUS wire matching more than one
445
+ * protocol's discriminant, a protocol whose offer this build cannot read, and any refusal from the protocol
446
+ * parser it routes to. There is no "try them all and take the first that works" path — that is a fallback
447
+ * chain, and it would let a malformed document of one protocol be silently reinterpreted as a valid document
448
+ * of another.
449
+ *
450
+ * The named parsers are DELEGATED to rather than reimplemented over the placement manifests, and one of them
451
+ * proves why that is not merely convenient: x402's wire carries `legalContextUrl` in BOTH `accepts[].extra`
452
+ * and `extensions.legalContext.info` — LCP v1.38 §C.4's own illustration uses the first — while
453
+ * `PlacementManifest.termsUrlField` is singular and declares only the second. Routing x402 through the
454
+ * manifest would drop the terms URL from a spec-legal challenge. The reference reconcile generalizes; the
455
+ * terms-URL locator does not yet, so this entry point delegates and {@link readAdvertisedTerms} REPORTS the
456
+ * shortfall as `undeclared-at-answering-carrier` rather than answering an absence it cannot vouch for.
457
+ */
458
+ export function parseProposalUniversal(wire, ctx) {
459
+ const matched = matchProtocols(wire);
460
+ if (matched.length > 1)
461
+ throw new Error(`ambiguous wire — it matches the discriminants of ${matched.length} protocols (${matched.join(", ")}); a document that is legitimately two protocols' documents is not resolved by preference, so name the protocol and call its parser directly`);
462
+ const protocol = matched[0];
463
+ if (protocol === undefined)
464
+ throw new Error("could not identify which commerce protocol this document belongs to — no declared discriminant matched");
465
+ const parse = PROPOSAL_PARSERS[protocol];
466
+ if (parse === undefined)
467
+ throw new Error(`no buyer proposal parser for ${protocol} — this build parses ${parseableProtocols().join(", ")}. ${protocol} quotes its offer in a shape no placement manifest declares; readAdvertisedTerms("${protocol}", …) reads its LCP reference today`);
468
+ return parse(wire, ctx);
469
+ }
470
+ //# sourceMappingURL=proposal-universal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal-universal.js","sourceRoot":"","sources":["../src/proposal-universal.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { Assurance } from "@integraledger/lcp-authority";
2
+ /** The TYPED inputs the gate decides on. It CANNOT carry natural-language prose — the prompt-injection
3
+ * boundary is architectural (LCP §12.7). Prose is fetched and retained as evidence, never fed to policy. */
4
+ export interface GateProposal {
5
+ readonly advertisedAtrHash: `0x${string}`;
6
+ readonly legalContextUrl: string;
7
+ readonly level: 1 | 2 | 3 | 4;
8
+ readonly offer: {
9
+ readonly amount: string;
10
+ readonly unit: string;
11
+ };
12
+ readonly sellerAssurance: Assurance;
13
+ }
14
+ /**
15
+ * Context the host challenge does NOT carry — the buyer's client establishes it: the LCP trust level and
16
+ * the seller's stated assurance.
17
+ *
18
+ * **There is deliberately no offer-validity window here.** An earlier shape carried `validFrom`/`validUntil`
19
+ * on every proposal and the gate read neither, which told a reader that expiry was gated when it was not.
20
+ * The window is not a gap to fill: whether a quote has gone stale is AGENT OPERATIONS, and LCP has no
21
+ * opinion on those — its subject is that final terms are provably bound to the payment. This gate's job
22
+ * stops at the binding. A buyer that wants offer-expiry policy holds it in its own client, where the
23
+ * decision belongs, and reaches `transact` only when it still intends to pay.
24
+ */
25
+ export interface ProposalContext {
26
+ readonly level: 1 | 2 | 3 | 4;
27
+ readonly sellerAssurance: Assurance;
28
+ }
29
+ /** Parse an x402 402 challenge into a typed GateProposal (the LCP §12.7 boundary — no prose field on the type).
30
+ * Fail-fast (throws) on a malformed challenge, a non-0x-32-byte atrHash, a non-HTTPS terms URL, or a
31
+ * non-base-unit-integer amount (this closes the empty/decimal-amount crack at the trust boundary). */
32
+ export declare function parseProposalFromChallenge(challenge: unknown, ctx: ProposalContext): GateProposal;
33
+ //# sourceMappingURL=proposal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal.d.ts","sourceRoot":"","sources":["../src/proposal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAG9D;6GAC6G;AAC7G,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,iBAAiB,EAAE,KAAK,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;CACrC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;CACrC;AA8ED;;uGAEuG;AACvG,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,OAAO,EAClB,GAAG,EAAE,eAAe,GACnB,YAAY,CAuDd"}
@@ -0,0 +1,107 @@
1
+ import { z } from "zod";
2
+ // Module-internal structural view of the x402 402 wire format (NOT the seller's type — buyer ≠ seller).
3
+ // z.object strips unknown keys, so a real challenge's other fields (scheme/payTo/x402Version) are harmlessly
4
+ // ignored. Kept internal + not z.infer-exported (isolatedDeclarations).
5
+ //
6
+ // x402 v2 carries the reference in TWO Tier A places and BOTH are optional here: the per-requirement
7
+ // `accepts[].extra` and the challenge-level `extensions.legalContext` map. Requiring `extra` — as this
8
+ // schema did — rejected a spec-legal seller that advertises only in `extensions`, which is a carrier shape
9
+ // real seller implementations emit. Which one is present is resolved below, not here:
10
+ // Zod's job is the shape, and "at least one of two carriers, agreeing if both" is a rule about meaning.
11
+ const X402ChallengeSchema = z.object({
12
+ accepts: z
13
+ .array(z.object({
14
+ amount: z.string(),
15
+ network: z.string(),
16
+ asset: z.string(),
17
+ extra: z
18
+ .object({
19
+ atrHash: z.string().optional(),
20
+ legalContextUrl: z.string().optional(),
21
+ })
22
+ .optional(),
23
+ }))
24
+ .min(1),
25
+ extensions: z
26
+ .object({
27
+ legalContext: z.object({
28
+ info: z.object({
29
+ type: z.string(),
30
+ value: z.string(),
31
+ legalContextUrl: z.string().optional(),
32
+ }),
33
+ }),
34
+ })
35
+ .optional(),
36
+ });
37
+ /**
38
+ * Reconcile ONE field across x402's two Tier A carriers.
39
+ *
40
+ * Field-by-field, not carrier-by-carrier, because the two carriers are not required to be symmetric. LCP
41
+ * v1.38 §C.4's own illustration puts `atrHash` + `legalContextUrl` in `accepts[].extra` while
42
+ * `extensions.legalContext.info` carries only `type` + `value` — so treating each carrier as an atomic
43
+ * {hash, url} pair rejects the spec's canonical example.
44
+ *
45
+ * `accepts[].extra` wins when both agree, because it is the per-requirement carrier and binds to the
46
+ * requirement actually being paid. Disagreement is NOT resolved by preference: two different values on one
47
+ * challenge would let a seller advertise different terms to different readers of the same document, and a
48
+ * buyer that quietly picked one would gate against terms the seller can later disown. That refuses.
49
+ */
50
+ function reconcileField(field, fromExtra, fromExtensions, equal) {
51
+ if (fromExtra !== undefined && fromExtensions !== undefined) {
52
+ if (!equal(fromExtra, fromExtensions))
53
+ throw new Error(`x402 carriers disagree on ${field} — accepts[].extra advertises "${fromExtra}", ` +
54
+ `extensions.legalContext advertises "${fromExtensions}"`);
55
+ return fromExtra;
56
+ }
57
+ const only = fromExtra ?? fromExtensions;
58
+ if (only === undefined)
59
+ throw new Error(`x402 challenge advertises no ${field} — neither accepts[].extra nor extensions.legalContext carries one`);
60
+ return only;
61
+ }
62
+ const ATR_HASH = /^0x[0-9a-fA-F]{64}$/;
63
+ const BASE_UNIT_INT = /^[0-9]+$/; // decimal base-unit integer — no sign, no decimal point, non-empty
64
+ /** Parse an x402 402 challenge into a typed GateProposal (the LCP §12.7 boundary — no prose field on the type).
65
+ * Fail-fast (throws) on a malformed challenge, a non-0x-32-byte atrHash, a non-HTTPS terms URL, or a
66
+ * non-base-unit-integer amount (this closes the empty/decimal-amount crack at the trust boundary). */
67
+ export function parseProposalFromChallenge(challenge, ctx) {
68
+ const parsed = X402ChallengeSchema.parse(challenge);
69
+ // THE FIRST REQUIREMENT, DELIBERATELY, AND THE GATE DOES NOT CHOOSE.
70
+ //
71
+ // x402's `accepts` is a list of ALTERNATIVE payment requirements. Which one to pay is the agent's own
72
+ // decision — a matter of rails, balances and preference — and this gate has no opinion on it, because
73
+ // choosing how to pay is agent operations rather than binding terms to a payment. A caller that wants a
74
+ // different requirement narrows `accepts` to it BEFORE calling, and gets a proposal gated against that
75
+ // one; the amount checked against the buyer's cap is always the amount on the requirement passed in.
76
+ //
77
+ // What this must never become is a preference rule invented here. Reconciling the reference across two
78
+ // carriers refuses on disagreement precisely because a silent choice lets a seller disown whichever
79
+ // reading lost; a silent choice of REQUIREMENT would gate the buyer against a price it did not pick.
80
+ const req = parsed.accepts[0];
81
+ if (req === undefined)
82
+ throw new Error("x402 challenge has no accepted requirement");
83
+ // The extensions carrier states its own carrier type; anything but sha256 is refused rather than read,
84
+ // because `advertisedAtrHash` is compared against a recomputed record hash and nothing else can be.
85
+ const info = parsed.extensions?.legalContext.info;
86
+ if (info !== undefined && info.type !== "sha256")
87
+ throw new Error(`x402 extensions.legalContext.info.type must be sha256, got "${info.type}"`);
88
+ const atrHash = reconcileField("atrHash", req.extra?.atrHash, info?.value, (a, b) => a.toLowerCase() === b.toLowerCase());
89
+ const legalContextUrl = reconcileField("legalContextUrl", req.extra?.legalContextUrl, info?.legalContextUrl, (a, b) => a === b);
90
+ if (!ATR_HASH.test(atrHash))
91
+ throw new Error(`advertised atrHash is not a 0x-prefixed 32-byte hex: ${atrHash}`);
92
+ if (!legalContextUrl.startsWith("https://"))
93
+ throw new Error(`legalContextUrl must be HTTPS: ${legalContextUrl}`);
94
+ if (!BASE_UNIT_INT.test(req.amount))
95
+ throw new Error(`offer amount must be a base-unit integer string: "${req.amount}"`);
96
+ return {
97
+ advertisedAtrHash: atrHash,
98
+ legalContextUrl,
99
+ level: ctx.level,
100
+ offer: {
101
+ amount: req.amount,
102
+ unit: `${req.network}:${req.asset}`,
103
+ },
104
+ sellerAssurance: ctx.sellerAssurance,
105
+ };
106
+ }
107
+ //# sourceMappingURL=proposal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposal.js","sourceRoot":"","sources":["../src/proposal.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA+BxB,wGAAwG;AACxG,6GAA6G;AAC7G,wEAAwE;AACxE,EAAE;AACF,qGAAqG;AACrG,uGAAuG;AACvG,2GAA2G;AAC3G,sFAAsF;AACtF,wGAAwG;AACxG,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,OAAO,EAAE,CAAC;SACP,KAAK,CACJ,CAAC,CAAC,MAAM,CAAC;QACP,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;QAClB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,KAAK,EAAE,CAAC;aACL,MAAM,CAAC;YACN,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;YAC9B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;SACvC,CAAC;aACD,QAAQ,EAAE;KACd,CAAC,CACH;SACA,GAAG,CAAC,CAAC,CAAC;IACT,UAAU,EAAE,CAAC;SACV,MAAM,CAAC;QACN,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC;YACrB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;gBACb,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;gBAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;gBACjB,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;aACvC,CAAC;SACH,CAAC;KACH,CAAC;SACD,QAAQ,EAAE;CACd,CAAC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,SAAS,cAAc,CACrB,KAAa,EACb,SAA6B,EAC7B,cAAkC,EAClC,KAAwC;IAExC,IAAI,SAAS,KAAK,SAAS,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;QAC5D,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,cAAc,CAAC;YACnC,MAAM,IAAI,KAAK,CACb,6BAA6B,KAAK,kCAAkC,SAAS,KAAK;gBAChF,uCAAuC,cAAc,GAAG,CAC3D,CAAC;QACJ,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,IAAI,GAAG,SAAS,IAAI,cAAc,CAAC;IACzC,IAAI,IAAI,KAAK,SAAS;QACpB,MAAM,IAAI,KAAK,CACb,gCAAgC,KAAK,oEAAoE,CAC1G,CAAC;IACJ,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,QAAQ,GAAG,qBAAqB,CAAC;AACvC,MAAM,aAAa,GAAG,UAAU,CAAC,CAAC,mEAAmE;AAErG;;uGAEuG;AACvG,MAAM,UAAU,0BAA0B,CACxC,SAAkB,EAClB,GAAoB;IAEpB,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACpD,qEAAqE;IACrE,EAAE;IACF,sGAAsG;IACtG,sGAAsG;IACtG,wGAAwG;IACxG,uGAAuG;IACvG,qGAAqG;IACrG,EAAE;IACF,uGAAuG;IACvG,oGAAoG;IACpG,qGAAqG;IACrG,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,GAAG,KAAK,SAAS;QACnB,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,uGAAuG;IACvG,oGAAoG;IACpG,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,IAAI,CAAC;IAClD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;QAC9C,MAAM,IAAI,KAAK,CACb,+DAA+D,IAAI,CAAC,IAAI,GAAG,CAC5E,CAAC;IACJ,MAAM,OAAO,GAAG,cAAc,CAC5B,SAAS,EACT,GAAG,CAAC,KAAK,EAAE,OAAO,EAClB,IAAI,EAAE,KAAK,EACX,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,WAAW,EAAE,CAC9C,CAAC;IACF,MAAM,eAAe,GAAG,cAAc,CACpC,iBAAiB,EACjB,GAAG,CAAC,KAAK,EAAE,eAAe,EAC1B,IAAI,EAAE,eAAe,EACrB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAClB,CAAC;IACF,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;QACzB,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,GAAG,CAAC,MAAM,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,qDAAqD,GAAG,CAAC,MAAM,GAAG,CACnE,CAAC;IACJ,OAAO;QACL,iBAAiB,EAAE,OAAwB;QAC3C,eAAe;QACf,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,KAAK,EAAE;YACL,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,IAAI,EAAE,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,KAAK,EAAE;SACpC;QACD,eAAe,EAAE,GAAG,CAAC,eAAe;KACrC,CAAC;AACJ,CAAC"}