@catena/sdk 0.0.0-bootstrap.0 → 0.1.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/dist/x402.mjs ADDED
@@ -0,0 +1,221 @@
1
+ import { C as extractRelayableOffer, S as x402ResourceSchema, b as x402CredentialFromIntentData, n as IntentSubmitError, t as ApiError, w as extractRelayableReceipt, x as x402PaymentRequirementsSchema } from "./client-CHZMO00P.mjs";
2
+ import { a as assertNever, i as isReplayableFetchRequest, n as reportSettlementHint$1, r as fetchRetryHeaders, t as EVM_TRANSACTION_HASH_PATTERN } from "./settlement-report-CT3EbtLL.mjs";
3
+ import * as v from "valibot";
4
+ //#region src/x402.ts
5
+ const PAYMENT_REQUIRED_HEADERS = ["PAYMENT-REQUIRED", "X-Payment-Required"];
6
+ const PAYMENT_SIGNATURE_HEADER = "PAYMENT-SIGNATURE";
7
+ const PAYMENT_RESPONSE_HEADER = "PAYMENT-RESPONSE";
8
+ const EVM_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
9
+ const paymentResponseSchema = v.looseObject({
10
+ success: v.boolean(),
11
+ transaction: v.string(),
12
+ extensions: v.optional(v.unknown())
13
+ });
14
+ const paymentRequiredSchema = v.looseObject({
15
+ x402Version: v.literal(2),
16
+ accepts: v.array(x402PaymentRequirementsSchema),
17
+ resource: v.optional(x402ResourceSchema),
18
+ extensions: v.optional(v.unknown())
19
+ });
20
+ const MAX_CANDIDATE_REQUIREMENTS = 5;
21
+ const X402_NETWORK_MISMATCH_CODE = "x402_network_mismatch";
22
+ const X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE = "x402_counterparty_rail_not_found";
23
+ const TRY_NEXT_REQUIREMENT_CODES = new Set([
24
+ "x402_requirement_not_payable",
25
+ X402_NETWORK_MISMATCH_CODE,
26
+ X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE
27
+ ]);
28
+ const NETWORK_LABELS = {
29
+ "eip155:8453": "Base",
30
+ "eip155:84532": "Base Sepolia"
31
+ };
32
+ function networkLabel(network) {
33
+ const label = Object.hasOwn(NETWORK_LABELS, network) ? NETWORK_LABELS[network] : void 0;
34
+ return label ? `${label} (${network})` : network;
35
+ }
36
+ function decodeBase64Json(value) {
37
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
38
+ const bytes = Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
39
+ return JSON.parse(new TextDecoder().decode(bytes));
40
+ }
41
+ function decodePaymentRequired(response) {
42
+ const header = PAYMENT_REQUIRED_HEADERS.map((name) => response.headers.get(name)).find((value) => value !== null);
43
+ if (!header) return;
44
+ try {
45
+ const parsed = v.safeParse(paymentRequiredSchema, decodeBase64Json(header));
46
+ return parsed.success ? parsed.output : void 0;
47
+ } catch {
48
+ return;
49
+ }
50
+ }
51
+ async function reportSettlementHint(client, intentId, retried) {
52
+ try {
53
+ const header = retried.headers.get(PAYMENT_RESPONSE_HEADER);
54
+ if (!header) return;
55
+ const parsed = v.safeParse(paymentResponseSchema, decodeBase64Json(header));
56
+ if (!parsed.success || !parsed.output.success || !EVM_TRANSACTION_HASH_PATTERN.test(parsed.output.transaction)) return;
57
+ const receipt = extractRelayableReceipt(parsed.output.extensions);
58
+ await reportSettlementHint$1(client, intentId, parsed.output.transaction, receipt);
59
+ } catch {}
60
+ }
61
+ var X402PaymentError = class extends Error {
62
+ reasons;
63
+ constructor(message, reasons = [], options = {}) {
64
+ super(reasons.length > 0 && (options.appendReasonsToMessage ?? true) ? `${message}: ${reasons.join("; ")}` : message);
65
+ this.name = "X402PaymentError";
66
+ this.reasons = reasons;
67
+ }
68
+ };
69
+ var X402NetworkMismatchError = class extends X402PaymentError {
70
+ accountId;
71
+ requiredNetworks;
72
+ constructor(accountId, requiredNetworks, reasons = []) {
73
+ super(`Account ${accountId} cannot fund this 402 challenge: it must be paid from a wallet account on ${requiredNetworks.map(networkLabel).join(" or ")}`, reasons, { appendReasonsToMessage: false });
74
+ this.name = "X402NetworkMismatchError";
75
+ this.accountId = accountId;
76
+ this.requiredNetworks = requiredNetworks;
77
+ }
78
+ };
79
+ var X402CounterpartyNotFoundError = class extends X402PaymentError {
80
+ payTo;
81
+ network;
82
+ serviceName;
83
+ constructor(payTo, network, serviceName, reasons = []) {
84
+ super(`This 402 pays ${payTo} on ${networkLabel(network)}, which is not a saved counterparty wallet rail; add it as a counterparty before paying`, reasons, { appendReasonsToMessage: false });
85
+ this.name = "X402CounterpartyNotFoundError";
86
+ this.payTo = payTo;
87
+ this.network = network;
88
+ this.serviceName = serviceName;
89
+ }
90
+ };
91
+ var X402ApprovalPendingError = class extends X402PaymentError {
92
+ intentId;
93
+ expiresAt;
94
+ constructor(intentId, reasons, expiresAt) {
95
+ super(`The payment is awaiting a human approval (intent ${intentId}); once an operator approves it, retry the request to complete the payment`, reasons);
96
+ this.name = "X402ApprovalPendingError";
97
+ this.intentId = intentId;
98
+ this.expiresAt = expiresAt;
99
+ }
100
+ };
101
+ var X402RetryFailedError = class extends X402PaymentError {
102
+ receipt;
103
+ constructor(receipt, cause) {
104
+ super(`The payment settled (intent ${receipt.intentId}) but the post-payment continuation failed; retry the request with the PAYMENT-SIGNATURE header from the receipt (or getIntent("${receipt.intentId}") data.paymentCredential.value) instead of paying again`);
105
+ this.name = "X402RetryFailedError";
106
+ this.receipt = receipt;
107
+ this.cause = cause;
108
+ }
109
+ };
110
+ async function preservePaymentReceipt(receipt, operation) {
111
+ try {
112
+ return await operation();
113
+ } catch (err) {
114
+ throw new X402RetryFailedError(receipt, err);
115
+ }
116
+ }
117
+ var X402SubmitInterruptedError = class extends X402PaymentError {
118
+ intentId;
119
+ requirements;
120
+ constructor(intentId, requirements, cause) {
121
+ super(`The payment could not be confirmed (intent ${intentId}); check getIntent("${intentId}") — completed: reuse its data.paymentCredential.value as the PAYMENT-SIGNATURE header; processing: poll until terminal; pay again only after blocked or failed`);
122
+ this.name = "X402SubmitInterruptedError";
123
+ this.intentId = intentId;
124
+ this.requirements = requirements;
125
+ this.cause = cause;
126
+ }
127
+ };
128
+ async function payX402Challenge(client, args) {
129
+ const exactCandidates = args.paymentRequired.accepts.filter((requirement) => requirement.scheme === "exact");
130
+ const candidates = exactCandidates.filter((requirement) => args.maxAtomicAmount === void 0 || /^\d+$/.test(requirement.amount) && BigInt(requirement.amount) <= args.maxAtomicAmount).slice(0, MAX_CANDIDATE_REQUIREMENTS);
131
+ 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");
132
+ const resource = args.resource ?? args.paymentRequired.resource;
133
+ const skips = [];
134
+ for (const requirements of candidates) {
135
+ const receipt = await payRequirement(client, {
136
+ accountId: args.accountId,
137
+ requirements,
138
+ resource,
139
+ signedOffer: extractRelayableOffer(args.paymentRequired.extensions, requirements)
140
+ }).catch((err) => {
141
+ if (err instanceof ApiError && err.code !== void 0 && TRY_NEXT_REQUIREMENT_CODES.has(err.code)) {
142
+ skips.push({
143
+ error: err,
144
+ requirement: requirements
145
+ });
146
+ return;
147
+ }
148
+ throw err;
149
+ });
150
+ if (receipt) return receipt;
151
+ }
152
+ const counterpartySkips = skips.filter((skip) => skip.error.code === X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE);
153
+ const mismatchSkips = skips.filter((skip) => skip.error.code === X402_NETWORK_MISMATCH_CODE);
154
+ const otherSkips = skips.filter((skip) => skip.error.code !== X402_COUNTERPARTY_RAIL_NOT_FOUND_CODE && skip.error.code !== X402_NETWORK_MISMATCH_CODE);
155
+ const reasons = [...new Set([
156
+ ...counterpartySkips,
157
+ ...mismatchSkips,
158
+ ...otherSkips
159
+ ].map((skip) => skip.error.message))];
160
+ const unsaved = counterpartySkips.find((skip) => skip.error.status === 404 && EVM_ADDRESS.test(skip.requirement.payTo));
161
+ if (unsaved) throw new X402CounterpartyNotFoundError(unsaved.requirement.payTo, unsaved.requirement.network, resource?.serviceName, reasons);
162
+ if (mismatchSkips.length > 0 && counterpartySkips.length === 0) throw new X402NetworkMismatchError(args.accountId, [...new Set(mismatchSkips.map((skip) => skip.requirement.network))], reasons);
163
+ throw new X402PaymentError("No requirement in the 402 challenge is payable from this account", reasons);
164
+ }
165
+ async function payRequirement(client, args) {
166
+ const intent = await client.submitIntent({
167
+ action: {
168
+ type: "x402",
169
+ accountId: args.accountId,
170
+ paymentRequirements: args.requirements,
171
+ ...args.resource !== void 0 && { resource: args.resource },
172
+ ...args.signedOffer !== void 0 && { signedOffer: args.signedOffer }
173
+ },
174
+ idempotencyKey: crypto.randomUUID()
175
+ }).catch((err) => {
176
+ if (err instanceof IntentSubmitError && err.outcome === "unknown") throw new X402SubmitInterruptedError(err.intentId, args.requirements, err);
177
+ throw err;
178
+ });
179
+ if (intent.status === "completed") {
180
+ const paymentSignature = x402CredentialFromIntentData(intent.data);
181
+ if (paymentSignature !== void 0) return {
182
+ intentId: intent.id,
183
+ paymentSignature,
184
+ requirements: args.requirements
185
+ };
186
+ throw new X402PaymentError("The completed payment did not carry an x402 payment signature");
187
+ }
188
+ switch (intent.status) {
189
+ case "pending": throw new X402ApprovalPendingError(intent.id, intent.reasons, intent.expiresAt);
190
+ case "processing": throw new X402PaymentError(`The payment is still processing (intent ${intent.id}); this client cannot resume an in-flight x402 settlement`);
191
+ case "blocked":
192
+ case "failed": throw new X402PaymentError("Catena declined the payment", intent.reasons);
193
+ }
194
+ return assertNever(intent.status);
195
+ }
196
+ function wrapFetchWithX402Payment(client, options) {
197
+ const baseFetch = (options.baseFetch ?? globalThis.fetch).bind(globalThis);
198
+ return async (input, init) => {
199
+ const response = await baseFetch(input, init);
200
+ if (response.status !== 402) return response;
201
+ const paymentRequired = decodePaymentRequired(response);
202
+ if (!paymentRequired) return response;
203
+ if (!isReplayableFetchRequest(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");
204
+ const receipt = await payX402Challenge(client, {
205
+ accountId: options.accountId,
206
+ paymentRequired,
207
+ ...options.maxAtomicAmount !== void 0 && { maxAtomicAmount: options.maxAtomicAmount }
208
+ });
209
+ await preservePaymentReceipt(receipt, () => options.onPayment?.(receipt));
210
+ const headers = fetchRetryHeaders(input, init);
211
+ headers.set(PAYMENT_SIGNATURE_HEADER, receipt.paymentSignature);
212
+ const retried = await preservePaymentReceipt(receipt, () => baseFetch(input, {
213
+ ...init,
214
+ headers
215
+ }));
216
+ reportSettlementHint(client, receipt.intentId, retried);
217
+ return retried;
218
+ };
219
+ }
220
+ //#endregion
221
+ export { X402ApprovalPendingError, X402CounterpartyNotFoundError, X402NetworkMismatchError, X402PaymentError, X402RetryFailedError, X402SubmitInterruptedError, decodePaymentRequired, payX402Challenge, wrapFetchWithX402Payment };
package/package.json CHANGED
@@ -1,9 +1,68 @@
1
1
  {
2
2
  "name": "@catena/sdk",
3
- "version": "0.0.0-bootstrap.0",
4
- "description": "Bootstrap placeholder for Catena SDK trusted publishing setup.",
3
+ "version": "0.1.0",
4
+ "description": "Typed client SDK for the Catena agent API",
5
+ "homepage": "https://catena.com/",
5
6
  "license": "Apache-2.0",
6
7
  "files": [
7
- "README.md"
8
- ]
9
- }
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
+ "./mpp": {
21
+ "types": "./dist/mpp.d.mts",
22
+ "default": "./dist/mpp.mjs"
23
+ },
24
+ "./x402": {
25
+ "types": "./dist/x402.d.mts",
26
+ "default": "./dist/x402.mjs"
27
+ },
28
+ "./viem": {
29
+ "types": "./dist/viem.d.mts",
30
+ "default": "./dist/viem.mjs"
31
+ }
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@turnkey/api-key-stamper": "0.6.12",
38
+ "valibot": "1.4.2"
39
+ },
40
+ "devDependencies": {
41
+ "mppx": "0.9.0",
42
+ "publint": "0.3.23",
43
+ "tsdown": "0.22.14",
44
+ "viem": "2.55.17"
45
+ },
46
+ "peerDependencies": {
47
+ "mppx": "0.9.0",
48
+ "viem": "^2.21.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "mppx": {
52
+ "optional": true
53
+ },
54
+ "viem": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "engines": {
59
+ "node": ">=20"
60
+ },
61
+ "gitHead": "1399d4f6d848c75a28aa8819cf05e47c36ee31b6",
62
+ "scripts": {
63
+ "build": "tsdown",
64
+ "clean": "git clean -xdf dist node_modules/.cache",
65
+ "publint": "pnpm run build && publint --pack pnpm --strict",
66
+ "verify:pack": "pnpm run publint && tsx bin/verify-pack.ts"
67
+ }
68
+ }