@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.
- package/CHANGELOG.md +57 -0
- package/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +154 -0
- package/dist/decision.d.ts +26 -0
- package/dist/decision.d.ts.map +1 -0
- package/dist/decision.js +28 -0
- package/dist/decision.js.map +1 -0
- package/dist/evaluate.d.ts +15 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +112 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/fetch.d.ts +61 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +126 -0
- package/dist/fetch.js.map +1 -0
- package/dist/fingerprint.d.ts +14 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +15 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/log.d.ts +32 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +8 -0
- package/dist/log.js.map +1 -0
- package/dist/mechanical.d.ts +30 -0
- package/dist/mechanical.d.ts.map +1 -0
- package/dist/mechanical.js +35 -0
- package/dist/mechanical.js.map +1 -0
- package/dist/policy.d.ts +36 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +96 -0
- package/dist/policy.js.map +1 -0
- package/dist/proposal-ack.d.ts +111 -0
- package/dist/proposal-ack.d.ts.map +1 -0
- package/dist/proposal-ack.js +114 -0
- package/dist/proposal-ack.js.map +1 -0
- package/dist/proposal-acp.d.ts +18 -0
- package/dist/proposal-acp.d.ts.map +1 -0
- package/dist/proposal-acp.js +72 -0
- package/dist/proposal-acp.js.map +1 -0
- package/dist/proposal-ap2.d.ts +121 -0
- package/dist/proposal-ap2.d.ts.map +1 -0
- package/dist/proposal-ap2.js +112 -0
- package/dist/proposal-ap2.js.map +1 -0
- package/dist/proposal-mpp.d.ts +44 -0
- package/dist/proposal-mpp.d.ts.map +1 -0
- package/dist/proposal-mpp.js +106 -0
- package/dist/proposal-mpp.js.map +1 -0
- package/dist/proposal-universal.d.ts +239 -0
- package/dist/proposal-universal.d.ts.map +1 -0
- package/dist/proposal-universal.js +470 -0
- package/dist/proposal-universal.js.map +1 -0
- package/dist/proposal.d.ts +33 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +107 -0
- package/dist/proposal.js.map +1 -0
- package/dist/transact.d.ts +24 -0
- package/dist/transact.d.ts.map +1 -0
- package/dist/transact.js +11 -0
- package/dist/transact.js.map +1 -0
- package/package.json +81 -0
- package/src/decision.ts +54 -0
- package/src/evaluate.ts +176 -0
- package/src/fetch.ts +169 -0
- package/src/fingerprint.ts +27 -0
- package/src/index.ts +68 -0
- package/src/log.ts +34 -0
- package/src/mechanical.ts +71 -0
- package/src/policy.ts +135 -0
- package/src/proposal-ack.ts +216 -0
- package/src/proposal-acp.ts +89 -0
- package/src/proposal-ap2.ts +195 -0
- package/src/proposal-mpp.ts +125 -0
- package/src/proposal-universal.ts +671 -0
- package/src/proposal.ts +170 -0
- 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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"}
|