@aura-payments/sdk 2.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/index.mjs ADDED
@@ -0,0 +1,985 @@
1
+ import { createHmac } from 'crypto';
2
+
3
+ // src/errors.ts
4
+ var AuraError = class _AuraError extends Error {
5
+ constructor(message, code, statusCode, details) {
6
+ super(message);
7
+ this.code = code;
8
+ this.statusCode = statusCode;
9
+ this.details = details;
10
+ this.name = "AuraError";
11
+ Object.setPrototypeOf(this, _AuraError.prototype);
12
+ }
13
+ };
14
+ var AuraAPIError = class _AuraAPIError extends AuraError {
15
+ constructor(message, statusCode, code, details) {
16
+ super(message, code, statusCode, details);
17
+ this.name = "AuraAPIError";
18
+ Object.setPrototypeOf(this, _AuraAPIError.prototype);
19
+ }
20
+ };
21
+ var AuraNetworkError = class _AuraNetworkError extends AuraError {
22
+ constructor(message, cause) {
23
+ super(message, "NETWORK_ERROR");
24
+ this.cause = cause;
25
+ this.name = "AuraNetworkError";
26
+ Object.setPrototypeOf(this, _AuraNetworkError.prototype);
27
+ }
28
+ };
29
+ var AuraTimeoutError = class _AuraTimeoutError extends AuraError {
30
+ constructor(message = "Request timeout") {
31
+ super(message, "TIMEOUT");
32
+ this.name = "AuraTimeoutError";
33
+ Object.setPrototypeOf(this, _AuraTimeoutError.prototype);
34
+ }
35
+ };
36
+ var AuraValidationError = class _AuraValidationError extends AuraError {
37
+ constructor(message, details) {
38
+ super(message, "VALIDATION_ERROR", 400, details);
39
+ this.name = "AuraValidationError";
40
+ Object.setPrototypeOf(this, _AuraValidationError.prototype);
41
+ }
42
+ };
43
+ var AuraAuthenticationError = class _AuraAuthenticationError extends AuraError {
44
+ constructor(message = "Authentication failed") {
45
+ super(message, "AUTHENTICATION_ERROR", 401);
46
+ this.name = "AuraAuthenticationError";
47
+ Object.setPrototypeOf(this, _AuraAuthenticationError.prototype);
48
+ }
49
+ };
50
+ var AuraNotFoundError = class _AuraNotFoundError extends AuraError {
51
+ constructor(resource) {
52
+ super(`${resource} not found`, "NOT_FOUND", 404);
53
+ this.name = "AuraNotFoundError";
54
+ Object.setPrototypeOf(this, _AuraNotFoundError.prototype);
55
+ }
56
+ };
57
+ var AuraRateLimitError = class _AuraRateLimitError extends AuraError {
58
+ constructor(message = "Rate limit exceeded", retryAfter) {
59
+ super(message, "RATE_LIMIT_EXCEEDED", 429);
60
+ this.retryAfter = retryAfter;
61
+ this.name = "AuraRateLimitError";
62
+ Object.setPrototypeOf(this, _AuraRateLimitError.prototype);
63
+ }
64
+ };
65
+ var AuraFaucetUnavailableError = class _AuraFaucetUnavailableError extends AuraError {
66
+ constructor(message = "Testnet faucet is not available on this deployment") {
67
+ super(message, "FAUCET_UNAVAILABLE", 501);
68
+ this.name = "AuraFaucetUnavailableError";
69
+ Object.setPrototypeOf(this, _AuraFaucetUnavailableError.prototype);
70
+ }
71
+ };
72
+ function isAuraError(error) {
73
+ return error instanceof AuraError;
74
+ }
75
+ function isAuraFaucetUnavailableError(error) {
76
+ return error instanceof AuraFaucetUnavailableError;
77
+ }
78
+ function isAuraAPIError(error) {
79
+ return error instanceof AuraAPIError;
80
+ }
81
+ function isAuraNetworkError(error) {
82
+ return error instanceof AuraNetworkError;
83
+ }
84
+ function isAuraTimeoutError(error) {
85
+ return error instanceof AuraTimeoutError;
86
+ }
87
+ function isRetryableError(error) {
88
+ if (!(error instanceof AuraError)) return false;
89
+ if (error instanceof AuraNetworkError) return true;
90
+ if (error instanceof AuraTimeoutError) return true;
91
+ if (error instanceof AuraRateLimitError) return true;
92
+ if (error.statusCode && error.statusCode >= 500) {
93
+ return true;
94
+ }
95
+ return false;
96
+ }
97
+
98
+ // src/resources/agents.ts
99
+ var Agents = class {
100
+ constructor(client) {
101
+ this.client = client;
102
+ }
103
+ /**
104
+ * Create a new AI agent with a dedicated wallet and (optional) initial
105
+ * policy. The platform provisions a Circle wallet on the requested chain
106
+ * during this call; expect a few seconds of latency for the on-chain part.
107
+ */
108
+ async create(params, idempotencyKey) {
109
+ return this.client["request"](
110
+ "POST",
111
+ "/v1/agents",
112
+ params,
113
+ { idempotencyKey }
114
+ );
115
+ }
116
+ /**
117
+ * List agents for the authenticated account, paginated.
118
+ */
119
+ async list(params) {
120
+ const query = new URLSearchParams();
121
+ if (params?.status) query.set("status", params.status);
122
+ if (params?.type) query.set("type", params.type);
123
+ if (params?.limit !== void 0) query.set("limit", String(params.limit));
124
+ if (params?.offset !== void 0) query.set("offset", String(params.offset));
125
+ if (params?.orderBy) query.set("orderBy", params.orderBy);
126
+ if (params?.orderDirection) query.set("orderDirection", params.orderDirection);
127
+ const qs = query.toString();
128
+ return this.client["request"](
129
+ "GET",
130
+ qs ? `/v1/agents?${qs}` : "/v1/agents"
131
+ );
132
+ }
133
+ /**
134
+ * Get a single agent by id.
135
+ */
136
+ async get(agentId) {
137
+ return this.client["request"]("GET", `/v1/agents/${agentId}`);
138
+ }
139
+ /**
140
+ * Patch an agent's metadata fields. Cannot be used to flip status — use
141
+ * `freeze`/`unfreeze` for that so the mandate-cascade and activity events
142
+ * fire correctly.
143
+ */
144
+ async update(agentId, params, idempotencyKey) {
145
+ return this.client["request"](
146
+ "PATCH",
147
+ `/v1/agents/${agentId}`,
148
+ params,
149
+ { idempotencyKey }
150
+ );
151
+ }
152
+ /**
153
+ * Freeze (kill switch) — sets status to `paused` and cascade-rejects every
154
+ * pending mandate the agent has in flight. Idempotent.
155
+ */
156
+ async freeze(params, idempotencyKey) {
157
+ const { agentId, ...body } = params;
158
+ return this.client["request"](
159
+ "POST",
160
+ `/v1/agents/${agentId}/freeze`,
161
+ body,
162
+ { idempotencyKey }
163
+ );
164
+ }
165
+ /**
166
+ * Unfreeze — sets status back to `active`. Previously cascade-rejected
167
+ * mandates are NOT restored; the agent re-creates them on its next loop.
168
+ */
169
+ async unfreeze(params, idempotencyKey) {
170
+ const { agentId } = params;
171
+ return this.client["request"](
172
+ "POST",
173
+ `/v1/agents/${agentId}/unfreeze`,
174
+ void 0,
175
+ { idempotencyKey }
176
+ );
177
+ }
178
+ /**
179
+ * Get the agent's wallet balance (USDC, on-chain at request time).
180
+ */
181
+ async getBalance(agentId) {
182
+ return this.client["request"](
183
+ "GET",
184
+ `/v1/agents/${agentId}/balance`
185
+ );
186
+ }
187
+ /**
188
+ * Run a hypothetical transaction through the policy engine without
189
+ * executing it. Returns the engine's decision (`auto_approve`, `auto_deny`,
190
+ * or `requires_human_approval`) along with the matched rules. When the
191
+ * decision is `requires_human_approval`, the engine creates a mandate row
192
+ * and returns its id so the caller can watch for the operator's decision.
193
+ */
194
+ async evaluate(params, idempotencyKey) {
195
+ const { agentId, ...body } = params;
196
+ return this.client["request"](
197
+ "POST",
198
+ `/v1/agents/${agentId}/evaluate`,
199
+ body,
200
+ { idempotencyKey }
201
+ );
202
+ }
203
+ };
204
+
205
+ // src/utils.ts
206
+ function generateIdempotencyKey() {
207
+ const timestamp = Date.now();
208
+ const random = Math.random().toString(36).substring(2, 15);
209
+ return `${timestamp}-${random}`;
210
+ }
211
+ function sleep(ms) {
212
+ return new Promise((resolve) => setTimeout(resolve, ms));
213
+ }
214
+ function calculateBackoff(attempt, baseDelay = 1e3, maxDelay = 3e4) {
215
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
216
+ const jitter = exponentialDelay * Math.random() * 0.25;
217
+ return Math.min(exponentialDelay + jitter, maxDelay);
218
+ }
219
+ async function retryWithBackoff(fn, maxRetries = 3, shouldRetry = isRetryableError) {
220
+ let lastError;
221
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
222
+ try {
223
+ return await fn();
224
+ } catch (error) {
225
+ lastError = error;
226
+ if (attempt === maxRetries) {
227
+ throw error;
228
+ }
229
+ if (!shouldRetry(error)) {
230
+ throw error;
231
+ }
232
+ const delay = calculateBackoff(attempt);
233
+ await sleep(delay);
234
+ }
235
+ }
236
+ throw lastError;
237
+ }
238
+ async function withTimeout(promise, timeoutMs, timeoutMessage) {
239
+ const timeoutPromise = new Promise((_, reject) => {
240
+ setTimeout(() => {
241
+ reject(new AuraTimeoutError(timeoutMessage || `Request timeout after ${timeoutMs}ms`));
242
+ }, timeoutMs);
243
+ });
244
+ return Promise.race([promise, timeoutPromise]);
245
+ }
246
+
247
+ // src/resources/escrows.ts
248
+ function isEscrowDeployed(e) {
249
+ if (e.blockchain?.deployed) return true;
250
+ return ["deployed", "funded", "locked", "released"].includes(e.state ?? "");
251
+ }
252
+ function isEscrowFunded(e) {
253
+ return ["funded", "locked", "released"].includes(e.state ?? "");
254
+ }
255
+ function parseUsdc(value) {
256
+ const n = Number.parseFloat(value);
257
+ return Number.isFinite(n) ? n : 0;
258
+ }
259
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
260
+ var Escrows = class {
261
+ constructor(client) {
262
+ this.client = client;
263
+ }
264
+ /**
265
+ * Create a new escrow
266
+ */
267
+ async create(params, idempotencyKey) {
268
+ return this.client["request"](
269
+ "POST",
270
+ "/v1/escrow/create",
271
+ params,
272
+ { idempotencyKey }
273
+ );
274
+ }
275
+ /**
276
+ * Get escrow by ID
277
+ */
278
+ async get(escrowId) {
279
+ return this.client["request"]("GET", `/v1/escrow/${escrowId}`);
280
+ }
281
+ /**
282
+ * List escrows with optional filters
283
+ */
284
+ async list(params) {
285
+ const queryParams = new URLSearchParams();
286
+ if (params?.search) queryParams.set("search", params.search);
287
+ if (params?.state) queryParams.set("state", params.state);
288
+ if (params?.sortBy) queryParams.set("sortBy", params.sortBy);
289
+ if (params?.limit) queryParams.set("limit", params.limit.toString());
290
+ if (params?.offset) queryParams.set("offset", params.offset.toString());
291
+ const query = queryParams.toString();
292
+ const path = query ? `/v1/escrow?${query}` : "/v1/escrow";
293
+ return this.client["request"]("GET", path);
294
+ }
295
+ /**
296
+ * Fund an escrow
297
+ */
298
+ async fund(params, idempotencyKey) {
299
+ const { escrowId, ...body } = params;
300
+ return this.client["request"](
301
+ "POST",
302
+ `/v1/escrow/${escrowId}/fund`,
303
+ body,
304
+ { idempotencyKey }
305
+ );
306
+ }
307
+ /**
308
+ * Release an escrow
309
+ */
310
+ async release(params, idempotencyKey) {
311
+ const { escrowId, ...body } = params;
312
+ return this.client["request"](
313
+ "POST",
314
+ `/v1/escrow/${escrowId}/release`,
315
+ body,
316
+ { idempotencyKey }
317
+ );
318
+ }
319
+ /**
320
+ * Refund an escrow
321
+ */
322
+ async refund(params, idempotencyKey) {
323
+ return this.client["request"](
324
+ "POST",
325
+ `/v1/escrow/refund`,
326
+ params,
327
+ { idempotencyKey }
328
+ );
329
+ }
330
+ /**
331
+ * Receive USDC and split it across recipients in a single call (board D4).
332
+ *
333
+ * Composes the existing, crash-safe escrow path:
334
+ * create → poll-until-deployed → pre-flight balance → fund → poll-until-funded → release.
335
+ *
336
+ * Funds move from an Aura wallet owned by `payerOwnerId` (the agent itself or a
337
+ * counterparty). The result's `stage` makes a partial run explicit:
338
+ * - `created` — deployment didn't confirm within `waitForDeploymentMs`
339
+ * - `deployed` — payer wallet lacks funds (see `fundingRequired`); no money moved
340
+ * - `funded` — funded but not released (`autoRelease:false`, or funding didn't
341
+ * settle within `waitForFundingMs` → `fundingPending: true`)
342
+ * - `released` — split to recipients (happy path)
343
+ *
344
+ * Resume note: a partial run is resumed by acting on the returned
345
+ * `escrow.escrowId` (call `fund`/`release` directly) — NOT by re-calling
346
+ * `receiveAndSplit`, which re-`create`s and would hit a duplicate-orderId error.
347
+ * Pass a stable `idempotencyKey` for retry-safe fund/release.
348
+ *
349
+ * Funding is asynchronous and the platform's fund/release endpoints return
350
+ * operation metadata (not a full escrow), so this method re-`get`s the escrow
351
+ * for authoritative state + splits rather than trusting those response bodies.
352
+ *
353
+ * @throws AuraValidationError if split percentages don't sum to 100 (pre-flight).
354
+ */
355
+ async receiveAndSplit(params) {
356
+ const {
357
+ orderId,
358
+ payerOwnerId,
359
+ amount,
360
+ splits,
361
+ chain,
362
+ actorId,
363
+ autoRelease = true,
364
+ waitForDeploymentMs = 6e4,
365
+ waitForFundingMs = 6e4,
366
+ pollIntervalMs = 2e3,
367
+ idempotencyKey
368
+ } = params;
369
+ const pctSum = splits.reduce((acc, s) => acc + s.percentage, 0);
370
+ if (Math.round(pctSum * 100) !== 1e4) {
371
+ throw new AuraValidationError(
372
+ `Split percentages must sum to 100 (got ${pctSum}).`,
373
+ { splits }
374
+ );
375
+ }
376
+ const runKey = idempotencyKey ?? generateIdempotencyKey();
377
+ const pollMs = Math.max(pollIntervalMs, 250);
378
+ const created = await this.create(
379
+ { orderId, buyerOwnerId: payerOwnerId, amount, chain, splits },
380
+ `${runKey}-create`
381
+ );
382
+ const escrowId = created.escrowId;
383
+ const deployed = await this.pollEscrowUntil(
384
+ escrowId,
385
+ isEscrowDeployed,
386
+ waitForDeploymentMs,
387
+ pollMs,
388
+ created
389
+ );
390
+ if (!isEscrowDeployed(deployed)) {
391
+ return {
392
+ stage: "created",
393
+ escrow: deployed,
394
+ splits: deployed.splits,
395
+ deploymentPending: true
396
+ };
397
+ }
398
+ const payerWalletId = deployed.buyerWalletId;
399
+ const balance = await this.client.wallets.getBalance(payerWalletId);
400
+ if (parseUsdc(balance.balance.usdc) < parseUsdc(amount)) {
401
+ return {
402
+ stage: "deployed",
403
+ escrow: deployed,
404
+ splits: deployed.splits,
405
+ fundingRequired: {
406
+ payerWalletId,
407
+ payerAddress: deployed.buyerWallet.address,
408
+ amount,
409
+ available: balance.balance.usdc
410
+ }
411
+ };
412
+ }
413
+ await this.fund(
414
+ { escrowId, buyerWalletId: payerWalletId, amount },
415
+ `${runKey}-fund`
416
+ );
417
+ const funded = await this.pollEscrowUntil(
418
+ escrowId,
419
+ isEscrowFunded,
420
+ waitForFundingMs,
421
+ pollMs,
422
+ deployed
423
+ );
424
+ if (!isEscrowFunded(funded)) {
425
+ return { stage: "funded", escrow: funded, splits: funded.splits, fundingPending: true };
426
+ }
427
+ if (!autoRelease) {
428
+ return { stage: "funded", escrow: funded, splits: funded.splits };
429
+ }
430
+ await this.release(
431
+ { escrowId, actorId: actorId ?? payerOwnerId, reason: "receiveAndSplit auto-release" },
432
+ `${runKey}-release`
433
+ );
434
+ const final = await this.get(escrowId);
435
+ return { stage: "released", escrow: final, splits: final.splits };
436
+ }
437
+ /**
438
+ * Poll `get(escrowId)` until `predicate` holds or `budgetMs` elapses. Returns
439
+ * the last-observed escrow; the caller re-checks the predicate to branch.
440
+ * `seed` is the escrow already in hand, checked before any poll so a
441
+ * synchronously-settled escrow returns without a network round-trip.
442
+ */
443
+ async pollEscrowUntil(escrowId, predicate, budgetMs, pollMs, seed) {
444
+ let escrow = seed;
445
+ if (predicate(escrow)) return escrow;
446
+ const deadline = Date.now() + budgetMs;
447
+ while (Date.now() < deadline) {
448
+ await sleep2(pollMs);
449
+ escrow = await this.get(escrowId);
450
+ if (predicate(escrow)) break;
451
+ }
452
+ return escrow;
453
+ }
454
+ /**
455
+ * Create a dispute for an escrow
456
+ */
457
+ async createDispute(params, idempotencyKey) {
458
+ return this.client["request"](
459
+ "POST",
460
+ `/v1/escrow/${params.escrowId}/dispute`,
461
+ params,
462
+ { idempotencyKey }
463
+ );
464
+ }
465
+ /**
466
+ * Get dispute details
467
+ */
468
+ async getDispute(escrowId, disputeId) {
469
+ return this.client["request"](
470
+ "GET",
471
+ `/v1/escrow/${escrowId}/dispute/${disputeId}`
472
+ );
473
+ }
474
+ };
475
+ var Mandates = class {
476
+ constructor(client) {
477
+ this.client = client;
478
+ }
479
+ /**
480
+ * List mandates for the authenticated account. Defaults to `status=pending`
481
+ * so this acts as the operator's inbox out of the box.
482
+ */
483
+ async list(params) {
484
+ const query = new URLSearchParams();
485
+ if (params?.status) query.set("status", params.status);
486
+ if (params?.agentId) query.set("agentId", params.agentId);
487
+ if (params?.limit !== void 0) query.set("limit", String(params.limit));
488
+ if (params?.cursor) query.set("cursor", params.cursor);
489
+ const qs = query.toString();
490
+ return this.client["request"](
491
+ "GET",
492
+ qs ? `/v1/agent/mandates?${qs}` : "/v1/agent/mandates"
493
+ );
494
+ }
495
+ /**
496
+ * Convenience wrapper for the most common case: pending mandates in
497
+ * urgency order (most-expiring first).
498
+ */
499
+ async listPending(extras) {
500
+ return this.list({ ...extras, status: "pending" });
501
+ }
502
+ /**
503
+ * Get a single mandate by id.
504
+ */
505
+ async get(mandateId) {
506
+ return this.client["request"](
507
+ "GET",
508
+ `/v1/agent/mandates/${mandateId}`
509
+ );
510
+ }
511
+ /**
512
+ * Approve a pending mandate. Caller is responsible for computing
513
+ * `decisionSignature` — use `MandateSignature.compute()` below.
514
+ *
515
+ * Idempotent: re-approving an already-`approved` mandate returns 200 with
516
+ * the existing decision. Approving a `rejected`/`cancelled` mandate
517
+ * returns 409.
518
+ */
519
+ async approve(params, idempotencyKey) {
520
+ const { mandateId, ...body } = params;
521
+ return this.client["request"](
522
+ "POST",
523
+ `/v1/agent/mandates/${mandateId}/approve`,
524
+ body,
525
+ { idempotencyKey }
526
+ );
527
+ }
528
+ /**
529
+ * Reject a pending mandate. Same signature scheme as `approve`.
530
+ */
531
+ async reject(params, idempotencyKey) {
532
+ const { mandateId, ...body } = params;
533
+ return this.client["request"](
534
+ "POST",
535
+ `/v1/agent/mandates/${mandateId}/reject`,
536
+ body,
537
+ { idempotencyKey }
538
+ );
539
+ }
540
+ };
541
+ var MandateSignature = {
542
+ compute(input) {
543
+ const payload = `${input.mandateId}:${input.decision}:${input.decidedAt}`;
544
+ return createHmac("sha256", input.secret).update(payload).digest("hex");
545
+ },
546
+ verify(input) {
547
+ const expected = MandateSignature.compute({
548
+ mandateId: input.mandateId,
549
+ decision: input.decision,
550
+ decidedAt: input.decidedAt,
551
+ secret: input.secret
552
+ });
553
+ if (expected.length !== input.signature.length) return false;
554
+ let diff = 0;
555
+ for (let i = 0; i < expected.length; i++) {
556
+ diff |= expected.charCodeAt(i) ^ input.signature.charCodeAt(i);
557
+ }
558
+ return diff === 0;
559
+ }
560
+ };
561
+
562
+ // src/resources/policies.ts
563
+ var Policies = class {
564
+ constructor(client) {
565
+ this.client = client;
566
+ }
567
+ /**
568
+ * List all policies attached to an agent.
569
+ */
570
+ async list(agentId) {
571
+ return this.client["request"](
572
+ "GET",
573
+ `/v1/agents/${agentId}/policies`
574
+ );
575
+ }
576
+ /**
577
+ * Get a single policy by id (scoped to the parent agent).
578
+ */
579
+ async get(agentId, policyId) {
580
+ return this.client["request"](
581
+ "GET",
582
+ `/v1/agents/${agentId}/policies/${policyId}`
583
+ );
584
+ }
585
+ /**
586
+ * Create a new policy for an agent. The agent's policy engine will start
587
+ * matching against this policy on the next evaluation.
588
+ */
589
+ async create(params, idempotencyKey) {
590
+ const { agentId, ...body } = params;
591
+ return this.client["request"](
592
+ "POST",
593
+ `/v1/agents/${agentId}/policies`,
594
+ body,
595
+ { idempotencyKey }
596
+ );
597
+ }
598
+ };
599
+
600
+ // src/resources/wallets.ts
601
+ var Wallets = class {
602
+ constructor(client) {
603
+ this.client = client;
604
+ }
605
+ /**
606
+ * Create a new wallet
607
+ */
608
+ async create(params, idempotencyKey) {
609
+ return this.client["request"](
610
+ "POST",
611
+ "/v1/wallets",
612
+ params,
613
+ { idempotencyKey }
614
+ );
615
+ }
616
+ /**
617
+ * Get wallet by ID
618
+ */
619
+ async get(walletId) {
620
+ return this.client["request"]("GET", `/v1/wallets/${walletId}`);
621
+ }
622
+ /**
623
+ * List wallets with optional filters
624
+ */
625
+ async list(params) {
626
+ const queryParams = new URLSearchParams();
627
+ if (params?.entityId) queryParams.set("entityId", params.entityId);
628
+ if (params?.chain) queryParams.set("chain", params.chain);
629
+ if (params?.type) queryParams.set("type", params.type);
630
+ if (params?.page) queryParams.set("page", params.page.toString());
631
+ if (params?.limit) queryParams.set("limit", params.limit.toString());
632
+ const query = queryParams.toString();
633
+ const path = query ? `/v1/wallets?${query}` : "/v1/wallets";
634
+ return this.client["request"]("GET", path);
635
+ }
636
+ /**
637
+ * Get wallet balance
638
+ */
639
+ async getBalance(walletId) {
640
+ return this.client["request"](
641
+ "GET",
642
+ `/v1/wallets/${walletId}/balance`
643
+ );
644
+ }
645
+ /**
646
+ * Transfer funds from wallet
647
+ */
648
+ async transfer(params) {
649
+ const idempotencyKey = params.idempotencyKey || this.generateIdempotencyKey();
650
+ return this.client["request"](
651
+ "POST",
652
+ `/v1/wallets/${params.fromWalletId}/transfer`,
653
+ params,
654
+ { idempotencyKey }
655
+ );
656
+ }
657
+ /**
658
+ * Request testnet funds (USDC) for a wallet from the platform faucet.
659
+ *
660
+ * Testnet only. Backed by `POST /v1/wallets/:id/faucet`, provided by the
661
+ * platform (AURA-010). Until that endpoint ships, this capability-probes and
662
+ * raises {@link AuraFaucetUnavailableError} (rather than a generic 404/501)
663
+ * so callers can degrade gracefully to manual funding.
664
+ */
665
+ async requestTestnetFunds(walletId, params) {
666
+ try {
667
+ return await this.client["request"](
668
+ "POST",
669
+ `/v1/wallets/${walletId}/faucet`,
670
+ params ?? {}
671
+ );
672
+ } catch (err) {
673
+ if (err instanceof AuraAPIError && err.statusCode === 501) {
674
+ throw new AuraFaucetUnavailableError(
675
+ "Testnet faucet is not enabled on this deployment yet (POST /v1/wallets/:id/faucet returned 501). Fund the wallet manually, or ask the operator to enable the faucet endpoint."
676
+ );
677
+ }
678
+ throw err;
679
+ }
680
+ }
681
+ /**
682
+ * Get transfer by ID
683
+ */
684
+ async getTransfer(walletId, transferId) {
685
+ return this.client["request"](
686
+ "GET",
687
+ `/v1/wallets/${walletId}/transfers/${transferId}`
688
+ );
689
+ }
690
+ /**
691
+ * Generate a unique idempotency key
692
+ */
693
+ generateIdempotencyKey() {
694
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
695
+ }
696
+ };
697
+ var Webhooks = class {
698
+ constructor(client) {
699
+ this.client = client;
700
+ }
701
+ /**
702
+ * Configure webhook endpoint and events
703
+ */
704
+ async configure(params) {
705
+ return this.client["request"](
706
+ "POST",
707
+ "/v1/webhooks/config",
708
+ params
709
+ );
710
+ }
711
+ /**
712
+ * Get current webhook configuration
713
+ */
714
+ async getConfig() {
715
+ return this.client["request"]("GET", "/v1/webhooks/config");
716
+ }
717
+ /**
718
+ * Delete webhook configuration
719
+ */
720
+ async delete() {
721
+ return this.client["request"](
722
+ "DELETE",
723
+ "/v1/webhooks/config"
724
+ );
725
+ }
726
+ /**
727
+ * Validate webhook signature
728
+ * Use this in your webhook handler to verify authenticity
729
+ */
730
+ static validateSignature(options) {
731
+ try {
732
+ const { payload, signature, secret, toleranceMs = 3e5 } = options;
733
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
734
+ const { timestamp, v1 } = this.parseSignatureHeader(signature);
735
+ const ageMs = Math.abs(Date.now() - timestamp);
736
+ if (ageMs > toleranceMs) {
737
+ return { valid: false, error: "Signature timestamp expired" };
738
+ }
739
+ const expectedSignature = this.computeHmacSignature(`${timestamp}.${payloadString}`, secret);
740
+ const valid = this.secureCompare(v1, expectedSignature);
741
+ if (!valid) {
742
+ return {
743
+ valid: false,
744
+ error: "Invalid signature"
745
+ };
746
+ }
747
+ const event = typeof payload === "string" ? JSON.parse(payload) : payload;
748
+ return {
749
+ valid: true,
750
+ event
751
+ };
752
+ } catch (error) {
753
+ return {
754
+ valid: false,
755
+ error: error instanceof Error ? error.message : "Unknown error"
756
+ };
757
+ }
758
+ }
759
+ /**
760
+ * Parse signature header: "t=timestamp,v1=signature"
761
+ */
762
+ static parseSignatureHeader(signatureHeader) {
763
+ const parts = signatureHeader.split(",").map((p) => p.trim());
764
+ const tPart = parts.find((p) => p.startsWith("t="));
765
+ const v1Part = parts.find((p) => p.startsWith("v1="));
766
+ if (!tPart || !v1Part) {
767
+ throw new Error("Invalid signature header format (expected: t=...,v1=...)");
768
+ }
769
+ const timestamp = parseInt(tPart.slice(2), 10);
770
+ const v1 = v1Part.slice(3);
771
+ if (!Number.isFinite(timestamp) || timestamp <= 0) {
772
+ throw new Error("Invalid signature timestamp");
773
+ }
774
+ if (!v1) {
775
+ throw new Error("Invalid signature value");
776
+ }
777
+ return { timestamp, v1 };
778
+ }
779
+ /**
780
+ * Compute HMAC-SHA256 signature
781
+ */
782
+ static computeHmacSignature(payload, secret) {
783
+ if (typeof crypto !== "undefined" && crypto.subtle) {
784
+ throw new Error("Web Crypto implementation needed for browser");
785
+ }
786
+ return createHmac("sha256", secret).update(payload).digest("hex");
787
+ }
788
+ /**
789
+ * Constant-time string comparison
790
+ */
791
+ static secureCompare(a, b) {
792
+ if (a.length !== b.length) {
793
+ return false;
794
+ }
795
+ let result = 0;
796
+ for (let i = 0; i < a.length; i++) {
797
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
798
+ }
799
+ return result === 0;
800
+ }
801
+ };
802
+
803
+ // src/client.ts
804
+ var AuraClient = class _AuraClient {
805
+ constructor(config) {
806
+ this.apiKey = config.apiKey;
807
+ this.baseUrl = _AuraClient.normalizeBaseUrl(config.baseUrl);
808
+ this.timeout = config.timeout || 3e4;
809
+ this.maxRetries = config.maxRetries || 3;
810
+ this.autoIdempotency = config.autoIdempotency !== false;
811
+ if (!this.apiKey) {
812
+ throw new AuraError("API key is required", "MISSING_API_KEY");
813
+ }
814
+ this.escrows = new Escrows(this);
815
+ this.wallets = new Wallets(this);
816
+ this.webhooks = new Webhooks(this);
817
+ this.agents = new Agents(this);
818
+ this.policies = new Policies(this);
819
+ this.mandates = new Mandates(this);
820
+ }
821
+ /**
822
+ * Normalize the base URL so resource paths like `/v1/escrow` reach the
823
+ * platform's `/api/v1/escrow` route regardless of how the caller framed
824
+ * `baseUrl`. Trailing slashes are trimmed; missing `/api` is appended.
825
+ *
826
+ * @internal exported pattern for tests; do not call directly.
827
+ */
828
+ static normalizeBaseUrl(input) {
829
+ const raw = (input || "https://getaura.sh/api").replace(/\/+$/, "");
830
+ if (/\/api(\/v\d+)?$/.test(raw) || /\/api$/.test(raw)) {
831
+ return raw;
832
+ }
833
+ return `${raw}/api`;
834
+ }
835
+ /**
836
+ * Get the API key
837
+ */
838
+ getApiKey() {
839
+ return this.apiKey;
840
+ }
841
+ /**
842
+ * Get the base URL
843
+ */
844
+ getBaseUrl() {
845
+ return this.baseUrl;
846
+ }
847
+ /**
848
+ * Get the timeout value
849
+ */
850
+ getTimeout() {
851
+ return this.timeout;
852
+ }
853
+ /**
854
+ * Get the max retries value
855
+ */
856
+ getMaxRetries() {
857
+ return this.maxRetries;
858
+ }
859
+ /**
860
+ * Make an authenticated request to the API with automatic retry logic
861
+ */
862
+ async request(method, path, body, options) {
863
+ const requestFn = () => this.executeRequest(method, path, body, options);
864
+ if (options?.skipRetry || method === "GET") {
865
+ return requestFn();
866
+ }
867
+ return retryWithBackoff(requestFn, this.maxRetries);
868
+ }
869
+ /**
870
+ * Execute a single HTTP request (internal)
871
+ */
872
+ async executeRequest(method, path, body, options) {
873
+ const url = `${this.baseUrl}${path}`;
874
+ const headers = {
875
+ "Content-Type": "application/json",
876
+ "Authorization": `Bearer ${this.apiKey}`,
877
+ "User-Agent": "@aura-payments/sdk/2.1.0"
878
+ };
879
+ if (options?.idempotencyKey) {
880
+ headers["Idempotency-Key"] = options.idempotencyKey;
881
+ } else if (this.autoIdempotency && (method === "POST" || method === "PUT")) {
882
+ headers["Idempotency-Key"] = generateIdempotencyKey();
883
+ }
884
+ try {
885
+ const response = await withTimeout(
886
+ fetch(url, {
887
+ method,
888
+ headers,
889
+ body: body ? JSON.stringify(body) : void 0
890
+ }),
891
+ this.timeout
892
+ );
893
+ if (!response.ok) {
894
+ await this.handleErrorResponse(response);
895
+ }
896
+ if (response.status === 204) {
897
+ return void 0;
898
+ }
899
+ const data = await response.json();
900
+ return this.normalizeResponse(data);
901
+ } catch (error) {
902
+ if (error instanceof TypeError && error.message.includes("fetch")) {
903
+ throw new AuraNetworkError("Network request failed", error);
904
+ }
905
+ if (error instanceof AuraError) {
906
+ throw error;
907
+ }
908
+ throw new AuraError(
909
+ error instanceof Error ? error.message : "Unknown error occurred",
910
+ "UNKNOWN_ERROR"
911
+ );
912
+ }
913
+ }
914
+ /**
915
+ * Normalize API responses across different wrapper formats.
916
+ *
917
+ * The platform API currently uses two common success wrappers:
918
+ * - { success: true, data: T, ... }
919
+ * - { data: T, meta: {...} } (and sometimes { data: T[], meta, pagination })
920
+ *
921
+ * SDK methods generally return the inner `data` payload, except for paginated
922
+ * endpoints where pagination metadata is meaningful and preserved.
923
+ */
924
+ normalizeResponse(payload) {
925
+ if (!payload || typeof payload !== "object") return payload;
926
+ const obj = payload;
927
+ if ("success" in obj && "data" in obj) {
928
+ return obj.data;
929
+ }
930
+ if ("data" in obj && ("meta" in obj || "metadata" in obj)) {
931
+ if ("pagination" in obj) {
932
+ return obj;
933
+ }
934
+ return obj.data;
935
+ }
936
+ if ("data" in obj && Object.keys(obj).length <= 2) {
937
+ return obj.data;
938
+ }
939
+ return payload;
940
+ }
941
+ /**
942
+ * Handle HTTP error responses and throw appropriate errors
943
+ */
944
+ async handleErrorResponse(response) {
945
+ const statusCode = response.status;
946
+ let errorData = {};
947
+ try {
948
+ const data = await response.json();
949
+ if (typeof data === "object" && data !== null) {
950
+ errorData = data;
951
+ }
952
+ } catch {
953
+ errorData = { message: response.statusText };
954
+ }
955
+ const message = errorData.message || `HTTP ${statusCode} error`;
956
+ const code = errorData.code;
957
+ const details = errorData.details;
958
+ switch (statusCode) {
959
+ case 400:
960
+ throw new AuraValidationError(message, details);
961
+ case 401:
962
+ throw new AuraAuthenticationError(message);
963
+ case 404:
964
+ throw new AuraNotFoundError(message);
965
+ case 429: {
966
+ const retryAfter = response.headers.get("Retry-After");
967
+ throw new AuraRateLimitError(message, retryAfter ? parseInt(retryAfter) : void 0);
968
+ }
969
+ case 408:
970
+ throw new AuraTimeoutError(message);
971
+ default:
972
+ throw new AuraAPIError(message, statusCode, code, details);
973
+ }
974
+ }
975
+ /**
976
+ * Get the auto-idempotency setting
977
+ */
978
+ getAutoIdempotency() {
979
+ return this.autoIdempotency;
980
+ }
981
+ };
982
+
983
+ export { Agents, AuraAPIError, AuraAuthenticationError, AuraClient, AuraError, AuraFaucetUnavailableError, AuraNetworkError, AuraNotFoundError, AuraRateLimitError, AuraTimeoutError, AuraValidationError, Escrows, MandateSignature, Mandates, Policies, Wallets, Webhooks, calculateBackoff, generateIdempotencyKey, isAuraAPIError, isAuraError, isAuraFaucetUnavailableError, isAuraNetworkError, isAuraTimeoutError, isRetryableError, retryWithBackoff, withTimeout };
984
+ //# sourceMappingURL=index.mjs.map
985
+ //# sourceMappingURL=index.mjs.map