@vapi-network/core 0.7.0 → 0.8.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.
Files changed (76) hide show
  1. package/dist/account-add.d.ts +8 -1
  2. package/dist/account-add.d.ts.map +1 -1
  3. package/dist/account-movement-lock.d.ts +10 -0
  4. package/dist/account-movement-lock.d.ts.map +1 -0
  5. package/dist/agent-link.d.ts +1 -0
  6. package/dist/agent-link.d.ts.map +1 -1
  7. package/dist/agent-link.js +21 -10
  8. package/dist/agent-link.js.map +1 -1
  9. package/dist/agent-profile.d.ts +14 -2
  10. package/dist/agent-profile.d.ts.map +1 -1
  11. package/dist/agent-run.d.ts +12 -0
  12. package/dist/agent-run.d.ts.map +1 -0
  13. package/dist/allocation.d.ts +35 -4
  14. package/dist/allocation.d.ts.map +1 -1
  15. package/dist/api-key.js.map +1 -1
  16. package/dist/atomic-file.d.ts +23 -0
  17. package/dist/atomic-file.d.ts.map +1 -0
  18. package/dist/audit.d.ts +4 -1
  19. package/dist/audit.d.ts.map +1 -1
  20. package/dist/backup.d.ts +153 -32
  21. package/dist/backup.d.ts.map +1 -1
  22. package/dist/backup.js +2535 -139
  23. package/dist/backup.js.map +3 -3
  24. package/dist/call-contracts-wire.d.ts +12 -12
  25. package/dist/ceiling.d.ts +8 -0
  26. package/dist/ceiling.d.ts.map +1 -1
  27. package/dist/cloud-backup.d.ts +8 -10
  28. package/dist/cloud-backup.d.ts.map +1 -1
  29. package/dist/cloud-backup.js +2806 -381
  30. package/dist/cloud-backup.js.map +3 -3
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/distribute.d.ts +9 -1
  33. package/dist/distribute.d.ts.map +1 -1
  34. package/dist/index.d.ts +8 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +5206 -1270
  37. package/dist/index.js.map +3 -3
  38. package/dist/marketplace-contracts.d.ts +14 -14
  39. package/dist/member-bundle.d.ts +110 -0
  40. package/dist/member-bundle.d.ts.map +1 -0
  41. package/dist/movement-open.d.ts +10 -0
  42. package/dist/movement-open.d.ts.map +1 -0
  43. package/dist/movement.d.ts +77 -7
  44. package/dist/movement.d.ts.map +1 -1
  45. package/dist/receipts.d.ts +9 -0
  46. package/dist/receipts.d.ts.map +1 -1
  47. package/dist/router-client.d.ts +9 -1
  48. package/dist/router-client.d.ts.map +1 -1
  49. package/dist/router-client.js +2073 -607
  50. package/dist/router-client.js.map +3 -3
  51. package/dist/run-budget.d.ts +17 -0
  52. package/dist/run-budget.d.ts.map +1 -0
  53. package/dist/secret-store.d.ts +6 -0
  54. package/dist/secret-store.d.ts.map +1 -1
  55. package/dist/secrets.d.ts +2 -1
  56. package/dist/secrets.d.ts.map +1 -1
  57. package/dist/secrets.js +884 -48
  58. package/dist/secrets.js.map +3 -3
  59. package/dist/support-report.d.ts +1 -1
  60. package/dist/swarm-capital.d.ts +147 -0
  61. package/dist/swarm-capital.d.ts.map +1 -0
  62. package/dist/swarm.d.ts +224 -0
  63. package/dist/swarm.d.ts.map +1 -0
  64. package/dist/transfer-journal.d.ts +11 -0
  65. package/dist/transfer-journal.d.ts.map +1 -0
  66. package/dist/transfer.d.ts +4 -1
  67. package/dist/transfer.d.ts.map +1 -1
  68. package/dist/vault.d.ts +11 -0
  69. package/dist/vault.d.ts.map +1 -1
  70. package/dist/wallet-store.d.ts +19 -8
  71. package/dist/wallet-store.d.ts.map +1 -1
  72. package/dist/x402-pay.d.ts +7 -0
  73. package/dist/x402-pay.d.ts.map +1 -1
  74. package/dist/x402.d.ts +3 -0
  75. package/dist/x402.d.ts.map +1 -1
  76. package/package.json +1 -1
