@agenttrust-sdk/mcp 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/embedded-data/devnet-attestor-trace.json +32 -0
  2. package/dist/embedded-data/devnet-chained-validation.json +52 -0
  3. package/dist/embedded-data/devnet-counterparties.json +53 -0
  4. package/dist/embedded-data/devnet-demo-policies.json +46 -0
  5. package/dist/embedded-data/devnet-namespaces.json +107 -0
  6. package/dist/embedded-data/devnet-smoke.json +24 -0
  7. package/dist/embedded-docs/getting-started/architecture-overview.mdx +85 -0
  8. package/dist/embedded-docs/getting-started/quickstart.mdx +100 -0
  9. package/dist/embedded-docs/index.mdx +64 -0
  10. package/dist/embedded-docs/integration-guides/capability-namespaces.mdx +15 -0
  11. package/dist/embedded-docs/integration-guides/custom-attestor.mdx +15 -0
  12. package/dist/embedded-docs/integration-guides/facilitator-adapters.mdx +85 -0
  13. package/dist/embedded-docs/integration-guides/pay-sh-adapter.mdx +110 -0
  14. package/dist/embedded-docs/integration-guides/x402-facilitator.mdx +79 -0
  15. package/dist/embedded-docs/programs/policy-vault/counterparty-tier-policy.mdx +15 -0
  16. package/dist/embedded-docs/programs/policy-vault/index.mdx +68 -0
  17. package/dist/embedded-docs/programs/policy-vault/kill-switch-policy.mdx +15 -0
  18. package/dist/embedded-docs/programs/policy-vault/require-validation-policy.mdx +15 -0
  19. package/dist/embedded-docs/programs/policy-vault/spending-policy.mdx +15 -0
  20. package/dist/embedded-docs/programs/policy-vault/velocity-policy.mdx +15 -0
  21. package/dist/embedded-docs/programs/trustgate.mdx +53 -0
  22. package/dist/embedded-docs/programs/validation-registry.mdx +49 -0
  23. package/dist/embedded-docs/reference/byte-offset-reference.mdx +20 -0
  24. package/dist/embedded-docs/reference/changelog.mdx +19 -0
  25. package/dist/embedded-docs/reference/devnet-program-ids.mdx +24 -0
  26. package/dist/embedded-docs/reference/discriminator-constants.mdx +16 -0
  27. package/dist/embedded-docs/reference/formal-verification.mdx +19 -0
  28. package/dist/embedded-docs/reference/mainnet-program-ids.mdx +16 -0
  29. package/dist/embedded-docs/reference/quantu-agent-registry.mdx +15 -0
  30. package/dist/embedded-docs/sdk/atomic-tx-invariant.mdx +37 -0
  31. package/dist/embedded-docs/sdk/gate-payment.mdx +22 -0
  32. package/dist/embedded-docs/sdk/index.mdx +73 -0
  33. package/dist/embedded-docs/sdk/mount-trustgate.mdx +15 -0
  34. package/dist/embedded-examples/attestor-demo/README.md +100 -0
  35. package/dist/embedded-examples/pay-sh-demo/README.md +136 -0
  36. package/dist/embedded-examples/pay-sh-demo/src/deps-real.ts +150 -0
  37. package/dist/embedded-examples/pay-sh-demo/src/deps.ts +150 -0
  38. package/dist/embedded-examples/pay-sh-demo/src/index.ts +471 -0
  39. package/dist/embedded-examples/pay-sh-demo/src/middleware.ts +198 -0
  40. package/dist/embedded-examples/pay-sh-demo/src/policy.ts +247 -0
  41. package/dist/embedded-examples/pay-sh-demo/src/x402.ts +140 -0
  42. package/dist/index.js +73 -18
  43. package/dist/index.js.map +1 -1
  44. package/dist/resources/docs.js +68 -46
  45. package/dist/resources/docs.js.map +1 -1
  46. package/dist/server.js +6 -1
  47. package/dist/server.js.map +1 -1
  48. package/dist/tools/discovery/docs.js +6 -0
  49. package/dist/tools/discovery/docs.js.map +1 -1
  50. package/dist/tools/discovery/facilitator-walkthrough.js +43 -18
  51. package/dist/tools/discovery/facilitator-walkthrough.js.map +1 -1
  52. package/dist/tools/read/demo-state.js +7 -4
  53. package/dist/tools/read/demo-state.js.map +1 -1
  54. package/dist/trustgate/server/src/facilitators/README.md +241 -0
  55. package/package.json +2 -2
