@catena/sdk 0.0.0-alpha-20260724191736

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.
@@ -0,0 +1,175 @@
1
+ import { T as X402Resource, i as FetchLike, n as CatenaClient, w as X402PaymentRequirements } from "./client-DZHdFs_y.mjs";
2
+ import * as v from "valibot";
3
+
4
+ //#region src/x402.d.ts
5
+ declare const paymentRequiredSchema: v.LooseObjectSchema<{
6
+ /**
7
+ * Pinned to the wire version this loop speaks. Paying a v1 or future
8
+ * incompatible challenge would settle money for a retry header the server
9
+ * cannot consume.
10
+ */
11
+ readonly x402Version: v.LiteralSchema<2, undefined>;
12
+ readonly accepts: v.ArraySchema<v.LooseObjectSchema<{
13
+ readonly scheme: v.StringSchema<undefined>;
14
+ readonly network: v.StringSchema<undefined>;
15
+ readonly asset: v.StringSchema<undefined>;
16
+ readonly amount: v.StringSchema<undefined>;
17
+ readonly payTo: v.StringSchema<undefined>;
18
+ readonly maxTimeoutSeconds: v.NumberSchema<undefined>;
19
+ }, undefined>, undefined>;
20
+ readonly resource: v.OptionalSchema<v.LooseObjectSchema<{
21
+ readonly url: v.StringSchema<undefined>;
22
+ readonly serviceName: v.SchemaWithFallback<v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MaxLengthAction<string, 255, undefined>]>, undefined>, undefined>;
23
+ }, undefined>, undefined>;
24
+ }, undefined>;
25
+ type X402PaymentRequired = v.InferOutput<typeof paymentRequiredSchema>;
26
+ /**
27
+ * Decode a 402 response's `PAYMENT-REQUIRED` challenge. Returns undefined for
28
+ * a 402 that carries no decodable v2 challenge (not an x402 endpoint).
29
+ */
30
+ declare function decodePaymentRequired(response: Response): X402PaymentRequired | undefined;
31
+ /**
32
+ * Payment failure raised by this module; `reasons` carries the policy or
33
+ * decline detail when the server provided any, and subclasses mark the cases
34
+ * with a specific recovery path. Not every loop failure is one of these:
35
+ * a create-phase `ApiError` the loop does not absorb as a candidate skip
36
+ * and a "not-submitted" `IntentSubmitError` propagate unchanged.
37
+ */
38
+ declare class X402PaymentError extends Error {
39
+ readonly reasons: readonly string[];
40
+ constructor(message: string, reasons?: readonly string[]);
41
+ }
42
+ /**
43
+ * Every payable candidate in the challenge targets a network the source
44
+ * account is not on, so nothing could be funded. Distinct from a generic
45
+ * non-payable requirement: the fix is to fund the payment from a wallet
46
+ * account on one of `requiredNetworks`, not to change policy. Consumers can
47
+ * name those networks (and, for a CLI, tell the user how to switch accounts).
48
+ */
49
+ declare class X402NetworkMismatchError extends X402PaymentError {
50
+ readonly accountId: string;
51
+ readonly requiredNetworks: readonly string[];
52
+ constructor(accountId: string, requiredNetworks: readonly string[]);
53
+ }
54
+ /**
55
+ * The challenge's `payTo` is not a saved counterparty wallet rail, so the
56
+ * allowlist has nothing to authorize and the payment fails closed. The 402's
57
+ * `payTo` is attacker-controlled, so Catena never auto-adds it: the fix is a
58
+ * deliberate `create_counterparty` (itself policy-gated) for `payTo` on
59
+ * `network`, then a re-run. Consumers can name the recipient (and, for a CLI,
60
+ * print the exact counterparty-create command).
61
+ */
62
+ declare class X402CounterpartyNotFoundError extends X402PaymentError {
63
+ readonly payTo: string;
64
+ readonly network: string;
65
+ /**
66
+ * The challenge's advertised service name, when present — a suggested
67
+ * counterparty name.
68
+ */
69
+ readonly serviceName: string | undefined;
70
+ constructor(payTo: string, network: string, serviceName?: string);
71
+ }
72
+ /**
73
+ * The payment exceeded a policy limit and is parked for a human approval.
74
+ * Not a decline: once an operator approves it in the console, re-running the
75
+ * same request (same endpoint, same price) consumes the approval and pays.
76
+ * `expiresAt` is when the pending approval self-expires.
77
+ */
78
+ declare class X402ApprovalPendingError extends X402PaymentError {
79
+ readonly intentId: string;
80
+ readonly expiresAt: string | null;
81
+ constructor(intentId: string, reasons: readonly string[], expiresAt: string | null);
82
+ }
83
+ /**
84
+ * The paid retry failed AFTER the payment settled. The receipt is the
85
+ * caller's recovery handle: retry the request manually with its
86
+ * `paymentSignature` as the `PAYMENT-SIGNATURE` header (also re-readable via
87
+ * `getIntent(intentId)` at `data.x402.paymentSignature` while the
88
+ * authorization is valid) — re-running the whole loop would mint a second
89
+ * payment.
90
+ */
91
+ declare class X402RetryFailedError extends X402PaymentError {
92
+ readonly receipt: X402PaymentReceipt;
93
+ constructor(receipt: X402PaymentReceipt, cause: unknown);
94
+ }
95
+ /**
96
+ * The submission failed after the intent was created with its outcome
97
+ * unknown — the server may have already claimed and settled (a response lost
98
+ * in transit is the canonical case). Re-running the loop would mint a second
99
+ * intent and risk a double-pay, so the caller must reconcile via
100
+ * `getIntent(intentId)`: if it completed, reuse
101
+ * `data.x402.paymentSignature` as the `PAYMENT-SIGNATURE` header; if
102
+ * processing, it is mid-settlement — keep polling. Pay again only once it is
103
+ * terminally blocked or failed.
104
+ */
105
+ declare class X402SubmitInterruptedError extends X402PaymentError {
106
+ readonly intentId: string;
107
+ readonly requirements: X402PaymentRequirements;
108
+ constructor(intentId: string, requirements: X402PaymentRequirements, cause: unknown);
109
+ }
110
+ interface X402PaymentReceipt {
111
+ /**
112
+ * The Catena intent that settled the payment; pass to `getIntent`.
113
+ */
114
+ intentId: string;
115
+ /**
116
+ * Encoded value for the retry's `PAYMENT-SIGNATURE` header. Re-readable
117
+ * via `getIntent(intentId)` at `data.x402.paymentSignature` while the
118
+ * authorization is valid.
119
+ */
120
+ paymentSignature: string;
121
+ /**
122
+ * The challenge candidate that was paid.
123
+ */
124
+ requirements: X402PaymentRequirements;
125
+ }
126
+ /**
127
+ * Pay one decoded 402 challenge from `accountId` and return the
128
+ * `PAYMENT-SIGNATURE` header value. Tries the challenge's exact-scheme
129
+ * candidates in order, skipping ones Catena reports as unpayable (wrong
130
+ * network/asset); everything else — policy blocks, counterparty rejections,
131
+ * signing failures — surfaces as `X402PaymentError` (or the underlying
132
+ * `ApiError` / `IntentSubmitError`).
133
+ */
134
+ declare function payX402Challenge(client: CatenaClient, args: {
135
+ accountId: string;
136
+ paymentRequired: X402PaymentRequired;
137
+ resource?: X402Resource;
138
+ /**
139
+ * Per-call ceiling in atomic USDC units (6 decimals; "1000000" = $1).
140
+ * The org policy's spend limits still govern server-side; this lets the
141
+ * caller refuse a price it never meant to accept, before any intent.
142
+ */
143
+ maxAtomicAmount?: bigint;
144
+ }): Promise<X402PaymentReceipt>;
145
+ interface X402FetchOptions {
146
+ /**
147
+ * Source wallet account the payments draw from.
148
+ */
149
+ accountId: string;
150
+ baseFetch?: FetchLike;
151
+ /**
152
+ * Per-call price ceiling in atomic USDC units (6 decimals). Challenges
153
+ * asking for more are refused before any intent is created.
154
+ */
155
+ maxAtomicAmount?: bigint;
156
+ /**
157
+ * Called after each successful payment, before the retry — for receipts,
158
+ * logging, or spend telemetry.
159
+ */
160
+ onPayment?: (receipt: X402PaymentReceipt) => void;
161
+ }
162
+ /**
163
+ * Wrap `fetch` so a v2 x402 402 is paid via Catena and retried once with the
164
+ * `PAYMENT-SIGNATURE` header. Non-402 responses (and 402s without a decodable
165
+ * challenge) pass through untouched. Throws `X402PaymentError` before paying
166
+ * when the request body cannot be replayed — pass bodies via the `init`
167
+ * argument as a string, `URLSearchParams`, Blob, ArrayBuffer, or typed
168
+ * array/DataView; a `Request` input must be bodiless. Payment failures surface
169
+ * as `X402PaymentError` (or a subclass documenting its recovery path); an
170
+ * `ApiError` the loop does not absorb as a candidate skip and a "not-submitted"
171
+ * `IntentSubmitError` propagate unchanged.
172
+ */
173
+ declare function wrapFetchWithX402Payment(client: CatenaClient, options: X402FetchOptions): FetchLike;
174
+ //#endregion
175
+ export { X402ApprovalPendingError, X402CounterpartyNotFoundError, X402FetchOptions, X402NetworkMismatchError, X402PaymentError, X402PaymentReceipt, X402PaymentRequired, type X402PaymentRequirements, type X402Resource, X402RetryFailedError, X402SubmitInterruptedError, decodePaymentRequired, payX402Challenge, wrapFetchWithX402Payment };
package/dist/x402.mjs ADDED
@@ -0,0 +1,225 @@
1
+ import { a as x402PaymentRequirementsSchema, n as IntentSubmitError, o as x402ResourceSchema, t as ApiError } from "./client-G-8NfBx_.mjs";
2
+ import * as v from "valibot";
3
+ //#region src/lib/assert-never.ts
4
+ function assertNever(value) {
5
+ throw new Error(`Unhandled case: ${String(value)}`);
6
+ }
7
+ //#endregion
8
+ //#region src/x402.ts
9
+ const PAYMENT_REQUIRED_HEADERS = ["PAYMENT-REQUIRED", "X-Payment-Required"];
10
+ const PAYMENT_SIGNATURE_HEADER = "PAYMENT-SIGNATURE";
11
+ const PAYMENT_RESPONSE_HEADER = "PAYMENT-RESPONSE";
12
+ const EVM_TX_HASH = /^0x[0-9a-fA-F]{64}$/;
13
+ const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
14
+ const paymentResponseSchema = v.looseObject({
15
+ success: v.boolean(),
16
+ transaction: v.string()
17
+ });
18
+ const paymentRequiredSchema = v.looseObject({
19
+ x402Version: v.literal(2),
20
+ accepts: v.array(x402PaymentRequirementsSchema),
21
+ resource: v.optional(x402ResourceSchema)
22
+ });
23
+ const MAX_CANDIDATE_REQUIREMENTS = 5;
24
+ const X402_NETWORK_MISMATCH_CODE = "x402_network_mismatch";
25
+ const X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE = "x402_counterparty_rail_not_found";
26
+ const TRY_NEXT_REQUIREMENT_CODES = new Set([
27
+ "x402_requirement_not_payable",
28
+ X402_NETWORK_MISMATCH_CODE,
29
+ X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE
30
+ ]);
31
+ const NETWORK_LABELS = {
32
+ "eip155:8453": "Base",
33
+ "eip155:84532": "Base Sepolia"
34
+ };
35
+ function networkLabel(network) {
36
+ const label = Object.hasOwn(NETWORK_LABELS, network) ? NETWORK_LABELS[network] : void 0;
37
+ return label ? `${label} (${network})` : network;
38
+ }
39
+ function decodeBase64Json(value) {
40
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
41
+ const bytes = Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
42
+ return JSON.parse(new TextDecoder().decode(bytes));
43
+ }
44
+ function decodePaymentRequired(response) {
45
+ const header = PAYMENT_REQUIRED_HEADERS.map((name) => response.headers.get(name)).find((value) => value !== null);
46
+ if (!header) return;
47
+ try {
48
+ const parsed = v.safeParse(paymentRequiredSchema, decodeBase64Json(header));
49
+ return parsed.success ? parsed.output : void 0;
50
+ } catch {
51
+ return;
52
+ }
53
+ }
54
+ async function reportSettlementHint(client, intentId, retried) {
55
+ try {
56
+ const header = retried.headers.get(PAYMENT_RESPONSE_HEADER);
57
+ if (!header) return;
58
+ const parsed = v.safeParse(paymentResponseSchema, decodeBase64Json(header));
59
+ if (!parsed.success || !parsed.output.success || !EVM_TX_HASH.test(parsed.output.transaction)) return;
60
+ await client.reportSettlement({
61
+ intentId,
62
+ txHash: parsed.output.transaction
63
+ });
64
+ } catch {}
65
+ }
66
+ var X402PaymentError = class extends Error {
67
+ reasons;
68
+ constructor(message, reasons = []) {
69
+ super(reasons.length > 0 ? `${message}: ${reasons.join("; ")}` : message);
70
+ this.name = "X402PaymentError";
71
+ this.reasons = reasons;
72
+ }
73
+ };
74
+ var X402NetworkMismatchError = class extends X402PaymentError {
75
+ accountId;
76
+ requiredNetworks;
77
+ constructor(accountId, requiredNetworks) {
78
+ super(`Account ${accountId} cannot fund this 402 challenge: it must be paid from a wallet account on ${requiredNetworks.map(networkLabel).join(" or ")}`);
79
+ this.name = "X402NetworkMismatchError";
80
+ this.accountId = accountId;
81
+ this.requiredNetworks = requiredNetworks;
82
+ }
83
+ };
84
+ var X402CounterpartyNotFoundError = class extends X402PaymentError {
85
+ payTo;
86
+ network;
87
+ serviceName;
88
+ constructor(payTo, network, serviceName) {
89
+ super(`This 402 pays ${payTo} on ${networkLabel(network)}, which is not a saved counterparty wallet rail; add it as a counterparty before paying`);
90
+ this.name = "X402CounterpartyNotFoundError";
91
+ this.payTo = payTo;
92
+ this.network = network;
93
+ this.serviceName = serviceName;
94
+ }
95
+ };
96
+ var X402ApprovalPendingError = class extends X402PaymentError {
97
+ intentId;
98
+ expiresAt;
99
+ constructor(intentId, reasons, expiresAt) {
100
+ super(`The payment is awaiting a human approval (intent ${intentId}); once an operator approves it, retry the request to complete the payment`, reasons);
101
+ this.name = "X402ApprovalPendingError";
102
+ this.intentId = intentId;
103
+ this.expiresAt = expiresAt;
104
+ }
105
+ };
106
+ var X402RetryFailedError = class extends X402PaymentError {
107
+ receipt;
108
+ constructor(receipt, cause) {
109
+ super(`The payment settled (intent ${receipt.intentId}) but the paid retry failed; retry the request with the PAYMENT-SIGNATURE header from the receipt (or getIntent("${receipt.intentId}") data.x402.paymentSignature) instead of paying again`);
110
+ this.name = "X402RetryFailedError";
111
+ this.receipt = receipt;
112
+ this.cause = cause;
113
+ }
114
+ };
115
+ var X402SubmitInterruptedError = class extends X402PaymentError {
116
+ intentId;
117
+ requirements;
118
+ constructor(intentId, requirements, cause) {
119
+ super(`The payment could not be confirmed (intent ${intentId}); check getIntent("${intentId}") — completed: reuse its data.x402.paymentSignature as the PAYMENT-SIGNATURE header; processing: poll until terminal; pay again only after blocked or failed`);
120
+ this.name = "X402SubmitInterruptedError";
121
+ this.intentId = intentId;
122
+ this.requirements = requirements;
123
+ this.cause = cause;
124
+ }
125
+ };
126
+ async function payX402Challenge(client, args) {
127
+ const exactCandidates = args.paymentRequired.accepts.filter((requirement) => requirement.scheme === "exact");
128
+ const candidates = exactCandidates.filter((requirement) => args.maxAtomicAmount === void 0 || /^\d+$/.test(requirement.amount) && BigInt(requirement.amount) <= args.maxAtomicAmount).slice(0, MAX_CANDIDATE_REQUIREMENTS);
129
+ if (candidates.length === 0) throw new X402PaymentError(exactCandidates.length > 0 && args.maxAtomicAmount !== void 0 ? "Every requirement in the 402 challenge costs more than the caller's maximum amount" : "The 402 challenge offers no exact-scheme requirement Catena can pay");
130
+ const resource = args.resource ?? args.paymentRequired.resource;
131
+ const skips = [];
132
+ for (const requirements of candidates) {
133
+ const receipt = await payRequirement(client, {
134
+ accountId: args.accountId,
135
+ requirements,
136
+ resource
137
+ }).catch((err) => {
138
+ if (err instanceof ApiError && err.code !== void 0 && TRY_NEXT_REQUIREMENT_CODES.has(err.code)) {
139
+ skips.push({
140
+ error: err,
141
+ requirement: requirements
142
+ });
143
+ return;
144
+ }
145
+ throw err;
146
+ });
147
+ if (receipt) return receipt;
148
+ }
149
+ if (skips.length > 0 && skips.every((skip) => skip.error.code === X402_NETWORK_MISMATCH_CODE)) throw new X402NetworkMismatchError(args.accountId, [...new Set(skips.map((skip) => skip.requirement.network))]);
150
+ if (skips.length > 0 && skips.every((skip) => skip.error.code === X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE && skip.error.status === 404) && EVM_ADDRESS.test(skips[0].requirement.payTo)) throw new X402CounterpartyNotFoundError(skips[0].requirement.payTo, skips[0].requirement.network, resource?.serviceName);
151
+ const lastSkip = skips.at(-1);
152
+ throw new X402PaymentError("No requirement in the 402 challenge is payable from this account", lastSkip ? [lastSkip.error.message] : []);
153
+ }
154
+ const x402IntentDataSchema = v.object({ x402: v.object({ paymentSignature: v.string() }) });
155
+ async function payRequirement(client, args) {
156
+ const intent = await client.submitIntent({
157
+ action: {
158
+ type: "x402",
159
+ accountId: args.accountId,
160
+ paymentRequirements: args.requirements,
161
+ ...args.resource !== void 0 && { resource: args.resource }
162
+ },
163
+ idempotencyKey: crypto.randomUUID()
164
+ }).catch((err) => {
165
+ if (err instanceof IntentSubmitError && err.outcome === "unknown") throw new X402SubmitInterruptedError(err.intentId, args.requirements, err);
166
+ throw err;
167
+ });
168
+ if (intent.status === "completed") {
169
+ const data = v.safeParse(x402IntentDataSchema, intent.data);
170
+ if (data.success) return {
171
+ intentId: intent.id,
172
+ paymentSignature: data.output.x402.paymentSignature,
173
+ requirements: args.requirements
174
+ };
175
+ throw new X402PaymentError("The completed payment did not carry an x402 payment signature");
176
+ }
177
+ switch (intent.status) {
178
+ case "pending": throw new X402ApprovalPendingError(intent.id, intent.reasons, intent.expiresAt);
179
+ case "processing": throw new X402PaymentError(`The payment is still processing (intent ${intent.id}); this client cannot resume an in-flight x402 settlement`);
180
+ case "blocked":
181
+ case "failed": throw new X402PaymentError("Catena declined the payment", intent.reasons);
182
+ }
183
+ return assertNever(intent.status);
184
+ }
185
+ function isReplayable(input, init) {
186
+ if (init?.body !== void 0 && init.body !== null) {
187
+ if (typeof init.body !== "string" && !(init.body instanceof URLSearchParams) && !(init.body instanceof Blob) && !(init.body instanceof ArrayBuffer) && !ArrayBuffer.isView(init.body)) return false;
188
+ }
189
+ if (typeof Request !== "undefined" && input instanceof Request) return input.body === null && !input.bodyUsed;
190
+ return true;
191
+ }
192
+ function wrapFetchWithX402Payment(client, options) {
193
+ const baseFetch = (options.baseFetch ?? globalThis.fetch).bind(globalThis);
194
+ return async (input, init) => {
195
+ const response = await baseFetch(input, init);
196
+ if (response.status !== 402) return response;
197
+ const paymentRequired = decodePaymentRequired(response);
198
+ if (!paymentRequired) return response;
199
+ if (!isReplayable(input, init)) throw new X402PaymentError("The request body cannot be replayed after payment; buffer the body (string, Blob, or ArrayBuffer) before using the x402 fetch wrapper");
200
+ const receipt = await payX402Challenge(client, {
201
+ accountId: options.accountId,
202
+ paymentRequired,
203
+ ...options.maxAtomicAmount !== void 0 && { maxAtomicAmount: options.maxAtomicAmount }
204
+ });
205
+ options.onPayment?.(receipt);
206
+ const headers = new Headers(typeof Request !== "undefined" && input instanceof Request ? input.headers : void 0);
207
+ new Headers(init?.headers).forEach((value, key) => {
208
+ headers.set(key, value);
209
+ });
210
+ headers.set(PAYMENT_SIGNATURE_HEADER, receipt.paymentSignature);
211
+ let retried;
212
+ try {
213
+ retried = await baseFetch(input, {
214
+ ...init,
215
+ headers
216
+ });
217
+ } catch (err) {
218
+ throw new X402RetryFailedError(receipt, err);
219
+ }
220
+ reportSettlementHint(client, receipt.intentId, retried);
221
+ return retried;
222
+ };
223
+ }
224
+ //#endregion
225
+ export { X402ApprovalPendingError, X402CounterpartyNotFoundError, X402NetworkMismatchError, X402PaymentError, X402RetryFailedError, X402SubmitInterruptedError, decodePaymentRequired, payX402Challenge, wrapFetchWithX402Payment };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@catena/sdk",
3
+ "version": "0.0.0-alpha-20260724191736",
4
+ "description": "Typed client SDK for the Catena agent API",
5
+ "homepage": "https://catena.com/",
6
+ "license": "Apache-2.0",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/client.d.mts",
14
+ "default": "./dist/client.mjs"
15
+ },
16
+ "./keypair": {
17
+ "types": "./dist/keypair.d.mts",
18
+ "default": "./dist/keypair.mjs"
19
+ },
20
+ "./x402": {
21
+ "types": "./dist/x402.d.mts",
22
+ "default": "./dist/x402.mjs"
23
+ }
24
+ },
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "tag": "alpha"
28
+ },
29
+ "dependencies": {
30
+ "@turnkey/api-key-stamper": "0.6.7",
31
+ "valibot": "1.4.1"
32
+ },
33
+ "devDependencies": {
34
+ "publint": "0.3.21",
35
+ "tsdown": "0.22.2"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "gitHead": "1e4e40d0767efc5d927803fec08eaea24035017d",
41
+ "scripts": {
42
+ "build": "tsdown",
43
+ "clean": "git clean -xdf dist node_modules/.cache",
44
+ "publint": "pnpm run build && publint --pack pnpm --strict"
45
+ }
46
+ }