@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.
@@ -0,0 +1,857 @@
1
+ import { InvalidKeyError, p256KeypairFromPrivateKeyHex, p256PointFromCompressedHex, p256PublicKeyThumbprintFromPoint } from "./keypair.mjs";
2
+ import { ApiKeyStamper } from "@turnkey/api-key-stamper";
3
+ import * as v from "valibot";
4
+ import { createHash, createPrivateKey, randomBytes, sign } from "node:crypto";
5
+ //#region src/request-signing.ts
6
+ const COVERED_COMPONENTS = "(\"@method\" \"@target-uri\" \"content-digest\" \"signature-agent\")";
7
+ const ALGORITHM = "ecdsa-p256-sha256";
8
+ const IDENTITY_URL = /^https?:\/\/[\x21\x23-\x7e]{1,255}$/;
9
+ const NONCE = /^[\x21\x23-\x3a\x3c-\x7e]{1,128}$/;
10
+ function agentKeyIdentityUrl(bankUrl, keyId) {
11
+ return `${bankUrl.replace(/\/+$/, "")}/agent-keys/${keyId}`;
12
+ }
13
+ function createAgentRequestSigner(options) {
14
+ const key = buildSignerKey(options.keypair);
15
+ const state = {
16
+ ...key,
17
+ identityUrl: validatedIdentityUrl(options.identityUrl ?? agentKeyIdentityUrl(options.baseUrl, key.keyId))
18
+ };
19
+ return { sign: async (request) => {
20
+ const body = new Uint8Array(await request.clone().arrayBuffer());
21
+ const signatureHeaders = signRequest(state, {
22
+ method: request.method,
23
+ url: request.url,
24
+ body
25
+ }, {
26
+ nonce: randomBytes(16).toString("base64url"),
27
+ createdAt: new Date()
28
+ });
29
+ const headers = new Headers(request.headers);
30
+ for (const [name, value] of Object.entries(signatureHeaders)) headers.set(name, value);
31
+ return new Request(request, { headers });
32
+ } };
33
+ }
34
+ function buildSignerKey(keypair) {
35
+ const point = p256PointFromCompressedHex(keypair.publicKeyHex);
36
+ if (point === null) throw new Error("signer keypair has an invalid P-256 public key");
37
+ return {
38
+ signingKey: p256SigningKey(point, keypair.privateKeyHex),
39
+ keyId: p256PublicKeyThumbprintFromPoint(point)
40
+ };
41
+ }
42
+ function validatedIdentityUrl(identityUrl) {
43
+ if (!IDENTITY_URL.test(identityUrl)) throw new Error(`identity URL is outside the signature profile: "${identityUrl}"`);
44
+ return identityUrl;
45
+ }
46
+ function signRequest(state, request, params) {
47
+ if (!NONCE.test(params.nonce)) throw new Error("nonce is outside the signature profile");
48
+ const contentDigest = contentDigestHeader(request.body);
49
+ const signatureAgent = `"${state.identityUrl}"`;
50
+ const created = Math.floor(params.createdAt.getTime() / 1e3);
51
+ const signatureParams = `${COVERED_COMPONENTS};created=${created};keyid="${state.keyId}";nonce="${params.nonce}";alg="${ALGORITHM}"`;
52
+ const signatureBase = [
53
+ `"@method": ${request.method.toUpperCase()}`,
54
+ `"@target-uri": ${canonicalTargetUri(new URL(request.url))}`,
55
+ `"content-digest": ${contentDigest}`,
56
+ `"signature-agent": ${signatureAgent}`,
57
+ `"@signature-params": ${signatureParams}`
58
+ ].join("\n");
59
+ const signature = sign("sha256", Buffer.from(signatureBase, "utf8"), {
60
+ key: state.signingKey,
61
+ dsaEncoding: "ieee-p1363"
62
+ });
63
+ return {
64
+ "Content-Digest": contentDigest,
65
+ "Signature-Agent": signatureAgent,
66
+ "Signature-Input": `sig=${signatureParams}`,
67
+ Signature: `sig=:${signature.toString("base64")}:`
68
+ };
69
+ }
70
+ function contentDigestHeader(body) {
71
+ return `sha-256=:${createHash("sha256").update(body ?? "").digest("base64")}:`;
72
+ }
73
+ function canonicalTargetUri(url) {
74
+ return `${url.protocol}//${stripDefaultPort(url)}${url.pathname}${url.search}`;
75
+ }
76
+ function stripDefaultPort(url) {
77
+ const host = url.host.toLowerCase();
78
+ if (url.protocol === "https:" && url.port === "443" || url.protocol === "http:" && url.port === "80") return url.hostname.toLowerCase();
79
+ return host;
80
+ }
81
+ function p256SigningKey(point, privateKeyHex) {
82
+ return createPrivateKey({
83
+ key: {
84
+ kty: "EC",
85
+ crv: "P-256",
86
+ d: Buffer.from(privateKeyHex, "hex").toString("base64url"),
87
+ x: point.subarray(1, 33).toString("base64url"),
88
+ y: point.subarray(33, 65).toString("base64url")
89
+ },
90
+ format: "jwk"
91
+ });
92
+ }
93
+ //#endregion
94
+ //#region package.json
95
+ var version = "0.1.0";
96
+ //#endregion
97
+ //#region src/user-agent.ts
98
+ const HTTP_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
99
+ const AGENT_CLIENT_VERSION = version;
100
+ function assertHttpToken(value, field) {
101
+ if (!HTTP_TOKEN_RE.test(value)) throw new Error(`${field} must be a valid HTTP token`);
102
+ }
103
+ function assertUaCommentSafe(value, field) {
104
+ let hasUnrepresentableCharacter = false;
105
+ for (let i = 0; i < value.length; i += 1) {
106
+ const code = value.charCodeAt(i);
107
+ if (code < 32 || code === 127 || code > 255) {
108
+ hasUnrepresentableCharacter = true;
109
+ break;
110
+ }
111
+ }
112
+ if (hasUnrepresentableCharacter || /[()\\]/.test(value)) throw new Error(`${field} must not contain parentheses, backslashes, control characters, or characters outside Latin-1`);
113
+ }
114
+ function buildUserAgent(appInfo) {
115
+ const sdkProduct = `catena-agent-client/${AGENT_CLIENT_VERSION}`;
116
+ if (!appInfo) return sdkProduct;
117
+ assertHttpToken(appInfo.name, "appInfo.name");
118
+ if (appInfo.version !== void 0) assertHttpToken(appInfo.version, "appInfo.version");
119
+ if (appInfo.url !== void 0) assertUaCommentSafe(appInfo.url, "appInfo.url");
120
+ const product = appInfo.version === void 0 ? appInfo.name : `${appInfo.name}/${appInfo.version}`;
121
+ return `${appInfo.url === void 0 ? product : `${product} (${appInfo.url})`} ${sdkProduct}`;
122
+ }
123
+ //#endregion
124
+ //#region src/lib/payment-credential.ts
125
+ const paymentCredentialDataSchema = v.object({ paymentCredential: v.object({
126
+ protocol: v.picklist(["mpp", "x402"]),
127
+ value: v.string()
128
+ }) });
129
+ function paymentCredentialFromIntentData(data, protocol) {
130
+ const credential = v.safeParse(paymentCredentialDataSchema, data);
131
+ return credential.success && credential.output.paymentCredential.protocol === protocol ? credential.output.paymentCredential.value : void 0;
132
+ }
133
+ //#endregion
134
+ //#region src/offer-receipt.ts
135
+ const OFFER_RECEIPT_EXTENSION_KEY = "offer-receipt";
136
+ const MAX_ARTIFACT_BYTES = 8192;
137
+ const jwsCompactSchema = v.pipe(v.string(), v.maxLength(MAX_ARTIFACT_BYTES), v.regex(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/, "Expected a JWS compact serialization"));
138
+ const acceptIndexSchema = v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)));
139
+ const eip712SignatureSchema = v.pipe(v.string(), v.regex(/^0x[0-9a-fA-F]{130}$/, "Expected a 65-byte hex signature"));
140
+ const x402SignedArtifactSchema = v.variant("format", [v.looseObject({
141
+ format: v.literal("eip712"),
142
+ acceptIndex: acceptIndexSchema,
143
+ payload: v.record(v.string(), v.unknown()),
144
+ signature: eip712SignatureSchema
145
+ }), v.looseObject({
146
+ format: v.literal("jws"),
147
+ acceptIndex: acceptIndexSchema,
148
+ signature: jwsCompactSchema
149
+ })]);
150
+ const x402SignedReceiptSchema = v.variant("format", [v.looseObject({
151
+ format: v.literal("eip712"),
152
+ payload: v.record(v.string(), v.unknown()),
153
+ signature: eip712SignatureSchema
154
+ }), v.looseObject({
155
+ format: v.literal("jws"),
156
+ signature: jwsCompactSchema
157
+ })]);
158
+ const offersEnvelopeSchema = v.object({ [OFFER_RECEIPT_EXTENSION_KEY]: v.object({ info: v.object({ offers: v.array(v.unknown()) }) }) });
159
+ const receiptEnvelopeSchema = v.object({ [OFFER_RECEIPT_EXTENSION_KEY]: v.object({ info: v.object({ receipt: v.unknown() }) }) });
160
+ const offerTermsSchema = v.looseObject({
161
+ version: v.literal(1),
162
+ resourceUrl: v.pipe(v.string(), v.maxLength(2048)),
163
+ scheme: v.pipe(v.string(), v.maxLength(64)),
164
+ network: v.pipe(v.string(), v.maxLength(64)),
165
+ asset: v.pipe(v.string(), v.maxLength(128)),
166
+ payTo: v.pipe(v.string(), v.maxLength(128)),
167
+ amount: v.pipe(v.string(), v.regex(/^\d+$/), v.maxLength(78)),
168
+ validUntil: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)))
169
+ });
170
+ function decodeBase64UrlJson(value) {
171
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
172
+ const bytes = Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
173
+ return JSON.parse(new TextDecoder().decode(bytes));
174
+ }
175
+ function artifactTerms(artifact) {
176
+ if (artifact.format === "eip712") {
177
+ const parsed = v.safeParse(offerTermsSchema, artifact.payload);
178
+ return parsed.success ? parsed.output : void 0;
179
+ }
180
+ const payloadSegment = artifact.signature.split(".")[1];
181
+ try {
182
+ const parsed = v.safeParse(offerTermsSchema, decodeBase64UrlJson(payloadSegment));
183
+ return parsed.success ? parsed.output : void 0;
184
+ } catch {
185
+ return;
186
+ }
187
+ }
188
+ function sameAddress(a, b) {
189
+ return a.toLowerCase() === b.toLowerCase();
190
+ }
191
+ function termsMatch(terms, requirement) {
192
+ return terms.scheme === requirement.scheme && terms.network === requirement.network && sameAddress(terms.asset, requirement.asset) && sameAddress(terms.payTo, requirement.payTo) && terms.amount === requirement.amount;
193
+ }
194
+ function fitsRelayCap(artifact) {
195
+ return new TextEncoder().encode(JSON.stringify(artifact)).length <= MAX_ARTIFACT_BYTES;
196
+ }
197
+ function extractRelayableOffer(extensions, requirement) {
198
+ const envelope = v.safeParse(offersEnvelopeSchema, extensions);
199
+ if (!envelope.success) return;
200
+ for (const entry of envelope.output[OFFER_RECEIPT_EXTENSION_KEY].info.offers) {
201
+ const artifact = v.safeParse(x402SignedArtifactSchema, entry);
202
+ if (!artifact.success) continue;
203
+ const terms = artifactTerms(artifact.output);
204
+ if (terms !== void 0 && termsMatch(terms, requirement) && fitsRelayCap(artifact.output)) return artifact.output;
205
+ }
206
+ }
207
+ function extractRelayableReceipt(extensions) {
208
+ const envelope = v.safeParse(receiptEnvelopeSchema, extensions);
209
+ if (!envelope.success) return;
210
+ const artifact = v.safeParse(x402SignedReceiptSchema, envelope.output[OFFER_RECEIPT_EXTENSION_KEY].info.receipt);
211
+ if (!artifact.success || !fitsRelayCap(artifact.output)) return;
212
+ return artifact.output;
213
+ }
214
+ //#endregion
215
+ //#region src/schemas.ts
216
+ const nonBlankResponseEnumSchema = v.pipe(v.string(), v.regex(/\S/, "Response enum values must not be blank"));
217
+ function extensibleStringSchema(_known) {
218
+ return nonBlankResponseEnumSchema;
219
+ }
220
+ const moneyJsonSchema = v.object({
221
+ amount: v.string(),
222
+ asset_id: v.string()
223
+ });
224
+ const isoTimestampSchema = v.pipe(v.string(), v.isoTimestamp());
225
+ const okSuccessResponseSchema = v.object({ success: v.literal(true) });
226
+ const agentSchema = v.object({
227
+ id: v.string(),
228
+ name: v.string(),
229
+ status: v.string(),
230
+ organizationId: v.string()
231
+ });
232
+ const whoamiResponseSchema = v.object({ agent: agentSchema });
233
+ const POLICY_RULE_TYPES = [
234
+ "per_transaction_amount",
235
+ "daily_amount",
236
+ "weekly_amount",
237
+ "monthly_amount",
238
+ "hourly_count",
239
+ "daily_count"
240
+ ];
241
+ const POLICY_RULE_ACTIONS = ["block", "require_approval"];
242
+ const ACCOUNT_AGGREGATION_SCOPES = ["per_account", "across_accounts"];
243
+ const ACTOR_AGGREGATION_SCOPES = ["per_agent", "per_policy"];
244
+ const policyRuleSchema = v.object({
245
+ id: v.string(),
246
+ ruleType: extensibleStringSchema(POLICY_RULE_TYPES),
247
+ thresholdAmount: v.optional(moneyJsonSchema),
248
+ thresholdCount: v.optional(v.number()),
249
+ action: extensibleStringSchema(POLICY_RULE_ACTIONS),
250
+ requiredApprovals: v.number(),
251
+ accountAggregationScope: extensibleStringSchema(ACCOUNT_AGGREGATION_SCOPES),
252
+ actorAggregationScope: extensibleStringSchema(ACTOR_AGGREGATION_SCOPES),
253
+ displayOrder: v.number()
254
+ });
255
+ const POLICY_CAPABILITIES = [
256
+ "query_balance",
257
+ "read",
258
+ "send",
259
+ "transfer"
260
+ ];
261
+ const policyCapabilitySchema = v.object({
262
+ id: v.string(),
263
+ organizationId: v.string(),
264
+ policyId: v.string(),
265
+ capability: extensibleStringSchema(POLICY_CAPABILITIES),
266
+ accountId: v.string(),
267
+ rules: v.array(policyRuleSchema),
268
+ createdAt: isoTimestampSchema,
269
+ updatedAt: isoTimestampSchema
270
+ });
271
+ const COUNTERPARTY_ACTIONS = [
272
+ "allow",
273
+ "block",
274
+ "require_approval"
275
+ ];
276
+ const COUNTERPARTY_RULE_MODES = ["open", "restricted"];
277
+ const counterpartyActionSchema = extensibleStringSchema(COUNTERPARTY_ACTIONS);
278
+ const counterpartyRulesSchema = v.object({
279
+ mode: extensibleStringSchema(COUNTERPARTY_RULE_MODES),
280
+ allowedCounterparties: v.optional(v.array(v.string())),
281
+ createAction: v.optional(counterpartyActionSchema),
282
+ requestPaymentDetailsAction: v.optional(counterpartyActionSchema),
283
+ allowCreate: v.optional(v.boolean())
284
+ });
285
+ const policySchema = v.object({
286
+ id: v.optional(v.string()),
287
+ description: v.optional(v.string()),
288
+ version: v.optional(v.number()),
289
+ name: v.string(),
290
+ capabilities: v.array(v.string()),
291
+ counterpartyRules: v.optional(counterpartyRulesSchema),
292
+ policyCapabilities: v.array(policyCapabilitySchema),
293
+ kind: v.optional(v.picklist(["base", "override"])),
294
+ expiresAt: v.optional(v.string())
295
+ });
296
+ const policyResponseSchema = v.object({ policy: policySchema });
297
+ const accountsResponseSchema = v.object({ accounts: v.array(v.object({
298
+ id: v.string(),
299
+ name: v.string(),
300
+ type: v.string(),
301
+ currency: v.string()
302
+ })) });
303
+ const accountBalanceResponseSchema = v.object({
304
+ accountId: v.string(),
305
+ balance: moneyJsonSchema,
306
+ balances: v.optional(v.object({
307
+ total: moneyJsonSchema,
308
+ available: moneyJsonSchema
309
+ }))
310
+ });
311
+ const ACCOUNT_TRANSACTION_TYPES = [
312
+ "operator-send",
313
+ "operator-transfer",
314
+ "operator-deposit",
315
+ "operator-refund",
316
+ "microdeposit"
317
+ ];
318
+ const ACCOUNT_TRANSACTION_STATUSES = [
319
+ "pending",
320
+ "processing",
321
+ "completed",
322
+ "failed",
323
+ "reversed"
324
+ ];
325
+ const ACCOUNT_TRANSACTION_MOVEMENT_STATUSES = [
326
+ "initiated",
327
+ "provider_pending",
328
+ "processing",
329
+ "completed",
330
+ "failed",
331
+ "reversed",
332
+ "manual_review"
333
+ ];
334
+ const SEND_METHODS = [
335
+ "ach",
336
+ "wire",
337
+ "on-chain"
338
+ ];
339
+ const ACCOUNT_TRANSACTION_METHODS = [
340
+ "ach",
341
+ "wire",
342
+ "on-chain"
343
+ ];
344
+ const accountTransactionSchema = v.object({
345
+ id: v.string(),
346
+ type: extensibleStringSchema(ACCOUNT_TRANSACTION_TYPES),
347
+ action: v.optional(v.string()),
348
+ status: extensibleStringSchema(ACCOUNT_TRANSACTION_STATUSES),
349
+ movementStatus: extensibleStringSchema(ACCOUNT_TRANSACTION_MOVEMENT_STATUSES),
350
+ direction: v.picklist(["incoming", "outgoing"]),
351
+ amount: moneyJsonSchema,
352
+ fee: moneyJsonSchema,
353
+ currency: v.string(),
354
+ counterpartyName: v.optional(v.string()),
355
+ method: v.optional(extensibleStringSchema(ACCOUNT_TRANSACTION_METHODS)),
356
+ memo: v.optional(v.string()),
357
+ description: v.optional(v.string()),
358
+ txHash: v.optional(v.string()),
359
+ createdAt: isoTimestampSchema,
360
+ completedAt: v.optional(isoTimestampSchema)
361
+ });
362
+ const accountTransactionsResponseSchema = v.object({
363
+ accountId: v.string(),
364
+ transactions: v.array(accountTransactionSchema),
365
+ total: v.number()
366
+ });
367
+ const ACCOUNT_DEPOSIT_ADDRESS_SOURCES = ["wallet", "liquidation"];
368
+ const accountDepositAddressResponseSchema = v.object({
369
+ accountId: v.string(),
370
+ address: v.string(),
371
+ liquidationAddressId: v.optional(v.string()),
372
+ source: extensibleStringSchema(ACCOUNT_DEPOSIT_ADDRESS_SOURCES)
373
+ });
374
+ const counterpartySchema = v.object({
375
+ id: v.string(),
376
+ name: v.string(),
377
+ status: v.optional(v.string()),
378
+ rails: v.array(v.object({
379
+ id: v.string(),
380
+ type: v.string(),
381
+ walletAddress: v.optional(v.string()),
382
+ network: v.optional(v.string()),
383
+ bankName: v.optional(v.string()),
384
+ accountNumber: v.optional(v.string())
385
+ }))
386
+ });
387
+ const counterpartiesResponseSchema = v.object({ counterparties: v.array(counterpartySchema) });
388
+ const intentStatusSchema = v.picklist([
389
+ "pending",
390
+ "processing",
391
+ "completed",
392
+ "blocked",
393
+ "failed"
394
+ ]);
395
+ const intentNextActionSchema = v.object({
396
+ type: v.literal("submit_stamp"),
397
+ signingRequestId: v.string(),
398
+ signer: v.object({
399
+ algorithm: v.literal("p256"),
400
+ publicKeyHex: v.string()
401
+ }),
402
+ preparedBody: v.object({
403
+ body: v.string(),
404
+ bodyHash: v.string(),
405
+ expiresAt: isoTimestampSchema,
406
+ prepareToken: v.string()
407
+ })
408
+ });
409
+ const INTENT_ACTION_TYPES = [
410
+ "send",
411
+ "transfer",
412
+ "create_counterparty",
413
+ "request_counterparty_details",
414
+ "x402",
415
+ "mpp",
416
+ "policy_override"
417
+ ];
418
+ const wireIntentResultSchema = v.object({
419
+ id: v.string(),
420
+ accountId: v.optional(v.string()),
421
+ type: extensibleStringSchema(INTENT_ACTION_TYPES),
422
+ status: intentStatusSchema,
423
+ reasons: v.array(v.string()),
424
+ expiresAt: v.optional(v.nullable(isoTimestampSchema), null),
425
+ replayed: v.optional(v.boolean(), false),
426
+ data: v.optional(v.nullable(v.record(v.string(), v.unknown())), null),
427
+ metadata: v.optional(v.object({ dataUrl: v.nullable(v.string()) })),
428
+ nextAction: v.optional(intentNextActionSchema)
429
+ });
430
+ function toIntentResult(wire) {
431
+ const { nextAction: _nextAction, ...intent } = wire;
432
+ return intent;
433
+ }
434
+ const legacyX402IntentDataSchema = v.object({ x402: v.object({ paymentSignature: v.string() }) });
435
+ function x402CredentialFromIntentData(data) {
436
+ const credential = paymentCredentialFromIntentData(data, "x402");
437
+ if (credential !== void 0) return credential;
438
+ const legacy = v.safeParse(legacyX402IntentDataSchema, data);
439
+ return legacy.success ? legacy.output.x402.paymentSignature : void 0;
440
+ }
441
+ const sendMethodSchema = v.picklist(SEND_METHODS);
442
+ const sendActionSchema = v.object({
443
+ type: v.literal("send"),
444
+ accountId: v.optional(v.pipe(v.string(), v.minLength(1))),
445
+ counterpartyRailId: v.string(),
446
+ amount: v.string(),
447
+ method: sendMethodSchema,
448
+ memo: v.optional(v.string()),
449
+ description: v.optional(v.string())
450
+ });
451
+ const transferActionSchema = v.object({
452
+ type: v.literal("transfer"),
453
+ accountId: v.string(),
454
+ toAccountId: v.string(),
455
+ amount: v.string(),
456
+ memo: v.optional(v.string()),
457
+ description: v.optional(v.string())
458
+ });
459
+ const createCounterpartyBaseFields = {
460
+ type: v.literal("create_counterparty"),
461
+ name: v.string()
462
+ };
463
+ const createCounterpartyActionSchema = v.union([v.object({
464
+ ...createCounterpartyBaseFields,
465
+ email: v.optional(v.string()),
466
+ rail: v.nonOptional(v.unknown())
467
+ }), v.object({
468
+ ...createCounterpartyBaseFields,
469
+ email: v.string()
470
+ })]);
471
+ const requestCounterpartyDetailsActionSchema = v.object({
472
+ type: v.literal("request_counterparty_details"),
473
+ counterpartyId: v.string(),
474
+ methods: v.pipe(v.object({
475
+ bank: v.boolean(),
476
+ wallet: v.boolean()
477
+ }), v.check(({ bank, wallet }) => bank || wallet, "Select at least one payment method to request"))
478
+ });
479
+ const x402PaymentRequirementsSchema = v.looseObject({
480
+ scheme: v.string(),
481
+ network: v.string(),
482
+ asset: v.string(),
483
+ amount: v.string(),
484
+ payTo: v.string(),
485
+ maxTimeoutSeconds: v.number()
486
+ });
487
+ const x402ResourceSchema = v.looseObject({
488
+ url: v.string(),
489
+ serviceName: v.fallback(v.optional(v.pipe(v.string(), v.maxLength(255))), void 0)
490
+ });
491
+ const x402AddressSchema = v.pipe(v.string(), v.regex(/^0x[0-9a-fA-F]{40}$/, "Expected a 20-byte hex address"));
492
+ const x402Uint256StringSchema = v.pipe(v.string(), v.maxLength(78), v.regex(/^(0|[1-9]\d*)$/, "Expected a canonical decimal integer string"));
493
+ const x402AuthorizationSchema = v.object({
494
+ from: x402AddressSchema,
495
+ to: x402AddressSchema,
496
+ value: x402Uint256StringSchema,
497
+ validAfter: x402Uint256StringSchema,
498
+ validBefore: x402Uint256StringSchema,
499
+ nonce: v.pipe(v.string(), v.regex(/^0x[0-9a-fA-F]{64}$/, "Expected a 32-byte hex nonce"))
500
+ });
501
+ const x402ActionSchema = v.object({
502
+ type: v.literal("x402"),
503
+ accountId: v.string(),
504
+ paymentRequirements: x402PaymentRequirementsSchema,
505
+ resource: v.optional(x402ResourceSchema),
506
+ authorization: v.optional(x402AuthorizationSchema),
507
+ signedOffer: v.optional(x402SignedArtifactSchema)
508
+ });
509
+ const mppChallengeSchema = v.object({
510
+ id: v.string(),
511
+ realm: v.string(),
512
+ method: v.picklist(["evm", "usdc"]),
513
+ intent: v.literal("charge"),
514
+ request: v.string(),
515
+ description: v.optional(v.string()),
516
+ digest: v.optional(v.string()),
517
+ expires: v.string(),
518
+ header: v.optional(v.literal("Payment-Authorization")),
519
+ opaque: v.optional(v.string())
520
+ });
521
+ const mppResourceSchema = v.object({
522
+ url: v.string(),
523
+ serviceName: v.optional(v.string())
524
+ });
525
+ const mppActionSchema = v.object({
526
+ type: v.literal("mpp"),
527
+ accountId: v.string(),
528
+ challenge: mppChallengeSchema,
529
+ resource: v.optional(mppResourceSchema)
530
+ });
531
+ const POLICY_OVERRIDE_CAPABILITIES = ["send", "transfer"];
532
+ const policyOverrideAmountOperationSchema = v.strictObject({
533
+ type: v.literal("increase_limit"),
534
+ accountId: v.pipe(v.string(), v.minLength(1)),
535
+ capability: v.picklist(POLICY_OVERRIDE_CAPABILITIES),
536
+ limitType: v.picklist([
537
+ "per_transaction_amount",
538
+ "daily_amount",
539
+ "weekly_amount",
540
+ "monthly_amount"
541
+ ]),
542
+ newLimit: v.strictObject({
543
+ amount: v.pipe(v.string(), v.regex(/^\d+(\.\d{1,6})?$/), v.check((value) => Number(value) > 0, "The amount must be greater than zero")),
544
+ assetId: v.literal("USD")
545
+ })
546
+ });
547
+ const policyOverrideCountOperationSchema = v.strictObject({
548
+ type: v.literal("increase_limit"),
549
+ accountId: v.pipe(v.string(), v.minLength(1)),
550
+ capability: v.picklist(POLICY_OVERRIDE_CAPABILITIES),
551
+ limitType: v.picklist(["hourly_count", "daily_count"]),
552
+ newLimit: v.strictObject({ count: v.pipe(v.number(), v.integer(), v.minValue(1)) })
553
+ });
554
+ const policyOverrideOperationsSchema = v.pipe(v.array(v.union([policyOverrideAmountOperationSchema, policyOverrideCountOperationSchema])), v.minLength(1), v.maxLength(10), v.check((operations) => new Set(operations.map((operation) => `${operation.accountId}|${operation.capability}|${operation.limitType}`)).size === operations.length, "Each account, capability, and limit type may appear only once"));
555
+ const policyOverrideActionSchema = v.object({
556
+ type: v.literal("policy_override"),
557
+ operations: policyOverrideOperationsSchema,
558
+ durationSeconds: v.pipe(v.number(), v.integer(), v.minValue(60), v.maxValue(604800)),
559
+ reason: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(280))
560
+ });
561
+ const intentActionSchema = v.union([
562
+ sendActionSchema,
563
+ transferActionSchema,
564
+ createCounterpartyActionSchema,
565
+ requestCounterpartyDetailsActionSchema,
566
+ x402ActionSchema,
567
+ mppActionSchema,
568
+ policyOverrideActionSchema
569
+ ]);
570
+ function toWireAction(action) {
571
+ const parsedAction = v.parse(intentActionSchema, action);
572
+ if (parsedAction.type === "send" || parsedAction.type === "transfer") return {
573
+ ...parsedAction,
574
+ amount: {
575
+ amount: parsedAction.amount,
576
+ asset_id: "USD"
577
+ }
578
+ };
579
+ if (parsedAction.type === "policy_override") return {
580
+ ...parsedAction,
581
+ schemaVersion: 1
582
+ };
583
+ return parsedAction;
584
+ }
585
+ const submitStampResponseSchema = v.object({ intent: wireIntentResultSchema });
586
+ //#endregion
587
+ //#region src/client.ts
588
+ var ApiError = class extends Error {
589
+ status;
590
+ code;
591
+ details;
592
+ constructor(status, message, code, details) {
593
+ super(code ? `API error ${status} [${code}]: ${message}` : `API error ${status}: ${message}`);
594
+ this.name = "ApiError";
595
+ this.status = status;
596
+ this.code = code;
597
+ this.details = details;
598
+ }
599
+ };
600
+ var TimeoutError = class extends Error {
601
+ timeoutMs;
602
+ constructor(timeoutMs) {
603
+ super(`Request timed out after ${timeoutMs}ms. The server may still have processed a state-changing request; follow that operation's documented recovery procedure before retrying.`);
604
+ this.name = "TimeoutError";
605
+ this.timeoutMs = timeoutMs;
606
+ }
607
+ };
608
+ const errorBodySchema = v.object({
609
+ error: v.nullish(v.string()),
610
+ message: v.nullish(v.string()),
611
+ code: v.nullish(v.string()),
612
+ details: v.nullish(v.record(v.string(), v.unknown()))
613
+ });
614
+ async function apiErrorFromResponse(res) {
615
+ const parsed = v.safeParse(errorBodySchema, await res.json().catch(() => void 0));
616
+ const body = parsed.success ? parsed.output : {};
617
+ if (body.error != null && body.message != null) return new ApiError(res.status, body.message, body.code ?? body.error, body.details ?? void 0);
618
+ return new ApiError(res.status, body.error ?? body.message ?? res.statusText, body.code ?? void 0, body.details ?? void 0);
619
+ }
620
+ async function parseApiResponse(res, schema) {
621
+ if (!res.ok) throw await apiErrorFromResponse(res);
622
+ return v.parse(schema, await res.json());
623
+ }
624
+ var IntentSubmitError = class extends Error {
625
+ intentId;
626
+ outcome;
627
+ constructor(params) {
628
+ const detail = params.cause instanceof Error ? params.cause.message : String(params.cause);
629
+ super(params.outcome === "not-submitted" ? `Intent ${params.intentId} was created but never submitted; no money moved: ${detail}` : `Intent ${params.intentId} submission could not be confirmed: ${detail}; poll getIntent("${params.intentId}") to a terminal status and resubmit only after blocked or failed`, { cause: params.cause });
630
+ this.name = "IntentSubmitError";
631
+ this.intentId = params.intentId;
632
+ this.outcome = params.outcome;
633
+ }
634
+ };
635
+ const DEFAULT_BASE_URL = "https://api.catena.com";
636
+ const DEFAULT_TIMEOUT_MS = 8e4;
637
+ const MAX_TIMEOUT_MS = 2147483647;
638
+ var CatenaClient = class {
639
+ #baseUrl;
640
+ #userAgent;
641
+ #defaultHeaders;
642
+ #keypair;
643
+ #signer;
644
+ #fetch;
645
+ #timeoutMs;
646
+ constructor(opts) {
647
+ const timeoutMs = opts.timeout === void 0 ? DEFAULT_TIMEOUT_MS : opts.timeout;
648
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMEOUT_MS) throw new TypeError(`timeout must be a positive integer no greater than ${MAX_TIMEOUT_MS}`);
649
+ this.#baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
650
+ this.#userAgent = buildUserAgent(opts.appInfo);
651
+ this.#defaultHeaders = opts.defaultHeaders ?? {};
652
+ this.#keypair = p256KeypairFromPrivateKeyHex(opts.privateKeyHex);
653
+ this.#fetch = (opts.fetch ?? globalThis.fetch).bind(globalThis);
654
+ this.#timeoutMs = timeoutMs;
655
+ this.#signer = createAgentRequestSigner({
656
+ keypair: this.#keypair,
657
+ baseUrl: this.#baseUrl,
658
+ identityUrl: opts.identityUrl
659
+ });
660
+ }
661
+ #buildRequestHeaders(requestHeaders) {
662
+ const headers = new Headers({ "Content-Type": "application/json" });
663
+ for (const [name, value] of Object.entries(this.#defaultHeaders)) headers.set(name, value);
664
+ for (const [name, value] of Object.entries(requestHeaders ?? {})) headers.set(name, value);
665
+ headers.set("User-Agent", this.#userAgent);
666
+ headers.delete("Authorization");
667
+ return headers;
668
+ }
669
+ async #signedRequest(options) {
670
+ const url = new URL(`${this.#baseUrl}/v1/agent-api${options.path}`);
671
+ for (const [key, value] of Object.entries(options.query ?? {})) if (value !== void 0) url.searchParams.set(key, value);
672
+ const request = new Request(url, {
673
+ method: options.method,
674
+ headers: this.#buildRequestHeaders(options.headers),
675
+ body: options.body !== void 0 ? JSON.stringify(options.body) : null
676
+ });
677
+ return this.#signer.sign(request);
678
+ }
679
+ async #request(options, schema) {
680
+ return this.#dispatch(await this.#signedRequest(options), schema);
681
+ }
682
+ async #dispatch(request, schema) {
683
+ const controller = new AbortController();
684
+ const timedRequest = new Request(request, { signal: controller.signal });
685
+ let timeoutId;
686
+ try {
687
+ const timeout = new Promise((_resolve, reject) => {
688
+ timeoutId = setTimeout(() => {
689
+ const timeoutError = new TimeoutError(this.#timeoutMs);
690
+ reject(timeoutError);
691
+ controller.abort(timeoutError);
692
+ }, this.#timeoutMs);
693
+ });
694
+ const exchange = this.#fetch(timedRequest).then((response) => parseApiResponse(response, schema));
695
+ return await Promise.race([exchange, timeout]);
696
+ } finally {
697
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
698
+ }
699
+ }
700
+ async whoami() {
701
+ return (await this.#request({
702
+ method: "GET",
703
+ path: "/me"
704
+ }, whoamiResponseSchema)).agent;
705
+ }
706
+ async submitFeedback(body) {
707
+ await this.#request({
708
+ method: "POST",
709
+ path: "/feedback",
710
+ body: { body }
711
+ }, okSuccessResponseSchema);
712
+ }
713
+ async getPolicy() {
714
+ return (await this.#request({
715
+ method: "GET",
716
+ path: "/policy"
717
+ }, policyResponseSchema)).policy;
718
+ }
719
+ async listAccounts() {
720
+ return this.#request({
721
+ method: "GET",
722
+ path: "/accounts"
723
+ }, accountsResponseSchema);
724
+ }
725
+ async getAccountBalance(accountId) {
726
+ return this.#request({
727
+ method: "GET",
728
+ path: `/accounts/${encodeURIComponent(accountId)}/balance`
729
+ }, accountBalanceResponseSchema);
730
+ }
731
+ async listAccountTransactions(accountId, params = {}) {
732
+ return this.#request({
733
+ method: "GET",
734
+ path: `/accounts/${encodeURIComponent(accountId)}/transactions`,
735
+ query: {
736
+ start: params.start,
737
+ end: params.end,
738
+ limit: params.limit?.toString(),
739
+ offset: params.offset?.toString()
740
+ }
741
+ }, accountTransactionsResponseSchema);
742
+ }
743
+ async getAccountDepositAddress(accountId, params) {
744
+ return this.#request({
745
+ method: "GET",
746
+ path: `/accounts/${encodeURIComponent(accountId)}/deposit-address`,
747
+ query: {
748
+ network: params.network,
749
+ asset: params.asset
750
+ }
751
+ }, accountDepositAddressResponseSchema);
752
+ }
753
+ async listCounterparties(params = {}) {
754
+ return this.#request({
755
+ method: "GET",
756
+ path: "/counterparties",
757
+ query: {
758
+ address: params.address,
759
+ network: params.network
760
+ }
761
+ }, counterpartiesResponseSchema);
762
+ }
763
+ async submitIntent(params) {
764
+ const created = await this.#createIntentRaw({
765
+ ...params,
766
+ idempotencyKey: params.idempotencyKey ?? crypto.randomUUID()
767
+ });
768
+ const nextAction = created.nextAction;
769
+ if (!nextAction) return toIntentResult(created);
770
+ let submitRequest;
771
+ try {
772
+ const stamp = await this.#stampNextAction(nextAction);
773
+ submitRequest = await this.#signedRequest({
774
+ method: "POST",
775
+ path: `/intents/${encodeURIComponent(created.id)}/submit-stamp`,
776
+ body: {
777
+ preparedBody: nextAction.preparedBody,
778
+ stamp
779
+ }
780
+ });
781
+ } catch (cause) {
782
+ throw new IntentSubmitError({
783
+ intentId: created.id,
784
+ outcome: "not-submitted",
785
+ cause
786
+ });
787
+ }
788
+ try {
789
+ return await this.#submitStamp(submitRequest, created.replayed);
790
+ } catch (cause) {
791
+ throw new IntentSubmitError({
792
+ intentId: created.id,
793
+ outcome: "unknown",
794
+ cause
795
+ });
796
+ }
797
+ }
798
+ async #createIntentRaw(params) {
799
+ return this.#request({
800
+ method: "POST",
801
+ path: "/intents",
802
+ body: { action: toWireAction(params.action) },
803
+ headers: { "Idempotency-Key": params.idempotencyKey }
804
+ }, wireIntentResultSchema);
805
+ }
806
+ async #stampNextAction(nextAction) {
807
+ if (nextAction.signer.publicKeyHex.toLowerCase() !== this.#keypair.publicKeyHex.toLowerCase()) throw new Error(`Local signer public key does not match requested signer ${nextAction.signer.publicKeyHex}; linked credential has ${this.#keypair.publicKeyHex}`);
808
+ const stamp = await new ApiKeyStamper({
809
+ apiPublicKey: this.#keypair.publicKeyHex,
810
+ apiPrivateKey: this.#keypair.privateKeyHex
811
+ }).stamp(nextAction.preparedBody.body);
812
+ return {
813
+ publicKeyHex: this.#keypair.publicKeyHex,
814
+ stampHeaderName: stamp.stampHeaderName,
815
+ stampHeaderValue: stamp.stampHeaderValue
816
+ };
817
+ }
818
+ async #submitStamp(request, replayed) {
819
+ const intent = toIntentResult((await this.#dispatch(request, submitStampResponseSchema)).intent);
820
+ return replayed ? {
821
+ ...intent,
822
+ replayed: true
823
+ } : intent;
824
+ }
825
+ async getIntent(id) {
826
+ return toIntentResult(await this.#request({
827
+ method: "GET",
828
+ path: `/intents/${encodeURIComponent(id)}`
829
+ }, wireIntentResultSchema));
830
+ }
831
+ async unlinkAgent() {
832
+ await this.#request({
833
+ method: "POST",
834
+ path: "/unlink"
835
+ }, okSuccessResponseSchema);
836
+ }
837
+ async reportSettlement(params) {
838
+ await this.#request({
839
+ method: "POST",
840
+ path: `/intents/${encodeURIComponent(params.intentId)}/settlement-report`,
841
+ body: {
842
+ txHash: params.txHash,
843
+ ...params.receipt !== void 0 && { receipt: params.receipt }
844
+ }
845
+ }, okSuccessResponseSchema);
846
+ }
847
+ };
848
+ function createCatenaClient(options = {}) {
849
+ const privateKeyHex = options.privateKeyHex === void 0 ? process.env.CATENA_SECRET_KEY : options.privateKeyHex;
850
+ if (privateKeyHex === void 0) throw new InvalidKeyError("privateKeyHex is required; pass it explicitly or set CATENA_SECRET_KEY");
851
+ return new CatenaClient({
852
+ ...options,
853
+ privateKeyHex
854
+ });
855
+ }
856
+ //#endregion
857
+ export { extractRelayableOffer as C, x402ResourceSchema as S, paymentCredentialFromIntentData as T, POLICY_RULE_ACTIONS as _, ACCOUNT_AGGREGATION_SCOPES as a, x402CredentialFromIntentData as b, ACCOUNT_TRANSACTION_MOVEMENT_STATUSES as c, ACTOR_AGGREGATION_SCOPES as d, COUNTERPARTY_ACTIONS as f, POLICY_OVERRIDE_CAPABILITIES as g, POLICY_CAPABILITIES as h, createCatenaClient as i, ACCOUNT_TRANSACTION_STATUSES as l, INTENT_ACTION_TYPES as m, IntentSubmitError as n, ACCOUNT_DEPOSIT_ADDRESS_SOURCES as o, COUNTERPARTY_RULE_MODES as p, TimeoutError as r, ACCOUNT_TRANSACTION_METHODS as s, ApiError as t, ACCOUNT_TRANSACTION_TYPES as u, POLICY_RULE_TYPES as v, extractRelayableReceipt as w, x402PaymentRequirementsSchema as x, SEND_METHODS as y };