@@ -0,0 +1,198 @@
1
+ /**
2
+ * `paymentMiddleware` — Express middleware that gates a route behind
3
+ * Pay.sh / x402 + AgentTrust.
4
+ *
5
+ * Flow:
6
+ * 1. No `PAYMENT-SIGNATURE` / `X-PAYMENT` header → emit 402 with the x402
7
+ * v2 envelope (base64) advertising the SERVICE's PaymentRequirements
8
+ * 2. Header present → parse the proof, run AgentTrust pipeline:
9
+ * adapter.parseRequest → decide(ctx) → adapter.formatChallenge
10
+ * (Deny path) OR adapter.validatePaymentProof + adapter.emitFeedback
11
+ * (Allow path)
12
+ * 3. Allow → forward to the wrapped handler with `X-Payment-Receipt`
13
+ * header set to the feedback CPI signature
14
+ *
15
+ * The middleware is the demo's bridge from x402 wire to AgentTrust's
16
+ * adapter pipeline. In production with a real `simulateGatePayment`-backed
17
+ * `decide`, this same shape works for any AgentTrust-aware service.
18
+ */
19
+
20
+ import type { NextFunction, Request, RequestHandler, Response } from "express";
21
+ import { PublicKey } from "@solana/web3.js";
22
+
23
+ import {
24
+ ConfirmedSettlement,
25
+ GateDecision,
26
+ PaySh,
27
+ VerifyContext,
28
+ } from "@agenttrust/trustgate-server";
29
+
30
+ import { DemoOnChainStub, buildConfirmedSettlement } from "./deps";
31
+ import {
32
+ PaymentRequirementsBuilder,
33
+ buildPaymentRequirements,
34
+ encodeChallengeEnvelope,
35
+ } from "./x402";
36
+
37
+ export type DecideFn = (ctx: VerifyContext) => Promise<GateDecision>;
38
+
39
+ export interface PaymentMiddlewareOptions {
40
+ readonly adapter: PaySh;
41
+ readonly chainStub: DemoOnChainStub;
42
+ readonly decide: DecideFn;
43
+ readonly buildPaymentRequirements: PaymentRequirementsBuilder;
44
+ readonly facilitator: PublicKey;
45
+ readonly network: string;
46
+ }
47
+
48
+ const PAY_SIG_HEADERS = ["payment-signature", "x-payment"];
49
+
50
+ export function paymentMiddleware(opts: PaymentMiddlewareOptions): RequestHandler {
51
+ return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
52
+ const requirements = opts.buildPaymentRequirements(req);
53
+
54
+ const proof = readProofHeader(req);
55
+ if (!proof) {
56
+ sendChallenge(res, requirements);
57
+ return;
58
+ }
59
+
60
+ let paymentPayload: unknown;
61
+ try {
62
+ paymentPayload = decodeProofHeader(proof);
63
+ } catch {
64
+ res.status(400).json({ error: "malformed_payment_proof" });
65
+ return;
66
+ }
67
+
68
+ const ctx = await opts.adapter.parseRequest(synthesizeRequest({
69
+ paymentRequirements: requirements,
70
+ paymentPayload,
71
+ }));
72
+ if (!ctx) {
73
+ res.status(400).json({ error: "facilitator could not parse request" });
74
+ return;
75
+ }
76
+
77
+ const decision = await opts.decide(ctx);
78
+
79
+ if (decision.kind !== "Allow") {
80
+ const challenge = opts.adapter.formatChallenge(decision, ctx);
81
+ applyHeaders(res, challenge.headers);
82
+ res.status(challenge.status).json(challenge.body ?? { decision });
83
+ return;
84
+ }
85
+
86
+ // Allow path — validate proof + emit feedback.
87
+ opts.chainStub.setHint({
88
+ payer: new PublicKey(extractPayerPubkey(paymentPayload) ?? ctx.payerAgent),
89
+ transferredAmount: ctx.amount,
90
+ transferredMint: ctx.mint,
91
+ transferRecipient: opts.adapter
92
+ ? new PublicKey(requirements.extra.agentTrust.payeeRecipient)
93
+ : ctx.payeeAgent,
94
+ });
95
+
96
+ const validation = await opts.adapter.validatePaymentProof(paymentPayload, ctx);
97
+ if (!validation.valid) {
98
+ res.status(402).json({
99
+ error: validation.reason,
100
+ detail: validation.detail,
101
+ });
102
+ return;
103
+ }
104
+
105
+ const settlement: ConfirmedSettlement = buildConfirmedSettlement(
106
+ ctx,
107
+ validation.txSignature,
108
+ validation.payer,
109
+ );
110
+
111
+ let feedbackSig: string;
112
+ try {
113
+ const fb = await opts.adapter.emitFeedback(ctx, settlement);
114
+ feedbackSig = fb.feedbackTxSignature;
115
+ } catch (e) {
116
+ res.status(500).json({
117
+ error: "feedback_emission_failed",
118
+ detail: e instanceof Error ? e.message : String(e),
119
+ });
120
+ return;
121
+ }
122
+
123
+ const settleResp = opts.adapter.formatSettlement(ctx);
124
+ res.setHeader("X-Payment-Receipt", feedbackSig);
125
+ res.setHeader("X-Payment-Network", opts.network);
126
+ Object.entries(settleResp.facilitatorMeta).forEach(([k, v]) => {
127
+ if (typeof v === "string") res.setHeader(k, v);
128
+ });
129
+ next();
130
+ };
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // helpers
135
+ // ---------------------------------------------------------------------------
136
+
137
+ function readProofHeader(req: Request): string | null {
138
+ for (const name of PAY_SIG_HEADERS) {
139
+ const v = req.header(name);
140
+ if (typeof v === "string" && v.length > 0) return v;
141
+ }
142
+ return null;
143
+ }
144
+
145
+ function decodeProofHeader(value: string): unknown {
146
+ // Pay.sh's PAYMENT-SIGNATURE header is the base64-JSON PaymentPayload.
147
+ // X-PAYMENT (v1) is JSON. Try base64-decode first; fall back to plain JSON.
148
+ try {
149
+ const decoded = Buffer.from(value, "base64").toString("utf-8");
150
+ const json = JSON.parse(decoded);
151
+ if (json && typeof json === "object") return json;
152
+ } catch { /* fall through */ }
153
+ return JSON.parse(value);
154
+ }
155
+
156
+ function extractPayerPubkey(paymentPayload: unknown): PublicKey | undefined {
157
+ if (!paymentPayload || typeof paymentPayload !== "object") return undefined;
158
+ const inner = (paymentPayload as Record<string, unknown>).payload;
159
+ if (!inner || typeof inner !== "object") return undefined;
160
+ const auth = (inner as Record<string, unknown>).authorization;
161
+ if (auth && typeof auth === "object") {
162
+ const from = (auth as Record<string, unknown>).from;
163
+ if (typeof from === "string") {
164
+ try { return new PublicKey(from); } catch { /* skip */ }
165
+ }
166
+ }
167
+ return undefined;
168
+ }
169
+
170
+ function sendChallenge(res: Response, requirements: ReturnType<PaymentRequirementsBuilder>): void {
171
+ const envelope = { x402Version: 2 as const, accepts: [requirements] };
172
+ const b64 = encodeChallengeEnvelope(envelope);
173
+ res.setHeader("PAYMENT-REQUIRED", b64);
174
+ res.setHeader("X-Payment-Required", b64);
175
+ res.setHeader(
176
+ "Access-Control-Expose-Headers",
177
+ "PAYMENT-REQUIRED, X-Payment-Required, X-Payment-Receipt",
178
+ );
179
+ res.status(402).json({
180
+ error: "payment_required",
181
+ accepts: [requirements],
182
+ });
183
+ }
184
+
185
+ function applyHeaders(
186
+ res: Response,
187
+ headers: Readonly<Record<string, string | readonly string[]>>,
188
+ ): void {
189
+ Object.entries(headers).forEach(([k, v]) => {
190
+ res.setHeader(k, v as string | string[]);
191
+ });
192
+ }
193
+
194
+ function synthesizeRequest(body: unknown): Request {
195
+ return { body, header: () => undefined } as unknown as Request;
196
+ }
197
+
198
+ export { buildPaymentRequirements };
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Demo policy gate. Implements the `decide` function used by the
3
+ * payment middleware in lieu of an on-chain `gate_payment` simulation.
4
+ *
5
+ * Three counterparties at tiers 0 / 1 / 3 give us the three branches the
6
+ * brief's smoke test exercises:
7
+ *
8
+ * - tier 0 → Deny (CounterpartyTierBelowMin)
9
+ * - tier 1 → Deny (CounterpartyTierBelowMin)
10
+ * - tier 3 → Allow
11
+ *
12
+ * Two factories:
13
+ *
14
+ * - `makeTierDecide(table, minTier)` — static lookup against an in-
15
+ * memory table. Used by the mock-chain demo + CI tests.
16
+ * - `makeLiveTierDecide({ connection, resolveAtomStats, minTier, ... })` —
17
+ * Phase J4. Reads the live `tier_immediate` byte off Quantu's
18
+ * `AtomStats` PDA on every gate, with a 60s in-process cache so RPC
19
+ * load stays bounded under burst traffic. Falls back to the static
20
+ * `fallbackTable` if the on-chain read fails / account not found.
21
+ * Wired into `createRealDemoApp` (real-chain demo path).
22
+ */
23
+
24
+ import { Connection, PublicKey } from "@solana/web3.js";
25
+
26
+ import { GateDecision, VerifyContext } from "@agenttrust/trustgate-server";
27
+
28
+ export const DEMO_POLICY_MIN_TIER = 2;
29
+
30
+ export interface CounterpartyEntry {
31
+ readonly tier: number;
32
+ readonly label: string;
33
+ }
34
+
35
+ export type CounterpartyTable = ReadonlyMap<string, CounterpartyEntry>;
36
+
37
+ const REASON_COUNTERPARTY_TIER_BELOW_MIN = {
38
+ code: 6,
39
+ name: "CounterpartyTierBelowMin",
40
+ } as const;
41
+
42
+ /**
43
+ * Build a `decide` function that resolves payerAgent → tier from a static
44
+ * map, compares against `minTier`, and returns Allow / Deny accordingly.
45
+ *
46
+ * Unknown payers map to "tier 0" (treated as Deny when minTier > 0). This
47
+ * matches the on-chain `default_unrated_treatment = UNRATED_DENY` behavior
48
+ * for the demo policy. Switch to an alternative resolver to change this.
49
+ */
50
+ export function makeTierDecide(
51
+ table: CounterpartyTable,
52
+ minTier: number,
53
+ ): (ctx: VerifyContext) => Promise<GateDecision> {
54
+ return async (ctx) => {
55
+ const payerB58 = ctx.payerAgent.toBase58();
56
+ const entry = table.get(payerB58);
57
+ const tier = entry?.tier ?? 0;
58
+
59
+ if (tier >= minTier) {
60
+ return { kind: "Allow" };
61
+ }
62
+ return {
63
+ kind: "Deny",
64
+ reasonCode: REASON_COUNTERPARTY_TIER_BELOW_MIN.code,
65
+ reasonName: REASON_COUNTERPARTY_TIER_BELOW_MIN.name,
66
+ };
67
+ };
68
+ }
69
+
70
+ export function lookupCounterparty(
71
+ table: CounterpartyTable,
72
+ pubkey: PublicKey,
73
+ ): CounterpartyEntry | undefined {
74
+ return table.get(pubkey.toBase58());
75
+ }
76
+
77
+ // ===========================================================================
78
+ // Phase J4 — live-tier sync against Quantu AtomStats
79
+ // ===========================================================================
80
+
81
+ // Pinned to the same commit `programs/policy-vault/src/ext/atom_engine.rs`
82
+ // reads on chain. Schema-version canary + tier-range checks prevent silent
83
+ // drift if Quantu re-shapes the account.
84
+ export const ATOM_STATS_SIZE = 561;
85
+ export const ATOM_STATS_TRUST_TIER_OFFSET = 551;
86
+ export const ATOM_STATS_SCHEMA_VERSION_OFFSET = 560;
87
+ export const ATOM_STATS_SCHEMA_VERSION_EXPECTED = 1;
88
+ export const ATOM_TIER_MAX = 4;
89
+
90
+ /** Default cache TTL — 60 seconds matches the brief and matches the
91
+ * facilitator's typical inter-payment interval; long enough to amortise
92
+ * RPC reads across a burst, short enough that a tier upgrade lands
93
+ * before it stales. */
94
+ export const DEFAULT_LIVE_TIER_TTL_MS = 60_000;
95
+
96
+ export interface CachedTier {
97
+ readonly tier: number;
98
+ readonly source: "atom-stats" | "fallback-table" | "unrated";
99
+ readonly fetchedAtMs: number;
100
+ }
101
+
102
+ export interface LiveTierCache {
103
+ get(payerB58: string): CachedTier | undefined;
104
+ set(payerB58: string, entry: CachedTier): void;
105
+ clear(): void;
106
+ size(): number;
107
+ }
108
+
109
+ /** Default in-process Map-backed cache. Drop-in replaceable via the
110
+ * `cache` arg of `makeLiveTierDecide` for distributed setups. */
111
+ export function makeInProcessLiveTierCache(): LiveTierCache {
112
+ const m = new Map<string, CachedTier>();
113
+ return {
114
+ get: (k) => m.get(k),
115
+ set: (k, v) => { m.set(k, v); },
116
+ clear: () => { m.clear(); },
117
+ size: () => m.size,
118
+ };
119
+ }
120
+
121
+ export interface MakeLiveTierDecideArgs {
122
+ readonly connection: Connection;
123
+ /** Map a payer agent_account pubkey to its `atom_stats` PDA. Returning
124
+ * `null` short-circuits to the fallback table. The demo wires this to
125
+ * the bundled `devnet-counterparties.json` lookup; real facilitators
126
+ * derive via `deriveAtomStatsPda` from the SDK. */
127
+ readonly resolveAtomStats: (payerAgent: PublicKey) => PublicKey | null;
128
+ readonly atomEngineId: PublicKey;
129
+ readonly minTier: number;
130
+ readonly ttlMs?: number;
131
+ readonly fallbackTable?: CounterpartyTable;
132
+ readonly cache?: LiveTierCache;
133
+ /** Lets tests pin the clock without monkey-patching `Date.now`. */
134
+ readonly nowMs?: () => number;
135
+ }
136
+
137
+ /**
138
+ * Live-tier `decide` factory.
139
+ *
140
+ * 1. Resolve the payer's atom_stats PDA. If the resolver returns null,
141
+ * the fallback static table (if any) is consulted; otherwise tier 0.
142
+ * 2. If the cache has a non-expired entry, return it.
143
+ * 3. Else `getAccountInfo(atom_stats)`, validate owner + size + schema
144
+ * version, then read `tier_immediate` at byte 551.
145
+ * 4. On any failure (account missing, owner mismatch, schema drift),
146
+ * consult the fallback table; if that's also empty, treat as tier 0.
147
+ * 5. Cache the result with `expiresAt = now + ttlMs`.
148
+ *
149
+ * Returns Allow when tier >= minTier; otherwise Deny with reasonCode
150
+ * `CounterpartyTierBelowMin = 6` (matches the chain enum).
151
+ */
152
+ export function makeLiveTierDecide(
153
+ args: MakeLiveTierDecideArgs,
154
+ ): (ctx: VerifyContext) => Promise<GateDecision> {
155
+ const ttlMs = args.ttlMs ?? DEFAULT_LIVE_TIER_TTL_MS;
156
+ const cache = args.cache ?? makeInProcessLiveTierCache();
157
+ const nowMs = args.nowMs ?? (() => Date.now());
158
+
159
+ return async (ctx) => {
160
+ const tier = await resolveLiveTier({
161
+ connection: args.connection,
162
+ atomEngineId: args.atomEngineId,
163
+ resolveAtomStats: args.resolveAtomStats,
164
+ fallbackTable: args.fallbackTable,
165
+ cache,
166
+ ttlMs,
167
+ nowMs,
168
+ payerAgent: ctx.payerAgent,
169
+ });
170
+
171
+ if (tier >= args.minTier) {
172
+ return { kind: "Allow" };
173
+ }
174
+ return {
175
+ kind: "Deny",
176
+ reasonCode: REASON_COUNTERPARTY_TIER_BELOW_MIN.code,
177
+ reasonName: REASON_COUNTERPARTY_TIER_BELOW_MIN.name,
178
+ };
179
+ };
180
+ }
181
+
182
+ interface ResolveLiveTierArgs {
183
+ connection: Connection;
184
+ atomEngineId: PublicKey;
185
+ resolveAtomStats: (payerAgent: PublicKey) => PublicKey | null;
186
+ fallbackTable?: CounterpartyTable;
187
+ cache: LiveTierCache;
188
+ ttlMs: number;
189
+ nowMs: () => number;
190
+ payerAgent: PublicKey;
191
+ }
192
+
193
+ async function resolveLiveTier(a: ResolveLiveTierArgs): Promise<number> {
194
+ const payerB58 = a.payerAgent.toBase58();
195
+
196
+ const cached = a.cache.get(payerB58);
197
+ if (cached && a.nowMs() - cached.fetchedAtMs < a.ttlMs) {
198
+ return cached.tier;
199
+ }
200
+
201
+ const atomStats = a.resolveAtomStats(a.payerAgent);
202
+ if (atomStats) {
203
+ try {
204
+ const info = await a.connection.getAccountInfo(atomStats, "confirmed");
205
+ const tier = info ? readTierImmediateFromAtomStats(info, a.atomEngineId) : null;
206
+ if (tier !== null) {
207
+ a.cache.set(payerB58, { tier, source: "atom-stats", fetchedAtMs: a.nowMs() });
208
+ return tier;
209
+ }
210
+ } catch (_) {
211
+ // Network errors fall through to fallback; we don't want a transient
212
+ // RPC blip to flip every payer to Deny.
213
+ }
214
+ }
215
+
216
+ const fb = a.fallbackTable?.get(payerB58)?.tier ?? 0;
217
+ a.cache.set(payerB58, {
218
+ tier: fb,
219
+ source: a.fallbackTable?.has(payerB58) ? "fallback-table" : "unrated",
220
+ fetchedAtMs: a.nowMs(),
221
+ });
222
+ return fb;
223
+ }
224
+
225
+ /**
226
+ * Decode `tier_immediate` from an AtomStats account. Returns `null` when
227
+ * the account is uninitialised, has the wrong owner, or has drifted schema.
228
+ *
229
+ * Mirrors `programs/policy-vault/src/ext/atom_engine.rs::parse_atom_stats_bytes`
230
+ * verbatim (size + schema version + tier-range checks). Importantly the
231
+ * v1 demo reads `tier_immediate` (byte 551) — `tier_confirmed` (byte 555)
232
+ * is the post-vesting production value tracked separately.
233
+ */
234
+ export function readTierImmediateFromAtomStats(
235
+ info: { data: Buffer | Uint8Array; owner: PublicKey },
236
+ atomEngineId: PublicKey,
237
+ ): number | null {
238
+ if (!info.owner.equals(atomEngineId)) return null;
239
+
240
+ const data = info.data instanceof Buffer ? info.data : Buffer.from(info.data);
241
+ if (data.length < ATOM_STATS_SIZE) return null;
242
+ if (data[ATOM_STATS_SCHEMA_VERSION_OFFSET] !== ATOM_STATS_SCHEMA_VERSION_EXPECTED) return null;
243
+
244
+ const tier = data[ATOM_STATS_TRUST_TIER_OFFSET];
245
+ if (tier > ATOM_TIER_MAX) return null;
246
+ return tier;
247
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * x402 v2 wire-format helpers — challenge envelope encoder + the
3
+ * `PaymentRequirements` builder shape the demo middleware uses.
4
+ *
5
+ * Implements the spec from `docs/plan/research/05-trustgate-x402-class.md`
6
+ * §A.4 (PaymentRequirements) + §A.5 (PaymentPayload). The demo emits v2;
7
+ * the `X-Payment-Required` header alias is set for v1 fallback compat.
8
+ */
9
+
10
+ import type { Request } from "express";
11
+
12
+ export interface AgentTrustExtraInput {
13
+ readonly payerAgentAsset: string;
14
+ readonly payeeAgentAsset: string;
15
+ readonly payeeRecipient: string;
16
+ readonly policyId: number;
17
+ readonly issuedAt: number;
18
+ readonly serviceSignature: string;
19
+ }
20
+
21
+ export interface PaymentRequirementsExtraInput {
22
+ readonly feePayer: string;
23
+ readonly memo: string;
24
+ readonly agentTrust: AgentTrustExtraInput;
25
+ }
26
+
27
+ export interface PaymentRequirementsInput {
28
+ readonly scheme: "exact";
29
+ readonly network: string;
30
+ readonly maxAmountRequired: string;
31
+ readonly asset: string;
32
+ readonly payTo: string;
33
+ readonly resource: string;
34
+ readonly description?: string;
35
+ readonly maxTimeoutSeconds: number;
36
+ readonly extra: PaymentRequirementsExtraInput;
37
+ }
38
+
39
+ export interface ChallengeEnvelope {
40
+ readonly x402Version: 2;
41
+ readonly accepts: ReadonlyArray<PaymentRequirementsInput>;
42
+ }
43
+
44
+ export type PaymentRequirementsBuilder = (req: Request) => PaymentRequirementsInput;
45
+
46
+ /** Per-challenge ed25519 signer the demo wires to the facilitator
47
+ * keypair. Signs canonical envelope bytes (B5). */
48
+ export type SignChallengeFn = (canonicalBytes: Uint8Array) => Uint8Array;
49
+
50
+ /** Per-request canonical-bytes builder — produced and used inside
51
+ * `buildPaymentRequirements` only. Exposed for the test path that
52
+ * builds requirements from outside the middleware. */
53
+ export interface AgentTrustHints {
54
+ readonly payerAgentAsset: string;
55
+ readonly payeeAgentAsset: string;
56
+ readonly payeeRecipient: string;
57
+ readonly policyId: number;
58
+ }
59
+
60
+ /**
61
+ * Build a PaymentRequirements builder. Captures static fields once and
62
+ * supplies per-request `memo` + `agentTrust` hints (so the signature
63
+ * binds to the correct payerAgentAsset for THIS request).
64
+ *
65
+ * `signChallenge` is invoked on every emission to bind the requirements
66
+ * to the facilitator's identity (B5).
67
+ * `canonicalBytesOf` derives the signing payload — pass the same fn the
68
+ * AgentTrust adapter uses to keep both sides byte-compatible.
69
+ */
70
+ export function buildPaymentRequirements(args: {
71
+ scheme: "exact";
72
+ network: string;
73
+ amount: bigint;
74
+ asset: string;
75
+ payTo: string;
76
+ resource: string;
77
+ description?: string;
78
+ maxTimeoutSeconds: number;
79
+ feePayer: string;
80
+ agentTrustFor: (req: Request) => AgentTrustHints;
81
+ memoFor: (req: Request) => string;
82
+ signChallenge: SignChallengeFn;
83
+ canonicalBytesOf: (input: {
84
+ issuedAt: number;
85
+ network: string;
86
+ amount: bigint;
87
+ asset: string;
88
+ payTo: string;
89
+ payerAgentAsset: string;
90
+ payeeAgentAsset: string;
91
+ payeeRecipient: string;
92
+ policyId: number;
93
+ paymentIdHashHex: string;
94
+ }) => Uint8Array;
95
+ bytesToHex: (bytes: Uint8Array) => string;
96
+ paymentIdHashHexFor: (memo: string) => string;
97
+ }): PaymentRequirementsBuilder {
98
+ return (req) => {
99
+ const memo = args.memoFor(req);
100
+ const agentTrust = args.agentTrustFor(req);
101
+ const issuedAt = Date.now();
102
+ const paymentIdHashHex = args.paymentIdHashHexFor(memo);
103
+ const canonical = args.canonicalBytesOf({
104
+ issuedAt,
105
+ network: args.network,
106
+ amount: args.amount,
107
+ asset: args.asset,
108
+ payTo: args.payTo,
109
+ payerAgentAsset: agentTrust.payerAgentAsset,
110
+ payeeAgentAsset: agentTrust.payeeAgentAsset,
111
+ payeeRecipient: agentTrust.payeeRecipient,
112
+ policyId: agentTrust.policyId,
113
+ paymentIdHashHex,
114
+ });
115
+ const sig = args.signChallenge(canonical);
116
+ return {
117
+ scheme: args.scheme,
118
+ network: args.network,
119
+ maxAmountRequired: args.amount.toString(),
120
+ asset: args.asset,
121
+ payTo: args.payTo,
122
+ resource: args.resource,
123
+ description: args.description,
124
+ maxTimeoutSeconds: args.maxTimeoutSeconds,
125
+ extra: {
126
+ feePayer: args.feePayer,
127
+ memo,
128
+ agentTrust: {
129
+ ...agentTrust,
130
+ issuedAt,
131
+ serviceSignature: args.bytesToHex(sig),
132
+ },
133
+ },
134
+ };
135
+ };
136
+ }
137
+
138
+ export function encodeChallengeEnvelope(envelope: ChallengeEnvelope): string {
139
+ return Buffer.from(JSON.stringify(envelope), "utf-8").toString("base64");
140
+ }