@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.
- package/LICENSE +191 -0
- package/README.md +147 -0
- package/dist/client-DZHdFs_y.d.mts +566 -0
- package/dist/client-G-8NfBx_.mjs +597 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.mjs +2 -0
- package/dist/keypair.d.mts +29 -0
- package/dist/keypair.mjs +43 -0
- package/dist/x402.d.mts +175 -0
- package/dist/x402.mjs +225 -0
- package/package.json +46 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { 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 signatureParams = `${COVERED_COMPONENTS};created=${Math.floor(params.createdAt.getTime() / 1e3)};keyid="${state.keyId}";nonce="${params.nonce}";alg="${ALGORITHM}"`;
|
|
51
|
+
const signatureBase = [
|
|
52
|
+
`"@method": ${request.method.toUpperCase()}`,
|
|
53
|
+
`"@target-uri": ${canonicalTargetUri(new URL(request.url))}`,
|
|
54
|
+
`"content-digest": ${contentDigest}`,
|
|
55
|
+
`"signature-agent": ${signatureAgent}`,
|
|
56
|
+
`"@signature-params": ${signatureParams}`
|
|
57
|
+
].join("\n");
|
|
58
|
+
const signature = sign("sha256", Buffer.from(signatureBase, "utf8"), {
|
|
59
|
+
key: state.signingKey,
|
|
60
|
+
dsaEncoding: "ieee-p1363"
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
"Content-Digest": contentDigest,
|
|
64
|
+
"Signature-Agent": signatureAgent,
|
|
65
|
+
"Signature-Input": `sig=${signatureParams}`,
|
|
66
|
+
Signature: `sig=:${signature.toString("base64")}:`
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function contentDigestHeader(body) {
|
|
70
|
+
return `sha-256=:${createHash("sha256").update(body ?? "").digest("base64")}:`;
|
|
71
|
+
}
|
|
72
|
+
function canonicalTargetUri(url) {
|
|
73
|
+
return `${url.protocol}//${stripDefaultPort(url)}${url.pathname}${url.search}`;
|
|
74
|
+
}
|
|
75
|
+
function stripDefaultPort(url) {
|
|
76
|
+
const host = url.host.toLowerCase();
|
|
77
|
+
if (url.protocol === "https:" && url.port === "443" || url.protocol === "http:" && url.port === "80") return url.hostname.toLowerCase();
|
|
78
|
+
return host;
|
|
79
|
+
}
|
|
80
|
+
function p256SigningKey(point, privateKeyHex) {
|
|
81
|
+
return createPrivateKey({
|
|
82
|
+
key: {
|
|
83
|
+
kty: "EC",
|
|
84
|
+
crv: "P-256",
|
|
85
|
+
d: Buffer.from(privateKeyHex, "hex").toString("base64url"),
|
|
86
|
+
x: point.subarray(1, 33).toString("base64url"),
|
|
87
|
+
y: point.subarray(33, 65).toString("base64url")
|
|
88
|
+
},
|
|
89
|
+
format: "jwk"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region package.json
|
|
94
|
+
var version = "0.0.0-alpha-20260724191736";
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/user-agent.ts
|
|
97
|
+
const HTTP_TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
98
|
+
const AGENT_CLIENT_VERSION = version;
|
|
99
|
+
function assertHttpToken(value, field) {
|
|
100
|
+
if (!HTTP_TOKEN_RE.test(value)) throw new Error(`${field} must be a valid HTTP token`);
|
|
101
|
+
}
|
|
102
|
+
function assertUaCommentSafe(value, field) {
|
|
103
|
+
let hasUnrepresentableCharacter = false;
|
|
104
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
105
|
+
const code = value.charCodeAt(i);
|
|
106
|
+
if (code < 32 || code === 127 || code > 255) {
|
|
107
|
+
hasUnrepresentableCharacter = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (hasUnrepresentableCharacter || /[()\\]/.test(value)) throw new Error(`${field} must not contain parentheses, backslashes, control characters, or characters outside Latin-1`);
|
|
112
|
+
}
|
|
113
|
+
function buildUserAgent(appInfo) {
|
|
114
|
+
const sdkProduct = `catena-agent-client/${AGENT_CLIENT_VERSION}`;
|
|
115
|
+
if (!appInfo) return sdkProduct;
|
|
116
|
+
assertHttpToken(appInfo.name, "appInfo.name");
|
|
117
|
+
if (appInfo.version !== void 0) assertHttpToken(appInfo.version, "appInfo.version");
|
|
118
|
+
if (appInfo.url !== void 0) assertUaCommentSafe(appInfo.url, "appInfo.url");
|
|
119
|
+
const product = appInfo.version === void 0 ? appInfo.name : `${appInfo.name}/${appInfo.version}`;
|
|
120
|
+
return `${appInfo.url === void 0 ? product : `${product} (${appInfo.url})`} ${sdkProduct}`;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/schemas.ts
|
|
124
|
+
const moneyJsonSchema = v.object({
|
|
125
|
+
amount: v.string(),
|
|
126
|
+
asset_id: v.string()
|
|
127
|
+
});
|
|
128
|
+
const isoTimestampSchema = v.pipe(v.string(), v.isoTimestamp());
|
|
129
|
+
const okSuccessResponseSchema = v.object({ success: v.literal(true) });
|
|
130
|
+
const agentSchema = v.object({
|
|
131
|
+
id: v.string(),
|
|
132
|
+
name: v.string(),
|
|
133
|
+
status: v.string(),
|
|
134
|
+
organizationId: v.string(),
|
|
135
|
+
operatorEmail: v.nullable(v.string())
|
|
136
|
+
});
|
|
137
|
+
const whoamiResponseSchema = v.object({ agent: agentSchema });
|
|
138
|
+
const policyRuleSchema = v.object({
|
|
139
|
+
id: v.string(),
|
|
140
|
+
ruleType: v.picklist([
|
|
141
|
+
"per_transaction_amount",
|
|
142
|
+
"daily_amount",
|
|
143
|
+
"weekly_amount",
|
|
144
|
+
"monthly_amount",
|
|
145
|
+
"hourly_count",
|
|
146
|
+
"daily_count"
|
|
147
|
+
]),
|
|
148
|
+
thresholdAmount: v.optional(moneyJsonSchema),
|
|
149
|
+
thresholdCount: v.optional(v.number()),
|
|
150
|
+
action: v.picklist(["block", "require_approval"]),
|
|
151
|
+
requiredApprovals: v.number(),
|
|
152
|
+
accountAggregationScope: v.picklist(["per_account", "across_accounts"]),
|
|
153
|
+
actorAggregationScope: v.picklist(["per_agent", "per_policy"]),
|
|
154
|
+
displayOrder: v.number()
|
|
155
|
+
});
|
|
156
|
+
const policyCapabilitySchema = v.object({
|
|
157
|
+
id: v.string(),
|
|
158
|
+
organizationId: v.string(),
|
|
159
|
+
policyId: v.string(),
|
|
160
|
+
capability: v.picklist([
|
|
161
|
+
"query_balance",
|
|
162
|
+
"read",
|
|
163
|
+
"send",
|
|
164
|
+
"transfer"
|
|
165
|
+
]),
|
|
166
|
+
accountId: v.string(),
|
|
167
|
+
rules: v.array(policyRuleSchema),
|
|
168
|
+
createdAt: isoTimestampSchema,
|
|
169
|
+
updatedAt: isoTimestampSchema
|
|
170
|
+
});
|
|
171
|
+
const counterpartyRulesSchema = v.object({
|
|
172
|
+
mode: v.picklist(["open", "restricted"]),
|
|
173
|
+
allowedCounterparties: v.optional(v.array(v.string())),
|
|
174
|
+
createAction: v.optional(v.picklist([
|
|
175
|
+
"allow",
|
|
176
|
+
"block",
|
|
177
|
+
"require_approval"
|
|
178
|
+
])),
|
|
179
|
+
allowCreate: v.optional(v.boolean())
|
|
180
|
+
});
|
|
181
|
+
const policySchema = v.object({
|
|
182
|
+
id: v.optional(v.string()),
|
|
183
|
+
description: v.optional(v.string()),
|
|
184
|
+
version: v.optional(v.number()),
|
|
185
|
+
name: v.string(),
|
|
186
|
+
capabilities: v.array(v.string()),
|
|
187
|
+
counterpartyRules: v.optional(counterpartyRulesSchema),
|
|
188
|
+
policyCapabilities: v.array(policyCapabilitySchema)
|
|
189
|
+
});
|
|
190
|
+
const policyResponseSchema = v.object({ policy: policySchema });
|
|
191
|
+
const accountsResponseSchema = v.object({ accounts: v.array(v.object({
|
|
192
|
+
id: v.string(),
|
|
193
|
+
name: v.string(),
|
|
194
|
+
type: v.string(),
|
|
195
|
+
currency: v.string()
|
|
196
|
+
})) });
|
|
197
|
+
const accountBalanceResponseSchema = v.object({
|
|
198
|
+
accountId: v.string(),
|
|
199
|
+
balance: moneyJsonSchema,
|
|
200
|
+
balances: v.optional(v.object({
|
|
201
|
+
total: moneyJsonSchema,
|
|
202
|
+
available: moneyJsonSchema
|
|
203
|
+
}))
|
|
204
|
+
});
|
|
205
|
+
const accountTransactionSchema = v.object({
|
|
206
|
+
id: v.string(),
|
|
207
|
+
type: v.picklist([
|
|
208
|
+
"operator-send",
|
|
209
|
+
"operator-transfer",
|
|
210
|
+
"operator-deposit",
|
|
211
|
+
"operator-refund",
|
|
212
|
+
"microdeposit"
|
|
213
|
+
]),
|
|
214
|
+
action: v.optional(v.string()),
|
|
215
|
+
status: v.picklist([
|
|
216
|
+
"pending",
|
|
217
|
+
"processing",
|
|
218
|
+
"completed",
|
|
219
|
+
"failed",
|
|
220
|
+
"reversed"
|
|
221
|
+
]),
|
|
222
|
+
movementStatus: v.picklist([
|
|
223
|
+
"initiated",
|
|
224
|
+
"provider_pending",
|
|
225
|
+
"processing",
|
|
226
|
+
"completed",
|
|
227
|
+
"failed",
|
|
228
|
+
"reversed",
|
|
229
|
+
"manual_review"
|
|
230
|
+
]),
|
|
231
|
+
direction: v.picklist(["incoming", "outgoing"]),
|
|
232
|
+
amount: moneyJsonSchema,
|
|
233
|
+
fee: moneyJsonSchema,
|
|
234
|
+
currency: v.string(),
|
|
235
|
+
counterpartyName: v.optional(v.string()),
|
|
236
|
+
method: v.optional(v.picklist([
|
|
237
|
+
"ach",
|
|
238
|
+
"wire",
|
|
239
|
+
"on-chain"
|
|
240
|
+
])),
|
|
241
|
+
memo: v.optional(v.string()),
|
|
242
|
+
description: v.optional(v.string()),
|
|
243
|
+
txHash: v.optional(v.string()),
|
|
244
|
+
createdAt: isoTimestampSchema,
|
|
245
|
+
completedAt: v.optional(isoTimestampSchema)
|
|
246
|
+
});
|
|
247
|
+
const accountTransactionsResponseSchema = v.object({
|
|
248
|
+
accountId: v.string(),
|
|
249
|
+
transactions: v.array(accountTransactionSchema),
|
|
250
|
+
total: v.number()
|
|
251
|
+
});
|
|
252
|
+
const accountDepositAddressResponseSchema = v.object({
|
|
253
|
+
accountId: v.string(),
|
|
254
|
+
address: v.string(),
|
|
255
|
+
liquidationAddressId: v.optional(v.string()),
|
|
256
|
+
source: v.picklist(["wallet", "liquidation"])
|
|
257
|
+
});
|
|
258
|
+
const counterpartySchema = v.object({
|
|
259
|
+
id: v.string(),
|
|
260
|
+
name: v.string(),
|
|
261
|
+
rails: v.array(v.object({
|
|
262
|
+
id: v.string(),
|
|
263
|
+
type: v.string(),
|
|
264
|
+
walletAddress: v.optional(v.string()),
|
|
265
|
+
network: v.optional(v.string()),
|
|
266
|
+
bankName: v.optional(v.string()),
|
|
267
|
+
accountNumber: v.optional(v.string())
|
|
268
|
+
}))
|
|
269
|
+
});
|
|
270
|
+
const counterpartiesResponseSchema = v.object({ counterparties: v.array(counterpartySchema) });
|
|
271
|
+
const intentStatusSchema = v.picklist([
|
|
272
|
+
"pending",
|
|
273
|
+
"processing",
|
|
274
|
+
"completed",
|
|
275
|
+
"blocked",
|
|
276
|
+
"failed"
|
|
277
|
+
]);
|
|
278
|
+
const intentNextActionSchema = v.object({
|
|
279
|
+
type: v.literal("submit_stamp"),
|
|
280
|
+
signingRequestId: v.string(),
|
|
281
|
+
signer: v.object({
|
|
282
|
+
algorithm: v.literal("p256"),
|
|
283
|
+
publicKeyHex: v.string()
|
|
284
|
+
}),
|
|
285
|
+
preparedBody: v.object({
|
|
286
|
+
body: v.string(),
|
|
287
|
+
bodyHash: v.string(),
|
|
288
|
+
expiresAt: isoTimestampSchema,
|
|
289
|
+
prepareToken: v.string()
|
|
290
|
+
})
|
|
291
|
+
});
|
|
292
|
+
const wireIntentResultSchema = v.object({
|
|
293
|
+
id: v.string(),
|
|
294
|
+
type: v.picklist([
|
|
295
|
+
"send",
|
|
296
|
+
"transfer",
|
|
297
|
+
"wallet_send",
|
|
298
|
+
"create_counterparty",
|
|
299
|
+
"x402"
|
|
300
|
+
]),
|
|
301
|
+
status: intentStatusSchema,
|
|
302
|
+
reasons: v.array(v.string()),
|
|
303
|
+
expiresAt: v.optional(v.nullable(isoTimestampSchema), null),
|
|
304
|
+
data: v.optional(v.nullable(v.record(v.string(), v.unknown())), null),
|
|
305
|
+
metadata: v.optional(v.object({ dataUrl: v.nullable(v.string()) })),
|
|
306
|
+
nextAction: v.optional(intentNextActionSchema)
|
|
307
|
+
});
|
|
308
|
+
function toIntentResult(wire) {
|
|
309
|
+
const { nextAction: _nextAction, ...intent } = wire;
|
|
310
|
+
return intent;
|
|
311
|
+
}
|
|
312
|
+
const SEND_METHODS = [
|
|
313
|
+
"ach",
|
|
314
|
+
"wire",
|
|
315
|
+
"on-chain"
|
|
316
|
+
];
|
|
317
|
+
const sendMethodSchema = v.picklist(SEND_METHODS);
|
|
318
|
+
const sendActionSchema = v.object({
|
|
319
|
+
type: v.literal("send"),
|
|
320
|
+
accountId: v.string(),
|
|
321
|
+
counterpartyRailId: v.string(),
|
|
322
|
+
amount: v.string(),
|
|
323
|
+
method: sendMethodSchema,
|
|
324
|
+
memo: v.optional(v.string()),
|
|
325
|
+
description: v.optional(v.string())
|
|
326
|
+
});
|
|
327
|
+
const transferActionSchema = v.object({
|
|
328
|
+
type: v.literal("transfer"),
|
|
329
|
+
accountId: v.string(),
|
|
330
|
+
toAccountId: v.string(),
|
|
331
|
+
amount: v.string(),
|
|
332
|
+
memo: v.optional(v.string()),
|
|
333
|
+
description: v.optional(v.string())
|
|
334
|
+
});
|
|
335
|
+
const createCounterpartyActionSchema = v.object({
|
|
336
|
+
type: v.literal("create_counterparty"),
|
|
337
|
+
name: v.string(),
|
|
338
|
+
email: v.optional(v.string()),
|
|
339
|
+
rail: v.unknown()
|
|
340
|
+
});
|
|
341
|
+
const x402PaymentRequirementsSchema = v.looseObject({
|
|
342
|
+
scheme: v.string(),
|
|
343
|
+
network: v.string(),
|
|
344
|
+
asset: v.string(),
|
|
345
|
+
amount: v.string(),
|
|
346
|
+
payTo: v.string(),
|
|
347
|
+
maxTimeoutSeconds: v.number()
|
|
348
|
+
});
|
|
349
|
+
const x402ResourceSchema = v.looseObject({
|
|
350
|
+
url: v.string(),
|
|
351
|
+
serviceName: v.fallback(v.optional(v.pipe(v.string(), v.maxLength(255))), void 0)
|
|
352
|
+
});
|
|
353
|
+
const x402ActionSchema = v.object({
|
|
354
|
+
type: v.literal("x402"),
|
|
355
|
+
accountId: v.string(),
|
|
356
|
+
paymentRequirements: x402PaymentRequirementsSchema,
|
|
357
|
+
resource: v.optional(x402ResourceSchema)
|
|
358
|
+
});
|
|
359
|
+
v.variant("type", [
|
|
360
|
+
sendActionSchema,
|
|
361
|
+
transferActionSchema,
|
|
362
|
+
createCounterpartyActionSchema,
|
|
363
|
+
x402ActionSchema
|
|
364
|
+
]);
|
|
365
|
+
function toWireAction(action) {
|
|
366
|
+
if (action.type === "send" || action.type === "transfer") return {
|
|
367
|
+
...action,
|
|
368
|
+
amount: {
|
|
369
|
+
amount: action.amount,
|
|
370
|
+
asset_id: "USD"
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
return action;
|
|
374
|
+
}
|
|
375
|
+
const submitStampResponseSchema = v.object({ intent: wireIntentResultSchema });
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/client.ts
|
|
378
|
+
var ApiError = class extends Error {
|
|
379
|
+
status;
|
|
380
|
+
code;
|
|
381
|
+
constructor(status, message, code) {
|
|
382
|
+
super(code ? `API error ${status} [${code}]: ${message}` : `API error ${status}: ${message}`);
|
|
383
|
+
this.name = "ApiError";
|
|
384
|
+
this.status = status;
|
|
385
|
+
this.code = code;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const errorBodySchema = v.object({
|
|
389
|
+
error: v.nullish(v.string()),
|
|
390
|
+
message: v.nullish(v.string()),
|
|
391
|
+
code: v.nullish(v.string())
|
|
392
|
+
});
|
|
393
|
+
async function apiErrorFromResponse(res) {
|
|
394
|
+
const parsed = v.safeParse(errorBodySchema, await res.json().catch(() => void 0));
|
|
395
|
+
const body = parsed.success ? parsed.output : {};
|
|
396
|
+
if (body.error != null && body.message != null) return new ApiError(res.status, body.message, body.code ?? body.error);
|
|
397
|
+
return new ApiError(res.status, body.error ?? body.message ?? res.statusText, body.code ?? void 0);
|
|
398
|
+
}
|
|
399
|
+
async function parseApiResponse(res, schema) {
|
|
400
|
+
if (!res.ok) throw await apiErrorFromResponse(res);
|
|
401
|
+
return v.parse(schema, await res.json());
|
|
402
|
+
}
|
|
403
|
+
var IntentSubmitError = class extends Error {
|
|
404
|
+
intentId;
|
|
405
|
+
outcome;
|
|
406
|
+
constructor(params) {
|
|
407
|
+
const detail = params.cause instanceof Error ? params.cause.message : String(params.cause);
|
|
408
|
+
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 });
|
|
409
|
+
this.name = "IntentSubmitError";
|
|
410
|
+
this.intentId = params.intentId;
|
|
411
|
+
this.outcome = params.outcome;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const DEFAULT_BASE_URL = "https://api.catena.com";
|
|
415
|
+
var CatenaClient = class {
|
|
416
|
+
#baseUrl;
|
|
417
|
+
#userAgent;
|
|
418
|
+
#defaultHeaders;
|
|
419
|
+
#keypair;
|
|
420
|
+
#signer;
|
|
421
|
+
#fetch;
|
|
422
|
+
constructor(opts) {
|
|
423
|
+
this.#baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
424
|
+
this.#userAgent = buildUserAgent(opts.appInfo);
|
|
425
|
+
this.#defaultHeaders = opts.defaultHeaders ?? {};
|
|
426
|
+
this.#keypair = p256KeypairFromPrivateKeyHex(opts.privateKeyHex);
|
|
427
|
+
this.#fetch = (opts.fetch ?? globalThis.fetch).bind(globalThis);
|
|
428
|
+
this.#signer = createAgentRequestSigner({
|
|
429
|
+
keypair: this.#keypair,
|
|
430
|
+
baseUrl: this.#baseUrl,
|
|
431
|
+
identityUrl: opts.identityUrl
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
#buildRequestHeaders(requestHeaders) {
|
|
435
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
436
|
+
for (const [name, value] of Object.entries(this.#defaultHeaders)) headers.set(name, value);
|
|
437
|
+
for (const [name, value] of Object.entries(requestHeaders ?? {})) headers.set(name, value);
|
|
438
|
+
headers.set("User-Agent", this.#userAgent);
|
|
439
|
+
headers.delete("Authorization");
|
|
440
|
+
return headers;
|
|
441
|
+
}
|
|
442
|
+
async #signedRequest(options) {
|
|
443
|
+
const url = new URL(`${this.#baseUrl}/v1/agent-api${options.path}`);
|
|
444
|
+
for (const [key, value] of Object.entries(options.query ?? {})) if (value !== void 0) url.searchParams.set(key, value);
|
|
445
|
+
const request = new Request(url, {
|
|
446
|
+
method: options.method,
|
|
447
|
+
headers: this.#buildRequestHeaders(options.headers),
|
|
448
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : null
|
|
449
|
+
});
|
|
450
|
+
return this.#signer.sign(request);
|
|
451
|
+
}
|
|
452
|
+
async #request(options, schema) {
|
|
453
|
+
return parseApiResponse(await this.#fetch(await this.#signedRequest(options)), schema);
|
|
454
|
+
}
|
|
455
|
+
async whoami() {
|
|
456
|
+
return (await this.#request({
|
|
457
|
+
method: "GET",
|
|
458
|
+
path: "/me"
|
|
459
|
+
}, whoamiResponseSchema)).agent;
|
|
460
|
+
}
|
|
461
|
+
async submitFeedback(body) {
|
|
462
|
+
await this.#request({
|
|
463
|
+
method: "POST",
|
|
464
|
+
path: "/feedback",
|
|
465
|
+
body: { body }
|
|
466
|
+
}, okSuccessResponseSchema);
|
|
467
|
+
}
|
|
468
|
+
async getPolicy() {
|
|
469
|
+
return (await this.#request({
|
|
470
|
+
method: "GET",
|
|
471
|
+
path: "/policy"
|
|
472
|
+
}, policyResponseSchema)).policy;
|
|
473
|
+
}
|
|
474
|
+
async listAccounts() {
|
|
475
|
+
return this.#request({
|
|
476
|
+
method: "GET",
|
|
477
|
+
path: "/accounts"
|
|
478
|
+
}, accountsResponseSchema);
|
|
479
|
+
}
|
|
480
|
+
async getAccountBalance(accountId) {
|
|
481
|
+
return this.#request({
|
|
482
|
+
method: "GET",
|
|
483
|
+
path: `/accounts/${encodeURIComponent(accountId)}/balance`
|
|
484
|
+
}, accountBalanceResponseSchema);
|
|
485
|
+
}
|
|
486
|
+
async listAccountTransactions(accountId, params = {}) {
|
|
487
|
+
return this.#request({
|
|
488
|
+
method: "GET",
|
|
489
|
+
path: `/accounts/${encodeURIComponent(accountId)}/transactions`,
|
|
490
|
+
query: {
|
|
491
|
+
start: params.start,
|
|
492
|
+
end: params.end,
|
|
493
|
+
limit: params.limit?.toString(),
|
|
494
|
+
offset: params.offset?.toString()
|
|
495
|
+
}
|
|
496
|
+
}, accountTransactionsResponseSchema);
|
|
497
|
+
}
|
|
498
|
+
async getAccountDepositAddress(accountId, params) {
|
|
499
|
+
return this.#request({
|
|
500
|
+
method: "GET",
|
|
501
|
+
path: `/accounts/${encodeURIComponent(accountId)}/deposit-address`,
|
|
502
|
+
query: {
|
|
503
|
+
network: params.network,
|
|
504
|
+
asset: params.asset
|
|
505
|
+
}
|
|
506
|
+
}, accountDepositAddressResponseSchema);
|
|
507
|
+
}
|
|
508
|
+
async listCounterparties(params = {}) {
|
|
509
|
+
return this.#request({
|
|
510
|
+
method: "GET",
|
|
511
|
+
path: "/counterparties",
|
|
512
|
+
query: {
|
|
513
|
+
address: params.address,
|
|
514
|
+
network: params.network
|
|
515
|
+
}
|
|
516
|
+
}, counterpartiesResponseSchema);
|
|
517
|
+
}
|
|
518
|
+
async submitIntent(params) {
|
|
519
|
+
const created = await this.#createIntentRaw(params);
|
|
520
|
+
const nextAction = created.nextAction;
|
|
521
|
+
if (!nextAction) return toIntentResult(created);
|
|
522
|
+
let submitRequest;
|
|
523
|
+
try {
|
|
524
|
+
const stamp = await this.#stampNextAction(nextAction);
|
|
525
|
+
submitRequest = await this.#signedRequest({
|
|
526
|
+
method: "POST",
|
|
527
|
+
path: `/intents/${encodeURIComponent(created.id)}/submit-stamp`,
|
|
528
|
+
body: {
|
|
529
|
+
preparedBody: nextAction.preparedBody,
|
|
530
|
+
stamp
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
} catch (cause) {
|
|
534
|
+
throw new IntentSubmitError({
|
|
535
|
+
intentId: created.id,
|
|
536
|
+
outcome: "not-submitted",
|
|
537
|
+
cause
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
return await this.#submitStamp(submitRequest);
|
|
542
|
+
} catch (cause) {
|
|
543
|
+
throw new IntentSubmitError({
|
|
544
|
+
intentId: created.id,
|
|
545
|
+
outcome: "unknown",
|
|
546
|
+
cause
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async #createIntentRaw(params) {
|
|
551
|
+
return this.#request({
|
|
552
|
+
method: "POST",
|
|
553
|
+
path: "/intents",
|
|
554
|
+
body: { action: toWireAction(params.action) },
|
|
555
|
+
...params.idempotencyKey !== void 0 && { headers: { "Idempotency-Key": params.idempotencyKey } }
|
|
556
|
+
}, wireIntentResultSchema);
|
|
557
|
+
}
|
|
558
|
+
async #stampNextAction(nextAction) {
|
|
559
|
+
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}`);
|
|
560
|
+
const stamp = await new ApiKeyStamper({
|
|
561
|
+
apiPublicKey: this.#keypair.publicKeyHex,
|
|
562
|
+
apiPrivateKey: this.#keypair.privateKeyHex
|
|
563
|
+
}).stamp(nextAction.preparedBody.body);
|
|
564
|
+
return {
|
|
565
|
+
publicKeyHex: this.#keypair.publicKeyHex,
|
|
566
|
+
stampHeaderName: stamp.stampHeaderName,
|
|
567
|
+
stampHeaderValue: stamp.stampHeaderValue
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
async #submitStamp(request) {
|
|
571
|
+
return toIntentResult((await parseApiResponse(await this.#fetch(request), submitStampResponseSchema)).intent);
|
|
572
|
+
}
|
|
573
|
+
async getIntent(id) {
|
|
574
|
+
return toIntentResult(await this.#request({
|
|
575
|
+
method: "GET",
|
|
576
|
+
path: `/intents/${encodeURIComponent(id)}`
|
|
577
|
+
}, wireIntentResultSchema));
|
|
578
|
+
}
|
|
579
|
+
async unlinkAgent() {
|
|
580
|
+
await this.#request({
|
|
581
|
+
method: "POST",
|
|
582
|
+
path: "/unlink"
|
|
583
|
+
}, okSuccessResponseSchema);
|
|
584
|
+
}
|
|
585
|
+
async reportSettlement(params) {
|
|
586
|
+
await this.#request({
|
|
587
|
+
method: "POST",
|
|
588
|
+
path: `/intents/${encodeURIComponent(params.intentId)}/settlement-report`,
|
|
589
|
+
body: { txHash: params.txHash }
|
|
590
|
+
}, okSuccessResponseSchema);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
function createCatenaClient(options) {
|
|
594
|
+
return new CatenaClient(options);
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
export { x402PaymentRequirementsSchema as a, SEND_METHODS as i, IntentSubmitError as n, x402ResourceSchema as o, createCatenaClient as r, ApiError as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { C as SendMethod, E as AppInfo, S as SEND_METHODS, T as X402Resource, _ as IntentAction, a as IntentSubmitError, b as OnchainNetworkInput, c as AccountDepositAddressParams, d as AccountTransactionsResponse, f as AccountsResponse, g as Counterparty, h as CounterpartiesResponse, i as FetchLike, l as AccountDepositAddressResponse, m as CounterpartiesParams, n as CatenaClient, o as createCatenaClient, p as Agent, r as CatenaClientOptions, s as AccountBalanceResponse, t as ApiError, u as AccountTransactionsParams, v as IntentResult, w as X402PaymentRequirements, x as Policy, y as OnchainAssetInput } from "./client-DZHdFs_y.mjs";
|
|
2
|
+
export { type AccountBalanceResponse, type AccountDepositAddressParams, type AccountDepositAddressResponse, type AccountTransactionsParams, type AccountTransactionsResponse, type AccountsResponse, type Agent, ApiError, type AppInfo, type CatenaClient, CatenaClientOptions, type CounterpartiesParams, type CounterpartiesResponse, type Counterparty, FetchLike, type IntentAction, type IntentResult, IntentSubmitError, type OnchainAssetInput, type OnchainNetworkInput, type Policy, SEND_METHODS, type SendMethod, type X402PaymentRequirements, type X402Resource, createCatenaClient };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/keypair.d.ts
|
|
2
|
+
interface P256Keypair {
|
|
3
|
+
algorithm: "p256";
|
|
4
|
+
privateKeyHex: string;
|
|
5
|
+
publicKeyHex: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Generate a fresh agent credential. Feed `privateKeyHex` to
|
|
9
|
+
* `createCatenaClient`; the public half (or its thumbprint) is what gets
|
|
10
|
+
* registered with Catena when the agent is linked. The SDK never persists
|
|
11
|
+
* keys — storage is the caller's responsibility.
|
|
12
|
+
*/
|
|
13
|
+
declare function generateP256Keypair(): P256Keypair;
|
|
14
|
+
/**
|
|
15
|
+
* Derive the full keypair from the private scalar alone, so a corrupt key
|
|
16
|
+
* throws at construction instead of surfacing as bank-side invalid_signature
|
|
17
|
+
* 401s — and a mismatched pair cannot exist at all.
|
|
18
|
+
*/
|
|
19
|
+
declare function p256KeypairFromPrivateKeyHex(privateKeyHex: string): P256Keypair;
|
|
20
|
+
declare function p256PointFromCompressedHex(publicKeyHex: string): Buffer | null;
|
|
21
|
+
/**
|
|
22
|
+
* RFC 7638 JWK thumbprint of a P-256 public key — the stable id the API
|
|
23
|
+
* knows a linked credential by. Returns null when the input is not a valid
|
|
24
|
+
* P-256 point.
|
|
25
|
+
*/
|
|
26
|
+
declare function computeP256PublicKeyThumbprint(publicKeyHex: string): string | null;
|
|
27
|
+
declare function p256PublicKeyThumbprintFromPoint(point: Buffer): string;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { P256Keypair, computeP256PublicKeyThumbprint, generateP256Keypair, p256KeypairFromPrivateKeyHex, p256PointFromCompressedHex, p256PublicKeyThumbprintFromPoint };
|
package/dist/keypair.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ECDH, createECDH, createHash } from "node:crypto";
|
|
2
|
+
//#region src/keypair.ts
|
|
3
|
+
function generateP256Keypair() {
|
|
4
|
+
const ecdh = createECDH("prime256v1");
|
|
5
|
+
ecdh.generateKeys();
|
|
6
|
+
return p256KeypairFromPrivateKeyHex(ecdh.getPrivateKey().toString("hex").padStart(64, "0"));
|
|
7
|
+
}
|
|
8
|
+
const PRIVATE_KEY_HEX = /^[0-9a-f]{64}$/i;
|
|
9
|
+
function p256KeypairFromPrivateKeyHex(privateKeyHex) {
|
|
10
|
+
if (!PRIVATE_KEY_HEX.test(privateKeyHex)) throw new Error("P-256 private key must be 64 hex characters");
|
|
11
|
+
const normalized = privateKeyHex.toLowerCase();
|
|
12
|
+
const ecdh = createECDH("prime256v1");
|
|
13
|
+
try {
|
|
14
|
+
ecdh.setPrivateKey(Buffer.from(normalized, "hex"));
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error("value is not a valid P-256 private key (scalar out of range)");
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
algorithm: "p256",
|
|
20
|
+
privateKeyHex: normalized,
|
|
21
|
+
publicKeyHex: ecdh.getPublicKey("hex", "compressed")
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function p256PointFromCompressedHex(publicKeyHex) {
|
|
25
|
+
try {
|
|
26
|
+
const uncompressed = ECDH.convertKey(publicKeyHex, "prime256v1", "hex", "hex", "uncompressed");
|
|
27
|
+
const point = Buffer.isBuffer(uncompressed) ? uncompressed : Buffer.from(uncompressed, "hex");
|
|
28
|
+
return point.length === 65 ? point : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function computeP256PublicKeyThumbprint(publicKeyHex) {
|
|
34
|
+
const point = p256PointFromCompressedHex(publicKeyHex);
|
|
35
|
+
return point === null ? null : p256PublicKeyThumbprintFromPoint(point);
|
|
36
|
+
}
|
|
37
|
+
function p256PublicKeyThumbprintFromPoint(point) {
|
|
38
|
+
const x = point.subarray(1, 33).toString("base64url");
|
|
39
|
+
const y = point.subarray(33, 65).toString("base64url");
|
|
40
|
+
return createHash("sha256").update(`{"crv":"P-256","kty":"EC","x":"${x}","y":"${y}"}`, "utf8").digest("base64url");
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { computeP256PublicKeyThumbprint, generateP256Keypair, p256KeypairFromPrivateKeyHex, p256PointFromCompressedHex, p256PublicKeyThumbprintFromPoint };
|