@@ -4373,7 +4373,7 @@ function weierstrass(curveDef) {
4373
4373
  function prepSig(msgHash, privateKey, opts = defaultSigOpts) {
4374
4374
  if (["recovered", "canonical"].some((k) => k in opts))
4375
4375
  throw new Error("sign() legacy options not supported");
4376
- const { hash: hash4, randomBytes: randomBytes5 } = CURVE;
4376
+ const { hash: hash4, randomBytes: randomBytes9 } = CURVE;
4377
4377
  let { lowS, prehash, extraEntropy: ent } = opts;
4378
4378
  if (lowS == null)
4379
4379
  lowS = true;
@@ -4385,7 +4385,7 @@ function weierstrass(curveDef) {
4385
4385
  const d = normPrivateKeyToScalar(privateKey);
4386
4386
  const seedArgs = [int2octets(d), int2octets(h1int)];
4387
4387
  if (ent != null && ent !== false) {
4388
- const e3 = ent === true ? randomBytes5(Fp.BYTES) : ent;
4388
+ const e3 = ent === true ? randomBytes9(Fp.BYTES) : ent;
4389
4389
  seedArgs.push(ensureBytes("extraEntropy", e3));
4390
4390
  }
4391
4391
  const seed = concatBytes3(...seedArgs);
@@ -28522,8 +28522,8 @@ function date4(params) {
28522
28522
 
28523
28523
  // src/agent-link.ts
28524
28524
  import { createHash } from "node:crypto";
28525
- import { mkdir as mkdir2, open as open2, stat, unlink } from "node:fs/promises";
28526
- import { dirname as dirname2 } from "node:path";
28525
+ import { mkdir as mkdir4, open as open3, stat as stat3, unlink as unlink2 } from "node:fs/promises";
28526
+ import { dirname as dirname3 } from "node:path";
28527
28527
 
28528
28528
  // ../../node_modules/.pnpm/viem@2.56.1_typescript@6.0.3_zod@4.5.4/node_modules/viem/_esm/utils/signature/hashMessage.js
28529
28529
  init_keccak256();
@@ -30917,12 +30917,14 @@ function uid(length = 11) {
30917
30917
  }
30918
30918
 
30919
30919
  // src/audit.ts
30920
- import { appendFile, chmod, mkdir, readFile } from "node:fs/promises";
30921
- import { join as join2 } from "node:path";
30920
+ import { appendFile, chmod, mkdir as mkdir3, readFile as readFile3 } from "node:fs/promises";
30921
+ import { join as join3 } from "node:path";
30922
30922
 
30923
30923
  // src/config.ts
30924
+ import { constants } from "node:fs";
30925
+ import { copyFile, mkdir as mkdir2, readFile as readFile2, rename as rename2, stat as stat2, writeFile } from "node:fs/promises";
30924
30926
  import { homedir } from "node:os";
30925
- import { dirname, join } from "node:path";
30927
+ import { dirname as dirname2, join as join2 } from "node:path";
30926
30928
 
30927
30929
  // ../../node_modules/.pnpm/viem@2.56.1_typescript@6.0.3_zod@4.5.4/node_modules/viem/_esm/utils/abi/encodeEventTopics.js
30928
30930
  init_abi();
@@ -36625,6 +36627,225 @@ init_getAddress();
36625
36627
  init_isAddress();
36626
36628
  init_toHex();
36627
36629
 
36630
+ // src/atomic-file.ts
36631
+ import { randomBytes as randomBytes2 } from "node:crypto";
36632
+ import { link, mkdir, open as open2, readdir, readFile, rename, stat, unlink } from "node:fs/promises";
36633
+ import { basename, dirname, join } from "node:path";
36634
+ var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
36635
+ var LOCK_RETRY_MS = 20;
36636
+ var LOCK_STALE_AFTER_MS = 12e4;
36637
+ var LOCK_HEARTBEAT_MS = 3e4;
36638
+ var atomicFileTestHooks;
36639
+ var FileLockedError = class extends Error {
36640
+ constructor(message = "Timed out waiting for the file lock.") {
36641
+ super(message);
36642
+ this.name = "FileLockedError";
36643
+ }
36644
+ };
36645
+ async function withFileLock(lockPath, fn, options = {}) {
36646
+ await mkdir(dirname(lockPath), { recursive: true, mode: 448 });
36647
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS);
36648
+ const ownerRecord = {
36649
+ pid: process.pid,
36650
+ token: randomBytes2(16).toString("hex")
36651
+ };
36652
+ const owner = JSON.stringify(ownerRecord);
36653
+ const releasedOwner = JSON.stringify({ ...ownerRecord, released: true });
36654
+ let handle;
36655
+ while (handle === void 0) {
36656
+ await atomicFileTestHooks?.beforeLockAttempt?.(lockPath);
36657
+ try {
36658
+ const candidate = await open2(lockPath, "wx", 384);
36659
+ try {
36660
+ await candidate.writeFile(owner, "utf8");
36661
+ await candidate.sync();
36662
+ await atomicFileTestHooks?.afterLockCreate?.(lockPath);
36663
+ if (await lockTakeoverInProgress(lockPath)) {
36664
+ await candidate.close();
36665
+ await removeOwnedLock(lockPath, [owner]);
36666
+ await removeAbandonedLockTombs(lockPath);
36667
+ if (Date.now() >= deadline) {
36668
+ throw new FileLockedError(options.lockedMessage);
36669
+ }
36670
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
36671
+ continue;
36672
+ }
36673
+ handle = candidate;
36674
+ } catch (error61) {
36675
+ await candidate.close().catch(() => void 0);
36676
+ await removeOwnedLock(lockPath, [owner]).catch(() => void 0);
36677
+ throw error61;
36678
+ }
36679
+ } catch (error61) {
36680
+ if (!hasCode(error61, "EEXIST")) throw error61;
36681
+ await removeStaleLock(lockPath);
36682
+ if (Date.now() >= deadline) {
36683
+ throw new FileLockedError(options.lockedMessage);
36684
+ }
36685
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
36686
+ }
36687
+ }
36688
+ const ownedHandle = handle;
36689
+ const heartbeat = setInterval(() => {
36690
+ const time3 = /* @__PURE__ */ new Date();
36691
+ void ownedHandle.utimes(time3, time3).catch(() => void 0);
36692
+ }, LOCK_HEARTBEAT_MS);
36693
+ heartbeat.unref();
36694
+ try {
36695
+ return await fn();
36696
+ } finally {
36697
+ clearInterval(heartbeat);
36698
+ try {
36699
+ await markLockReleased(ownedHandle, releasedOwner);
36700
+ } finally {
36701
+ try {
36702
+ await ownedHandle.close();
36703
+ } finally {
36704
+ await removeOwnedLock(lockPath, [owner, releasedOwner]);
36705
+ }
36706
+ }
36707
+ }
36708
+ }
36709
+ async function removeStaleLock(path) {
36710
+ try {
36711
+ const owner = await readFile(path, "utf8");
36712
+ if (lockOwnerReleased(owner)) {
36713
+ await removeOwnedLock(path, [owner]);
36714
+ return;
36715
+ }
36716
+ const metadata = await stat(path);
36717
+ if (Date.now() - metadata.mtimeMs <= LOCK_STALE_AFTER_MS) return;
36718
+ const pid = lockOwnerPid(owner);
36719
+ if (pid !== void 0 && processIsAlive(pid)) return;
36720
+ await atomicFileTestHooks?.beforeStaleLockBreak?.(path);
36721
+ await removeOwnedLock(path, [owner]);
36722
+ } catch (error61) {
36723
+ if (!hasCode(error61, "ENOENT")) throw error61;
36724
+ }
36725
+ }
36726
+ async function removeOwnedLock(path, owners) {
36727
+ const tombPath = `${path}.stale.${randomBytes2(16).toString("hex")}`;
36728
+ try {
36729
+ await rename(path, tombPath);
36730
+ } catch (error61) {
36731
+ if (hasCode(error61, "ENOENT")) {
36732
+ await removeOwnedTombs(path, owners);
36733
+ const currentOwner = await readFile(path, "utf8").catch(() => void 0);
36734
+ if (currentOwner !== void 0 && owners.includes(currentOwner)) {
36735
+ await removeOwnedLock(path, owners);
36736
+ }
36737
+ return;
36738
+ }
36739
+ throw error61;
36740
+ }
36741
+ let pendingError;
36742
+ try {
36743
+ const renamedOwner = await readFile(tombPath, "utf8");
36744
+ if (!owners.includes(renamedOwner) && !lockOwnerReleased(renamedOwner)) {
36745
+ await atomicFileTestHooks?.afterMismatchedLockRename?.(path);
36746
+ const currentOwner = await readFile(tombPath, "utf8");
36747
+ if (!lockOwnerReleased(currentOwner)) await restoreRenamedLock(path, tombPath);
36748
+ await atomicFileTestHooks?.afterMismatchedLockRestore?.(path);
36749
+ }
36750
+ } catch (error61) {
36751
+ pendingError = error61;
36752
+ }
36753
+ try {
36754
+ await unlink(tombPath);
36755
+ } catch (error61) {
36756
+ if (!hasCode(error61, "ENOENT") && pendingError === void 0) {
36757
+ pendingError = error61;
36758
+ }
36759
+ }
36760
+ if (pendingError !== void 0) throw pendingError;
36761
+ }
36762
+ async function lockTakeoverInProgress(path) {
36763
+ const prefix = `${basename(path)}.stale.`;
36764
+ return (await readdir(dirname(path))).some((entry) => entry.startsWith(prefix));
36765
+ }
36766
+ async function removeAbandonedLockTombs(path) {
36767
+ const directory = dirname(path);
36768
+ const prefix = `${basename(path)}.stale.`;
36769
+ for (const entry of await readdir(directory)) {
36770
+ if (!entry.startsWith(prefix)) continue;
36771
+ const tombPath = join(directory, entry);
36772
+ const [metadata, owner] = await Promise.all([
36773
+ stat(tombPath).catch(() => void 0),
36774
+ readFile(tombPath, "utf8").catch(() => void 0)
36775
+ ]);
36776
+ if (metadata === void 0 || owner === void 0) continue;
36777
+ const pid = lockOwnerPid(owner);
36778
+ const abandoned = lockOwnerReleased(owner) || Date.now() - metadata.mtimeMs > LOCK_STALE_AFTER_MS && (pid === void 0 || !processIsAlive(pid));
36779
+ if (!abandoned) continue;
36780
+ await unlink(tombPath).catch((error61) => {
36781
+ if (!hasCode(error61, "ENOENT")) throw error61;
36782
+ });
36783
+ }
36784
+ }
36785
+ async function removeOwnedTombs(path, owners) {
36786
+ const directory = dirname(path);
36787
+ const prefix = `${basename(path)}.stale.`;
36788
+ for (const entry of await readdir(directory)) {
36789
+ if (!entry.startsWith(prefix)) continue;
36790
+ const tombPath = join(directory, entry);
36791
+ const owner = await readFile(tombPath, "utf8").catch(() => void 0);
36792
+ if (owner === void 0 || !owners.includes(owner)) continue;
36793
+ await unlink(tombPath).catch((error61) => {
36794
+ if (!hasCode(error61, "ENOENT")) throw error61;
36795
+ });
36796
+ }
36797
+ }
36798
+ async function restoreRenamedLock(path, tombPath) {
36799
+ while (true) {
36800
+ try {
36801
+ await link(tombPath, path);
36802
+ return;
36803
+ } catch (error61) {
36804
+ if (hasCode(error61, "ENOENT")) return;
36805
+ if (!hasCode(error61, "EEXIST")) throw error61;
36806
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
36807
+ }
36808
+ }
36809
+ }
36810
+ async function markLockReleased(handle, owner) {
36811
+ const contents = Buffer.from(owner, "utf8");
36812
+ await handle.write(contents, 0, contents.length, 0);
36813
+ await handle.truncate(contents.length);
36814
+ await handle.sync();
36815
+ }
36816
+ function lockOwnerPid(owner) {
36817
+ try {
36818
+ const value = JSON.parse(owner);
36819
+ if (typeof value !== "object" || value === null) return void 0;
36820
+ const pid = Reflect.get(value, "pid");
36821
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
36822
+ } catch {
36823
+ return void 0;
36824
+ }
36825
+ }
36826
+ function lockOwnerReleased(owner) {
36827
+ try {
36828
+ const value = JSON.parse(owner);
36829
+ return typeof value === "object" && value !== null && Reflect.get(value, "released") === true;
36830
+ } catch {
36831
+ return false;
36832
+ }
36833
+ }
36834
+ function processIsAlive(pid) {
36835
+ if (atomicFileTestHooks?.isProcessAlive !== void 0) {
36836
+ return atomicFileTestHooks.isProcessAlive(pid);
36837
+ }
36838
+ try {
36839
+ process.kill(pid, 0);
36840
+ return true;
36841
+ } catch (error61) {
36842
+ return hasCode(error61, "EPERM");
36843
+ }
36844
+ }
36845
+ function hasCode(error61, code) {
36846
+ return error61 instanceof Error && "code" in error61 && error61.code === code;
36847
+ }
36848
+
36628
36849
  // src/x402-networks.ts
36629
36850
  var BASE_MAINNET_CAIP2 = "eip155:8453";
36630
36851
  var ARC_MAINNET_CAIP2 = "eip155:5042";
@@ -37044,6 +37265,12 @@ function createNetworkHttpTransport(rpcUrl, options = {}) {
37044
37265
  var DEFAULT_REGISTRY_URL = "https://api.vapinetwork.ai";
37045
37266
  var DEFAULT_DISCOVERY_URL = `${DEFAULT_REGISTRY_URL}/api/call/services`;
37046
37267
  var DEFAULT_MARKETPLACE_DISCOVERY_URL = `${DEFAULT_REGISTRY_URL}/api/call/discovery`;
37268
+ var DEFAULT_REGISTRY_FALLBACKS = [
37269
+ {
37270
+ discoveryUrl: DEFAULT_DISCOVERY_URL,
37271
+ marketplaceDiscoveryUrl: DEFAULT_MARKETPLACE_DISCOVERY_URL
37272
+ }
37273
+ ];
37047
37274
  var DEFAULT_SPEND_CAPS = {
37048
37275
  perCallAtomic: "100000",
37049
37276
  perDayAtomic: "1000000"
@@ -37110,31 +37337,269 @@ var configSchema = external_exports.object({
37110
37337
  }
37111
37338
  }
37112
37339
  });
37113
- function getVapiPaths(directory = process.env.VAPI_HOME?.trim() || join(homedir(), ".vapi")) {
37340
+ function getVapiPaths(directory = process.env.VAPI_HOME?.trim() || join2(homedir(), ".vapi")) {
37114
37341
  return {
37115
37342
  directory,
37116
- agentsDir: join(directory, "agents"),
37117
- config: join(directory, "config.json"),
37118
- keystore: join(directory, "keystore.json"),
37119
- receipts: join(directory, "receipts.jsonl"),
37120
- searches: join(directory, "searches.jsonl"),
37121
- ledger: join(directory, "spend-ledger.json")
37343
+ agentsDir: join2(directory, "agents"),
37344
+ config: join2(directory, "config.json"),
37345
+ keystore: join2(directory, "keystore.json"),
37346
+ receipts: join2(directory, "receipts.jsonl"),
37347
+ searches: join2(directory, "searches.jsonl"),
37348
+ ledger: join2(directory, "spend-ledger.json")
37122
37349
  };
37123
37350
  }
37124
37351
  var getAgentCashPaths = getVapiPaths;
37352
+ async function migrateLegacyVapiHome(options = {}) {
37353
+ const target = getVapiPaths(options.targetDirectory);
37354
+ const legacyDirectory = options.legacyDirectory ?? join2(homedir(), ".vapi", "agent-cash");
37355
+ if (legacyDirectory === target.directory || !await pathExists(legacyDirectory)) return [];
37356
+ await mkdir2(target.directory, { recursive: true, mode: 448 });
37357
+ const copied = [];
37358
+ for (const filename of [
37359
+ "config.json",
37360
+ "keystore.json",
37361
+ "receipts.jsonl",
37362
+ "searches.jsonl",
37363
+ "spend-ledger.json"
37364
+ ]) {
37365
+ const source = join2(legacyDirectory, filename);
37366
+ const destination = join2(target.directory, filename);
37367
+ try {
37368
+ const copy = async () => await copyFile(source, destination, constants.COPYFILE_EXCL);
37369
+ if (filename === "receipts.jsonl") {
37370
+ await withFileLock(`${destination}.lock`, copy, {
37371
+ lockedMessage: "The receipt journal is busy. Wait for the other vapi command to finish, then retry."
37372
+ });
37373
+ } else {
37374
+ await copy();
37375
+ }
37376
+ copied.push(destination);
37377
+ } catch (error61) {
37378
+ if (isMissingFile(error61) || isAlreadyExists(error61)) continue;
37379
+ throw error61;
37380
+ }
37381
+ }
37382
+ if (copied.length > 0) {
37383
+ const message = `Migrated legacy vAPI wallet files from ${legacyDirectory} to ${target.directory}; originals were kept.`;
37384
+ (options.notice ?? ((value) => process.stderr.write(`${value}
37385
+ `)))(message);
37386
+ }
37387
+ return copied;
37388
+ }
37389
+ var LEGACY_REGISTRY_HOSTS = /* @__PURE__ */ new Map([
37390
+ ["console.vapinetwork.ai", "api.vapinetwork.ai"],
37391
+ ["console-staging.vapinetwork.ai", "api-staging.vapinetwork.ai"]
37392
+ ]);
37393
+ var LEGACY_REGISTRY_PATHS = /* @__PURE__ */ new Map([
37394
+ ["/api/network/services", "/api/call/services"],
37395
+ ["/api/marketplace/discovery", "/api/call/discovery"]
37396
+ ]);
37397
+ var REGISTRY_DOMAIN = "vapinetwork.ai";
37398
+ function rewriteLegacyRegistryUrl(value) {
37399
+ let url2;
37400
+ try {
37401
+ url2 = new URL(value);
37402
+ } catch {
37403
+ return value;
37404
+ }
37405
+ let rewritten = false;
37406
+ const host = LEGACY_REGISTRY_HOSTS.get(url2.hostname.toLowerCase());
37407
+ if (host) {
37408
+ url2.hostname = host;
37409
+ rewritten = true;
37410
+ }
37411
+ const hostname3 = url2.hostname.toLowerCase();
37412
+ if (hostname3 === REGISTRY_DOMAIN || hostname3.endsWith(`.${REGISTRY_DOMAIN}`)) {
37413
+ const path = LEGACY_REGISTRY_PATHS.get(url2.pathname.replace(/\/+$/, ""));
37414
+ if (path) {
37415
+ url2.pathname = path;
37416
+ rewritten = true;
37417
+ }
37418
+ }
37419
+ return rewritten ? url2.href : value;
37420
+ }
37421
+ function rewriteLegacyRegistryUrls(config2) {
37422
+ const changes = [];
37423
+ const rewrite = (field, from15) => {
37424
+ const to = rewriteLegacyRegistryUrl(from15);
37425
+ if (to !== from15) changes.push({ field, from: from15, to });
37426
+ return to;
37427
+ };
37428
+ const next = {
37429
+ ...config2,
37430
+ discoveryUrl: rewrite("discoveryUrl", config2.discoveryUrl),
37431
+ marketplaceDiscoveryUrl: rewrite("marketplaceDiscoveryUrl", config2.marketplaceDiscoveryUrl),
37432
+ ...config2.registryFallbacks ? {
37433
+ registryFallbacks: config2.registryFallbacks.map((fallback, index2) => ({
37434
+ discoveryUrl: rewrite(
37435
+ `registryFallbacks[${index2}].discoveryUrl`,
37436
+ fallback.discoveryUrl
37437
+ ),
37438
+ marketplaceDiscoveryUrl: rewrite(
37439
+ `registryFallbacks[${index2}].marketplaceDiscoveryUrl`,
37440
+ fallback.marketplaceDiscoveryUrl
37441
+ )
37442
+ }))
37443
+ } : {}
37444
+ };
37445
+ return changes.length === 0 ? { config: config2, changes } : { config: next, changes };
37446
+ }
37447
+ function formatLegacyRegistryRewrites(changes) {
37448
+ return [
37449
+ "Rewrote retired vAPI registry URLs in the local config:",
37450
+ ...changes.map((change) => ` ${change.field}: ${change.from} -> ${change.to}`)
37451
+ ].join("\n");
37452
+ }
37453
+ function getDefaultConfig(source = process.env, options = {}) {
37454
+ const registry2 = registryEndpoints(source.VAPI_REGISTRY_URL?.trim() || DEFAULT_REGISTRY_URL);
37455
+ const requested = new Set(options.networks ?? ["base"]);
37456
+ const networks = {};
37457
+ if (requested.has("base") || requested.has(BASE_MAINNET_CAIP2)) {
37458
+ networks[BASE_MAINNET_CAIP2] = {
37459
+ rpcUrl: source.BASE_RPC_URL?.trim() || NETWORKS[BASE_MAINNET_CAIP2].publicRpcUrl,
37460
+ usdc: NETWORKS[BASE_MAINNET_CAIP2].usdc
37461
+ };
37462
+ }
37463
+ const arcMainnetRpcUrl = source.ARC_RPC_URL?.trim();
37464
+ if (arcMainnetRpcUrl || requested.has("arc") || requested.has(ARC_MAINNET_CAIP2)) {
37465
+ networks[ARC_MAINNET_CAIP2] = {
37466
+ rpcUrl: arcMainnetRpcUrl || NETWORKS[ARC_MAINNET_CAIP2].publicRpcUrl,
37467
+ usdc: NETWORKS[ARC_MAINNET_CAIP2].usdc
37468
+ };
37469
+ }
37470
+ const arcTestnetRpcUrl = source.ARC_TESTNET_RPC_URL?.trim();
37471
+ if (arcTestnetRpcUrl || requested.has("arc-testnet") || requested.has(ARC_TESTNET_CAIP2)) {
37472
+ if (!arcTestnetRpcUrl) {
37473
+ throw new Error(
37474
+ "Arc testnet RPC is required. Set ARC_TESTNET_RPC_URL before enabling Arc testnet."
37475
+ );
37476
+ }
37477
+ networks[ARC_TESTNET_CAIP2] = {
37478
+ rpcUrl: arcTestnetRpcUrl,
37479
+ usdc: NETWORKS[ARC_TESTNET_CAIP2].usdc
37480
+ };
37481
+ }
37482
+ const solanaRpcUrl = source.SOLANA_RPC_URL?.trim();
37483
+ if (solanaRpcUrl || requested.has("solana") || requested.has(SOLANA_MAINNET_CAIP2)) {
37484
+ networks[SOLANA_MAINNET_CAIP2] = {
37485
+ rpcUrl: solanaRpcUrl || NETWORKS[SOLANA_MAINNET_CAIP2].publicRpcUrl,
37486
+ usdc: NETWORKS[SOLANA_MAINNET_CAIP2].usdc
37487
+ };
37488
+ }
37489
+ return {
37490
+ discoveryUrl: source.VAPI_DISCOVERY_URL?.trim() || registry2.discoveryUrl,
37491
+ marketplaceDiscoveryUrl: source.VAPI_MARKETPLACE_DISCOVERY_URL?.trim() || registry2.marketplaceDiscoveryUrl,
37492
+ registryFallbacks: DEFAULT_REGISTRY_FALLBACKS.map((fallback) => ({ ...fallback })),
37493
+ allowPrivateNetwork: false,
37494
+ networks,
37495
+ spendCaps: { ...DEFAULT_SPEND_CAPS }
37496
+ };
37497
+ }
37498
+ async function loadConfig(path = getVapiPaths().config, source = process.env, options = {}) {
37499
+ if (!process.env.VAPI_HOME?.trim() && path === getVapiPaths(join2(homedir(), ".vapi")).config) {
37500
+ await migrateLegacyVapiHome({ targetDirectory: join2(homedir(), ".vapi") });
37501
+ }
37502
+ let raw;
37503
+ try {
37504
+ raw = await readFile2(path, "utf8");
37505
+ } catch (error61) {
37506
+ if (isMissingFile(error61)) return getDefaultConfig(source);
37507
+ throw error61;
37508
+ }
37509
+ const parsed = configSchema.parse(JSON.parse(raw));
37510
+ parsed.registryFallbacks ??= DEFAULT_REGISTRY_FALLBACKS.map((fallback) => ({ ...fallback }));
37511
+ const migrated = rewriteLegacyRegistryUrls(parsed);
37512
+ const config2 = migrated.config;
37513
+ if (migrated.changes.length > 0 && options.notice) {
37514
+ options.notice(
37515
+ `${formatLegacyRegistryRewrites(migrated.changes)}
37516
+ Run vapi init to write the new URLs to ${path}.`
37517
+ );
37518
+ }
37519
+ const baseRpcUrl = source.BASE_RPC_URL?.trim();
37520
+ const arcMainnetRpcUrl = source.ARC_RPC_URL?.trim();
37521
+ const arcTestnetRpcUrl = source.ARC_TESTNET_RPC_URL?.trim();
37522
+ const solanaRpcUrl = source.SOLANA_RPC_URL?.trim();
37523
+ const discoveryUrl = source.VAPI_DISCOVERY_URL?.trim();
37524
+ const marketplaceDiscoveryUrl = source.VAPI_MARKETPLACE_DISCOVERY_URL?.trim();
37525
+ const registryUrl = source.VAPI_REGISTRY_URL?.trim();
37526
+ if (config2.networks[BASE_MAINNET_CAIP2] && baseRpcUrl) {
37527
+ config2.networks[BASE_MAINNET_CAIP2].rpcUrl = baseRpcUrl;
37528
+ }
37529
+ if (arcMainnetRpcUrl) {
37530
+ const existing = config2.networks[ARC_MAINNET_CAIP2];
37531
+ config2.networks[ARC_MAINNET_CAIP2] = {
37532
+ ...existing ?? { usdc: NETWORKS[ARC_MAINNET_CAIP2].usdc },
37533
+ rpcUrl: arcMainnetRpcUrl
37534
+ };
37535
+ }
37536
+ if (arcTestnetRpcUrl) {
37537
+ const existing = config2.networks[ARC_TESTNET_CAIP2];
37538
+ config2.networks[ARC_TESTNET_CAIP2] = {
37539
+ ...existing ?? { usdc: NETWORKS[ARC_TESTNET_CAIP2].usdc },
37540
+ rpcUrl: arcTestnetRpcUrl
37541
+ };
37542
+ }
37543
+ if (solanaRpcUrl) {
37544
+ config2.networks[SOLANA_MAINNET_CAIP2] = {
37545
+ rpcUrl: solanaRpcUrl,
37546
+ usdc: config2.networks[SOLANA_MAINNET_CAIP2]?.usdc ?? NETWORKS[SOLANA_MAINNET_CAIP2].usdc
37547
+ };
37548
+ }
37549
+ if (registryUrl) {
37550
+ const registry2 = registryEndpoints(registryUrl);
37551
+ config2.discoveryUrl = registry2.discoveryUrl;
37552
+ config2.marketplaceDiscoveryUrl = registry2.marketplaceDiscoveryUrl;
37553
+ }
37554
+ if (discoveryUrl) config2.discoveryUrl = discoveryUrl;
37555
+ if (marketplaceDiscoveryUrl) config2.marketplaceDiscoveryUrl = marketplaceDiscoveryUrl;
37556
+ for (const [network, configured] of Object.entries(config2.networks)) {
37557
+ configured.rpcUrl = configured.rpcUrl.trim();
37558
+ configured.usdc = configured.usdc.trim();
37559
+ if (isSolanaNetwork(network) && configured.usdc !== NETWORKS[SOLANA_MAINNET_CAIP2].usdc) {
37560
+ throw new Error(`Network ${network} must use canonical Solana mainnet USDC.`);
37561
+ }
37562
+ if (!configured.rpcUrl) delete config2.networks[network];
37563
+ }
37564
+ return config2;
37565
+ }
37566
+ function registryEndpoints(baseUrl) {
37567
+ const base = new URL(baseUrl);
37568
+ base.search = "";
37569
+ base.hash = "";
37570
+ const prefix = base.pathname === "/" ? "" : base.pathname.replace(/\/+$/, "");
37571
+ const discovery = new URL(base);
37572
+ discovery.pathname = `${prefix}/api/call/services`;
37573
+ const marketplace = new URL(base);
37574
+ marketplace.pathname = `${prefix}/api/call/discovery`;
37575
+ return { discoveryUrl: discovery.href, marketplaceDiscoveryUrl: marketplace.href };
37576
+ }
37577
+ async function pathExists(path) {
37578
+ try {
37579
+ await stat2(path);
37580
+ return true;
37581
+ } catch (error61) {
37582
+ if (isMissingFile(error61)) return false;
37583
+ throw error61;
37584
+ }
37585
+ }
37125
37586
  function isMissingFile(error61) {
37126
37587
  return error61 instanceof Error && "code" in error61 && error61.code === "ENOENT";
37127
37588
  }
37589
+ function isAlreadyExists(error61) {
37590
+ return error61 instanceof Error && "code" in error61 && error61.code === "EEXIST";
37591
+ }
37128
37592
 
37129
37593
  // src/audit.ts
37130
37594
  var AUDIT_FILE = "audit.log";
37131
37595
  function auditLogPath(home) {
37132
- return join2(home, AUDIT_FILE);
37596
+ return join3(home, AUDIT_FILE);
37133
37597
  }
37134
37598
  async function appendAudit(home, record2, options = {}) {
37135
37599
  const entry = {
37136
37600
  time: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
37137
37601
  event: record2.event,
37602
+ ...record2.run === void 0 ? {} : { run: record2.run },
37138
37603
  ...record2.wallet === void 0 ? {} : { wallet: record2.wallet },
37139
37604
  ...record2.owner === void 0 ? {} : { owner: record2.owner },
37140
37605
  tty: record2.tty,
@@ -37142,15 +37607,19 @@ async function appendAudit(home, record2, options = {}) {
37142
37607
  ...record2.detail === void 0 ? {} : { detail: record2.detail }
37143
37608
  };
37144
37609
  const path = auditLogPath(home);
37145
- await mkdir(home, { recursive: true, mode: 448 });
37610
+ await mkdir3(home, { recursive: true, mode: 448 });
37146
37611
  await appendFile(path, `${JSON.stringify(entry)}
37147
37612
  `, { encoding: "utf8", mode: 384 });
37148
37613
  await chmod(path, 384);
37149
37614
  return entry;
37150
37615
  }
37151
37616
 
37617
+ // src/device.ts
37618
+ var DEVICE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
37619
+
37152
37620
  // src/agent-link.ts
37153
37621
  var AGENT_LINK_REVOKED_MESSAGE = "This agent's link was revoked or has expired. Run vapi login to link it again.";
37622
+ var REMOTE_AGENT_TOKEN_EXPIRED_MESSAGE = "The Railway run's short-lived vAPI access token expired. Start a new Railway run from the owner machine.";
37154
37623
  var AgentLinkError = class extends Error {
37155
37624
  constructor(code, message) {
37156
37625
  super(message);
@@ -37184,8 +37653,8 @@ async function agentFetch(args, url2, init) {
37184
37653
  }
37185
37654
  async function resolveAgentAccessToken(args, options) {
37186
37655
  await args.wallets.reload();
37187
- const link = args.wallets.entry(args.wallet)?.link;
37188
- if (link === void 0 || !args.secrets.available) throw notLinkedError();
37656
+ const link2 = args.wallets.entry(args.wallet)?.link;
37657
+ if (link2 === void 0 || !args.secrets.available) throw notLinkedError();
37189
37658
  const accounts = agentSecretAccounts(args.wallet);
37190
37659
  const tokens = parseTokens(await safeGetSecret(args.secrets, accounts.tokens));
37191
37660
  if (tokens === void 0) throw notLinkedError();
@@ -37193,7 +37662,7 @@ async function resolveAgentAccessToken(args, options) {
37193
37662
  if (options.rejectedAccessToken === void 0 && tokens.expiresAt - 6e4 > now()) {
37194
37663
  return tokens.accessToken;
37195
37664
  }
37196
- return await withAgentCredentialLock(args.wallets, link.clientId, async () => {
37665
+ return await withAgentCredentialLock(args.wallets, link2.clientId, async () => {
37197
37666
  await args.wallets.reload();
37198
37667
  const currentLink = args.wallets.entry(args.wallet)?.link;
37199
37668
  if (currentLink === void 0) throw notLinkedError();
@@ -37206,6 +37675,9 @@ async function resolveAgentAccessToken(args, options) {
37206
37675
  if (options.rejectedAccessToken === void 0 && current.expiresAt - 6e4 > now()) {
37207
37676
  return current.accessToken;
37208
37677
  }
37678
+ if (current.refreshToken === void 0) {
37679
+ throw new AgentLinkError("not_linked", REMOTE_AGENT_TOKEN_EXPIRED_MESSAGE);
37680
+ }
37209
37681
  const response = await safeFetch(
37210
37682
  args.fetchImpl ?? publicFetch,
37211
37683
  apiEndpoint(currentLink.apiBase, "/oauth/token"),
@@ -37239,12 +37711,12 @@ async function resolveAgentAccessToken(args, options) {
37239
37711
  async function withAgentCredentialLock(wallets, clientId, operation) {
37240
37712
  const digest = createHash("sha256").update(clientId).digest("hex").slice(0, 24);
37241
37713
  const lockPath = `${wallets.registryPath}.${digest}.agent-link.lock`;
37242
- await mkdir2(dirname2(lockPath), { recursive: true, mode: 448 });
37714
+ await mkdir4(dirname3(lockPath), { recursive: true, mode: 448 });
37243
37715
  const deadline = Date.now() + 3e4;
37244
37716
  let handle;
37245
37717
  while (!handle) {
37246
37718
  try {
37247
- handle = await open2(lockPath, "wx", 384);
37719
+ handle = await open3(lockPath, "wx", 384);
37248
37720
  } catch (error61) {
37249
37721
  if (!(error61 instanceof Error && "code" in error61 && error61.code === "EEXIST")) throw error61;
37250
37722
  await removeStaleCredentialLock(lockPath);
@@ -37258,13 +37730,13 @@ async function withAgentCredentialLock(wallets, clientId, operation) {
37258
37730
  return await operation();
37259
37731
  } finally {
37260
37732
  await handle.close();
37261
- await unlink(lockPath).catch(() => void 0);
37733
+ await unlink2(lockPath).catch(() => void 0);
37262
37734
  }
37263
37735
  }
37264
37736
  async function removeStaleCredentialLock(path) {
37265
37737
  try {
37266
- const metadata = await stat(path);
37267
- if (Date.now() - metadata.mtimeMs > 5 * 6e4) await unlink(path);
37738
+ const metadata = await stat3(path);
37739
+ if (Date.now() - metadata.mtimeMs > 5 * 6e4) await unlink2(path);
37268
37740
  } catch {
37269
37741
  }
37270
37742
  }
@@ -37293,12 +37765,18 @@ function parseTokens(value) {
37293
37765
  if (!isRecord(parsed)) return void 0;
37294
37766
  const accessToken = stringField(parsed, "accessToken");
37295
37767
  const refreshToken = stringField(parsed, "refreshToken");
37768
+ const refreshable = parsed.refreshable;
37296
37769
  const expiresAt = numberField(parsed, "expiresAt");
37297
37770
  const scopes = parsed.scopes;
37298
- if (accessToken === void 0 || refreshToken === void 0 || expiresAt === void 0 || !Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string")) {
37771
+ if (accessToken === void 0 || expiresAt === void 0 || !Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string") || (refreshToken === void 0 ? refreshable !== false : refreshable === false)) {
37299
37772
  return void 0;
37300
37773
  }
37301
- return { accessToken, refreshToken, expiresAt, scopes };
37774
+ return {
37775
+ accessToken,
37776
+ ...refreshToken === void 0 ? {} : { refreshToken },
37777
+ expiresAt,
37778
+ scopes
37779
+ };
37302
37780
  }
37303
37781
  function withBearer(init, accessToken) {
37304
37782
  const headers = new Headers(init?.headers);
@@ -37364,8 +37842,92 @@ function splitScopes(scope) {
37364
37842
  }
37365
37843
 
37366
37844
  // src/ceiling.ts
37367
- import { randomBytes as randomBytes4 } from "node:crypto";
37368
- import { open as open7, readFile as readFile5, stat as stat5, unlink as unlink5 } from "node:fs/promises";
37845
+ import { randomBytes as randomBytes7 } from "node:crypto";
37846
+
37847
+ // src/account-movement-lock.ts
37848
+ import { randomBytes as randomBytes3 } from "node:crypto";
37849
+ import { mkdir as mkdir5, open as open4, readFile as readFile4, stat as stat4, unlink as unlink3 } from "node:fs/promises";
37850
+ import { join as join4 } from "node:path";
37851
+ var AccountMovementLockedError = class extends Error {
37852
+ constructor(account) {
37853
+ super(`Another capital movement is already running for account ${account}.`);
37854
+ this.account = account;
37855
+ this.name = "AccountMovementLockedError";
37856
+ }
37857
+ account;
37858
+ code = "account_movement_locked";
37859
+ };
37860
+ async function withAccountMovementLock(home, account, timeoutMs, operation, signal) {
37861
+ const directory = join4(home, "movements");
37862
+ await mkdir5(directory, { recursive: true, mode: 448 });
37863
+ const path = join4(directory, `.${account}.lock`);
37864
+ const deadline = Date.now() + (timeoutMs ?? 2e3);
37865
+ const owner = JSON.stringify({ pid: process.pid, token: randomBytes3(16).toString("hex") });
37866
+ let handle;
37867
+ while (handle === void 0) {
37868
+ signal?.throwIfAborted();
37869
+ try {
37870
+ handle = await open4(path, "wx", 384);
37871
+ await handle.writeFile(owner, "utf8");
37872
+ await handle.sync();
37873
+ } catch (error61) {
37874
+ if (!(error61 instanceof Error && "code" in error61 && error61.code === "EEXIST")) throw error61;
37875
+ await removeStaleLock2(path);
37876
+ if (Date.now() >= deadline) throw new AccountMovementLockedError(account);
37877
+ await new Promise((resolve) => setTimeout(resolve, 20));
37878
+ }
37879
+ }
37880
+ const ownedHandle = handle;
37881
+ const heartbeat = setInterval(() => {
37882
+ const time3 = /* @__PURE__ */ new Date();
37883
+ void ownedHandle.utimes(time3, time3).catch(() => void 0);
37884
+ }, 3e4);
37885
+ heartbeat.unref();
37886
+ try {
37887
+ return await operation();
37888
+ } finally {
37889
+ clearInterval(heartbeat);
37890
+ await ownedHandle.close();
37891
+ await unlinkOwnedLock(path, owner);
37892
+ }
37893
+ }
37894
+ async function removeStaleLock2(path) {
37895
+ try {
37896
+ const metadata = await stat4(path);
37897
+ if (Date.now() - metadata.mtimeMs <= 12e4) return;
37898
+ const owner = await readFile4(path, "utf8").catch(() => "");
37899
+ const pid = lockOwnerPid2(owner);
37900
+ if (pid !== void 0 && processIsAlive2(pid)) return;
37901
+ await unlinkOwnedLock(path, owner);
37902
+ } catch (error61) {
37903
+ if (!isMissingFile(error61)) throw error61;
37904
+ }
37905
+ }
37906
+ async function unlinkOwnedLock(path, owner) {
37907
+ try {
37908
+ if (await readFile4(path, "utf8") === owner) await unlink3(path);
37909
+ } catch (error61) {
37910
+ if (!isMissingFile(error61)) throw error61;
37911
+ }
37912
+ }
37913
+ function lockOwnerPid2(owner) {
37914
+ try {
37915
+ const value = JSON.parse(owner);
37916
+ if (typeof value !== "object" || value === null) return void 0;
37917
+ const pid = Reflect.get(value, "pid");
37918
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
37919
+ } catch {
37920
+ return void 0;
37921
+ }
37922
+ }
37923
+ function processIsAlive2(pid) {
37924
+ try {
37925
+ process.kill(pid, 0);
37926
+ return true;
37927
+ } catch (error61) {
37928
+ return error61 instanceof Error && "code" in error61 && error61.code === "EPERM";
37929
+ }
37930
+ }
37369
37931
 
37370
37932
  // src/authorization-state.ts
37371
37933
  var EIP3009_AUTHORIZATION_STATE_ABI = [
@@ -37429,9 +37991,47 @@ function missingAuthorization(receipt) {
37429
37991
  return `Receipt ${receipt.id} records no signed payment authorization, so nothing from that call can settle.`;
37430
37992
  }
37431
37993
 
37432
- // src/receipts.ts
37433
- import { mkdir as mkdir3, open as open3, readFile as readFile2, rename, writeFile } from "node:fs/promises";
37434
- import { dirname as dirname3 } from "node:path";
37994
+ // src/movement.ts
37995
+ import { randomBytes as randomBytes6 } from "node:crypto";
37996
+ import { chmod as chmod3, mkdir as mkdir10, open as open9, readFile as readFile9, readdir as readdir3, rename as rename6, unlink as unlink7 } from "node:fs/promises";
37997
+ import { join as join5 } from "node:path";
37998
+
37999
+ // src/allocation.ts
38000
+ var AllocationError = class extends Error {
38001
+ constructor(code, message) {
38002
+ super(message);
38003
+ this.code = code;
38004
+ this.name = "AllocationError";
38005
+ }
38006
+ code;
38007
+ };
38008
+ function parseUsdCents(value) {
38009
+ const raw = String(value);
38010
+ if (!/^\d+(?:\.\d{1,2})?$/.test(raw)) {
38011
+ throw new AllocationError("invalid_amount", "Enter a positive USDC amount in whole cents.");
38012
+ }
38013
+ const [whole = "0", fraction = ""] = raw.split(".");
38014
+ const cents = BigInt(whole) * 100n + BigInt(fraction.padEnd(2, "0") || "0");
38015
+ if (cents <= 0n) {
38016
+ throw new AllocationError("invalid_amount", "Enter a positive USDC amount in whole cents.");
38017
+ }
38018
+ return cents;
38019
+ }
38020
+
38021
+ // src/movement-open.ts
38022
+ function isOpenMovementLeg(leg, journalEvidence) {
38023
+ if (leg.status === "sent" || leg.status === "cancelled") return false;
38024
+ if (leg.restored === true) return true;
38025
+ if (leg.status === "planned" || leg.status === "unknown") return true;
38026
+ if (leg.status !== "failed") return false;
38027
+ if (leg.retryable === true) return true;
38028
+ return journalEvidence === "signed";
38029
+ }
38030
+
38031
+ // src/spend-policy.ts
38032
+ import { randomBytes as randomBytes4 } from "node:crypto";
38033
+ import { mkdir as mkdir6, open as open5, readFile as readFile5, rename as rename3, stat as stat5, unlink as unlink4, writeFile as writeFile2 } from "node:fs/promises";
38034
+ import { dirname as dirname4 } from "node:path";
37435
38035
 
37436
38036
  // src/wallet-name.ts
37437
38037
  var DEFAULT_WALLET_NAME = "main";
@@ -37441,11 +38041,230 @@ var walletNameSchema = external_exports.string().regex(
37441
38041
  "A wallet name is 1 to 32 characters of lowercase letters, digits and dashes, starting with a letter or digit."
37442
38042
  );
37443
38043
 
38044
+ // src/spend-policy.ts
38045
+ var ledgerSchema = external_exports.object({
38046
+ date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/),
38047
+ spentAtomic: external_exports.string().regex(/^\d+$/)
38048
+ });
38049
+ var spendReservationSchema = external_exports.object({
38050
+ id: external_exports.string().min(1),
38051
+ amountAtomic: external_exports.string().regex(/^\d+$/)
38052
+ });
38053
+ var ledgerRowSchema = ledgerSchema.extend({
38054
+ wallet: walletNameSchema.optional(),
38055
+ reservations: external_exports.array(spendReservationSchema).optional()
38056
+ });
38057
+ var ledgerFileSchema = external_exports.union([
38058
+ external_exports.object({ version: external_exports.literal(1), rows: external_exports.array(ledgerRowSchema) }),
38059
+ ledgerRowSchema
38060
+ ]);
38061
+ var SpendCapError = class extends Error {
38062
+ constructor(code, message) {
38063
+ super(message);
38064
+ this.code = code;
38065
+ this.name = "SpendCapError";
38066
+ }
38067
+ code;
38068
+ };
38069
+ function utcDateKey(now = /* @__PURE__ */ new Date()) {
38070
+ return now.toISOString().slice(0, 10);
38071
+ }
38072
+ async function readStoredSpendLedgerRows(path, now) {
38073
+ let raw;
38074
+ try {
38075
+ raw = await readFile5(path, "utf8");
38076
+ } catch (error61) {
38077
+ if (isMissingFile(error61)) return [];
38078
+ throw error61;
38079
+ }
38080
+ const parsed = ledgerFileSchema.parse(JSON.parse(raw));
38081
+ const rows = "rows" in parsed ? parsed.rows : [parsed];
38082
+ const today = utcDateKey(now);
38083
+ return rows.filter((row) => row.date === today).map((row) => ({
38084
+ date: row.date,
38085
+ spentAtomic: row.spentAtomic,
38086
+ wallet: row.wallet ?? DEFAULT_WALLET_NAME,
38087
+ ...row.reservations === void 0 ? {} : { reservations: row.reservations }
38088
+ }));
38089
+ }
38090
+ async function reserveSpend(amountAtomic, caps, options) {
38091
+ if (amountAtomic < 0n) {
38092
+ throw new Error("Spend amount cannot be negative.");
38093
+ }
38094
+ const perCallAtomic = parseAtomicCap(caps.perCallAtomic, "per-call");
38095
+ const perDayAtomic = parseAtomicCap(caps.perDayAtomic, "per-day");
38096
+ if (amountAtomic > perCallAtomic) {
38097
+ throw new SpendCapError(
38098
+ "per_call_cap_exceeded",
38099
+ `Payment quote ${amountAtomic} atomic USDC exceeds the per-call cap ${perCallAtomic}. Refusing to sign.`
38100
+ );
38101
+ }
38102
+ const ledgerPath = options?.ledgerPath ?? getAgentCashPaths().ledger;
38103
+ const now = options?.now ?? /* @__PURE__ */ new Date();
38104
+ const wallet = options?.wallet ?? DEFAULT_WALLET_NAME;
38105
+ return await withLedgerLock(ledgerPath, async () => {
38106
+ const rows = await readStoredSpendLedgerRows(ledgerPath, now);
38107
+ const current = rows.find((row) => row.wallet === wallet);
38108
+ if (options?.reservationId !== void 0) {
38109
+ if (current?.reservations?.some((reservation) => reservation.id === options.reservationId)) {
38110
+ throw new Error(`Spend reservation ${options.reservationId} already exists.`);
38111
+ }
38112
+ }
38113
+ const spentAtomic = current?.spentAtomic ?? "0";
38114
+ const nextSpent = BigInt(spentAtomic) + amountAtomic;
38115
+ if (nextSpent > perDayAtomic) {
38116
+ throw new SpendCapError(
38117
+ "per_day_cap_exceeded",
38118
+ `Payment quote ${amountAtomic} atomic USDC would raise today's spend to ${nextSpent} for wallet ${wallet}, above the per-day cap ${perDayAtomic}. Refusing to sign.`
38119
+ );
38120
+ }
38121
+ const nextRow = {
38122
+ date: utcDateKey(now),
38123
+ spentAtomic: nextSpent.toString(),
38124
+ wallet,
38125
+ ...(current?.reservations?.length ?? 0) === 0 && options?.reservationId === void 0 ? {} : {
38126
+ reservations: [
38127
+ ...current?.reservations ?? [],
38128
+ ...options?.reservationId === void 0 ? [] : [{ id: options.reservationId, amountAtomic: amountAtomic.toString() }]
38129
+ ]
38130
+ }
38131
+ };
38132
+ const nextRows = [...rows.filter((row) => row.wallet !== wallet), nextRow].sort(
38133
+ (a, b) => a.wallet.localeCompare(b.wallet)
38134
+ );
38135
+ await writeLedgerAtomically(ledgerPath, nextRows);
38136
+ return { date: nextRow.date, spentAtomic: nextRow.spentAtomic };
38137
+ });
38138
+ }
38139
+ async function releaseSpend(amountAtomic, options) {
38140
+ if (amountAtomic < 0n) {
38141
+ throw new Error("Spend amount cannot be negative.");
38142
+ }
38143
+ const ledgerPath = options.ledgerPath ?? getAgentCashPaths().ledger;
38144
+ const now = options.now ?? /* @__PURE__ */ new Date();
38145
+ const wallet = options.wallet ?? DEFAULT_WALLET_NAME;
38146
+ return await withLedgerLock(ledgerPath, async () => {
38147
+ const today = utcDateKey(now);
38148
+ const rows = await readStoredSpendLedgerRows(ledgerPath, now);
38149
+ const current = rows.find((row) => row.wallet === wallet);
38150
+ if (options.reservedOn !== today) {
38151
+ return { date: today, spentAtomic: current?.spentAtomic ?? "0" };
38152
+ }
38153
+ const reservation = options.reservationId === void 0 ? void 0 : current?.reservations?.find((candidate) => candidate.id === options.reservationId);
38154
+ if (options.reservationId !== void 0 && reservation === void 0) {
38155
+ return { date: today, spentAtomic: current?.spentAtomic ?? "0" };
38156
+ }
38157
+ if (reservation !== void 0 && reservation.amountAtomic !== amountAtomic.toString()) {
38158
+ throw new Error(`Spend reservation ${options.reservationId} has a different amount.`);
38159
+ }
38160
+ const spentAtomic = BigInt(current?.spentAtomic ?? "0");
38161
+ const releasedAmount = reservation === void 0 ? amountAtomic : BigInt(reservation.amountAtomic);
38162
+ const nextSpent = spentAtomic > releasedAmount ? spentAtomic - releasedAmount : 0n;
38163
+ const remainingReservations = current?.reservations?.filter(
38164
+ (candidate) => candidate.id !== options.reservationId
38165
+ );
38166
+ const nextRow = {
38167
+ date: today,
38168
+ spentAtomic: nextSpent.toString(),
38169
+ wallet,
38170
+ ...remainingReservations === void 0 || remainingReservations.length === 0 ? {} : { reservations: remainingReservations }
38171
+ };
38172
+ const nextRows = [...rows.filter((row) => row.wallet !== wallet), nextRow].sort(
38173
+ (a, b) => a.wallet.localeCompare(b.wallet)
38174
+ );
38175
+ await writeLedgerAtomically(ledgerPath, nextRows);
38176
+ return { date: nextRow.date, spentAtomic: nextRow.spentAtomic };
38177
+ });
38178
+ }
38179
+ function parseAtomicCap(value, label) {
38180
+ if (!/^\d+$/.test(value)) {
38181
+ throw new Error(`Invalid ${label} spend cap ${JSON.stringify(value)}.`);
38182
+ }
38183
+ return BigInt(value);
38184
+ }
38185
+ async function writeLedgerAtomically(path, rows) {
38186
+ const ledger = rows.length === 1 && rows[0].wallet === DEFAULT_WALLET_NAME ? {
38187
+ date: rows[0].date,
38188
+ spentAtomic: rows[0].spentAtomic,
38189
+ ...rows[0].reservations === void 0 ? {} : { reservations: rows[0].reservations }
38190
+ } : { version: 1, rows };
38191
+ await mkdir6(dirname4(path), { recursive: true, mode: 448 });
38192
+ const temporaryPath = `${path}.${process.pid}.${randomBytes4(6).toString("hex")}.tmp`;
38193
+ await writeFile2(temporaryPath, `${JSON.stringify(ledger, null, 2)}
38194
+ `, {
38195
+ encoding: "utf8",
38196
+ mode: 384,
38197
+ flag: "wx"
38198
+ });
38199
+ await rename3(temporaryPath, path);
38200
+ }
38201
+ async function withLedgerLock(path, operation) {
38202
+ await mkdir6(dirname4(path), { recursive: true, mode: 448 });
38203
+ const lockPath = `${path}.lock`;
38204
+ const deadline = Date.now() + 2e3;
38205
+ let handle;
38206
+ while (!handle) {
38207
+ try {
38208
+ handle = await open5(lockPath, "wx", 384);
38209
+ } catch (error61) {
38210
+ if (!(error61 instanceof Error && "code" in error61 && error61.code === "EEXIST")) {
38211
+ throw error61;
38212
+ }
38213
+ await removeStaleLock3(lockPath);
38214
+ if (Date.now() >= deadline) {
38215
+ throw new Error("Timed out waiting for the spend ledger lock.");
38216
+ }
38217
+ await new Promise((resolve) => setTimeout(resolve, 20));
38218
+ }
38219
+ }
38220
+ try {
38221
+ return await operation();
38222
+ } finally {
38223
+ await handle.close();
38224
+ await unlink4(lockPath).catch(() => void 0);
38225
+ }
38226
+ }
38227
+ async function removeStaleLock3(path) {
38228
+ try {
38229
+ const metadata = await stat5(path);
38230
+ if (Date.now() - metadata.mtimeMs > 3e4) {
38231
+ await unlink4(path);
38232
+ }
38233
+ } catch (error61) {
38234
+ if (!isMissingFile(error61)) {
38235
+ throw error61;
38236
+ }
38237
+ }
38238
+ }
38239
+
38240
+ // src/transfer-journal.ts
38241
+ import { readFile as readFile7 } from "node:fs/promises";
38242
+
38243
+ // src/receipts.ts
38244
+ import { mkdir as mkdir7, open as open6, readFile as readFile6, rename as rename4, writeFile as writeFile3 } from "node:fs/promises";
38245
+ import { dirname as dirname5 } from "node:path";
38246
+
38247
+ // src/agent-run.ts
38248
+ var RUN_ID_PATTERN = /^run_[0-9a-f]{24}$/;
38249
+ var runRefSchema = external_exports.strictObject({
38250
+ id: external_exports.string().regex(RUN_ID_PATTERN),
38251
+ swarm: external_exports.string().optional(),
38252
+ member: external_exports.string().optional(),
38253
+ parentRunId: external_exports.string().optional()
38254
+ });
38255
+
37444
38256
  // src/receipts.ts
38257
+ var RECEIPT_JOURNAL_BUSY_MESSAGE = "The receipt journal is busy. Wait for the other vapi command to finish, then retry.";
38258
+ async function withReceiptJournalLock(path, operation) {
38259
+ return await withFileLock(`${path}.lock`, operation, {
38260
+ lockedMessage: RECEIPT_JOURNAL_BUSY_MESSAGE
38261
+ });
38262
+ }
37445
38263
  var durationSchema = external_exports.number().nonnegative().finite();
37446
38264
  var receiptSchema = external_exports.strictObject({
37447
38265
  id: external_exports.string().min(1),
37448
38266
  timestamp: external_exports.iso.datetime(),
38267
+ run: runRefSchema.optional(),
37449
38268
  kind: external_exports.literal("transfer").optional(),
37450
38269
  wallet: walletNameSchema.optional(),
37451
38270
  resourceUrl: external_exports.url(),
@@ -37552,20 +38371,22 @@ async function appendReceipt(receipt, path = getVapiPaths().receipts, options =
37552
38371
  const parsed = parseReceipt(
37553
38372
  receipt.wallet === void 0 && options.wallet !== void 0 ? { ...receipt, wallet: options.wallet } : receipt
37554
38373
  );
37555
- await mkdir3(dirname3(path), { recursive: true, mode: 448 });
37556
- const handle = await open3(path, "a", 384);
37557
- try {
37558
- await handle.writeFile(`${JSON.stringify(parsed)}
38374
+ await withReceiptJournalLock(path, async () => {
38375
+ await mkdir7(dirname5(path), { recursive: true, mode: 448 });
38376
+ const handle = await open6(path, "a", 384);
38377
+ try {
38378
+ await handle.writeFile(`${JSON.stringify(parsed)}
37559
38379
  `, "utf8");
37560
- } finally {
37561
- await handle.close();
37562
- }
38380
+ } finally {
38381
+ await handle.close();
38382
+ }
38383
+ });
37563
38384
  return parsed;
37564
38385
  }
37565
38386
  async function readReceipts(path = getVapiPaths().receipts, options = {}) {
37566
38387
  let raw;
37567
38388
  try {
37568
- raw = await readFile2(path, "utf8");
38389
+ raw = await readFile6(path, "utf8");
37569
38390
  } catch (error61) {
37570
38391
  if (isMissingFile(error61)) return [];
37571
38392
  throw error61;
@@ -37607,6 +38428,45 @@ function filterReceiptsByWallet(receipts, name) {
37607
38428
  return receipts.filter((receipt) => receiptWallet(receipt) === name);
37608
38429
  }
37609
38430
 
38431
+ // src/transfer-journal.ts
38432
+ async function readTransferJournal(home, from15, nonce) {
38433
+ let raw;
38434
+ try {
38435
+ raw = await readFile7(getVapiPaths(home).receipts, "utf8");
38436
+ } catch (error61) {
38437
+ if (isMissingFile(error61)) return { signed: false };
38438
+ throw error61;
38439
+ }
38440
+ let latest;
38441
+ let request;
38442
+ for (const [index2, line] of raw.split("\n").entries()) {
38443
+ if (!line.trim()) continue;
38444
+ let receipt;
38445
+ try {
38446
+ receipt = parseReceipt(JSON.parse(line));
38447
+ } catch (error61) {
38448
+ throw new Error(`Invalid receipt on JSONL line ${index2 + 1}.`, { cause: error61 });
38449
+ }
38450
+ if (receiptWallet(receipt) !== from15 || receipt.transfer?.nonce.toLowerCase() !== nonce.toLowerCase()) {
38451
+ continue;
38452
+ }
38453
+ latest = receipt;
38454
+ if (receipt.transfer.request !== void 0) request = receipt;
38455
+ }
38456
+ return {
38457
+ signed: latest !== void 0,
38458
+ ...latest === void 0 ? {} : { latest },
38459
+ ...request === void 0 ? {} : { request },
38460
+ ...request?.transfer?.request?.authorization.validBefore === void 0 ? {} : { validBefore: request.transfer.request.authorization.validBefore },
38461
+ ...(latest?.transfer?.reservedOn ?? request?.transfer?.reservedOn) === void 0 ? {} : { reservedOn: latest?.transfer?.reservedOn ?? request?.transfer?.reservedOn }
38462
+ };
38463
+ }
38464
+
38465
+ // src/transfer.ts
38466
+ import { randomBytes as randomBytes5, randomUUID } from "node:crypto";
38467
+ import { mkdir as mkdir9, open as open8, stat as stat7, unlink as unlink6 } from "node:fs/promises";
38468
+ import { dirname as dirname6 } from "node:path";
38469
+
37610
38470
  // src/siblings.ts
37611
38471
  var SiblingsError = class extends Error {
37612
38472
  constructor(code, message, status) {
@@ -37645,8 +38505,8 @@ async function fetchSiblings(args) {
37645
38505
  try {
37646
38506
  response = await withAgentCredentialLock(args.wallets, `wallet:${args.account}`, async () => {
37647
38507
  await args.wallets.reload();
37648
- const link = args.wallets.entry(args.account)?.link;
37649
- if (link === void 0 || new URL(link.apiBase).origin !== new URL(args.apiBase).origin) {
38508
+ const link2 = args.wallets.entry(args.account)?.link;
38509
+ if (link2 === void 0 || new URL(link2.apiBase).origin !== new URL(args.apiBase).origin) {
37650
38510
  throw new SiblingsError(
37651
38511
  "not_linked",
37652
38512
  "The selected account is not linked to this vAPI server."
@@ -37660,7 +38520,7 @@ async function fetchSiblings(args) {
37660
38520
  ...args.fetchImpl === void 0 ? {} : { fetchImpl: args.fetchImpl },
37661
38521
  ...args.now === void 0 ? {} : { now: args.now }
37662
38522
  },
37663
- new URL("/api/agents/self/siblings", new URL(link.apiBase).origin).toString(),
38523
+ new URL("/api/agents/self/siblings", new URL(link2.apiBase).origin).toString(),
37664
38524
  { method: "GET", signal }
37665
38525
  );
37666
38526
  });
@@ -37699,6 +38559,20 @@ function invalidResponseError() {
37699
38559
  return new SiblingsError("invalid_response", "The vAPI siblings response was invalid.");
37700
38560
  }
37701
38561
 
38562
+ // src/wallet-store.ts
38563
+ import {
38564
+ chmod as chmod2,
38565
+ lstat,
38566
+ mkdir as mkdir8,
38567
+ open as open7,
38568
+ readdir as readdir2,
38569
+ readFile as readFile8,
38570
+ rename as rename5,
38571
+ stat as stat6,
38572
+ unlink as unlink5,
38573
+ writeFile as writeFile4
38574
+ } from "node:fs/promises";
38575
+
37702
38576
  // ../../node_modules/.pnpm/@solana+errors@5.5.1_typescript@6.0.3/node_modules/@solana/errors/dist/index.node.mjs
37703
38577
  var SOLANA_ERROR__BLOCK_HEIGHT_EXCEEDED = 1;
37704
38578
  var SOLANA_ERROR__INVALID_NONCE = 2;
@@ -41618,6 +42492,62 @@ async function signModifyingAndPartialTransactionSigners(transactionMessage, mod
41618
42492
  }
41619
42493
  var o2 = globalThis.TextEncoder;
41620
42494
 
42495
+ // src/wallet-store.ts
42496
+ var agentLinkSchema = external_exports.object({
42497
+ apiBase: external_exports.string(),
42498
+ clientId: external_exports.string(),
42499
+ owner: external_exports.custom((value) => typeof value === "string" && value.startsWith("0x")),
42500
+ label: external_exports.string(),
42501
+ scopes: external_exports.array(external_exports.string()),
42502
+ linkedAt: external_exports.iso.datetime(),
42503
+ routerBaseUrl: external_exports.string().optional()
42504
+ });
42505
+ var ROUTER_TOPUP_TIERS = [1, 5, 20, 50];
42506
+ var routerRefillSchema = external_exports.object({
42507
+ belowUsd: external_exports.number().finite().min(0),
42508
+ tierUsd: external_exports.union([external_exports.literal(1), external_exports.literal(5), external_exports.literal(20), external_exports.literal(50)])
42509
+ });
42510
+ var ceilingSweepPendingSchema = external_exports.strictObject({
42511
+ owner: external_exports.string().regex(/^0x[0-9a-fA-F]{40}$/),
42512
+ target: external_exports.string().regex(/^0x[0-9a-fA-F]{40}$/).optional(),
42513
+ amountAtomic: external_exports.string().regex(/^\d+$/),
42514
+ nonce: external_exports.string().regex(/^0x[0-9a-fA-F]{64}$/),
42515
+ createdAt: external_exports.iso.datetime(),
42516
+ status: external_exports.enum(["planned", "unknown"])
42517
+ });
42518
+ var walletEntrySchema = external_exports.object({
42519
+ createdAt: external_exports.iso.datetime(),
42520
+ label: external_exports.string().trim().min(1).max(80).optional(),
42521
+ spendCaps: spendCapsSchema,
42522
+ // On disk, null explicitly disables ceiling sweeps; an absent field is the
42523
+ // backwards-compatible 5 USDC default and is not written during reads.
42524
+ ceilingAtomic: external_exports.string().regex(/^\d+$/).nullable().optional(),
42525
+ ceilingSweepAttemptedAt: external_exports.iso.datetime().optional(),
42526
+ ceilingSweepPending: ceilingSweepPendingSchema.optional(),
42527
+ routerRefill: routerRefillSchema.optional(),
42528
+ link: agentLinkSchema.optional()
42529
+ });
42530
+ var walletRegistrySchema = external_exports.object({
42531
+ version: external_exports.literal(1),
42532
+ default: walletNameSchema.optional(),
42533
+ wallets: external_exports.record(walletNameSchema, walletEntrySchema)
42534
+ });
42535
+ async function spendCapsForWallet(store, name = DEFAULT_WALLET_NAME) {
42536
+ const entry = store.entry(name);
42537
+ if (entry) return { ...entry.spendCaps };
42538
+ return await readConfiguredSpendCaps(getVapiPaths(store.home).config);
42539
+ }
42540
+ async function readConfiguredSpendCaps(configPath) {
42541
+ try {
42542
+ const parsed = JSON.parse(await readFile8(configPath, "utf8"));
42543
+ const caps = typeof parsed === "object" && parsed !== null ? Reflect.get(parsed, "spendCaps") : void 0;
42544
+ const result = spendCapsSchema.safeParse(caps);
42545
+ if (result.success) return result.data;
42546
+ } catch {
42547
+ }
42548
+ return { ...DEFAULT_SPEND_CAPS };
42549
+ }
42550
+
41621
42551
  // ../../node_modules/.pnpm/@solana-program+token-2022@0.6.1_@solana+kit@5.5.1_typescript@6.0.3__@solana+sysvars@5.5.1_typescript@6.0.3_/node_modules/@solana-program/token-2022/dist/src/index.mjs
41622
42552
  var AccountState = /* @__PURE__ */ ((AccountState2) => {
41623
42553
  AccountState2[AccountState2["Uninitialized"] = 0] = "Uninitialized";
@@ -42627,6 +43557,104 @@ var ExactSvmScheme = class {
42627
43557
  var addressEncoder = getAddressEncoder();
42628
43558
  var compiledMessageDecoder = getCompiledTransactionMessageDecoder();
42629
43559
 
43560
+ // src/svm.ts
43561
+ var MAX_RPC_RESPONSE_BYTES = 4 * 1024 * 1024;
43562
+ function isSolanaAddress(value) {
43563
+ try {
43564
+ address(value);
43565
+ return true;
43566
+ } catch {
43567
+ return false;
43568
+ }
43569
+ }
43570
+ async function readSolanaUsdcBalance(args) {
43571
+ const accounts = await readSolanaUsdcAccounts(args);
43572
+ return accounts.reduce((total, account) => total + account.amountAtomic, 0n);
43573
+ }
43574
+ async function readSolanaUsdcAccounts(args) {
43575
+ if (!isSolanaAddress(args.address)) {
43576
+ throw new Error(`Invalid Solana address: ${args.address}.`);
43577
+ }
43578
+ const rpcUrl = requireRpcUrl(args.network, args.configured);
43579
+ const value = await solanaRpcCall(
43580
+ rpcUrl,
43581
+ "getTokenAccountsByOwner",
43582
+ [
43583
+ args.address,
43584
+ { mint: args.configured.usdc },
43585
+ { encoding: "jsonParsed", commitment: "confirmed" }
43586
+ ],
43587
+ rpcOptionsFrom(args)
43588
+ );
43589
+ return value.map((entry) => {
43590
+ const info = entry.account.data.parsed.info;
43591
+ if (!isSolanaAddress(entry.pubkey) || info.mint !== args.configured.usdc || info.owner !== args.address || info.tokenAmount.decimals !== USDC_DECIMALS || !/^\d+$/.test(info.tokenAmount.amount)) {
43592
+ throw new Error("Solana RPC returned a malformed USDC token account.");
43593
+ }
43594
+ return { address: entry.pubkey, amountAtomic: BigInt(info.tokenAmount.amount) };
43595
+ });
43596
+ }
43597
+ async function solanaRpcCall(rpcUrl, method, params, options) {
43598
+ const fetchImpl = options.fetchImpl ?? createPublicFetch({
43599
+ allowPrivateNetwork: options.allowPrivateNetwork ?? false,
43600
+ ...options.lookup ? { lookup: options.lookup } : {}
43601
+ });
43602
+ const response = await fetchImpl(rpcUrl, {
43603
+ method: "POST",
43604
+ headers: { "content-type": "application/json" },
43605
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
43606
+ redirect: "manual"
43607
+ });
43608
+ if (!response.ok) {
43609
+ discardBody(response);
43610
+ const rateLimit = response.status === 429 ? " (public RPC rate limit)" : "";
43611
+ throw new Error(`Solana RPC ${method} failed with HTTP ${response.status}${rateLimit}.`);
43612
+ }
43613
+ const envelope = await readBoundedRpcJson(response);
43614
+ if (!isRecord2(envelope) || envelope.jsonrpc !== "2.0") {
43615
+ throw new Error(`Solana RPC ${method} returned an invalid JSON-RPC envelope.`);
43616
+ }
43617
+ if (envelope.error !== void 0) {
43618
+ throw new Error(`Solana RPC ${method} failed: ${JSON.stringify(envelope.error)}.`);
43619
+ }
43620
+ const result = envelope.result;
43621
+ if (isRecord2(result) && "value" in result) return result.value;
43622
+ return result;
43623
+ }
43624
+ async function readBoundedRpcJson(response) {
43625
+ if (!response.body) throw new Error("Solana RPC response body is empty.");
43626
+ const reader = response.body.getReader();
43627
+ const chunks = [];
43628
+ let length = 0;
43629
+ while (true) {
43630
+ const next = await reader.read();
43631
+ if (next.done) break;
43632
+ length += next.value.byteLength;
43633
+ if (length > MAX_RPC_RESPONSE_BYTES) {
43634
+ await reader.cancel();
43635
+ throw new Error(`Solana RPC response exceeds ${MAX_RPC_RESPONSE_BYTES} bytes.`);
43636
+ }
43637
+ chunks.push(next.value);
43638
+ }
43639
+ const bytes = new Uint8Array(length);
43640
+ let offset = 0;
43641
+ for (const chunk of chunks) {
43642
+ bytes.set(chunk, offset);
43643
+ offset += chunk.byteLength;
43644
+ }
43645
+ return JSON.parse(new TextDecoder().decode(bytes));
43646
+ }
43647
+ function rpcOptionsFrom(options) {
43648
+ return {
43649
+ allowPrivateNetwork: options.allowPrivateNetwork,
43650
+ ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {},
43651
+ ...options.lookup ? { lookup: options.lookup } : {}
43652
+ };
43653
+ }
43654
+ function isRecord2(value) {
43655
+ return typeof value === "object" && value !== null && !Array.isArray(value);
43656
+ }
43657
+
42630
43658
  // src/x402.ts
42631
43659
  var MAX_CHALLENGE_BYTES = 1048576;
42632
43660
  var MAX_UINT256 = (1n << 256n) - 1n;
@@ -42659,15 +43687,15 @@ var X402Error = class extends Error {
42659
43687
  function isValidPaymentId(value) {
42660
43688
  return typeof value === "string" && PAYMENT_ID_PATTERN.test(value);
42661
43689
  }
42662
- function createPaymentId(randomBytes5 = fillRandomBytes) {
43690
+ function createPaymentId(randomBytes9 = fillRandomBytes) {
42663
43691
  const bytes = new Uint8Array(16);
42664
- randomBytes5(bytes);
43692
+ randomBytes9(bytes);
42665
43693
  return `pay_${bytesToHex(bytes).slice(2)}`;
42666
43694
  }
42667
43695
  function buildPaymentExtensions(serverExtensions, options = {}) {
42668
43696
  const declaredBuilderCode = serverExtensions?.["builder-code"];
42669
- const builderInfo = isRecord2(declaredBuilderCode) ? declaredBuilderCode.info : void 0;
42670
- const serverBuilderInfo = isRecord2(builderInfo) ? builderInfo : void 0;
43697
+ const builderInfo = isRecord3(declaredBuilderCode) ? declaredBuilderCode.info : void 0;
43698
+ const serverBuilderInfo = isRecord3(builderInfo) ? builderInfo : void 0;
42671
43699
  const declaredServerCodes = serverBuilderInfo?.s;
42672
43700
  const serverCodes = typeof declaredServerCodes === "string" ? [declaredServerCodes] : Array.isArray(declaredServerCodes) ? declaredServerCodes : void 0;
42673
43701
  const validServerCodes = [];
@@ -42684,10 +43712,10 @@ function buildPaymentExtensions(serverExtensions, options = {}) {
42684
43712
  ...typeof appCode === "string" && BUILDER_CODE_PATTERN.test(appCode) ? { a: appCode } : {},
42685
43713
  s: [VAPI_BUILDER_CODE, ...validServerCodes]
42686
43714
  },
42687
- ...isRecord2(declaredBuilderCode) && declaredBuilderCode.schema !== void 0 ? { schema: declaredBuilderCode.schema } : {}
43715
+ ...isRecord3(declaredBuilderCode) && declaredBuilderCode.schema !== void 0 ? { schema: declaredBuilderCode.schema } : {}
42688
43716
  };
42689
43717
  const paymentIdentifier = serverExtensions?.["payment-identifier"];
42690
- if (!isRecord2(paymentIdentifier)) {
43718
+ if (!isRecord3(paymentIdentifier)) {
42691
43719
  return { ...serverExtensions, "builder-code": builderCode };
42692
43720
  }
42693
43721
  if (!isValidPaymentId(options.paymentId)) {
@@ -42696,7 +43724,7 @@ function buildPaymentExtensions(serverExtensions, options = {}) {
42696
43724
  "paymentId must be 16-128 characters containing only letters, digits, underscores, or hyphens."
42697
43725
  );
42698
43726
  }
42699
- const paymentInfo = isRecord2(paymentIdentifier.info) ? paymentIdentifier.info : {};
43727
+ const paymentInfo = isRecord3(paymentIdentifier.info) ? paymentIdentifier.info : {};
42700
43728
  return {
42701
43729
  ...serverExtensions,
42702
43730
  "builder-code": builderCode,
@@ -42707,7 +43735,7 @@ function buildPaymentExtensions(serverExtensions, options = {}) {
42707
43735
  };
42708
43736
  }
42709
43737
  function parse402Challenge(challenge2, configuredNetworks, fallbackResourceUrl = "", requiredNetwork, expectedPayTo) {
42710
- if (!isRecord2(challenge2) || !Array.isArray(challenge2.accepts)) {
43738
+ if (!isRecord3(challenge2) || !Array.isArray(challenge2.accepts)) {
42711
43739
  throw new X402Error("invalid_challenge", "x402 challenge has no accepts array.");
42712
43740
  }
42713
43741
  if (challenge2.x402Version !== void 0 && challenge2.x402Version !== 2) {
@@ -42716,7 +43744,7 @@ function parse402Challenge(challenge2, configuredNetworks, fallbackResourceUrl =
42716
43744
  `Unsupported x402 version: ${String(challenge2.x402Version)}.`
42717
43745
  );
42718
43746
  }
42719
- const candidates = challenge2.accepts.filter(isRecord2);
43747
+ const candidates = challenge2.accepts.filter(isRecord3);
42720
43748
  const accepted = candidates.find((candidate) => {
42721
43749
  const network2 = readString(candidate, "network");
42722
43750
  const scheme = readString(candidate, "scheme");
@@ -42905,17 +43933,24 @@ function buildEip3009TypedData(args) {
42905
43933
  if (!Number.isSafeInteger(args.nowSeconds) || args.nowSeconds < 0) {
42906
43934
  throw new Error("nowSeconds must be a non-negative integer.");
42907
43935
  }
43936
+ if (args.maximumValidBeforeSeconds !== void 0 && (!Number.isSafeInteger(args.maximumValidBeforeSeconds) || args.maximumValidBeforeSeconds <= args.nowSeconds)) {
43937
+ throw new Error("The payment allowance has expired; no authorization was signed.");
43938
+ }
42908
43939
  if (!isEvmPaymentRequirements(args.quote.accepted)) {
42909
43940
  throw new Error("EIP-3009 typed data can only be built for an EVM x402 quote.");
42910
43941
  }
42911
43942
  const accepted = args.quote.accepted;
42912
43943
  const windowSeconds = Math.min(MAX_AUTHORIZATION_WINDOW_SECONDS, accepted.maxTimeoutSeconds);
43944
+ const validBefore = Math.min(
43945
+ args.nowSeconds + windowSeconds,
43946
+ args.maximumValidBeforeSeconds ?? Number.MAX_SAFE_INTEGER
43947
+ );
42913
43948
  const authorization = {
42914
43949
  from: getAddress(args.from),
42915
43950
  to: accepted.payTo,
42916
43951
  value: args.quote.amountAtomic.toString(),
42917
43952
  validAfter: "0",
42918
- validBefore: (args.nowSeconds + windowSeconds).toString(),
43953
+ validBefore: validBefore.toString(),
42919
43954
  nonce: args.nonce
42920
43955
  };
42921
43956
  return {
@@ -42941,7 +43976,7 @@ function buildEip3009TypedData(args) {
42941
43976
  };
42942
43977
  }
42943
43978
  async function buildX402Payment(args) {
42944
- const advertisesPaymentIdentifier = isRecord2(args.quote.extensions?.["payment-identifier"]);
43979
+ const advertisesPaymentIdentifier = isRecord3(args.quote.extensions?.["payment-identifier"]);
42945
43980
  const paymentId = advertisesPaymentIdentifier ? args.paymentId ?? createPaymentId() : void 0;
42946
43981
  const extensions = buildPaymentExtensions(args.quote.extensions, { paymentId });
42947
43982
  if (isSvmPaymentRequirements(args.quote.accepted)) {
@@ -42986,7 +44021,8 @@ async function buildX402Payment(args) {
42986
44021
  from: args.signer.address,
42987
44022
  quote: args.quote,
42988
44023
  nonce,
42989
- nowSeconds: args.nowSeconds ?? Math.floor(Date.now() / 1e3)
44024
+ nowSeconds: args.nowSeconds ?? Math.floor(Date.now() / 1e3),
44025
+ ...args.maximumValidBeforeSeconds === void 0 ? {} : { maximumValidBeforeSeconds: args.maximumValidBeforeSeconds }
42990
44026
  });
42991
44027
  const signature = await args.signer.signTypedData(typedData);
42992
44028
  const payload = {
@@ -43009,6 +44045,7 @@ async function buildCompatibleX402Payment(args) {
43009
44045
  signer: args.account,
43010
44046
  quote: args.quote,
43011
44047
  ...args.nowSeconds === void 0 ? {} : { nowSeconds: args.nowSeconds },
44048
+ ...args.maximumValidBeforeSeconds === void 0 ? {} : { maximumValidBeforeSeconds: args.maximumValidBeforeSeconds },
43012
44049
  ...args.nonce === void 0 ? {} : { nonce: args.nonce },
43013
44050
  ...args.fetchImpl === void 0 ? {} : { fetchImpl: args.fetchImpl },
43014
44051
  ...args.paymentId === void 0 ? {} : { paymentId: args.paymentId }
@@ -43068,7 +44105,7 @@ function parseSettlementResponse(headers) {
43068
44105
  }
43069
44106
  }
43070
44107
  function classifySettlement(value) {
43071
- if (!isRecord2(value) || typeof value.success !== "boolean") {
44108
+ if (!isRecord3(value) || typeof value.success !== "boolean") {
43072
44109
  return "unknown";
43073
44110
  }
43074
44111
  return value.success ? "succeeded" : "rejected";
@@ -43164,12 +44201,12 @@ function canonicalTokenDomain(network, configured) {
43164
44201
  return configuredDomain;
43165
44202
  }
43166
44203
  function readBoundedJsonRecord(value) {
43167
- if (!isRecord2(value)) return null;
44204
+ if (!isRecord3(value)) return null;
43168
44205
  const seen = /* @__PURE__ */ new WeakSet();
43169
44206
  const budget = { entries: 0 };
43170
44207
  try {
43171
44208
  const cloned = cloneBoundedJson(value, 0, seen, budget);
43172
- return isRecord2(cloned) ? cloned : null;
44209
+ return isRecord3(cloned) ? cloned : null;
43173
44210
  } catch {
43174
44211
  return null;
43175
44212
  }
@@ -43178,7 +44215,7 @@ function readChallengeExtensions(value) {
43178
44215
  if (value === void 0) return void 0;
43179
44216
  const extensions = readBoundedJsonRecord(value);
43180
44217
  if (extensions) return extensions;
43181
- if (!isRecord2(value)) return void 0;
44218
+ if (!isRecord3(value)) return void 0;
43182
44219
  const paymentExtensions = /* @__PURE__ */ Object.create(null);
43183
44220
  for (const name of ["builder-code", "payment-identifier"]) {
43184
44221
  const extension = readBoundedJsonRecord(value[name]);
@@ -43231,7 +44268,7 @@ function cloneBoundedJson(value, depth, seen, budget) {
43231
44268
  }
43232
44269
  function readResource(value, fallbackUrl) {
43233
44270
  if (typeof value === "string") return { url: value };
43234
- if (!isRecord2(value)) return { url: fallbackUrl };
44271
+ if (!isRecord3(value)) return { url: fallbackUrl };
43235
44272
  const url2 = readString(value, "url") ?? fallbackUrl;
43236
44273
  return {
43237
44274
  url: url2,
@@ -43246,7 +44283,7 @@ function readString(value, key) {
43246
44283
  const field = value[key];
43247
44284
  return typeof field === "string" && field.length > 0 ? field : null;
43248
44285
  }
43249
- function isRecord2(value) {
44286
+ function isRecord3(value) {
43250
44287
  return typeof value === "object" && value !== null && !Array.isArray(value);
43251
44288
  }
43252
44289
  function decodeBase64Json(value) {
@@ -43262,421 +44299,6 @@ function encodeBase64Json(value) {
43262
44299
  return globalThis.btoa(binary);
43263
44300
  }
43264
44301
 
43265
- // src/svm.ts
43266
- var MAX_RPC_RESPONSE_BYTES = 4 * 1024 * 1024;
43267
- function isSolanaAddress(value) {
43268
- try {
43269
- address(value);
43270
- return true;
43271
- } catch {
43272
- return false;
43273
- }
43274
- }
43275
- async function readSolanaUsdcBalance(args) {
43276
- const accounts = await readSolanaUsdcAccounts(args);
43277
- return accounts.reduce((total, account) => total + account.amountAtomic, 0n);
43278
- }
43279
- async function readSolanaUsdcAccounts(args) {
43280
- if (!isSolanaAddress(args.address)) {
43281
- throw new Error(`Invalid Solana address: ${args.address}.`);
43282
- }
43283
- const rpcUrl = requireRpcUrl(args.network, args.configured);
43284
- const value = await solanaRpcCall(
43285
- rpcUrl,
43286
- "getTokenAccountsByOwner",
43287
- [
43288
- args.address,
43289
- { mint: args.configured.usdc },
43290
- { encoding: "jsonParsed", commitment: "confirmed" }
43291
- ],
43292
- rpcOptionsFrom(args)
43293
- );
43294
- return value.map((entry) => {
43295
- const info = entry.account.data.parsed.info;
43296
- if (!isSolanaAddress(entry.pubkey) || info.mint !== args.configured.usdc || info.owner !== args.address || info.tokenAmount.decimals !== USDC_DECIMALS || !/^\d+$/.test(info.tokenAmount.amount)) {
43297
- throw new Error("Solana RPC returned a malformed USDC token account.");
43298
- }
43299
- return { address: entry.pubkey, amountAtomic: BigInt(info.tokenAmount.amount) };
43300
- });
43301
- }
43302
- async function solanaRpcCall(rpcUrl, method, params, options) {
43303
- const fetchImpl = options.fetchImpl ?? createPublicFetch({
43304
- allowPrivateNetwork: options.allowPrivateNetwork ?? false,
43305
- ...options.lookup ? { lookup: options.lookup } : {}
43306
- });
43307
- const response = await fetchImpl(rpcUrl, {
43308
- method: "POST",
43309
- headers: { "content-type": "application/json" },
43310
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
43311
- redirect: "manual"
43312
- });
43313
- if (!response.ok) {
43314
- discardBody(response);
43315
- const rateLimit = response.status === 429 ? " (public RPC rate limit)" : "";
43316
- throw new Error(`Solana RPC ${method} failed with HTTP ${response.status}${rateLimit}.`);
43317
- }
43318
- const envelope = await readBoundedRpcJson(response);
43319
- if (!isRecord3(envelope) || envelope.jsonrpc !== "2.0") {
43320
- throw new Error(`Solana RPC ${method} returned an invalid JSON-RPC envelope.`);
43321
- }
43322
- if (envelope.error !== void 0) {
43323
- throw new Error(`Solana RPC ${method} failed: ${JSON.stringify(envelope.error)}.`);
43324
- }
43325
- const result = envelope.result;
43326
- if (isRecord3(result) && "value" in result) return result.value;
43327
- return result;
43328
- }
43329
- async function readBoundedRpcJson(response) {
43330
- if (!response.body) throw new Error("Solana RPC response body is empty.");
43331
- const reader = response.body.getReader();
43332
- const chunks = [];
43333
- let length = 0;
43334
- while (true) {
43335
- const next = await reader.read();
43336
- if (next.done) break;
43337
- length += next.value.byteLength;
43338
- if (length > MAX_RPC_RESPONSE_BYTES) {
43339
- await reader.cancel();
43340
- throw new Error(`Solana RPC response exceeds ${MAX_RPC_RESPONSE_BYTES} bytes.`);
43341
- }
43342
- chunks.push(next.value);
43343
- }
43344
- const bytes = new Uint8Array(length);
43345
- let offset = 0;
43346
- for (const chunk of chunks) {
43347
- bytes.set(chunk, offset);
43348
- offset += chunk.byteLength;
43349
- }
43350
- return JSON.parse(new TextDecoder().decode(bytes));
43351
- }
43352
- function rpcOptionsFrom(options) {
43353
- return {
43354
- allowPrivateNetwork: options.allowPrivateNetwork,
43355
- ...options.fetchImpl ? { fetchImpl: options.fetchImpl } : {},
43356
- ...options.lookup ? { lookup: options.lookup } : {}
43357
- };
43358
- }
43359
- function isRecord3(value) {
43360
- return typeof value === "object" && value !== null && !Array.isArray(value);
43361
- }
43362
-
43363
- // src/sweep.ts
43364
- var ERC20_ABI = [
43365
- {
43366
- type: "function",
43367
- name: "balanceOf",
43368
- stateMutability: "view",
43369
- inputs: [{ name: "account", type: "address" }],
43370
- outputs: [{ name: "balance", type: "uint256" }]
43371
- },
43372
- {
43373
- type: "function",
43374
- name: "transfer",
43375
- stateMutability: "nonpayable",
43376
- inputs: [
43377
- { name: "to", type: "address" },
43378
- { name: "amount", type: "uint256" }
43379
- ],
43380
- outputs: [{ name: "success", type: "bool" }]
43381
- }
43382
- ];
43383
- async function readUsdcBalance(args) {
43384
- if (isSolanaNetwork(args.network)) {
43385
- return await readSolanaUsdcBalance({
43386
- network: args.network,
43387
- configured: args.configured,
43388
- address: args.address,
43389
- allowPrivateNetwork: args.allowPrivateNetwork,
43390
- ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {},
43391
- ...args.lookup ? { lookup: args.lookup } : {}
43392
- });
43393
- }
43394
- const client = createNetworkPublicClient(args.network, args.configured, {
43395
- allowPrivateNetwork: args.allowPrivateNetwork,
43396
- ...args.fetchImpl ? { fetch: args.fetchImpl } : {},
43397
- ...args.lookup ? { lookup: args.lookup } : {}
43398
- });
43399
- return await client.readContract({
43400
- address: getAddress(
43401
- usesUsdcGas(args.network) ? getNetworkDefinition(args.network).usdc : args.configured.usdc
43402
- ),
43403
- abi: ERC20_ABI,
43404
- functionName: "balanceOf",
43405
- args: [getAddress(args.address)]
43406
- });
43407
- }
43408
-
43409
- // src/transfer.ts
43410
- import { randomBytes as randomBytes3, randomUUID } from "node:crypto";
43411
- import { mkdir as mkdir6, open as open6, stat as stat4, unlink as unlink4 } from "node:fs/promises";
43412
- import { dirname as dirname5 } from "node:path";
43413
-
43414
- // src/spend-policy.ts
43415
- import { randomBytes as randomBytes2 } from "node:crypto";
43416
- import { mkdir as mkdir4, open as open4, readFile as readFile3, rename as rename2, stat as stat2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
43417
- import { dirname as dirname4 } from "node:path";
43418
- var ledgerSchema = external_exports.object({
43419
- date: external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/),
43420
- spentAtomic: external_exports.string().regex(/^\d+$/)
43421
- });
43422
- var spendReservationSchema = external_exports.object({
43423
- id: external_exports.string().min(1),
43424
- amountAtomic: external_exports.string().regex(/^\d+$/)
43425
- });
43426
- var ledgerRowSchema = ledgerSchema.extend({
43427
- wallet: walletNameSchema.optional(),
43428
- reservations: external_exports.array(spendReservationSchema).optional()
43429
- });
43430
- var ledgerFileSchema = external_exports.union([
43431
- external_exports.object({ version: external_exports.literal(1), rows: external_exports.array(ledgerRowSchema) }),
43432
- ledgerRowSchema
43433
- ]);
43434
- var SpendCapError = class extends Error {
43435
- constructor(code, message) {
43436
- super(message);
43437
- this.code = code;
43438
- this.name = "SpendCapError";
43439
- }
43440
- code;
43441
- };
43442
- function utcDateKey(now = /* @__PURE__ */ new Date()) {
43443
- return now.toISOString().slice(0, 10);
43444
- }
43445
- async function readStoredSpendLedgerRows(path, now) {
43446
- let raw;
43447
- try {
43448
- raw = await readFile3(path, "utf8");
43449
- } catch (error61) {
43450
- if (isMissingFile(error61)) return [];
43451
- throw error61;
43452
- }
43453
- const parsed = ledgerFileSchema.parse(JSON.parse(raw));
43454
- const rows = "rows" in parsed ? parsed.rows : [parsed];
43455
- const today = utcDateKey(now);
43456
- return rows.filter((row) => row.date === today).map((row) => ({
43457
- date: row.date,
43458
- spentAtomic: row.spentAtomic,
43459
- wallet: row.wallet ?? DEFAULT_WALLET_NAME,
43460
- ...row.reservations === void 0 ? {} : { reservations: row.reservations }
43461
- }));
43462
- }
43463
- async function reserveSpend(amountAtomic, caps, options) {
43464
- if (amountAtomic < 0n) {
43465
- throw new Error("Spend amount cannot be negative.");
43466
- }
43467
- const perCallAtomic = parseAtomicCap(caps.perCallAtomic, "per-call");
43468
- const perDayAtomic = parseAtomicCap(caps.perDayAtomic, "per-day");
43469
- if (amountAtomic > perCallAtomic) {
43470
- throw new SpendCapError(
43471
- "per_call_cap_exceeded",
43472
- `Payment quote ${amountAtomic} atomic USDC exceeds the per-call cap ${perCallAtomic}. Refusing to sign.`
43473
- );
43474
- }
43475
- const ledgerPath = options?.ledgerPath ?? getAgentCashPaths().ledger;
43476
- const now = options?.now ?? /* @__PURE__ */ new Date();
43477
- const wallet = options?.wallet ?? DEFAULT_WALLET_NAME;
43478
- return await withLedgerLock(ledgerPath, async () => {
43479
- const rows = await readStoredSpendLedgerRows(ledgerPath, now);
43480
- const current = rows.find((row) => row.wallet === wallet);
43481
- if (options?.reservationId !== void 0) {
43482
- if (current?.reservations?.some((reservation) => reservation.id === options.reservationId)) {
43483
- throw new Error(`Spend reservation ${options.reservationId} already exists.`);
43484
- }
43485
- }
43486
- const spentAtomic = current?.spentAtomic ?? "0";
43487
- const nextSpent = BigInt(spentAtomic) + amountAtomic;
43488
- if (nextSpent > perDayAtomic) {
43489
- throw new SpendCapError(
43490
- "per_day_cap_exceeded",
43491
- `Payment quote ${amountAtomic} atomic USDC would raise today's spend to ${nextSpent} for wallet ${wallet}, above the per-day cap ${perDayAtomic}. Refusing to sign.`
43492
- );
43493
- }
43494
- const nextRow = {
43495
- date: utcDateKey(now),
43496
- spentAtomic: nextSpent.toString(),
43497
- wallet,
43498
- ...(current?.reservations?.length ?? 0) === 0 && options?.reservationId === void 0 ? {} : {
43499
- reservations: [
43500
- ...current?.reservations ?? [],
43501
- ...options?.reservationId === void 0 ? [] : [{ id: options.reservationId, amountAtomic: amountAtomic.toString() }]
43502
- ]
43503
- }
43504
- };
43505
- const nextRows = [...rows.filter((row) => row.wallet !== wallet), nextRow].sort(
43506
- (a, b) => a.wallet.localeCompare(b.wallet)
43507
- );
43508
- await writeLedgerAtomically(ledgerPath, nextRows);
43509
- return { date: nextRow.date, spentAtomic: nextRow.spentAtomic };
43510
- });
43511
- }
43512
- async function releaseSpend(amountAtomic, options) {
43513
- if (amountAtomic < 0n) {
43514
- throw new Error("Spend amount cannot be negative.");
43515
- }
43516
- const ledgerPath = options.ledgerPath ?? getAgentCashPaths().ledger;
43517
- const now = options.now ?? /* @__PURE__ */ new Date();
43518
- const wallet = options.wallet ?? DEFAULT_WALLET_NAME;
43519
- return await withLedgerLock(ledgerPath, async () => {
43520
- const today = utcDateKey(now);
43521
- const rows = await readStoredSpendLedgerRows(ledgerPath, now);
43522
- const current = rows.find((row) => row.wallet === wallet);
43523
- if (options.reservedOn !== today) {
43524
- return { date: today, spentAtomic: current?.spentAtomic ?? "0" };
43525
- }
43526
- const reservation = options.reservationId === void 0 ? void 0 : current?.reservations?.find((candidate) => candidate.id === options.reservationId);
43527
- if (options.reservationId !== void 0 && reservation === void 0) {
43528
- return { date: today, spentAtomic: current?.spentAtomic ?? "0" };
43529
- }
43530
- if (reservation !== void 0 && reservation.amountAtomic !== amountAtomic.toString()) {
43531
- throw new Error(`Spend reservation ${options.reservationId} has a different amount.`);
43532
- }
43533
- const spentAtomic = BigInt(current?.spentAtomic ?? "0");
43534
- const releasedAmount = reservation === void 0 ? amountAtomic : BigInt(reservation.amountAtomic);
43535
- const nextSpent = spentAtomic > releasedAmount ? spentAtomic - releasedAmount : 0n;
43536
- const remainingReservations = current?.reservations?.filter(
43537
- (candidate) => candidate.id !== options.reservationId
43538
- );
43539
- const nextRow = {
43540
- date: today,
43541
- spentAtomic: nextSpent.toString(),
43542
- wallet,
43543
- ...remainingReservations === void 0 || remainingReservations.length === 0 ? {} : { reservations: remainingReservations }
43544
- };
43545
- const nextRows = [...rows.filter((row) => row.wallet !== wallet), nextRow].sort(
43546
- (a, b) => a.wallet.localeCompare(b.wallet)
43547
- );
43548
- await writeLedgerAtomically(ledgerPath, nextRows);
43549
- return { date: nextRow.date, spentAtomic: nextRow.spentAtomic };
43550
- });
43551
- }
43552
- function parseAtomicCap(value, label) {
43553
- if (!/^\d+$/.test(value)) {
43554
- throw new Error(`Invalid ${label} spend cap ${JSON.stringify(value)}.`);
43555
- }
43556
- return BigInt(value);
43557
- }
43558
- async function writeLedgerAtomically(path, rows) {
43559
- const ledger = rows.length === 1 && rows[0].wallet === DEFAULT_WALLET_NAME ? {
43560
- date: rows[0].date,
43561
- spentAtomic: rows[0].spentAtomic,
43562
- ...rows[0].reservations === void 0 ? {} : { reservations: rows[0].reservations }
43563
- } : { version: 1, rows };
43564
- await mkdir4(dirname4(path), { recursive: true, mode: 448 });
43565
- const temporaryPath = `${path}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
43566
- await writeFile2(temporaryPath, `${JSON.stringify(ledger, null, 2)}
43567
- `, {
43568
- encoding: "utf8",
43569
- mode: 384,
43570
- flag: "wx"
43571
- });
43572
- await rename2(temporaryPath, path);
43573
- }
43574
- async function withLedgerLock(path, operation) {
43575
- await mkdir4(dirname4(path), { recursive: true, mode: 448 });
43576
- const lockPath = `${path}.lock`;
43577
- const deadline = Date.now() + 2e3;
43578
- let handle;
43579
- while (!handle) {
43580
- try {
43581
- handle = await open4(lockPath, "wx", 384);
43582
- } catch (error61) {
43583
- if (!(error61 instanceof Error && "code" in error61 && error61.code === "EEXIST")) {
43584
- throw error61;
43585
- }
43586
- await removeStaleLock(lockPath);
43587
- if (Date.now() >= deadline) {
43588
- throw new Error("Timed out waiting for the spend ledger lock.");
43589
- }
43590
- await new Promise((resolve) => setTimeout(resolve, 20));
43591
- }
43592
- }
43593
- try {
43594
- return await operation();
43595
- } finally {
43596
- await handle.close();
43597
- await unlink2(lockPath).catch(() => void 0);
43598
- }
43599
- }
43600
- async function removeStaleLock(path) {
43601
- try {
43602
- const metadata = await stat2(path);
43603
- if (Date.now() - metadata.mtimeMs > 3e4) {
43604
- await unlink2(path);
43605
- }
43606
- } catch (error61) {
43607
- if (!isMissingFile(error61)) {
43608
- throw error61;
43609
- }
43610
- }
43611
- }
43612
-
43613
- // src/wallet-store.ts
43614
- import {
43615
- chmod as chmod2,
43616
- lstat,
43617
- mkdir as mkdir5,
43618
- open as open5,
43619
- readdir,
43620
- readFile as readFile4,
43621
- rename as rename3,
43622
- stat as stat3,
43623
- unlink as unlink3,
43624
- writeFile as writeFile3
43625
- } from "node:fs/promises";
43626
- var agentLinkSchema = external_exports.object({
43627
- apiBase: external_exports.string(),
43628
- clientId: external_exports.string(),
43629
- owner: external_exports.custom((value) => typeof value === "string" && value.startsWith("0x")),
43630
- label: external_exports.string(),
43631
- scopes: external_exports.array(external_exports.string()),
43632
- linkedAt: external_exports.iso.datetime(),
43633
- routerBaseUrl: external_exports.string().optional()
43634
- });
43635
- var ROUTER_TOPUP_TIERS = [1, 5, 20, 50];
43636
- var routerRefillSchema = external_exports.object({
43637
- belowUsd: external_exports.number().finite().min(0),
43638
- tierUsd: external_exports.union([external_exports.literal(1), external_exports.literal(5), external_exports.literal(20), external_exports.literal(50)])
43639
- });
43640
- var ceilingSweepPendingSchema = external_exports.strictObject({
43641
- owner: external_exports.string().regex(/^0x[0-9a-fA-F]{40}$/),
43642
- amountAtomic: external_exports.string().regex(/^\d+$/),
43643
- nonce: external_exports.string().regex(/^0x[0-9a-fA-F]{64}$/),
43644
- createdAt: external_exports.iso.datetime(),
43645
- status: external_exports.enum(["planned", "unknown"])
43646
- });
43647
- var walletEntrySchema = external_exports.object({
43648
- createdAt: external_exports.iso.datetime(),
43649
- label: external_exports.string().trim().min(1).max(80).optional(),
43650
- spendCaps: spendCapsSchema,
43651
- // On disk, null explicitly disables ceiling sweeps; an absent field is the
43652
- // backwards-compatible 5 USDC default and is not written during reads.
43653
- ceilingAtomic: external_exports.string().regex(/^\d+$/).nullable().optional(),
43654
- ceilingSweepAttemptedAt: external_exports.iso.datetime().optional(),
43655
- ceilingSweepPending: ceilingSweepPendingSchema.optional(),
43656
- routerRefill: routerRefillSchema.optional(),
43657
- link: agentLinkSchema.optional()
43658
- });
43659
- var walletRegistrySchema = external_exports.object({
43660
- version: external_exports.literal(1),
43661
- default: walletNameSchema.optional(),
43662
- wallets: external_exports.record(walletNameSchema, walletEntrySchema)
43663
- });
43664
- async function spendCapsForWallet(store, name = DEFAULT_WALLET_NAME) {
43665
- const entry = store.entry(name);
43666
- if (entry) return { ...entry.spendCaps };
43667
- return await readConfiguredSpendCaps(getVapiPaths(store.home).config);
43668
- }
43669
- async function readConfiguredSpendCaps(configPath) {
43670
- try {
43671
- const parsed = JSON.parse(await readFile4(configPath, "utf8"));
43672
- const caps = typeof parsed === "object" && parsed !== null ? Reflect.get(parsed, "spendCaps") : void 0;
43673
- const result = spendCapsSchema.safeParse(caps);
43674
- if (result.success) return result.data;
43675
- } catch {
43676
- }
43677
- return { ...DEFAULT_SPEND_CAPS };
43678
- }
43679
-
43680
44302
  // src/transfer.ts
43681
44303
  var TransferError = class extends Error {
43682
44304
  constructor(code, message, options = {}) {
@@ -43731,6 +44353,9 @@ async function transferBetweenAccounts(args) {
43731
44353
  throw transferError("not_linked");
43732
44354
  }
43733
44355
  const fromAddress = getAddress(rawFromAddress);
44356
+ if (args.expectedFromAddress !== void 0 && (!isAddress(args.expectedFromAddress, { strict: false }) || !sameAddress(fromAddress, args.expectedFromAddress))) {
44357
+ throw transferError("account_address_mismatch");
44358
+ }
43734
44359
  let siblingResult;
43735
44360
  try {
43736
44361
  siblingResult = await fetchSiblings({
@@ -43762,8 +44387,12 @@ async function transferBetweenAccounts(args) {
43762
44387
  owner: siblingResult.owner,
43763
44388
  siblings: siblingResult.siblings
43764
44389
  });
44390
+ if (args.expectedToAddress !== void 0 && (!isAddress(args.expectedToAddress, { strict: false }) || !sameAddress(recipient.address, args.expectedToAddress))) {
44391
+ throw transferError("account_address_mismatch");
44392
+ }
43765
44393
  const purpose = args.purpose ?? "send";
43766
- if (purpose === "sweep" && recipient.toKind !== "owner") {
44394
+ const isSweepTarget = recipient.toKind === "owner" || recipient.toKind === "account" && args.sweepParent !== void 0 && isAddress(args.sweepParent, { strict: false }) && getAddress(args.sweepParent) === getAddress(recipient.address);
44395
+ if (purpose === "sweep" && !isSweepTarget) {
43767
44396
  throw transferError("sweep_not_owner");
43768
44397
  }
43769
44398
  const home = args.home ?? args.store.home;
@@ -43784,7 +44413,7 @@ async function transferBetweenAccounts(args) {
43784
44413
  if (args.resume !== void 0) {
43785
44414
  return await resumeTransfer(context, args.resume);
43786
44415
  }
43787
- const nonce = args.nonce ?? bytesToHex(randomBytes3(32));
44416
+ const nonce = args.nonce ?? bytesToHex(randomBytes5(32));
43788
44417
  if (!NONCE_PATTERN.test(nonce)) {
43789
44418
  throw new TransferError(
43790
44419
  "authorization_invalid",
@@ -44009,8 +44638,8 @@ async function postRelay(context, request, options) {
44009
44638
  `wallet:${context.args.from}`,
44010
44639
  async () => {
44011
44640
  await context.args.store.reload();
44012
- const link = context.args.store.entry(context.args.from)?.link;
44013
- if (link === void 0 || safeOrigin(link.apiBase) !== safeOrigin(context.args.apiBase)) {
44641
+ const link2 = context.args.store.entry(context.args.from)?.link;
44642
+ if (link2 === void 0 || safeOrigin(link2.apiBase) !== safeOrigin(context.args.apiBase)) {
44014
44643
  throw transferError("not_linked");
44015
44644
  }
44016
44645
  return await agentFetch(
@@ -44167,7 +44796,7 @@ async function findTransferReceipt(context, nonce) {
44167
44796
  return [...receipts].reverse().find((candidate) => candidate.transfer?.nonce.toLowerCase() === nonce.toLowerCase());
44168
44797
  }
44169
44798
  async function withTransferLock(context, nonce, operation) {
44170
- await mkdir6(dirname5(context.receiptsPath), { recursive: true, mode: 448 });
44799
+ await mkdir9(dirname6(context.receiptsPath), { recursive: true, mode: 448 });
44171
44800
  const lockPath = `${context.receiptsPath}.${context.args.from}.${nonce.toLowerCase()}.transfer.lock`;
44172
44801
  const requestTimeout = Math.max(0, context.args.timeoutMs ?? DEFAULT_TIMEOUT_MS);
44173
44802
  const deadline = Date.now() + Math.max(5e3, requestTimeout + 5e3);
@@ -44175,7 +44804,7 @@ async function withTransferLock(context, nonce, operation) {
44175
44804
  let handle;
44176
44805
  while (!handle) {
44177
44806
  try {
44178
- handle = await open6(lockPath, "wx", 384);
44807
+ handle = await open8(lockPath, "wx", 384);
44179
44808
  } catch (error61) {
44180
44809
  if (!(error61 instanceof Error && "code" in error61 && error61.code === "EEXIST")) throw error61;
44181
44810
  await removeStaleTransferLock(lockPath, staleAfterMs);
@@ -44189,13 +44818,13 @@ async function withTransferLock(context, nonce, operation) {
44189
44818
  return await operation();
44190
44819
  } finally {
44191
44820
  await handle.close();
44192
- await unlink4(lockPath).catch(() => void 0);
44821
+ await unlink6(lockPath).catch(() => void 0);
44193
44822
  }
44194
44823
  }
44195
44824
  async function removeStaleTransferLock(path, staleAfterMs) {
44196
44825
  try {
44197
- const metadata = await stat4(path);
44198
- if (Date.now() - metadata.mtimeMs > staleAfterMs) await unlink4(path);
44826
+ const metadata = await stat7(path);
44827
+ if (Date.now() - metadata.mtimeMs > staleAfterMs) await unlink6(path);
44199
44828
  } catch (error61) {
44200
44829
  if (!(error61 instanceof Error && "code" in error61 && error61.code === "ENOENT")) throw error61;
44201
44830
  }
@@ -44349,6 +44978,7 @@ function isTransferErrorCode(value) {
44349
44978
  "resume_not_found",
44350
44979
  "resume_mismatch",
44351
44980
  "signer_mismatch",
44981
+ "account_address_mismatch",
44352
44982
  "signing_failed"
44353
44983
  ].includes(value);
44354
44984
  }
@@ -44360,7 +44990,7 @@ function transferError(code, options = {}) {
44360
44990
  unsupported: "This vAPI server does not relay transfers yet. No money moved.",
44361
44991
  recipient_not_allowed: "The recipient is not the owner or an active sibling account. No money moved.",
44362
44992
  ambiguous_recipient: "The sibling account name is ambiguous. No money moved.",
44363
- sweep_not_owner: "A sweep can only send funds to the owner. No money moved.",
44993
+ sweep_not_owner: "A sweep can only send funds to the owner or its treasury. No money moved.",
44364
44994
  per_call_cap_exceeded: "The amount exceeds the sending account's per-call cap. No money moved.",
44365
44995
  per_day_cap_exceeded: "The amount exceeds the sending account's remaining daily cap. No money moved.",
44366
44996
  payer_mismatch: "The relay rejected the sending account. No money moved.",
@@ -44373,17 +45003,644 @@ function transferError(code, options = {}) {
44373
45003
  resume_not_found: "No resumable transfer was found for that nonce. No money moved.",
44374
45004
  resume_mismatch: "The resumed transfer does not match the original request. No money moved.",
44375
45005
  signer_mismatch: "The unlocked signer does not match the sending account. No money moved.",
45006
+ account_address_mismatch: "The sending or recipient account no longer matches the address recorded by its movement. No money moved.",
44376
45007
  signing_failed: "The transfer authorization could not be signed. No money moved."
44377
45008
  };
44378
45009
  return new TransferError(code, messages[code], options);
44379
45010
  }
44380
45011
 
45012
+ // src/movement.ts
45013
+ var movementIdSchema = external_exports.string().regex(/^mv_[A-Za-z0-9_-]{8,128}$/);
45014
+ var nonceSchema = external_exports.string().regex(/^0x[0-9a-fA-F]{64}$/);
45015
+ var amountSchema = external_exports.string().regex(/^\d+\.\d{2}$/);
45016
+ var movementReasonSchema = external_exports.enum([
45017
+ "distribute",
45018
+ "allocate",
45019
+ "rebalance",
45020
+ "delegate",
45021
+ "sweep",
45022
+ "send"
45023
+ ]);
45024
+ var movementPurposeSchema = external_exports.enum(["send", "sweep"]);
45025
+ var movementAddressSchema = external_exports.string().refine((value) => isAddress(value, { strict: false }));
45026
+ var movementLegSchema = external_exports.strictObject({
45027
+ from: walletNameSchema,
45028
+ to: external_exports.string().trim().min(1),
45029
+ fromAddress: movementAddressSchema.optional(),
45030
+ toAddress: movementAddressSchema.optional(),
45031
+ amountUsd: amountSchema,
45032
+ purpose: movementPurposeSchema,
45033
+ nonce: nonceSchema,
45034
+ status: external_exports.enum(["planned", "sent", "failed", "unknown", "cancelled"]),
45035
+ txHash: external_exports.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
45036
+ reason: external_exports.string().min(1).optional(),
45037
+ retryable: external_exports.boolean().optional(),
45038
+ restored: external_exports.literal(true).optional(),
45039
+ addressBindingSource: external_exports.enum(["journal", "terminal-review"]).optional()
45040
+ });
45041
+ var movementSchema = external_exports.strictObject({
45042
+ v: external_exports.literal(2),
45043
+ id: movementIdSchema,
45044
+ reason: movementReasonSchema,
45045
+ from: walletNameSchema,
45046
+ // The only account a `sweep` leg may name besides the owner. Sweeps skip the
45047
+ // spend caps, so the parent comes from the plan, never from a leg's `to`.
45048
+ treasury: walletNameSchema.optional(),
45049
+ network: external_exports.enum([BASE_MAINNET_CAIP2, ARC_MAINNET_CAIP2]),
45050
+ createdAt: external_exports.iso.datetime(),
45051
+ legs: external_exports.array(movementLegSchema).min(1)
45052
+ });
45053
+ var legacyMovementSchema = external_exports.strictObject({
45054
+ v: external_exports.literal(1),
45055
+ id: movementIdSchema,
45056
+ reason: external_exports.literal("distribute"),
45057
+ from: walletNameSchema,
45058
+ network: external_exports.enum([BASE_MAINNET_CAIP2, ARC_MAINNET_CAIP2]),
45059
+ createdAt: external_exports.iso.datetime(),
45060
+ legs: external_exports.array(
45061
+ external_exports.strictObject({
45062
+ to: external_exports.string().trim().min(1),
45063
+ amountUsd: amountSchema,
45064
+ nonce: nonceSchema,
45065
+ status: external_exports.enum(["planned", "sent", "failed", "unknown"]),
45066
+ txHash: external_exports.string().regex(/^0x[0-9a-fA-F]+$/).optional(),
45067
+ reason: external_exports.string().min(1).optional(),
45068
+ retryable: external_exports.boolean().optional()
45069
+ })
45070
+ ).min(1)
45071
+ });
45072
+ var MovementError = class extends Error {
45073
+ constructor(code, message, movementId) {
45074
+ super(message);
45075
+ this.code = code;
45076
+ this.movementId = movementId;
45077
+ this.name = "MovementError";
45078
+ }
45079
+ code;
45080
+ movementId;
45081
+ };
45082
+ async function listUnfinishedMovements(options) {
45083
+ const directory = movementsDirectory(options.home);
45084
+ let names;
45085
+ try {
45086
+ names = await readdir3(directory);
45087
+ } catch (error61) {
45088
+ if (isMissingFile(error61)) return [];
45089
+ throw error61;
45090
+ }
45091
+ const summaries = [];
45092
+ for (const name of names.filter((candidate) => candidate.endsWith(".json")).sort()) {
45093
+ const movement = parseMovement(JSON.parse(await readFile9(join5(directory, name), "utf8")));
45094
+ if (options.from !== void 0 && !movement.legs.some((leg) => leg.from === options.from)) {
45095
+ continue;
45096
+ }
45097
+ const pendingLegs = movement.legs.filter((leg) => leg.status === "planned").length;
45098
+ const unknownLegs = movement.legs.filter((leg) => leg.status === "unknown").length;
45099
+ if (!await movementHasOpenLeg(options.home, movement, options.from)) continue;
45100
+ summaries.push({
45101
+ id: movement.id,
45102
+ from: movement.from,
45103
+ network: movement.network,
45104
+ createdAt: movement.createdAt,
45105
+ pendingLegs,
45106
+ unknownLegs
45107
+ });
45108
+ }
45109
+ return summaries.sort(
45110
+ (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
45111
+ );
45112
+ }
45113
+ async function movementHasOpenLeg(home, movement, from15) {
45114
+ for (const leg of movement.legs) {
45115
+ if (from15 !== void 0 && leg.from !== from15) continue;
45116
+ if (isOpenMovementLeg(leg, "unsigned")) return true;
45117
+ if (leg.status !== "failed") continue;
45118
+ const journal = await readTransferJournal(home, leg.from, leg.nonce);
45119
+ if (isOpenMovementLeg(leg, journal.signed ? "signed" : "unsigned")) return true;
45120
+ }
45121
+ return false;
45122
+ }
45123
+ async function readMovement(home, id) {
45124
+ const safeId = parseMovementId(id);
45125
+ try {
45126
+ return parseMovement(
45127
+ JSON.parse(await readFile9(join5(movementsDirectory(home), `${safeId}.json`), "utf8"))
45128
+ );
45129
+ } catch (error61) {
45130
+ if (isMissingFile(error61)) {
45131
+ throw new MovementError(
45132
+ "movement_not_found",
45133
+ `No movement named ${safeId} was found.`,
45134
+ safeId
45135
+ );
45136
+ }
45137
+ throw error61;
45138
+ }
45139
+ }
45140
+ async function reconcileUnresolvedMovementsForAccountLocked(home, account, deps) {
45141
+ for (const summary of await listUnfinishedMovements({ home, from: account })) {
45142
+ let movement = await readMovement(home, summary.id);
45143
+ try {
45144
+ for (let index2 = 0; index2 < movement.legs.length; index2 += 1) {
45145
+ const leg = movement.legs[index2];
45146
+ if (leg?.from !== account || leg.status !== "planned" && !(leg.status === "failed" && leg.retryable === true)) {
45147
+ continue;
45148
+ }
45149
+ const reconciled = await reconcileJournaledLeg(movement, index2, home);
45150
+ if (reconciled !== movement) {
45151
+ movement = reconciled;
45152
+ await writeMovement(home, movement);
45153
+ }
45154
+ }
45155
+ for (let index2 = 0; index2 < movement.legs.length; index2 += 1) {
45156
+ const leg = movement.legs[index2];
45157
+ if (leg?.from !== account || leg.status !== "unknown") continue;
45158
+ movement = await resolveUnknownLeg(movement, index2, home, deps);
45159
+ }
45160
+ for (let index2 = 0; index2 < movement.legs.length; index2 += 1) {
45161
+ const leg = movement.legs[index2];
45162
+ if (leg?.from !== account || leg.status !== "failed") continue;
45163
+ movement = await prepareFailedLeg(movement, index2, home, deps);
45164
+ }
45165
+ } catch {
45166
+ }
45167
+ if (await movementHasOpenLeg(home, movement, account)) return movement;
45168
+ }
45169
+ return void 0;
45170
+ }
45171
+ async function resolveUnknownLeg(movement, index2, home, deps) {
45172
+ const leg = movement.legs[index2];
45173
+ if (isExpiredRestoredReview(leg)) {
45174
+ if (deps.replaceExpiredRestored !== true) {
45175
+ await throwRestoredLegReview(movement, leg, deps);
45176
+ }
45177
+ const journal2 = await readTransferJournal(home, leg.from, leg.nonce);
45178
+ await releaseExpiredReservation(leg, journal2, home, deps);
45179
+ const bound = leg.fromAddress === void 0 || leg.toAddress === void 0 ? await bindLegAddresses(leg, deps) : leg;
45180
+ movement = replaceLeg(
45181
+ movement,
45182
+ index2,
45183
+ plannedLeg(bound, (deps.randomNonce ?? createMovementNonce)())
45184
+ );
45185
+ await writeMovement(home, movement);
45186
+ return movement;
45187
+ }
45188
+ try {
45189
+ const result = await callTransfer(movement, leg, deps, true);
45190
+ if (result.status === "sent") {
45191
+ movement = replaceLeg(movement, index2, sentLeg(leg, result.txHash));
45192
+ await writeMovement(home, movement);
45193
+ return movement;
45194
+ }
45195
+ } catch (error61) {
45196
+ if (error61 instanceof TransferError || error61 instanceof SweepParentUnknownError) {
45197
+ } else {
45198
+ await reconcileCrash(movement, index2, home);
45199
+ throw error61;
45200
+ }
45201
+ }
45202
+ const journal = await readTransferJournal(home, leg.from, leg.nonce);
45203
+ const state = await authorizationState(movement, leg, journal.validBefore, home, deps);
45204
+ if (state === "settled") {
45205
+ movement = replaceLeg(movement, index2, sentLeg(leg));
45206
+ } else if (state === "expired") {
45207
+ if (leg.restored === true && deps.replaceExpiredRestored !== true) {
45208
+ movement = replaceLeg(movement, index2, restoredExpiredReviewLeg(leg));
45209
+ await writeMovement(home, movement);
45210
+ await throwRestoredLegReview(movement, movement.legs[index2], deps);
45211
+ }
45212
+ await releaseExpiredReservation(leg, journal, home, deps);
45213
+ movement = replaceLeg(
45214
+ movement,
45215
+ index2,
45216
+ plannedLeg(leg, (deps.randomNonce ?? createMovementNonce)())
45217
+ );
45218
+ }
45219
+ await writeMovement(home, movement);
45220
+ return movement;
45221
+ }
45222
+ async function reconcileJournaledLeg(movement, index2, home) {
45223
+ const leg = movement.legs[index2];
45224
+ const journal = await readTransferJournal(home, leg.from, leg.nonce);
45225
+ if (journal.latest?.transfer?.status === "sent") {
45226
+ return replaceLeg(
45227
+ movement,
45228
+ index2,
45229
+ sentLeg(
45230
+ leg,
45231
+ journal.latest.transfer.txHash === null ? void 0 : journal.latest.transfer.txHash
45232
+ )
45233
+ );
45234
+ }
45235
+ return journal.signed ? replaceLeg(movement, index2, unknownLeg(leg)) : movement;
45236
+ }
45237
+ async function prepareFailedLeg(movement, index2, home, deps) {
45238
+ const leg = movement.legs[index2];
45239
+ if (leg.retryable === true) {
45240
+ const reconciled = await reconcileJournaledLeg(movement, index2, home);
45241
+ if (reconciled !== movement) {
45242
+ await writeMovement(home, reconciled);
45243
+ return reconciled;
45244
+ }
45245
+ return replaceLeg(movement, index2, plannedLeg(leg, leg.nonce, true));
45246
+ }
45247
+ const journal = await readTransferJournal(home, leg.from, leg.nonce);
45248
+ if (!journal.signed) return movement;
45249
+ const state = await authorizationState(movement, leg, journal.validBefore, home, deps);
45250
+ if (state === "settled") {
45251
+ movement = replaceLeg(movement, index2, sentLeg(leg));
45252
+ } else if (state === "expired") {
45253
+ if (leg.restored === true && deps.replaceExpiredRestored !== true) {
45254
+ movement = replaceLeg(movement, index2, restoredExpiredReviewLeg(leg));
45255
+ await writeMovement(home, movement);
45256
+ await throwRestoredLegReview(movement, movement.legs[index2], deps);
45257
+ }
45258
+ await releaseExpiredReservation(leg, journal, home, deps);
45259
+ movement = replaceLeg(
45260
+ movement,
45261
+ index2,
45262
+ plannedLeg(leg, (deps.randomNonce ?? createMovementNonce)())
45263
+ );
45264
+ }
45265
+ await writeMovement(home, movement);
45266
+ return movement;
45267
+ }
45268
+ async function callTransfer(movement, leg, deps, resume) {
45269
+ if (leg.fromAddress !== void 0 && leg.toAddress !== void 0) {
45270
+ await assertLegAddressBinding(movement, leg, deps);
45271
+ } else if (!resume) {
45272
+ throw movementAddressUnboundError(movement);
45273
+ }
45274
+ const transfer = deps.transfer ?? transferBetweenAccounts;
45275
+ const sweepParent = await readSweepParent(movement, leg, deps);
45276
+ return await transfer({
45277
+ store: deps.store,
45278
+ secrets: deps.secrets,
45279
+ apiBase: deps.apiBase,
45280
+ from: leg.from,
45281
+ to: leg.to,
45282
+ amountUsd: leg.amountUsd,
45283
+ network: movement.network,
45284
+ purpose: leg.purpose,
45285
+ expectedFromAddress: leg.fromAddress,
45286
+ expectedToAddress: leg.toAddress,
45287
+ ...sweepParent === void 0 ? {} : { sweepParent },
45288
+ home: deps.home ?? deps.store.home,
45289
+ ...resume ? { resume: leg.nonce } : { nonce: leg.nonce },
45290
+ ...deps.unlock === void 0 ? {} : { unlock: deps.unlock },
45291
+ ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
45292
+ ...deps.now === void 0 ? {} : { now: deps.now },
45293
+ ...deps.timeoutMs === void 0 ? {} : { timeoutMs: deps.timeoutMs },
45294
+ ...deps.ledgerPath === void 0 ? {} : { ledgerPath: deps.ledgerPath }
45295
+ });
45296
+ }
45297
+ async function reconcileCrash(movement, index2, home) {
45298
+ const leg = movement.legs[index2];
45299
+ const journal = await readTransferJournal(home, leg.from, leg.nonce);
45300
+ let reconciled = leg;
45301
+ if (journal.latest?.transfer?.status === "sent") {
45302
+ reconciled = sentLeg(
45303
+ leg,
45304
+ journal.latest.transfer.txHash === null ? void 0 : journal.latest.transfer.txHash
45305
+ );
45306
+ } else if (journal.latest?.transfer?.status === "failed") {
45307
+ reconciled = journalFailedLeg(leg, journal.latest.error?.code ?? "transfer_failed");
45308
+ } else if (journal.signed) {
45309
+ reconciled = unknownLeg(leg);
45310
+ }
45311
+ await writeMovement(home, replaceLeg(movement, index2, reconciled));
45312
+ }
45313
+ async function authorizationState(movement, leg, validBefore, home, deps) {
45314
+ if (deps.authorizationState !== void 0) {
45315
+ return await deps.authorizationState({
45316
+ movement,
45317
+ leg,
45318
+ ...validBefore === void 0 ? {} : { validBefore }
45319
+ });
45320
+ }
45321
+ const journal = await readTransferJournal(home, leg.from, leg.nonce);
45322
+ const receipt = journal.request;
45323
+ const request = receipt?.transfer?.request;
45324
+ if (receipt === void 0 || request === void 0) {
45325
+ throw new MovementError(
45326
+ "invalid_movement",
45327
+ `Movement ${movement.id} has no journaled authorization for nonce ${leg.nonce}.`,
45328
+ movement.id
45329
+ );
45330
+ }
45331
+ const config2 = await loadConfig(getVapiPaths(home).config);
45332
+ const settlementReceipt = {
45333
+ ...receipt,
45334
+ authorization: {
45335
+ from: request.authorization.from,
45336
+ nonce: request.authorization.nonce,
45337
+ validBefore: request.authorization.validBefore
45338
+ }
45339
+ };
45340
+ return (await checkReceiptSettlement(settlementReceipt, config2, {
45341
+ ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl }
45342
+ })).state;
45343
+ }
45344
+ async function releaseExpiredReservation(leg, journal, home, deps) {
45345
+ if (journal.reservedOn === void 0) return;
45346
+ await releaseSpend(parseUsdCents(leg.amountUsd) * 10000n, {
45347
+ reservedOn: journal.reservedOn,
45348
+ ledgerPath: deps.ledgerPath ?? getVapiPaths(home).ledger,
45349
+ now: new Date((deps.now ?? Date.now)()),
45350
+ wallet: leg.from,
45351
+ reservationId: leg.nonce.toLowerCase()
45352
+ });
45353
+ }
45354
+ function sentLeg(leg, txHash) {
45355
+ return {
45356
+ from: leg.from,
45357
+ to: leg.to,
45358
+ ...legAddressBinding(leg),
45359
+ amountUsd: leg.amountUsd,
45360
+ purpose: leg.purpose,
45361
+ nonce: leg.nonce,
45362
+ status: "sent",
45363
+ ...restoredMarker(leg),
45364
+ ...txHash === void 0 || txHash === null ? {} : { txHash }
45365
+ };
45366
+ }
45367
+ function unknownLeg(leg) {
45368
+ return {
45369
+ from: leg.from,
45370
+ to: leg.to,
45371
+ ...legAddressBinding(leg),
45372
+ amountUsd: leg.amountUsd,
45373
+ purpose: leg.purpose,
45374
+ nonce: leg.nonce,
45375
+ status: "unknown",
45376
+ ...restoredMarker(leg)
45377
+ };
45378
+ }
45379
+ function journalFailedLeg(leg, reason) {
45380
+ return {
45381
+ from: leg.from,
45382
+ to: leg.to,
45383
+ ...legAddressBinding(leg),
45384
+ amountUsd: leg.amountUsd,
45385
+ purpose: leg.purpose,
45386
+ nonce: leg.nonce,
45387
+ status: "failed",
45388
+ reason,
45389
+ ...restoredMarker(leg)
45390
+ };
45391
+ }
45392
+ function plannedLeg(leg, nonce, preserveRestored = false) {
45393
+ return {
45394
+ from: leg.from,
45395
+ to: leg.to,
45396
+ ...legAddressBinding(leg),
45397
+ amountUsd: leg.amountUsd,
45398
+ purpose: leg.purpose,
45399
+ nonce,
45400
+ status: "planned",
45401
+ ...preserveRestored ? restoredMarker(leg) : {}
45402
+ };
45403
+ }
45404
+ function restoredExpiredReviewLeg(leg) {
45405
+ return {
45406
+ from: leg.from,
45407
+ to: leg.to,
45408
+ ...legAddressBinding(leg),
45409
+ amountUsd: leg.amountUsd,
45410
+ purpose: leg.purpose,
45411
+ nonce: leg.nonce,
45412
+ status: "unknown",
45413
+ reason: "restored_nonce_expired",
45414
+ restored: true
45415
+ };
45416
+ }
45417
+ function restoredMarker(leg) {
45418
+ return leg.restored === true ? { restored: true } : {};
45419
+ }
45420
+ function legAddressBinding(leg) {
45421
+ return {
45422
+ ...leg.fromAddress === void 0 ? {} : { fromAddress: leg.fromAddress },
45423
+ ...leg.toAddress === void 0 ? {} : { toAddress: leg.toAddress },
45424
+ ...leg.addressBindingSource === void 0 ? {} : { addressBindingSource: leg.addressBindingSource }
45425
+ };
45426
+ }
45427
+ function isExpiredRestoredReview(leg) {
45428
+ return leg.restored === true && leg.reason === "restored_nonce_expired";
45429
+ }
45430
+ async function throwRestoredLegReview(movement, leg, deps, kind = "expired") {
45431
+ const address2 = await (deps.addressReader === void 0 ? deps.store.readAddress(leg.from) : deps.addressReader(leg.from));
45432
+ const explorer = address2 === void 0 ? void 0 : explorerAddressUrl(movement.network, address2);
45433
+ const addressHint = address2 ?? `account ${leg.from}`;
45434
+ const explorerHint = explorer === void 0 ? "a block explorer" : explorer;
45435
+ throw new MovementError(
45436
+ "restored_leg_review",
45437
+ `Movement ${movement.id} restored a ${leg.amountUsd} USDC transfer from ${leg.from} to ${leg.to}, ${kind === "expired" ? `but its saved authorization ${leg.nonce} has expired` : "that the backup recorded before it was signed"}. The original device may already have paid it with a later nonce, so nothing was signed. Check the balance with vapi balance --account ${leg.from}, then inspect ${addressHint} on ${explorerHint}. If the transfer was not paid, continue deliberately in a terminal with vapi accounts distribute --resume ${movement.id} --replace-expired-restored. This override is unavailable through MCP.`,
45438
+ movement.id
45439
+ );
45440
+ }
45441
+ function replaceLeg(movement, index2, leg) {
45442
+ const legs = [...movement.legs];
45443
+ legs[index2] = movementLegSchema.parse(leg);
45444
+ return movementSchema.parse({ ...movement, legs });
45445
+ }
45446
+ function parseMovement(value) {
45447
+ const current = movementSchema.safeParse(value);
45448
+ if (current.success) return current.data;
45449
+ const legacy = legacyMovementSchema.parse(value);
45450
+ return movementSchema.parse({
45451
+ ...legacy,
45452
+ v: 2,
45453
+ legs: legacy.legs.map((leg) => ({
45454
+ ...leg,
45455
+ from: legacy.from,
45456
+ purpose: "send"
45457
+ }))
45458
+ });
45459
+ }
45460
+ async function bindLegAddresses(leg, deps) {
45461
+ const fromAddress = await resolveAccountAddress(leg.from, deps);
45462
+ if (fromAddress === void 0) {
45463
+ throw new MovementError(
45464
+ "invalid_movement",
45465
+ `Cannot plan a movement from ${leg.from}: its local address is unavailable. Restore that account, then retry.`
45466
+ );
45467
+ }
45468
+ const toAddress = await resolveRecipientAddress(leg.from, leg.to, deps);
45469
+ if (toAddress === void 0) {
45470
+ throw new MovementError(
45471
+ "invalid_movement",
45472
+ `Cannot plan a movement to ${leg.to}: its address is unavailable. Restore or relink that account, then retry.`
45473
+ );
45474
+ }
45475
+ return { ...leg, fromAddress, toAddress };
45476
+ }
45477
+ async function assertLegAddressBinding(movement, leg, deps) {
45478
+ if (leg.fromAddress === void 0 || leg.toAddress === void 0) {
45479
+ throw movementAddressUnboundError(movement);
45480
+ }
45481
+ const fromAddress = await resolveAccountAddress(leg.from, deps);
45482
+ if (fromAddress === void 0 || !sameAddress2(fromAddress, leg.fromAddress)) {
45483
+ throw new MovementError(
45484
+ "account_address_mismatch",
45485
+ `Movement ${movement.id} bound sender ${leg.from} to ${leg.fromAddress}, but that name now resolves differently. Restore ${leg.from} to the recorded address before resuming, or wait for any signed authorization to expire and run vapi accounts distribute --cancel ${movement.id}.`,
45486
+ movement.id
45487
+ );
45488
+ }
45489
+ const toAddress = await resolveRecipientAddress(leg.from, leg.to, deps);
45490
+ if (toAddress === void 0 || !sameAddress2(toAddress, leg.toAddress)) {
45491
+ throw new MovementError(
45492
+ "account_address_mismatch",
45493
+ `Movement ${movement.id} bound recipient ${leg.to} to ${leg.toAddress}, but that name now resolves differently. Restore or relink ${leg.to} to the recorded address before resuming, or wait for any signed authorization to expire and run vapi accounts distribute --cancel ${movement.id}.`,
45494
+ movement.id
45495
+ );
45496
+ }
45497
+ }
45498
+ function movementAddressUnboundError(movement) {
45499
+ return new MovementError(
45500
+ "movement_address_unbound",
45501
+ `Movement ${movement.id} predates address binding, so vAPI cannot safely sign its planned leg. Check balances and the saved nonce, then create a new movement deliberately if no authorization can settle.`,
45502
+ movement.id
45503
+ );
45504
+ }
45505
+ async function resolveAccountAddress(account, deps) {
45506
+ const raw = await (deps.addressReader ?? ((name) => deps.store.readAddress(name)))(account);
45507
+ return raw !== void 0 && isAddress(raw, { strict: false }) ? getAddress(raw) : void 0;
45508
+ }
45509
+ async function resolveRecipientAddress(from15, recipient, deps) {
45510
+ const requested = recipient.trim();
45511
+ if (isAddress(requested, { strict: false })) return getAddress(requested);
45512
+ if (requested === "owner") {
45513
+ const owner = deps.store.entry(from15)?.link?.owner;
45514
+ if (owner !== void 0 && isAddress(owner, { strict: false })) return getAddress(owner);
45515
+ } else {
45516
+ const account = walletNameSchema.safeParse(requested);
45517
+ if (account.success) {
45518
+ const local = await resolveAccountAddress(account.data, deps);
45519
+ if (local !== void 0) return local;
45520
+ }
45521
+ }
45522
+ const resolved = await deps.recipientAddressReader?.(from15, requested);
45523
+ return resolved !== void 0 && isAddress(resolved, { strict: false }) ? getAddress(resolved) : void 0;
45524
+ }
45525
+ function sameAddress2(left, right) {
45526
+ return isAddress(left, { strict: false }) && isAddress(right, { strict: false }) && getAddress(left) === getAddress(right);
45527
+ }
45528
+ var SweepParentUnknownError = class extends Error {
45529
+ };
45530
+ function isOwnerSweep(to) {
45531
+ return to.trim() === "owner" || isAddress(to, { strict: false });
45532
+ }
45533
+ async function readSweepParent(movement, leg, deps) {
45534
+ if (leg.purpose !== "sweep" || isOwnerSweep(leg.to)) return void 0;
45535
+ if (movement.treasury === void 0 || leg.to !== movement.treasury) {
45536
+ throw new SweepParentUnknownError();
45537
+ }
45538
+ const account = walletNameSchema.safeParse(leg.to);
45539
+ if (!account.success) throw new SweepParentUnknownError();
45540
+ const address2 = deps.addressReader === void 0 ? await deps.store.readAddress(account.data) : await deps.addressReader(account.data);
45541
+ if (address2 === void 0 || !isAddress(address2, { strict: false })) {
45542
+ throw new SweepParentUnknownError();
45543
+ }
45544
+ return address2;
45545
+ }
45546
+ function parseMovementId(value) {
45547
+ const parsed = movementIdSchema.safeParse(value);
45548
+ if (!parsed.success) throw new MovementError("invalid_movement", "The movement id is invalid.");
45549
+ return parsed.data;
45550
+ }
45551
+ function movementsDirectory(home) {
45552
+ return join5(home, "movements");
45553
+ }
45554
+ function createMovementNonce() {
45555
+ return bytesToHex(randomBytes6(32));
45556
+ }
45557
+ async function writeMovement(home, movement) {
45558
+ const parsed = movementSchema.parse(movement);
45559
+ const directory = movementsDirectory(home);
45560
+ await mkdir10(directory, { recursive: true, mode: 448 });
45561
+ await chmod3(directory, 448);
45562
+ const path = join5(directory, `${parsed.id}.json`);
45563
+ const temporary = join5(
45564
+ directory,
45565
+ `.${parsed.id}.${process.pid}.${randomBytes6(6).toString("hex")}.tmp`
45566
+ );
45567
+ let handle;
45568
+ try {
45569
+ handle = await open9(temporary, "wx", 384);
45570
+ await handle.writeFile(serializeMovement(parsed), "utf8");
45571
+ await handle.sync();
45572
+ await handle.close();
45573
+ handle = void 0;
45574
+ await rename6(temporary, path);
45575
+ const directoryHandle = await open9(directory, "r");
45576
+ try {
45577
+ await directoryHandle.sync();
45578
+ } finally {
45579
+ await directoryHandle.close();
45580
+ }
45581
+ } catch (error61) {
45582
+ await handle?.close().catch(() => void 0);
45583
+ await unlink7(temporary).catch(() => void 0);
45584
+ throw error61;
45585
+ }
45586
+ }
45587
+ function serializeMovement(movement) {
45588
+ return `${JSON.stringify(movementSchema.parse(movement), null, 2)}
45589
+ `;
45590
+ }
45591
+
45592
+ // src/sweep.ts
45593
+ var ERC20_ABI = [
45594
+ {
45595
+ type: "function",
45596
+ name: "balanceOf",
45597
+ stateMutability: "view",
45598
+ inputs: [{ name: "account", type: "address" }],
45599
+ outputs: [{ name: "balance", type: "uint256" }]
45600
+ },
45601
+ {
45602
+ type: "function",
45603
+ name: "transfer",
45604
+ stateMutability: "nonpayable",
45605
+ inputs: [
45606
+ { name: "to", type: "address" },
45607
+ { name: "amount", type: "uint256" }
45608
+ ],
45609
+ outputs: [{ name: "success", type: "bool" }]
45610
+ }
45611
+ ];
45612
+ async function readUsdcBalance(args) {
45613
+ if (isSolanaNetwork(args.network)) {
45614
+ return await readSolanaUsdcBalance({
45615
+ network: args.network,
45616
+ configured: args.configured,
45617
+ address: args.address,
45618
+ allowPrivateNetwork: args.allowPrivateNetwork,
45619
+ ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {},
45620
+ ...args.lookup ? { lookup: args.lookup } : {}
45621
+ });
45622
+ }
45623
+ const client = createNetworkPublicClient(args.network, args.configured, {
45624
+ allowPrivateNetwork: args.allowPrivateNetwork,
45625
+ ...args.fetchImpl ? { fetch: args.fetchImpl } : {},
45626
+ ...args.lookup ? { lookup: args.lookup } : {}
45627
+ });
45628
+ return await client.readContract({
45629
+ address: getAddress(
45630
+ usesUsdcGas(args.network) ? getNetworkDefinition(args.network).usdc : args.configured.usdc
45631
+ ),
45632
+ abi: ERC20_ABI,
45633
+ functionName: "balanceOf",
45634
+ args: [getAddress(args.address)]
45635
+ });
45636
+ }
45637
+
44381
45638
  // src/ceiling.ts
44382
45639
  var CEILING_SWEEP_MINIMUM_ATOMIC = 100000n;
44383
45640
  var CEILING_SWEEP_GUARD_MS = 10 * 60 * 1e3;
44384
45641
  var CEILING_SWEEP_TIMEOUT_MS = 1e4;
44385
45642
  function sweepTarget(account) {
44386
- return account.owner;
45643
+ return account.parent?.address ?? account.owner;
44387
45644
  }
44388
45645
  async function sweepAboveCeiling(args) {
44389
45646
  try {
@@ -44410,6 +45667,25 @@ async function sweepAboveCeilingLocked(args) {
44410
45667
  if (caps.effectiveCeilingAtomic === null) {
44411
45668
  return skipped(args.account, "ceiling_off");
44412
45669
  }
45670
+ const unresolvedMovement = await reconcileUnresolvedMovementsForAccountLocked(
45671
+ auditHome,
45672
+ args.account,
45673
+ {
45674
+ store: args.store,
45675
+ secrets: args.secrets,
45676
+ apiBase: args.apiBase,
45677
+ home: auditHome,
45678
+ ...args.transfer === void 0 ? {} : { transfer: args.transfer },
45679
+ ...args.addressReader === void 0 ? {} : { addressReader: args.addressReader },
45680
+ ...args.fetchImpl === void 0 ? {} : { fetchImpl: args.fetchImpl },
45681
+ ...args.now === void 0 ? {} : { now: args.now },
45682
+ ...args.timeoutMs === void 0 ? {} : { timeoutMs: args.timeoutMs }
45683
+ }
45684
+ );
45685
+ if (unresolvedMovement !== void 0) {
45686
+ await auditFailure(auditHome, args.account, void 0, "unfinished_movement", now);
45687
+ return skipped(args.account, "unfinished_movement");
45688
+ }
44413
45689
  const rawAddress = await (args.addressReader ?? ((account) => args.store.readAddress(account)))(
44414
45690
  args.account
44415
45691
  );
@@ -44436,12 +45712,12 @@ async function sweepAboveCeilingLocked(args) {
44436
45712
  }
44437
45713
  throw error61;
44438
45714
  }
44439
- const self = siblingResult.siblings.find((candidate) => candidate.self) ?? siblingResult.siblings.find((candidate) => sameAddress2(candidate.address, address2));
45715
+ const self = siblingResult.siblings.find((candidate) => candidate.self) ?? siblingResult.siblings.find((candidate) => sameAddress3(candidate.address, address2));
44440
45716
  if (self?.status === "revoked") return skipped(args.account, "revoked");
44441
45717
  if (siblingResult.owner === null) return skipped(args.account, "no_known_owner");
44442
45718
  const serverOwner = siblingResult.owner;
44443
45719
  const linkedOwner = isAddress(entry.link.owner, { strict: false }) ? getAddress(entry.link.owner) : void 0;
44444
- if (linkedOwner === void 0 || !sameAddress2(linkedOwner, serverOwner)) {
45720
+ if (linkedOwner === void 0 || !sameAddress3(linkedOwner, serverOwner)) {
44445
45721
  safeWarn(
44446
45722
  args.warn,
44447
45723
  `Refusing to sweep ${args.account}: its stored owner differs from the linked server owner.`
@@ -44469,12 +45745,30 @@ async function sweepAboveCeilingLocked(args) {
44469
45745
  CEILING_SWEEP_GUARD_MS
44470
45746
  );
44471
45747
  if (!claimed) return skipped(args.account, "throttled");
45748
+ if (pending !== void 0) {
45749
+ if (!sameAddress3(pending.owner, owner)) {
45750
+ return {
45751
+ account: args.account,
45752
+ status: "unknown",
45753
+ reason: "pending_owner_changed",
45754
+ amountUsd: formatAtomicUsd(BigInt(pending.amountAtomic))
45755
+ };
45756
+ }
45757
+ }
45758
+ const parent = await args.resolveParent?.(args.account);
45759
+ const target = sweepTarget({
45760
+ account: args.account,
45761
+ owner,
45762
+ ...parent === void 0 ? {} : { parent }
45763
+ });
44472
45764
  if (pending !== void 0) {
44473
45765
  const reconciled = await executePendingSweep({
44474
45766
  args,
44475
45767
  pending,
44476
45768
  address: address2,
44477
45769
  owner,
45770
+ target,
45771
+ sweepParent: parent?.address,
44478
45772
  readBalance: readBalance2
44479
45773
  });
44480
45774
  if (reconciled !== void 0) return reconciled;
@@ -44482,7 +45776,7 @@ async function sweepAboveCeilingLocked(args) {
44482
45776
  }
44483
45777
  await args.store.reload();
44484
45778
  const currentEntry = args.store.entry(args.account);
44485
- if (currentEntry?.link === void 0 || !sameOrigin(currentEntry.link.apiBase, args.apiBase) || !sameAddress2(currentEntry.link.owner, owner)) {
45779
+ if (currentEntry?.link === void 0 || !sameOrigin(currentEntry.link.apiBase, args.apiBase) || !sameAddress3(currentEntry.link.owner, owner)) {
44486
45780
  return skipped(args.account, "not_linked");
44487
45781
  }
44488
45782
  const currentCeiling = args.store.ceilingCaps(args.account).effectiveCeilingAtomic;
@@ -44498,8 +45792,9 @@ async function sweepAboveCeilingLocked(args) {
44498
45792
  args.signal?.throwIfAborted();
44499
45793
  pending = {
44500
45794
  owner,
45795
+ target,
44501
45796
  amountAtomic: amountAtomic.toString(),
44502
- nonce: (args.randomNonce ?? (() => bytesToHex(randomBytes4(32))))(),
45797
+ nonce: (args.randomNonce ?? (() => bytesToHex(randomBytes7(32))))(),
44503
45798
  createdAt: new Date(now()).toISOString(),
44504
45799
  status: "planned"
44505
45800
  };
@@ -44513,6 +45808,8 @@ async function sweepAboveCeilingLocked(args) {
44513
45808
  pending: stored,
44514
45809
  address: address2,
44515
45810
  owner,
45811
+ target,
45812
+ sweepParent: parent?.address,
44516
45813
  readBalance: readBalance2
44517
45814
  }) ?? skipped(args.account, "below_threshold");
44518
45815
  } catch (error61) {
@@ -44522,22 +45819,28 @@ async function sweepAboveCeilingLocked(args) {
44522
45819
  }
44523
45820
  }
44524
45821
  async function executePendingSweep(input2) {
44525
- const { args, pending, address: address2, owner, readBalance: readBalance2 } = input2;
45822
+ const {
45823
+ args,
45824
+ pending,
45825
+ address: address2,
45826
+ owner,
45827
+ target: currentTarget,
45828
+ sweepParent: currentSweepParent,
45829
+ readBalance: readBalance2
45830
+ } = input2;
44526
45831
  const now = args.now ?? Date.now;
44527
45832
  const auditHome = args.auditHome ?? args.store.home;
44528
- if (!sameAddress2(pending.owner, owner)) {
44529
- return {
44530
- account: args.account,
44531
- status: "unknown",
44532
- reason: "pending_owner_changed",
44533
- amountUsd: formatAtomicUsd(BigInt(pending.amountAtomic))
44534
- };
44535
- }
44536
45833
  const amountAtomic = BigInt(pending.amountAtomic);
44537
45834
  const amountUsd = formatAtomicUsd(amountAtomic);
44538
- const target = sweepTarget({ account: args.account, owner });
44539
45835
  const hasJournal = await hasTransferJournal(auditHome, args.account, pending.nonce);
44540
45836
  const resume = pending.status === "unknown" || hasJournal;
45837
+ const storedTarget = getAddress(pending.target ?? owner);
45838
+ if (!resume && !sameAddress3(storedTarget, currentTarget)) {
45839
+ await clearPendingSweep(args.store, args.account, pending.nonce);
45840
+ return void 0;
45841
+ }
45842
+ const target = resume ? storedTarget : currentTarget;
45843
+ const sweepParent = resume ? !sameAddress3(target, owner) ? target : void 0 : currentSweepParent;
44541
45844
  try {
44542
45845
  const result = await (args.transfer ?? transferBetweenAccounts)({
44543
45846
  store: args.store,
@@ -44548,6 +45851,7 @@ async function executePendingSweep(input2) {
44548
45851
  amountUsd,
44549
45852
  network: "base",
44550
45853
  purpose: "sweep",
45854
+ ...sweepParent === void 0 ? {} : { sweepParent },
44551
45855
  ...resume ? { resume: pending.nonce } : { nonce: pending.nonce },
44552
45856
  unlock: guardedSweepUnlock(args, address2, owner, amountAtomic, readBalance2),
44553
45857
  ...args.fetchImpl === void 0 ? {} : { fetchImpl: args.fetchImpl },
@@ -44685,7 +45989,7 @@ function guardedSweepUnlock(args, address2, owner, amountAtomic, readBalance2) {
44685
45989
  args.signal?.throwIfAborted();
44686
45990
  await args.store.reload();
44687
45991
  const entry = args.store.entry(account);
44688
- if (entry?.link === void 0 || !sameOrigin(entry.link.apiBase, args.apiBase) || !sameAddress2(entry.link.owner, owner)) {
45992
+ if (entry?.link === void 0 || !sameOrigin(entry.link.apiBase, args.apiBase) || !sameAddress3(entry.link.owner, owner)) {
44689
45993
  throw new CeilingPolicyChangedError();
44690
45994
  }
44691
45995
  const ceiling = args.store.ceilingCaps(account).effectiveCeilingAtomic;
@@ -44751,77 +46055,17 @@ var CeilingPolicyChangedError = class extends Error {
44751
46055
  code = "policy_changed";
44752
46056
  };
44753
46057
  async function withCeilingSweepLock(args, operation) {
44754
- const path = `${args.store.registryPath}.${args.account}.ceiling.lock`;
44755
- const waitMs = args.timeoutMs ?? CEILING_SWEEP_TIMEOUT_MS;
44756
- const deadline = Date.now() + Math.max(0, waitMs);
44757
- const staleAfterMs = Math.max(12e4, waitMs * 3);
44758
- const owner = JSON.stringify({ pid: process.pid, token: randomBytes4(16).toString("hex") });
44759
- let handle;
44760
- while (handle === void 0) {
44761
- args.signal?.throwIfAborted();
44762
- try {
44763
- handle = await open7(path, "wx", 384);
44764
- await handle.writeFile(owner, "utf8");
44765
- await handle.sync();
44766
- } catch (error61) {
44767
- if (!(error61 instanceof Error && "code" in error61 && error61.code === "EEXIST")) throw error61;
44768
- await removeStaleSweepLock(path, staleAfterMs);
44769
- if (Date.now() >= deadline) throw new CeilingSweepTimeoutError();
44770
- await new Promise((resolve) => setTimeout(resolve, 20));
44771
- }
44772
- }
44773
- const ownedHandle = handle;
44774
- const heartbeat = setInterval(
44775
- () => {
44776
- const time3 = /* @__PURE__ */ new Date();
44777
- void ownedHandle.utimes(time3, time3).catch(() => void 0);
44778
- },
44779
- Math.min(3e4, Math.max(1e3, Math.floor(staleAfterMs / 3)))
44780
- );
44781
- heartbeat.unref();
44782
46058
  try {
44783
- return await operation();
44784
- } finally {
44785
- clearInterval(heartbeat);
44786
- await ownedHandle.close();
44787
- await unlinkOwnedSweepLock(path, owner);
44788
- }
44789
- }
44790
- async function removeStaleSweepLock(path, staleAfterMs) {
44791
- try {
44792
- const metadata = await stat5(path);
44793
- if (Date.now() - metadata.mtimeMs <= staleAfterMs) return;
44794
- const owner = await readFile5(path, "utf8").catch(() => "");
44795
- const pid = lockOwnerPid(owner);
44796
- if (pid !== void 0 && processIsAlive(pid)) return;
44797
- await unlinkOwnedSweepLock(path, owner);
44798
- } catch (error61) {
44799
- if (!isMissingFile(error61)) throw error61;
44800
- }
44801
- }
44802
- async function unlinkOwnedSweepLock(path, owner) {
44803
- try {
44804
- if (await readFile5(path, "utf8") === owner) await unlink5(path);
44805
- } catch (error61) {
44806
- if (!isMissingFile(error61)) throw error61;
44807
- }
44808
- }
44809
- function lockOwnerPid(owner) {
44810
- try {
44811
- const value = JSON.parse(owner);
44812
- if (typeof value !== "object" || value === null) return void 0;
44813
- const pid = Reflect.get(value, "pid");
44814
- return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : void 0;
44815
- } catch {
44816
- return void 0;
44817
- }
44818
- }
44819
- function processIsAlive(pid) {
44820
- try {
44821
- process.kill(pid, 0);
44822
- return true;
46059
+ return await withAccountMovementLock(
46060
+ args.store.home,
46061
+ args.account,
46062
+ args.timeoutMs ?? CEILING_SWEEP_TIMEOUT_MS,
46063
+ operation,
46064
+ args.signal
46065
+ );
44823
46066
  } catch (error61) {
44824
- return error61 instanceof Error && "code" in error61 && error61.code === "EPERM";
46067
+ if (error61 instanceof AccountMovementLockedError) throw new CeilingSweepTimeoutError();
46068
+ throw error61;
44825
46069
  }
44826
46070
  }
44827
46071
  function defaultBalanceReader(args) {
@@ -44868,7 +46112,7 @@ function sameOrigin(left, right) {
44868
46112
  return false;
44869
46113
  }
44870
46114
  }
44871
- function sameAddress2(left, right) {
46115
+ function sameAddress3(left, right) {
44872
46116
  return isAddress(left, { strict: false }) && isAddress(right, { strict: false }) && getAddress(left) === getAddress(right);
44873
46117
  }
44874
46118
  function safeWarn(warn, message) {
@@ -44903,11 +46147,160 @@ async function safeAppendAudit(home, record2, now) {
44903
46147
  await appendAudit(home, record2, { now: () => new Date(now()) }).catch(() => void 0);
44904
46148
  }
44905
46149
 
46150
+ // src/swarm.ts
46151
+ import { readdir as readdir4, readFile as readFile10, stat as stat8, unlink as unlink8 } from "node:fs/promises";
46152
+ import { join as join6 } from "node:path";
46153
+ var SWARM_NAME_PATTERN = /^[a-z][a-z0-9-]{0,15}$/;
46154
+ var SWARM_ROLE_PATTERN = /^[a-z][a-z0-9]{0,7}$/;
46155
+ var atomicSchema = external_exports.string().regex(/^\d+$/);
46156
+ var addressSchema2 = external_exports.string().regex(/^0x[0-9a-fA-F]{40}$/);
46157
+ var capsSchema = external_exports.strictObject({ perCallAtomic: atomicSchema, perDayAtomic: atomicSchema });
46158
+ var movementIdSchema2 = external_exports.string().regex(/^mv_[A-Za-z0-9_-]{8,128}$/u);
46159
+ var amountUsdSchema = external_exports.string().regex(/^\d+\.\d{2}$/u);
46160
+ var setupFundingStatusSchema = external_exports.enum(["planned", "sent", "incomplete"]);
46161
+ var blockedFundingLegSchema = external_exports.strictObject({
46162
+ to: external_exports.string().trim().min(1),
46163
+ amountUsd: amountUsdSchema,
46164
+ reason: external_exports.string().trim().min(1)
46165
+ });
46166
+ var setupFundingSchema = external_exports.strictObject({
46167
+ amountUsd: amountUsdSchema,
46168
+ from: walletNameSchema,
46169
+ movementId: movementIdSchema2.optional(),
46170
+ status: setupFundingStatusSchema,
46171
+ policy: external_exports.strictObject({
46172
+ strategy: external_exports.enum(["targets", "even", "weights"]),
46173
+ movementId: movementIdSchema2.optional(),
46174
+ status: setupFundingStatusSchema,
46175
+ blocked: external_exports.array(blockedFundingLegSchema).optional()
46176
+ }).optional()
46177
+ });
46178
+ var accountSteps = {
46179
+ creating: external_exports.boolean().default(false),
46180
+ created: external_exports.boolean(),
46181
+ capped: external_exports.boolean(),
46182
+ linked: external_exports.boolean()
46183
+ };
46184
+ var treasurySchema = external_exports.strictObject({
46185
+ account: walletNameSchema,
46186
+ // A planned account has no address until its vault derivation succeeds.
46187
+ address: addressSchema2.optional(),
46188
+ steps: external_exports.strictObject(accountSteps)
46189
+ }).refine(
46190
+ (value) => !value.steps.created || value.address !== void 0,
46191
+ "Created accounts require an address."
46192
+ );
46193
+ var memberSchema = external_exports.strictObject({
46194
+ account: walletNameSchema,
46195
+ address: addressSchema2.optional(),
46196
+ role: external_exports.string().regex(SWARM_ROLE_PATTERN),
46197
+ weight: external_exports.number().int().positive(),
46198
+ targetAtomic: atomicSchema,
46199
+ ceilingAtomic: atomicSchema.nullable(),
46200
+ steps: external_exports.strictObject({ ...accountSteps, profiled: external_exports.boolean() })
46201
+ }).refine(
46202
+ (value) => !value.steps.created || value.address !== void 0,
46203
+ "Created accounts require an address."
46204
+ );
46205
+ var swarmFileSchema = external_exports.strictObject({
46206
+ v: external_exports.literal(1),
46207
+ name: external_exports.string().regex(SWARM_NAME_PATTERN),
46208
+ device: external_exports.string().regex(DEVICE_NAME_PATTERN),
46209
+ network: external_exports.enum([BASE_MAINNET_CAIP2, ARC_MAINNET_CAIP2]),
46210
+ createdAt: external_exports.iso.datetime(),
46211
+ treasury: treasurySchema,
46212
+ members: external_exports.array(memberSchema),
46213
+ setupFunding: setupFundingSchema.optional(),
46214
+ policy: external_exports.strictObject({
46215
+ strategy: external_exports.enum(["targets", "even", "weights"]),
46216
+ treasuryCaps: capsSchema
46217
+ })
46218
+ }).refine(
46219
+ (value) => (/* @__PURE__ */ new Set([value.treasury.account, ...value.members.map((member) => member.account)])).size === value.members.length + 1,
46220
+ "Swarm accounts must be unique."
46221
+ );
46222
+ var SwarmError = class extends Error {
46223
+ constructor(code, message) {
46224
+ super(message);
46225
+ this.code = code;
46226
+ this.name = "SwarmError";
46227
+ }
46228
+ code;
46229
+ };
46230
+ function swarmsDirectory(home) {
46231
+ return join6(home, "swarms");
46232
+ }
46233
+ function swarmFilePath(home, name) {
46234
+ assertName(name);
46235
+ return join6(swarmsDirectory(home), `${name}.json`);
46236
+ }
46237
+ async function readSwarm(home, name) {
46238
+ const path = swarmFilePath(home, name);
46239
+ try {
46240
+ const swarm = parseSwarm(JSON.parse(await readFile10(path, "utf8")));
46241
+ if (swarm.name !== name)
46242
+ throw new SwarmError("invalid_swarm", "Swarm name does not match its file.");
46243
+ return swarm;
46244
+ } catch (error61) {
46245
+ if (isMissingFile(error61)) throw new SwarmError("swarm_not_found", `No swarm named ${name}.`);
46246
+ if (error61 instanceof SwarmError) throw error61;
46247
+ throw new SwarmError("invalid_swarm", `Invalid swarm file for ${name}.`);
46248
+ }
46249
+ }
46250
+ async function listSwarms(home) {
46251
+ let files;
46252
+ try {
46253
+ files = await readdir4(swarmsDirectory(home));
46254
+ } catch (error61) {
46255
+ if (isMissingFile(error61)) return [];
46256
+ throw error61;
46257
+ }
46258
+ return await Promise.all(
46259
+ files.filter((file2) => file2.endsWith(".json")).sort().map((file2) => readSwarm(home, file2.slice(0, -5)))
46260
+ );
46261
+ }
46262
+ function swarmParentResolver(home) {
46263
+ return async (account) => {
46264
+ const swarms = await listSwarms(home);
46265
+ const matches = swarms.filter(
46266
+ (swarm) => swarm.members.some((member) => member.account === account)
46267
+ );
46268
+ if (matches.length > 1)
46269
+ throw new SwarmError(
46270
+ "account_in_other_swarm",
46271
+ `Account ${account} belongs to multiple swarms.`
46272
+ );
46273
+ const treasury = matches[0]?.treasury;
46274
+ if (treasury === void 0) return void 0;
46275
+ if (treasury.address === void 0)
46276
+ throw new SwarmError("invalid_swarm", "The swarm treasury is not created yet.");
46277
+ return { account: treasury.account, address: treasury.address };
46278
+ };
46279
+ }
46280
+ function parseSwarm(value) {
46281
+ const result = swarmFileSchema.safeParse(value);
46282
+ if (!result.success) throw new SwarmError("invalid_swarm", "Invalid swarm state.");
46283
+ return result.data;
46284
+ }
46285
+ function assertName(name) {
46286
+ if (!SWARM_NAME_PATTERN.test(name))
46287
+ throw new SwarmError(
46288
+ "invalid_swarm",
46289
+ "A swarm name must be 1 to 16 lowercase letters, digits or dashes, starting with a letter."
46290
+ );
46291
+ }
46292
+
44906
46293
  // src/x402-pay.ts
44907
- import { randomUUID as randomUUID2 } from "node:crypto";
46294
+ import { randomBytes as randomBytes8, randomUUID as randomUUID2 } from "node:crypto";
46295
+
46296
+ // src/run-budget.ts
46297
+ var RunBudgetError = class extends Error {
46298
+ name = "RunBudgetError";
46299
+ code = "run_budget_exceeded";
46300
+ };
44908
46301
 
44909
46302
  // src/support-report.ts
44910
- var VAPI_CLIENT_VERSION = "0.7.0";
46303
+ var VAPI_CLIENT_VERSION = "0.8.0";
44911
46304
 
44912
46305
  // src/x402-pay.ts
44913
46306
  var X402PaymentError = class extends Error {
@@ -44926,6 +46319,7 @@ var X402PaymentError = class extends Error {
44926
46319
  };
44927
46320
  async function payRequest(args) {
44928
46321
  const startedAt = args.now ?? /* @__PURE__ */ new Date();
46322
+ assertAllowanceCurrent(startedAt, args.allowanceExpiresAt);
44929
46323
  const nowMs = args.nowMs ?? (() => performance.now());
44930
46324
  const startedMs = nowMs();
44931
46325
  const receiptId = randomUUID2();
@@ -44965,8 +46359,10 @@ async function payRequest(args) {
44965
46359
  ...args.preferNetworks === void 0 ? {} : { preferNetworks: args.preferNetworks },
44966
46360
  ...args.paymentId === void 0 ? {} : { paymentId: args.paymentId },
44967
46361
  now: startedAt,
46362
+ ...args.allowanceExpiresAt === void 0 ? {} : { allowanceExpiresAt: args.allowanceExpiresAt },
44968
46363
  nowMs,
44969
46364
  trace,
46365
+ ...args.runBudget === void 0 ? {} : { runBudget: args.runBudget },
44970
46366
  ...args.ceilingSweep === void 0 ? {} : { ceilingSweep: args.ceilingSweep }
44971
46367
  });
44972
46368
  response = completed.response;
@@ -45000,6 +46396,8 @@ async function payRequest(args) {
45000
46396
  return { response, receipt };
45001
46397
  }
45002
46398
  async function completeX402Payment(args) {
46399
+ const paymentTime = args.now ?? /* @__PURE__ */ new Date();
46400
+ assertAllowanceCurrent(paymentTime, args.allowanceExpiresAt);
45003
46401
  const nowMs = args.nowMs ?? (() => performance.now());
45004
46402
  const trace = args.trace;
45005
46403
  const quoteStarted = args.quoteStartedMs ?? nowMs();
@@ -45022,11 +46420,29 @@ async function completeX402Payment(args) {
45022
46420
  if (trace) trace.capsApplied = true;
45023
46421
  assertMaximumAtomic(quote.amountAtomic, args.maxAtomic);
45024
46422
  assertMaxPrice(quote.amountAtomic, args.maxPriceUsd);
45025
- await reserveSpend(quote.amountAtomic, args.caps, {
46423
+ const spendReservation = {
45026
46424
  ledgerPath: args.ledgerPath ?? getVapiPaths().ledger,
45027
46425
  now: args.now,
45028
46426
  ...args.wallet === void 0 ? {} : { wallet: args.wallet }
45029
- });
46427
+ };
46428
+ let reservationId;
46429
+ let reservedOn;
46430
+ if (args.runBudget === void 0) {
46431
+ await reserveSpend(quote.amountAtomic, args.caps, spendReservation);
46432
+ } else {
46433
+ reservationId = args.paymentId ?? `x402_${randomBytes8(12).toString("hex")}`;
46434
+ const reservation = await reserveSpend(quote.amountAtomic, args.caps, {
46435
+ ...spendReservation,
46436
+ reservationId
46437
+ });
46438
+ reservedOn = reservation.date;
46439
+ try {
46440
+ args.runBudget.reserve(quote.amountAtomic, reservationId);
46441
+ } catch (error61) {
46442
+ await releaseUnsentReservation(args, quote.amountAtomic, reservationId, reservedOn);
46443
+ throw error61;
46444
+ }
46445
+ }
45030
46446
  const signStarted = nowMs();
45031
46447
  let payment;
45032
46448
  try {
@@ -45037,6 +46453,7 @@ async function completeX402Payment(args) {
45037
46453
  allowPrivateNetwork: args.config.allowPrivateNetwork ?? false
45038
46454
  }),
45039
46455
  ...args.now === void 0 ? {} : { nowSeconds: Math.floor(args.now.getTime() / 1e3) },
46456
+ ...args.allowanceExpiresAt === void 0 ? {} : { maximumValidBeforeSeconds: Math.floor(args.allowanceExpiresAt.getTime() / 1e3) },
45040
46457
  ...args.paymentId === void 0 ? {} : { paymentId: args.paymentId }
45041
46458
  });
45042
46459
  const payer2 = isSvmPaymentRequirements(quote.accepted) ? args.account.solana?.address ?? args.account.address : args.account.address;
@@ -45048,6 +46465,17 @@ async function completeX402Payment(args) {
45048
46465
  trace.authorization = { from: from15, nonce, validBefore };
45049
46466
  }
45050
46467
  }
46468
+ } catch (error61) {
46469
+ if (args.runBudget !== void 0 && reservationId !== void 0 && reservedOn !== void 0) {
46470
+ const released = await releaseUnsentReservation(
46471
+ args,
46472
+ quote.amountAtomic,
46473
+ reservationId,
46474
+ reservedOn
46475
+ );
46476
+ if (released) args.runBudget.release(reservationId);
46477
+ }
46478
+ throw error61;
45051
46479
  } finally {
45052
46480
  if (trace) trace.phases.signMs = elapsed(nowMs, signStarted);
45053
46481
  }
@@ -45182,6 +46610,26 @@ async function completeX402Payment(args) {
45182
46610
  paidAttempt.finish();
45183
46611
  }
45184
46612
  }
46613
+ function assertAllowanceCurrent(now, expiresAt) {
46614
+ if (expiresAt === void 0 || now.getTime() < expiresAt.getTime()) return;
46615
+ throw new Error(
46616
+ `Remote allowance expired at ${expiresAt.toISOString()}. Start a new Railway run before paying.`
46617
+ );
46618
+ }
46619
+ async function releaseUnsentReservation(args, amountAtomic, reservationId, reservedOn) {
46620
+ try {
46621
+ await releaseSpend(amountAtomic, {
46622
+ reservedOn,
46623
+ ledgerPath: args.ledgerPath ?? getVapiPaths().ledger,
46624
+ now: args.now,
46625
+ ...args.wallet === void 0 ? {} : { wallet: args.wallet },
46626
+ reservationId
46627
+ });
46628
+ return true;
46629
+ } catch {
46630
+ return false;
46631
+ }
46632
+ }
45185
46633
  async function selectQuote(args) {
45186
46634
  const networks = toX402Networks(args.config.networks);
45187
46635
  const waitFor = args.challenge.waitFor.bind(args.challenge);
@@ -45276,6 +46724,9 @@ function sanitizeThrownError(error61, secrets) {
45276
46724
  if (error61 instanceof SpendCapError) {
45277
46725
  return new SpendCapError(error61.code, message);
45278
46726
  }
46727
+ if (error61 instanceof RunBudgetError) {
46728
+ return new RunBudgetError(message);
46729
+ }
45279
46730
  if (error61 instanceof X402PaymentError) {
45280
46731
  return new X402PaymentError(error61.code, message, error61.possibleSettlement, {
45281
46732
  ...cause === void 0 ? {} : { cause },
@@ -45365,6 +46816,7 @@ function directReceipt(args, request, trace, receiptId, startedAt, latencyMs, re
45365
46816
  resourceUrl: request.url,
45366
46817
  method: request.method,
45367
46818
  source: args.source ?? "direct",
46819
+ ...args.run === void 0 ? {} : { run: args.run },
45368
46820
  ...trace.quote ? { quote: trace.quote } : {},
45369
46821
  ...trace.payer && result.outcome !== "declined_policy" ? { payer: trace.payer } : {},
45370
46822
  ...trace.paymentId ? { paymentId: trace.paymentId } : {},
@@ -45392,7 +46844,7 @@ function receiptQuote(quote) {
45392
46844
  };
45393
46845
  }
45394
46846
  function paymentErrorOutcome(error61) {
45395
- if (error61 instanceof SpendCapError || error61 instanceof X402Error && error61.code === "max_price_exceeded") {
46847
+ if (error61 instanceof SpendCapError || error61 instanceof RunBudgetError || error61 instanceof X402Error && error61.code === "max_price_exceeded") {
45396
46848
  return "declined_policy";
45397
46849
  }
45398
46850
  if (error61 instanceof X402PaymentError) {
@@ -45409,7 +46861,7 @@ function errorCode(error61) {
45409
46861
  return "request_failed";
45410
46862
  }
45411
46863
  function receiptSafeErrorMessage(error61) {
45412
- if (error61 instanceof SpendCapError || error61 instanceof X402Error || error61 instanceof X402PaymentError) {
46864
+ if (error61 instanceof SpendCapError || error61 instanceof RunBudgetError || error61 instanceof X402Error || error61 instanceof X402PaymentError) {
45413
46865
  return error61.message;
45414
46866
  }
45415
46867
  return "The x402 request failed before it completed.";
@@ -45545,9 +46997,9 @@ async function buyRouterBalance(deps, tierUsd) {
45545
46997
  if (!isRouterTopupTier(tierUsd)) {
45546
46998
  throw new RouterClientError("http", "Router balance must use a $1, $5, $20, or $50 tier.");
45547
46999
  }
45548
- const { link, accessToken } = await purchaseCredentialSnapshot(deps);
47000
+ const { link: link2, accessToken } = await purchaseCredentialSnapshot(deps);
45549
47001
  const { response, receipt } = await payRequest({
45550
- url: `${trimTrailingSlashes(link.apiBase)}/api/router/top-up/${tierUsd}`,
47002
+ url: `${trimTrailingSlashes(link2.apiBase)}/api/router/top-up/${tierUsd}`,
45551
47003
  init: {
45552
47004
  method: "POST",
45553
47005
  headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" }
@@ -45561,6 +47013,9 @@ async function buyRouterBalance(deps, tierUsd) {
45561
47013
  maxAtomic: BigInt(tierUsd) * 1000000n,
45562
47014
  preferNetworks: ["eip155:8453"],
45563
47015
  ...deps.now === void 0 ? {} : { now: deps.now },
47016
+ ...deps.allowanceExpiresAt === void 0 ? {} : { allowanceExpiresAt: deps.allowanceExpiresAt },
47017
+ ...deps.runBudget === void 0 ? {} : { runBudget: deps.runBudget },
47018
+ ...deps.run === void 0 ? {} : { run: deps.run },
45564
47019
  source: "router.topup",
45565
47020
  ceilingSweep: {
45566
47021
  account: deps.wallet,
@@ -45568,10 +47023,11 @@ async function buyRouterBalance(deps, tierUsd) {
45568
47023
  run: async (signal) => await sweepAboveCeiling({
45569
47024
  store: deps.wallets,
45570
47025
  secrets: deps.secrets,
45571
- apiBase: link.apiBase,
47026
+ apiBase: link2.apiBase,
45572
47027
  account: deps.wallet,
45573
47028
  config: deps.config,
45574
47029
  signal,
47030
+ resolveParent: swarmParentResolver(deps.wallets.home),
45575
47031
  ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
45576
47032
  unlock: async () => deps.account
45577
47033
  })
@@ -45590,14 +47046,14 @@ async function buyRouterBalance(deps, tierUsd) {
45590
47046
  {
45591
47047
  event: "router.buy",
45592
47048
  wallet: deps.wallet,
45593
- owner: link.owner,
47049
+ owner: link2.owner,
45594
47050
  tty: Boolean(process.stderr.isTTY),
45595
47051
  detail: `$${tierUsd} on ${receipt.quote?.network ?? "eip155:8453"}`
45596
47052
  },
45597
47053
  deps.now === void 0 ? {} : { now: () => deps.now }
45598
47054
  );
45599
47055
  try {
45600
- await ensureBalanceRouterKey(deps, link);
47056
+ await ensureBalanceRouterKey(deps, link2);
45601
47057
  } catch {
45602
47058
  throw new RouterClientError(
45603
47059
  "http",
@@ -45636,7 +47092,12 @@ async function routerChat(deps, request) {
45636
47092
  config: deps.refill.config,
45637
47093
  caps,
45638
47094
  ...deps.refill.paths === void 0 ? {} : { paths: deps.refill.paths },
45639
- ...deps.refill.now === void 0 ? {} : { now: deps.refill.now }
47095
+ ...deps.refill.now === void 0 ? {} : {
47096
+ now: typeof deps.refill.now === "function" ? deps.refill.now() : deps.refill.now
47097
+ },
47098
+ ...deps.refill.allowanceExpiresAt === void 0 ? {} : { allowanceExpiresAt: deps.refill.allowanceExpiresAt },
47099
+ ...deps.refill.runBudget === void 0 ? {} : { runBudget: deps.refill.runBudget },
47100
+ ...deps.refill.run === void 0 ? {} : { run: deps.refill.run }
45640
47101
  },
45641
47102
  refill.tierUsd
45642
47103
  )).balance;
@@ -45648,6 +47109,7 @@ async function routerChat(deps, request) {
45648
47109
  tty: Boolean(process.stderr.isTTY),
45649
47110
  detail: refillDeclinedDetail(refillError)
45650
47111
  });
47112
+ if (isRunBudgetError(refillError)) throw refillError;
45651
47113
  }
45652
47114
  }
45653
47115
  const mayHaveBalance = usage === void 0 || (balance?.remainingUsd ?? 0) > 0;
@@ -45755,8 +47217,8 @@ async function ensureBalanceRouterKey(deps, paidLink) {
45755
47217
  deps.wallets,
45756
47218
  `${paidLink.clientId}:router-balance-key`,
45757
47219
  async () => {
45758
- const link = await linkedWallet(deps);
45759
- if (!sameAgentLink(link, paidLink)) {
47220
+ const link2 = await linkedWallet(deps);
47221
+ if (!sameAgentLink(link2, paidLink)) {
45760
47222
  throw new RouterClientError(
45761
47223
  "http",
45762
47224
  "The agent link changed while fetching the Router balance key."
@@ -45775,7 +47237,7 @@ async function ensureBalanceRouterKey(deps, paidLink) {
45775
47237
  "/api/agents/self/router-balance-key",
45776
47238
  { method: "POST" },
45777
47239
  "The Router balance key request failed after payment.",
45778
- link
47240
+ link2
45779
47241
  );
45780
47242
  if (!response.ok) {
45781
47243
  throw new RouterClientError(
@@ -45796,7 +47258,7 @@ async function ensureBalanceRouterKey(deps, paidLink) {
45796
47258
  );
45797
47259
  }
45798
47260
  const returnedBase = validRouterBaseUrl(parsed.data.router_base_url);
45799
- const expectedBase = validRouterBaseUrl(link.routerBaseUrl ?? DEFAULT_ROUTER_BASE_URL);
47261
+ const expectedBase = validRouterBaseUrl(link2.routerBaseUrl ?? DEFAULT_ROUTER_BASE_URL);
45800
47262
  if (returnedBase !== expectedBase) {
45801
47263
  throw new RouterClientError(
45802
47264
  "http",
@@ -45815,8 +47277,8 @@ async function routerBalanceCredentialSnapshot(deps) {
45815
47277
  const observedLink = await linkedWallet(deps);
45816
47278
  try {
45817
47279
  return await withAgentCredentialLock(deps.wallets, observedLink.clientId, async () => {
45818
- const link = await linkedWallet(deps);
45819
- if (link.clientId !== observedLink.clientId) {
47280
+ const link2 = await linkedWallet(deps);
47281
+ if (link2.clientId !== observedLink.clientId) {
45820
47282
  throw new RouterClientError(
45821
47283
  "http",
45822
47284
  "The agent link changed while reading Router balance credentials."
@@ -45831,8 +47293,8 @@ async function routerBalanceCredentialSnapshot(deps) {
45831
47293
  }
45832
47294
  if (routerKey === void 0 || routerKey.length === 0) return void 0;
45833
47295
  return {
45834
- clientId: link.clientId,
45835
- routerBaseUrl: validRouterBaseUrl(link.routerBaseUrl ?? DEFAULT_ROUTER_BASE_URL),
47296
+ clientId: link2.clientId,
47297
+ routerBaseUrl: validRouterBaseUrl(link2.routerBaseUrl ?? DEFAULT_ROUTER_BASE_URL),
45836
47298
  routerKey
45837
47299
  };
45838
47300
  });
@@ -45847,8 +47309,8 @@ async function routerBalanceCredentialSnapshot(deps) {
45847
47309
  async function removeRejectedBalanceKey(deps, rejected) {
45848
47310
  if (!deps.secrets.available) return;
45849
47311
  await withAgentCredentialLock(deps.wallets, rejected.clientId, async () => {
45850
- const link = await linkedWallet(deps);
45851
- if (link.clientId !== rejected.clientId) return;
47312
+ const link2 = await linkedWallet(deps);
47313
+ if (link2.clientId !== rejected.clientId) return;
45852
47314
  const account = agentSecretAccounts(deps.wallet).routerBalance;
45853
47315
  if (await deps.secrets.get(account) === rejected.routerKey) {
45854
47316
  await deps.secrets.remove(account);
@@ -45897,16 +47359,20 @@ function budgetExhaustedError(resetsAt, status) {
45897
47359
  }
45898
47360
  function refillDeclinedDetail(error61) {
45899
47361
  if (error61 instanceof SpendCapError) return `${error61.name}: ${error61.code}`;
47362
+ if (isRunBudgetError(error61)) return `RunBudgetError: ${error61.code}`;
45900
47363
  if (error61 instanceof RouterClientError) {
45901
47364
  return `${error61.name}: ${error61.code}${error61.status === void 0 ? "" : ` (${error61.status})`}`;
45902
47365
  }
45903
47366
  return "Error: Router balance refill failed.";
45904
47367
  }
47368
+ function isRunBudgetError(error61) {
47369
+ return typeof error61 === "object" && error61 !== null && Reflect.get(error61, "code") === "run_budget_exceeded";
47370
+ }
45905
47371
  function isRouterTopupTier(value) {
45906
47372
  return ROUTER_TOPUP_TIERS.some((tier) => tier === value);
45907
47373
  }
45908
47374
  async function rotateRouterKey(deps) {
45909
- const link = await linkedWallet(deps);
47375
+ const link2 = await linkedWallet(deps);
45910
47376
  if (!deps.secrets.available) {
45911
47377
  throw new Error(
45912
47378
  "No OS secret store is available on this machine, so the Router key cannot be stored safely."
@@ -45917,7 +47383,7 @@ async function rotateRouterKey(deps) {
45917
47383
  "/api/agents/self/router-key",
45918
47384
  { method: "POST" },
45919
47385
  "The vAPI Router key rotation request failed.",
45920
- link
47386
+ link2
45921
47387
  );
45922
47388
  if (!response.ok) {
45923
47389
  throw new RouterClientError(
@@ -45932,9 +47398,9 @@ async function rotateRouterKey(deps) {
45932
47398
  }
45933
47399
  validRouterBaseUrl(parsed.data.router_base_url);
45934
47400
  try {
45935
- await withAgentCredentialLock(deps.wallets, link.clientId, async () => {
47401
+ await withAgentCredentialLock(deps.wallets, link2.clientId, async () => {
45936
47402
  const currentLink = await linkedWallet(deps);
45937
- if (!sameAgentLink(currentLink, link)) {
47403
+ if (!sameAgentLink(currentLink, link2)) {
45938
47404
  throw new RouterClientError("http", "The agent link changed during Router key rotation.");
45939
47405
  }
45940
47406
  const account = agentSecretAccounts(deps.wallet).routerStake;
@@ -45998,15 +47464,15 @@ async function routerCredentialSnapshot(deps) {
45998
47464
  const observedLink = await linkedWallet(deps);
45999
47465
  try {
46000
47466
  return await withAgentCredentialLock(deps.wallets, observedLink.clientId, async () => {
46001
- const link = await linkedWallet(deps);
46002
- if (link.clientId !== observedLink.clientId) {
47467
+ const link2 = await linkedWallet(deps);
47468
+ if (link2.clientId !== observedLink.clientId) {
46003
47469
  throw new RouterClientError(
46004
47470
  "http",
46005
47471
  "The agent link changed while reading Router credentials."
46006
47472
  );
46007
47473
  }
46008
47474
  return {
46009
- routerBaseUrl: validRouterBaseUrl(link.routerBaseUrl ?? DEFAULT_ROUTER_BASE_URL),
47475
+ routerBaseUrl: validRouterBaseUrl(link2.routerBaseUrl ?? DEFAULT_ROUTER_BASE_URL),
46010
47476
  routerKey: await storedRouterKey(deps)
46011
47477
  };
46012
47478
  });
@@ -46027,9 +47493,9 @@ async function linkedWallet(deps) {
46027
47493
  } catch {
46028
47494
  throw new RouterClientError("http", "The wallet link could not be read.");
46029
47495
  }
46030
- const link = deps.wallets.entry(deps.wallet)?.link;
46031
- if (link === void 0) throw notLinkedError2();
46032
- return link;
47496
+ const link2 = deps.wallets.entry(deps.wallet)?.link;
47497
+ if (link2 === void 0) throw notLinkedError2();
47498
+ return link2;
46033
47499
  }
46034
47500
  async function storedRouterKey(deps) {
46035
47501
  if (!deps.secrets.available) throw noRouterKeyError();
@@ -46046,7 +47512,7 @@ function sameAgentLink(left, right) {
46046
47512
  return left.apiBase === right.apiBase && left.clientId === right.clientId && left.owner === right.owner && left.label === right.label && left.linkedAt === right.linkedAt && left.routerBaseUrl === right.routerBaseUrl && left.scopes.length === right.scopes.length && left.scopes.every((scope, index2) => scope === right.scopes[index2]);
46047
47513
  }
46048
47514
  async function consoleRequest(deps, path, init, failureMessage, knownLink) {
46049
- const link = knownLink ?? await linkedWallet(deps);
47515
+ const link2 = knownLink ?? await linkedWallet(deps);
46050
47516
  try {
46051
47517
  return await agentFetch(
46052
47518
  {
@@ -46055,7 +47521,7 @@ async function consoleRequest(deps, path, init, failureMessage, knownLink) {
46055
47521
  wallet: deps.wallet,
46056
47522
  ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl }
46057
47523
  },
46058
- `${trimTrailingSlashes(link.apiBase)}${path}`,
47524
+ `${trimTrailingSlashes(link2.apiBase)}${path}`,
46059
47525
  init
46060
47526
  );
46061
47527
  } catch (error61) {
@@ -46066,7 +47532,7 @@ async function consoleRequest(deps, path, init, failureMessage, knownLink) {
46066
47532
  }
46067
47533
  }
46068
47534
  async function verifyAgentLink(deps) {
46069
- const link = await linkedWallet(deps);
47535
+ const link2 = await linkedWallet(deps);
46070
47536
  try {
46071
47537
  await agentFetch(
46072
47538
  {
@@ -46075,7 +47541,7 @@ async function verifyAgentLink(deps) {
46075
47541
  wallet: deps.wallet,
46076
47542
  ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl }
46077
47543
  },
46078
- `${trimTrailingSlashes(link.apiBase)}/api/agents/self/router`,
47544
+ `${trimTrailingSlashes(link2.apiBase)}/api/agents/self/router`,
46079
47545
  { method: "GET" }
46080
47546
  );
46081
47547
  } catch (error61) {