@dvmkit/sdk 0.1.0-rc.1 → 0.1.0-rc.3

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 (42) hide show
  1. package/README.md +10 -2
  2. package/dist/{revenue-reporter-M35KP6V7.js → chunk-2ABMGUDS.js} +78 -2
  3. package/dist/chunk-5SO7ZVOH.js +375 -0
  4. package/dist/chunk-66HGCPBU.js +25 -0
  5. package/dist/{chunk-KQAJVVZT.js → chunk-AZBXSXQT.js} +27 -287
  6. package/dist/chunk-C2DC4FKR.js +21424 -0
  7. package/dist/{chunk-H25M54MI.js → chunk-C3MTFLC6.js} +16 -0
  8. package/dist/{chunk-NTK5DJ6R.js → chunk-EXHBXA4U.js} +11 -1
  9. package/dist/chunk-FT7IM66W.js +1557 -0
  10. package/dist/{chunk-KXWROQGK.js → chunk-FUJ36YDV.js} +1 -24
  11. package/dist/{tempo-charge-store-6GJEMNUU.js → chunk-JZWELPFH.js} +1 -0
  12. package/dist/{chunk-RPXHKMYE.js → chunk-LWUR4CGG.js} +348 -34
  13. package/dist/chunk-RHP3BRTH.js +1090 -0
  14. package/dist/{tempo-session-store-FTEEGZXA.js → chunk-RU7SXHLO.js} +2 -1
  15. package/dist/{chunk-7IH5SG2A.js → chunk-TKA6ZP4M.js} +62 -41
  16. package/dist/chunk-TVI4V7GF.js +283 -0
  17. package/dist/chunk-X3IKFWJA.js +754 -0
  18. package/dist/{chunk-DCNT4PJS.js → chunk-XY5Y5REG.js} +6 -258
  19. package/dist/chunk-XYTSDAPH.js +232 -0
  20. package/dist/chunk-YD3TZNXV.js +1042 -0
  21. package/dist/{credit-ledger-EDMEZSA2.js → credit-ledger-ED6JXKVD.js} +2 -2
  22. package/dist/credit-menu-DONAtGVf.d.ts +5076 -0
  23. package/dist/{ssrf-BdHsrrIb.d.ts → fx-Bq4cvn16.d.ts} +37 -119
  24. package/dist/index.d.ts +8 -66
  25. package/dist/index.js +11 -219
  26. package/dist/internal/index.d.ts +5745 -0
  27. package/dist/internal/index.js +6379 -0
  28. package/dist/{job-store-C5n6bhap.d.ts → job-store-m2pYmvbr.d.ts} +1772 -31
  29. package/dist/{memory-credit-ledger-7TTZDSRS.js → memory-credit-ledger-XJ5VQEVP.js} +3 -3
  30. package/dist/payout-reporter-3UB5WRCV.js +13 -0
  31. package/dist/revenue-reporter-JIKUPXOK.js +7 -0
  32. package/dist/server/index.d.ts +14 -3530
  33. package/dist/server/index.js +327 -20636
  34. package/dist/ssrf-DbFkpDv0.d.ts +118 -0
  35. package/dist/tempo-charge-store-RIFTALZK.js +8 -0
  36. package/dist/tempo-session-store-DALMRIWN.js +11 -0
  37. package/dist/testing/index.d.ts +3 -2
  38. package/dist/testing/index.js +3 -2
  39. package/dist/usd-DjVAPMlf.d.ts +97 -0
  40. package/dist/x402-5EVIUSEP.js +81 -0
  41. package/package.json +6 -2
  42. package/dist/x402-35VLYFKZ.js +0 -1272
@@ -0,0 +1,1557 @@
1
+ import {
2
+ CONFIG_AUDIT_FILE,
3
+ CONFIG_BACKUP_FILE,
4
+ CONFIG_FILE,
5
+ CONFIG_PINNED_FILE,
6
+ CREDITS_FILE,
7
+ DvmError,
8
+ ensureConfigDir
9
+ } from "./chunk-5SO7ZVOH.js";
10
+
11
+ // src/lib/payment-rails/types.ts
12
+ var ADVERTISED_AMOUNT_TOLERANCE_BPS = 1000n;
13
+ function maxAdvertisedMicro(localMicro) {
14
+ return localMicro * (10000n + ADVERTISED_AMOUNT_TOLERANCE_BPS) / 10000n;
15
+ }
16
+ function withinAdvertisedTolerance(advertisedMicro, localMicro) {
17
+ return advertisedMicro * 10000n <= localMicro * (10000n + ADVERTISED_AMOUNT_TOLERANCE_BPS);
18
+ }
19
+
20
+ // src/lib/transport/types.ts
21
+ var SUPPORTED_PROTOCOL_VERSION = 1;
22
+ function parseCreditTerms(raw) {
23
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
24
+ const credit = raw;
25
+ if (typeof credit.min_micro !== "number" || typeof credit.max_micro !== "number" || typeof credit.ttl_ms !== "number" || typeof credit.currency !== "string" || !Array.isArray(credit.funding) || !credit.funding.every((method) => typeof method === "string") || credit.lightning_min_micro !== void 0 && typeof credit.lightning_min_micro !== "number")
26
+ return void 0;
27
+ const tempo = credit.tempo === void 0 ? void 0 : parseCreditTempoTerms(credit.tempo);
28
+ if (credit.tempo !== void 0 && !tempo) return void 0;
29
+ const x402 = credit.x402 === void 0 ? void 0 : parseCreditX402Terms(credit.x402);
30
+ if (credit.x402 !== void 0 && !x402) return void 0;
31
+ return {
32
+ min_micro: credit.min_micro,
33
+ max_micro: credit.max_micro,
34
+ ttl_ms: credit.ttl_ms,
35
+ currency: credit.currency,
36
+ funding: [...credit.funding],
37
+ ...tempo && { tempo },
38
+ ...x402 && { x402 },
39
+ ...typeof credit.lightning_min_micro === "number" && {
40
+ lightning_min_micro: credit.lightning_min_micro
41
+ }
42
+ };
43
+ }
44
+ function parseCreditMenu(raw) {
45
+ const terms = parseCreditTerms(raw);
46
+ if (!terms) return void 0;
47
+ const credit = raw;
48
+ return {
49
+ ...terms,
50
+ ...typeof credit.credit_id === "string" && { credit_id: credit.credit_id },
51
+ ...typeof credit.balance_micro === "number" && { balance_micro: credit.balance_micro },
52
+ ...typeof credit.remaining_micro === "number" && {
53
+ remaining_micro: credit.remaining_micro
54
+ },
55
+ ...typeof credit.expiry_ms === "number" && { expiry_ms: credit.expiry_ms }
56
+ };
57
+ }
58
+ function parseCreditTempoTerms(raw) {
59
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
60
+ const block = raw;
61
+ if (!Array.isArray(block.methods) || !block.methods.every(isCreditTempoMethod)) return void 0;
62
+ if (block.withheld !== void 0 && (!Array.isArray(block.withheld) || !block.withheld.every(isCreditTempoWithheldMethod)))
63
+ return void 0;
64
+ return {
65
+ methods: block.methods.map((method) => ({
66
+ method: method.method,
67
+ intent: method.intent
68
+ })),
69
+ ...Array.isArray(block.withheld) && {
70
+ withheld: block.withheld.map((method) => ({
71
+ method: method.method,
72
+ intent: method.intent,
73
+ reason: method.reason
74
+ }))
75
+ }
76
+ };
77
+ }
78
+ function isCreditTempoMethod(raw) {
79
+ return !!raw && typeof raw === "object" && typeof raw.method === "string" && (raw.intent === "charge" || raw.intent === "session");
80
+ }
81
+ function isCreditTempoWithheldMethod(raw) {
82
+ return isCreditTempoMethod(raw) && typeof raw.reason === "string";
83
+ }
84
+ function parseCreditX402Terms(raw) {
85
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
86
+ const schemes = raw.schemes;
87
+ if (!Array.isArray(schemes) || !schemes.every(
88
+ (entry) => !!entry && typeof entry === "object" && typeof entry.scheme === "string" && typeof entry.network === "string"
89
+ ))
90
+ return void 0;
91
+ return {
92
+ schemes: schemes.map((entry) => ({
93
+ scheme: entry.scheme,
94
+ network: entry.network
95
+ }))
96
+ };
97
+ }
98
+
99
+ // src/lib/output.ts
100
+ import { writeSync } from "fs";
101
+ function info(msg) {
102
+ writeAllSync(2, msg + "\n");
103
+ }
104
+ function progress(event) {
105
+ writeAllSync(2, JSON.stringify(event) + "\n");
106
+ }
107
+ function writeAllSync(fd, text) {
108
+ const buf = Buffer.from(text, "utf8");
109
+ let offset = 0;
110
+ while (offset < buf.length) {
111
+ try {
112
+ offset += writeSync(fd, buf, offset, buf.length - offset);
113
+ } catch (err) {
114
+ const code = err.code;
115
+ if (code === "EAGAIN" || code === "EINTR") continue;
116
+ throw err;
117
+ }
118
+ }
119
+ }
120
+
121
+ // src/lib/evm-private-key.ts
122
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
123
+ import { hexToBytes } from "@noble/hashes/utils.js";
124
+ var EVM_PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
125
+ function isValidEvmPrivateKey(value) {
126
+ return EVM_PRIVATE_KEY_RE.test(value) && secp256k1.utils.isValidSecretKey(hexToBytes(value.slice(2)));
127
+ }
128
+
129
+ // src/lib/config-mutation.ts
130
+ import {
131
+ appendFileSync,
132
+ chmodSync,
133
+ existsSync,
134
+ readFileSync,
135
+ renameSync,
136
+ statSync,
137
+ unlinkSync,
138
+ writeFileSync
139
+ } from "fs";
140
+ var PROTECTED_CONFIG_FIELDS = ["nwcUri", "defaultIdentity", "tempo", "x402"];
141
+ var ProtectedFieldAuthorization = class {
142
+ /** Targets this authorization covers. A field also covers its own leaves. */
143
+ targets;
144
+ /** Prefer {@link authorizeProtectedRemoval}; this exists for it to call. */
145
+ constructor(targets) {
146
+ this.targets = [...targets];
147
+ }
148
+ /**
149
+ * True when `target` is one this authorization names. Naming a field covers
150
+ * the leaves under it; naming a leaf covers only that leaf, so a command
151
+ * entitled to clear `tempo.apiKey` still cannot take the whole `tempo` block.
152
+ */
153
+ covers(target) {
154
+ if (this.targets.includes(target)) return true;
155
+ const field = target.split(".")[0];
156
+ return target !== field && this.targets.includes(field);
157
+ }
158
+ };
159
+ function authorizeProtectedRemoval(...targets) {
160
+ return new ProtectedFieldAuthorization(targets);
161
+ }
162
+ function applyConfigPatch(base, patch) {
163
+ if (!isPlainObject(patch))
164
+ throw patchShapeError("A change must be an object with `set` and/or `unset`.");
165
+ if (patch.set !== void 0 && !isPlainObject(patch.set)) {
166
+ throw patchShapeError("A change's `set` must be an object of fields to write.");
167
+ }
168
+ if (patch.unset !== void 0 && !Array.isArray(patch.unset)) {
169
+ throw patchShapeError("A change's `unset` must be an array of field names.");
170
+ }
171
+ const set = patch.set ?? {};
172
+ const unset = patch.unset ?? [];
173
+ for (const [field, value] of Object.entries(set)) {
174
+ if (value !== void 0) continue;
175
+ throw new DvmError(
176
+ "config_patch_invalid",
177
+ `Config field '${field}' was declared with no value.`,
178
+ "Give the field a value, or name it in the change's `unset` list to remove it. An undefined value is how a field gets dropped without anyone deciding to drop it."
179
+ );
180
+ }
181
+ for (const field of unset) {
182
+ if (typeof field !== "string" || field.length === 0) {
183
+ throw patchShapeError("Every entry in a change's `unset` must be a field name.");
184
+ }
185
+ if (!Object.hasOwn(set, field)) continue;
186
+ throw new DvmError(
187
+ "config_patch_invalid",
188
+ `Config field '${field}' is both written and removed by the same change.`,
189
+ "Declare each field once: in `set` with its new value, or in `unset` to remove it."
190
+ );
191
+ }
192
+ const merged = { ...base, ...set };
193
+ const removing = new Set(unset);
194
+ return Object.fromEntries(
195
+ Object.entries(merged).filter(([field]) => !removing.has(field))
196
+ );
197
+ }
198
+ function deepFreezeConfig(config) {
199
+ freezeDeep(config);
200
+ return config;
201
+ }
202
+ function protectedRemovals(before, after) {
203
+ const previous = before;
204
+ const next = after;
205
+ const removed = [];
206
+ for (const field of PROTECTED_CONFIG_FIELDS) {
207
+ const held = previous[field];
208
+ if (!isPresentValue(held)) continue;
209
+ const kept = next[field];
210
+ if (!isPresentValue(kept)) {
211
+ removed.push(field);
212
+ continue;
213
+ }
214
+ const spec = PROTECTED_FIELDS[field];
215
+ if (spec.credentialLeaves.length === 0) continue;
216
+ if (!isPlainObject(held)) continue;
217
+ if (!isPlainObject(kept)) {
218
+ removed.push(field);
219
+ continue;
220
+ }
221
+ for (const leaf of spec.credentialLeaves) {
222
+ if (!isPresentValue(held[leaf]) || isPresentValue(kept[leaf])) continue;
223
+ removed.push(`${field}.${leaf}`);
224
+ }
225
+ }
226
+ return removed;
227
+ }
228
+ function supersededProtectedSecrets(before, after) {
229
+ const previous = before;
230
+ const next = after;
231
+ const superseded = [];
232
+ for (const field of PROTECTED_CONFIG_FIELDS) {
233
+ const spec = PROTECTED_FIELDS[field];
234
+ if (!spec.secret) continue;
235
+ const held = previous[field];
236
+ const kept = next[field];
237
+ if (spec.credentialLeaves.length === 0) {
238
+ if (isPresentValue(held) && isPresentValue(kept) && held !== kept) superseded.push(field);
239
+ continue;
240
+ }
241
+ if (!isPlainObject(held) || !isPlainObject(kept)) continue;
242
+ for (const leaf of spec.credentialLeaves) {
243
+ const was = held[leaf];
244
+ const now = kept[leaf];
245
+ if (!isPresentValue(was) || !isPresentValue(now) || was === now) continue;
246
+ superseded.push(`${field}.${leaf}`);
247
+ }
248
+ }
249
+ return superseded;
250
+ }
251
+ function changedConfigPaths(before, after) {
252
+ const found = /* @__PURE__ */ new Set();
253
+ collectChangedPaths(
254
+ before,
255
+ after,
256
+ CONFIG_PATH_SHAPE,
257
+ "",
258
+ found
259
+ );
260
+ return [...found].sort();
261
+ }
262
+ function assertValidConfig(config, fields) {
263
+ if (!isPlainObject(config)) {
264
+ throw new DvmError(
265
+ "config_invalid",
266
+ "The caller configuration is not a JSON object.",
267
+ `Expected an object at ${CONFIG_FILE}.${rollbackSuffix()}`
268
+ );
269
+ }
270
+ const record = config;
271
+ const scope = fields ? new Set(fields) : null;
272
+ for (const [field, kind] of Object.entries(CONFIG_FIELD_TYPES)) {
273
+ if (scope && !scope.has(field)) continue;
274
+ const value = record[field];
275
+ if (value === void 0) continue;
276
+ if (!matchesKind(value, kind)) throw invalidFieldError(field, kind);
277
+ }
278
+ for (const [path, kind] of Object.entries(CONFIG_LEAF_TYPES)) {
279
+ const [field, leaf] = path.split(".");
280
+ if (scope && !scope.has(field)) continue;
281
+ const owner = record[field];
282
+ if (!isPlainObject(owner)) continue;
283
+ const value = owner[leaf];
284
+ if (value === void 0 || !matchesKind(value, kind)) throw invalidFieldError(path, kind);
285
+ }
286
+ }
287
+ function readRawConfig() {
288
+ if (!existsSync(CONFIG_FILE)) return null;
289
+ try {
290
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
291
+ } catch {
292
+ throw new DvmError(
293
+ "config_corrupt",
294
+ "Config file is corrupt.",
295
+ // Not "delete it": those bytes are the only copy on this machine of the
296
+ // NWC connection string and both private keys, however badly they parse.
297
+ `Run 'dvm config rollback' to restore a stored copy, or move ${CONFIG_FILE} aside yourself and run 'dvm init'.${rollbackSuffix()}`
298
+ );
299
+ }
300
+ }
301
+ function writeConfigFile(config) {
302
+ writeJsonAtomic(CONFIG_FILE, config);
303
+ }
304
+ var CONFIG_SNAPSHOT_SLOTS = ["backup", "pinned"];
305
+ function configSnapshotPath(slot) {
306
+ return slot === "pinned" ? CONFIG_PINNED_FILE : CONFIG_BACKUP_FILE;
307
+ }
308
+ function describeConfigBackup(slot = "backup") {
309
+ const path = configSnapshotPath(slot);
310
+ try {
311
+ if (!existsSync(path)) return null;
312
+ const stat = statSync(path);
313
+ const posix = process.platform !== "win32";
314
+ const mode = stat.mode & 511;
315
+ return {
316
+ path,
317
+ slot,
318
+ bytes: stat.size,
319
+ modifiedAt: stat.mtime.toISOString(),
320
+ modeOctal: posix ? toOctal(mode) : null,
321
+ ownerOnly: !posix || (mode & 63) === 0
322
+ };
323
+ } catch {
324
+ return null;
325
+ }
326
+ }
327
+ function readConfigBackup(slot = "backup") {
328
+ const path = configSnapshotPath(slot);
329
+ if (!existsSync(path)) return null;
330
+ try {
331
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
332
+ return isPlainObject(parsed) ? parsed : null;
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+ function retainConfigBackup(slot = "backup") {
338
+ if (!existsSync(CONFIG_FILE)) return false;
339
+ ensureConfigDir();
340
+ const path = configSnapshotPath(slot);
341
+ const tmp = `${path}.tmp`;
342
+ writeFileSync(tmp, readFileSync(CONFIG_FILE), { mode: 384 });
343
+ chmodSync(tmp, 384);
344
+ commitTmp(tmp, path);
345
+ return true;
346
+ }
347
+ function writeConfigBackup(config, slot = "backup") {
348
+ writeJsonAtomic(configSnapshotPath(slot), config);
349
+ }
350
+ function removeConfigBackup(slot = "backup") {
351
+ const path = configSnapshotPath(slot);
352
+ if (!existsSync(path)) return false;
353
+ unlinkSync(path);
354
+ return true;
355
+ }
356
+ function protectedTargetValue(config, target) {
357
+ const [field, named] = target.split(".");
358
+ const held = config[field];
359
+ const leaves = PROTECTED_FIELDS[field].credentialLeaves;
360
+ const leaf = named ?? (leaves.length > 0 ? leaves[0] : void 0);
361
+ if (leaf === void 0) return held;
362
+ return isPlainObject(held) ? held[leaf] : void 0;
363
+ }
364
+ function protectedValuesMatch(target, left, right) {
365
+ const normalized = comparableProtectedValue(target, left);
366
+ return normalized !== void 0 && normalized === comparableProtectedValue(target, right);
367
+ }
368
+ function isSecretProtectedTarget(target) {
369
+ return PROTECTED_FIELDS[target.split(".")[0]].secret;
370
+ }
371
+ function withoutProtectedTargets(config, targets) {
372
+ const dropping = /* @__PURE__ */ new Set();
373
+ for (const target of targets) {
374
+ const field = target.split(".")[0];
375
+ dropping.add(field);
376
+ for (const alias of PROTECTED_FIELDS[field].aliases) dropping.add(alias);
377
+ }
378
+ return Object.fromEntries(
379
+ Object.entries(config).filter(([field]) => !dropping.has(field))
380
+ );
381
+ }
382
+ function appendConfigAudit(record) {
383
+ try {
384
+ ensureConfigDir();
385
+ appendFileSync(CONFIG_AUDIT_FILE, JSON.stringify(record) + "\n", { mode: 384 });
386
+ chmodSync(CONFIG_AUDIT_FILE, 384);
387
+ } catch {
388
+ }
389
+ }
390
+ function protectedRemovalError(targets, reason) {
391
+ const named = targets.join(", ");
392
+ const one = targets.length === 1;
393
+ return new DvmError(
394
+ "config_protected_field",
395
+ `The '${reason}' change would remove ${named} from the local configuration.`,
396
+ `Nothing was written. ${named} ${one ? "is a credential or identity pointer" : "are credentials or identity pointers"} nothing else on this machine records, so only a command that names ${one ? "it" : "them"} explicitly may remove ${one ? "it" : "them"} \u2014 'dvm wallet disconnect', 'dvm wallet tempo-disconnect' and 'dvm wallet x402-disconnect' are those commands.`,
397
+ { fields: targets }
398
+ );
399
+ }
400
+ var PROTECTED_FIELDS = {
401
+ nwcUri: { aliases: [], credentialLeaves: [], secret: true, hexCredential: false },
402
+ // A pointer at a key, not a key: worth protecting from removal, and never
403
+ // worth stripping out of a stored copy — that is recovery lost for nothing.
404
+ defaultIdentity: { aliases: [], credentialLeaves: [], secret: false, hexCredential: false },
405
+ // `mpp` is the pre-internal-review name; a caller who has not run a mutating command
406
+ // since the rename still has their private key under it.
407
+ tempo: { aliases: ["mpp"], credentialLeaves: ["apiKey"], secret: true, hexCredential: true },
408
+ x402: { aliases: [], credentialLeaves: ["privateKey"], secret: true, hexCredential: true }
409
+ };
410
+ var CONFIG_PATH_SHAPE = {
411
+ float: { statedBudget: {}, connectedAt: {}, alias: {}, methods: {} },
412
+ x402: { privateKey: {}, network: {} },
413
+ tempo: { method: {}, accountId: {}, apiKey: {} },
414
+ credit: {
415
+ targetJobs: {},
416
+ posture: {},
417
+ tier0Cap: {},
418
+ tier1Cap: {},
419
+ pocketTarget: {},
420
+ fundingRail: {},
421
+ perDvm: { "*": { targetJobs: {}, posture: {}, fundingRail: {}, dvmId: {}, builderPubkey: {} } },
422
+ trustOverrides: { "*": {} }
423
+ },
424
+ feedback: { posture: {} }
425
+ };
426
+ var CONFIG_FIELD_TYPES = {
427
+ nwcUri: "string",
428
+ float: "object",
429
+ defaultBudget: "string",
430
+ autoPayThreshold: "string",
431
+ defaultMaxPayments: "number",
432
+ cashuMints: "string[]",
433
+ x402: "object",
434
+ tempo: "object",
435
+ defaultRail: "string",
436
+ defaultIdentity: "string",
437
+ credit: "object",
438
+ feedback: "object"
439
+ };
440
+ var CONFIG_LEAF_TYPES = {
441
+ "float.connectedAt": "string",
442
+ "x402.privateKey": "string",
443
+ "tempo.method": "string"
444
+ };
445
+ function collectChangedPaths(before, after, shape, prefix, found) {
446
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
447
+ const held = before[key];
448
+ const kept = after[key];
449
+ if (deepEqual(held, kept)) continue;
450
+ const known = shape !== void 0 && Object.hasOwn(shape, key);
451
+ const wildcard = !known && shape !== void 0 && Object.hasOwn(shape, "*");
452
+ const child = known ? shape[key] : wildcard ? shape["*"] : void 0;
453
+ const path = prefix ? `${prefix}.${wildcard ? "*" : key}` : key;
454
+ const descendable = child !== void 0 && Object.keys(child).length > 0 && (held === void 0 || isPlainObject(held)) && (kept === void 0 || isPlainObject(kept));
455
+ if (descendable) {
456
+ collectChangedPaths(
457
+ isPlainObject(held) ? held : {},
458
+ isPlainObject(kept) ? kept : {},
459
+ child,
460
+ path,
461
+ found
462
+ );
463
+ } else {
464
+ found.add(path);
465
+ }
466
+ }
467
+ }
468
+ function invalidFieldError(path, kind) {
469
+ return new DvmError(
470
+ "config_invalid",
471
+ `Config field '${path}' must be ${describeKind(kind)}.`,
472
+ `Nothing was written. Correct the field in ${CONFIG_FILE}, or run 'dvm doctor' for the full picture.${rollbackSuffix()}`
473
+ );
474
+ }
475
+ function describeKind(kind) {
476
+ switch (kind) {
477
+ case "string":
478
+ return "a string";
479
+ case "number":
480
+ return "a number";
481
+ case "object":
482
+ return "an object";
483
+ case "string[]":
484
+ return "an array of strings";
485
+ }
486
+ }
487
+ function matchesKind(value, kind) {
488
+ switch (kind) {
489
+ case "string":
490
+ return typeof value === "string";
491
+ case "number":
492
+ return typeof value === "number" && Number.isFinite(value);
493
+ case "object":
494
+ return isPlainObject(value);
495
+ case "string[]":
496
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
497
+ }
498
+ }
499
+ function rollbackSuffix() {
500
+ const stored = describeConfigBackup() ?? describeConfigBackup("pinned");
501
+ return stored === null ? "" : ` A stored copy of the previous configuration is at ${stored.path}.`;
502
+ }
503
+ function writeJsonAtomic(path, value) {
504
+ ensureConfigDir();
505
+ const tmp = `${path}.tmp`;
506
+ writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
507
+ chmodSync(tmp, 384);
508
+ commitTmp(tmp, path);
509
+ }
510
+ function commitTmp(tmp, path) {
511
+ try {
512
+ renameSync(tmp, path);
513
+ } catch (err) {
514
+ try {
515
+ unlinkSync(tmp);
516
+ } catch {
517
+ }
518
+ throw err;
519
+ }
520
+ }
521
+ function deepEqual(a, b) {
522
+ if (a === b) return true;
523
+ if (Array.isArray(a) || Array.isArray(b)) {
524
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
525
+ return a.every((item, index) => deepEqual(item, b[index]));
526
+ }
527
+ if (!isPlainObject(a) || !isPlainObject(b)) return false;
528
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
529
+ if (!deepEqual(a[key], b[key])) return false;
530
+ }
531
+ return true;
532
+ }
533
+ function isPresentValue(value) {
534
+ if (value === void 0 || value === null) return false;
535
+ return typeof value !== "string" || value.trim().length > 0;
536
+ }
537
+ function comparableProtectedValue(target, value) {
538
+ if (typeof value !== "string") return void 0;
539
+ const trimmed = value.trim();
540
+ if (trimmed.length === 0) return void 0;
541
+ const field = target.split(".")[0];
542
+ return PROTECTED_FIELDS[field].hexCredential ? trimmed.toLowerCase() : trimmed;
543
+ }
544
+ function patchShapeError(message) {
545
+ return new DvmError(
546
+ "config_patch_invalid",
547
+ message,
548
+ "A change declares `set` (fields to write) and `unset` (field names to remove). Anything else cannot be applied, and nothing was written."
549
+ );
550
+ }
551
+ function freezeDeep(value) {
552
+ if (typeof value !== "object" || value === null || Object.isFrozen(value)) return;
553
+ Object.freeze(value);
554
+ for (const nested of Object.values(value)) freezeDeep(nested);
555
+ }
556
+ function isPlainObject(value) {
557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
558
+ }
559
+ function toOctal(mode) {
560
+ return "0" + mode.toString(8).padStart(3, "0");
561
+ }
562
+
563
+ // src/lib/identity.ts
564
+ import lockfile from "proper-lockfile";
565
+ var CREDIT_POSTURES = ["suggest", "auto", "off"];
566
+ function isCreditPosture(value) {
567
+ return typeof value === "string" && CREDIT_POSTURES.includes(value);
568
+ }
569
+ function migrateCreditConfig(config) {
570
+ const credit = config.credit;
571
+ if (!credit) return config;
572
+ const migrateLayer = (layer) => {
573
+ const railed = migrateFundingRail(layer);
574
+ if (!("disabled" in railed)) return railed;
575
+ const { disabled, ...rest } = railed;
576
+ const migrated = { ...rest };
577
+ if (migrated.posture === void 0 && disabled === true) migrated.posture = "off";
578
+ return migrated;
579
+ };
580
+ const perDvm = credit.perDvm;
581
+ return {
582
+ ...config,
583
+ credit: {
584
+ ...migrateLayer(credit),
585
+ ...perDvm ? {
586
+ perDvm: Object.fromEntries(
587
+ Object.entries(perDvm).map(([endpoint, per]) => [endpoint, migrateLayer(per)])
588
+ )
589
+ } : {}
590
+ }
591
+ };
592
+ }
593
+ function migrateFundingRail(layer) {
594
+ const loose = layer;
595
+ if (loose.fundingRail !== "mpp") return layer;
596
+ return { ...layer, fundingRail: "tempo" };
597
+ }
598
+ function migrateTempoWalletKey(config) {
599
+ const loose = config;
600
+ if (!loose.mpp || loose.tempo) return config;
601
+ const { mpp, ...rest } = loose;
602
+ return { ...rest, tempo: mpp };
603
+ }
604
+ function migrateConfig(config) {
605
+ return migrateCreditConfig(migrateTempoWalletKey(config));
606
+ }
607
+ function loadConfig() {
608
+ const raw = readRawConfig();
609
+ return raw === null ? null : migrateConfig(raw);
610
+ }
611
+ async function updateConfig(update) {
612
+ return withConfigLock(async () => {
613
+ const raw = readRawConfig();
614
+ const snapshot = raw === null ? {} : structuredClone(raw);
615
+ const base = raw === null ? null : migrateConfig(structuredClone(raw));
616
+ const handed = raw === null ? null : deepFreezeConfig(migrateConfig(structuredClone(raw)));
617
+ const next = applyConfigPatch(base ?? {}, await runPatch(update, handed));
618
+ const changed = changedConfigPaths(snapshot, next);
619
+ if (changed.length === 0) {
620
+ return {
621
+ config: base ?? {},
622
+ previous: base,
623
+ changed: [],
624
+ written: false,
625
+ backup: describeConfigBackup()
626
+ };
627
+ }
628
+ const removals = protectedRemovals(base ?? {}, next);
629
+ const unauthorized = removals.filter((target) => update.authorize?.covers(target) !== true);
630
+ if (unauthorized.length > 0) throw protectedRemovalError(unauthorized, update.reason);
631
+ assertValidConfig(
632
+ next,
633
+ changed.map((path) => path.split(".")[0])
634
+ );
635
+ const superseded = supersededProtectedSecrets(base ?? {}, next);
636
+ const stripped = [
637
+ .../* @__PURE__ */ new Set([...removals, ...superseded, ...update.sanitizeBackup ?? []])
638
+ ].filter(isSecretProtectedTarget);
639
+ const sweeping = update.sweepSlots ?? CONFIG_SNAPSHOT_SLOTS;
640
+ const swept = [];
641
+ if (raw !== null) {
642
+ if (stripped.length > 0) {
643
+ writeConfigBackup(withoutProtectedTargets(raw, stripped));
644
+ swept.push("backup");
645
+ } else {
646
+ retainConfigBackup();
647
+ }
648
+ }
649
+ if (stripped.length > 0 && sweeping.includes("pinned")) {
650
+ if (sweepPinnedSnapshot(stripped, base ?? {}, update.reason)) swept.push("pinned");
651
+ }
652
+ const backup = describeConfigBackup();
653
+ writeConfigFile(next);
654
+ appendConfigAudit({
655
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
656
+ event: "update",
657
+ reason: update.reason,
658
+ fields: changed,
659
+ ...removals.length > 0 ? { removed: removals } : {},
660
+ ...superseded.length > 0 ? { superseded } : {},
661
+ ...swept.length > 0 ? { sweptSlots: swept } : {},
662
+ backup: backup !== null,
663
+ pid: process.pid
664
+ });
665
+ return { config: next, previous: base, changed, written: true, backup };
666
+ });
667
+ }
668
+ async function withConfigLock(fn) {
669
+ ensureConfigDir();
670
+ let release;
671
+ try {
672
+ release = await lockfile.lock(CONFIG_FILE, {
673
+ lockfilePath: `${CONFIG_FILE}.lock`,
674
+ realpath: false,
675
+ stale: 3e4,
676
+ retries: { retries: 10, factor: 1, minTimeout: 100, maxTimeout: 100 }
677
+ });
678
+ } catch {
679
+ throw new DvmError(
680
+ "config_locked",
681
+ "Another dvm process is updating the local configuration.",
682
+ "Wait a moment and retry."
683
+ );
684
+ }
685
+ try {
686
+ return await fn();
687
+ } finally {
688
+ await release().catch(() => void 0);
689
+ }
690
+ }
691
+ async function runPatch(update, current) {
692
+ try {
693
+ return await update.patch(current);
694
+ } catch (err) {
695
+ if (err instanceof TypeError && /read only|not extensible|frozen|cannot delete property|cannot add property/i.test(
696
+ err.message
697
+ )) {
698
+ throw new DvmError(
699
+ "config_patch_invalid",
700
+ `The '${update.reason}' change tried to modify the configuration it was handed.`,
701
+ "Nothing was written. A change declares what it wants through its `set` and `unset` lists; editing the current config in place bypasses the protected-field gate, so the object is frozen."
702
+ );
703
+ }
704
+ throw err;
705
+ }
706
+ }
707
+ function sweepPinnedSnapshot(targets, departing, reason) {
708
+ if (describeConfigBackup("pinned") === null) return false;
709
+ const pinned = readConfigBackup("pinned");
710
+ if (pinned === null) {
711
+ return retireSlot(`${reason}:unreadable_pin`, "pinned");
712
+ }
713
+ const migrated = migrateConfig(structuredClone(pinned));
714
+ const matching = targets.filter(
715
+ (target) => protectedValuesMatch(
716
+ target,
717
+ protectedTargetValue(departing, target),
718
+ protectedTargetValue(migrated, target)
719
+ )
720
+ );
721
+ if (matching.length === 0) return false;
722
+ const sanitized = withoutProtectedTargets(pinned, matching);
723
+ const changed = changedConfigPaths(pinned, sanitized);
724
+ if (changed.length === 0) return false;
725
+ writeConfigBackup(sanitized, "pinned");
726
+ appendConfigAudit({
727
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
728
+ event: "backup_sanitized",
729
+ reason,
730
+ fields: changed,
731
+ removed: matching,
732
+ slot: "pinned",
733
+ backup: describeConfigBackup() !== null,
734
+ pid: process.pid
735
+ });
736
+ return true;
737
+ }
738
+ function retireSlot(reason, slot) {
739
+ if (!removeConfigBackup(slot)) return false;
740
+ appendConfigAudit({
741
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
742
+ event: "backup_retired",
743
+ reason,
744
+ fields: [],
745
+ slot,
746
+ backup: describeConfigBackup() !== null,
747
+ pid: process.pid
748
+ });
749
+ return true;
750
+ }
751
+
752
+ // src/lib/credit-store.ts
753
+ import { existsSync as existsSync2, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
754
+ import { dirname } from "path";
755
+ import lockfile2 from "proper-lockfile";
756
+ function withCreditLock(fn, opts = {}) {
757
+ if (lockDepth > 0) {
758
+ lockDepth += 1;
759
+ try {
760
+ return rejectAsyncBody(fn());
761
+ } finally {
762
+ lockDepth -= 1;
763
+ }
764
+ }
765
+ const release = acquireCreditLock(opts);
766
+ lockDepth = 1;
767
+ try {
768
+ return rejectAsyncBody(fn());
769
+ } finally {
770
+ lockDepth = 0;
771
+ try {
772
+ release();
773
+ } catch {
774
+ }
775
+ }
776
+ }
777
+ function normalizeCreditEndpoint(endpoint) {
778
+ try {
779
+ return new URL(endpoint).origin;
780
+ } catch {
781
+ return endpoint.replace(/\/+$/, "");
782
+ }
783
+ }
784
+ function dvmLocationIdentity(record) {
785
+ if (typeof record.dvmId !== "string" || record.dvmId.length === 0) return void 0;
786
+ const pubkey = typeof record.attestedBuilderPubkey === "string" ? record.attestedBuilderPubkey.trim().toLowerCase() : "";
787
+ if (!/^[0-9a-f]{64}$/.test(pubkey)) return void 0;
788
+ return { dvmId: record.dvmId, builderPubkey: pubkey };
789
+ }
790
+ function findDvmLocated(records, args) {
791
+ const where = args.where ?? (() => true);
792
+ const identity = args.identity;
793
+ if (identity) {
794
+ const attested = records.find(
795
+ (record) => where(record) && matchesDvmIdentity(record, identity)
796
+ );
797
+ if (attested) return attested;
798
+ }
799
+ return records.find(
800
+ (record) => where(record) && matchesDvmOrigin(record, args.endpoint, identity)
801
+ );
802
+ }
803
+ function attachDvmLocation(record, endpoint, identity) {
804
+ let changed = false;
805
+ if (identity) {
806
+ if (record.dvmId !== identity.dvmId) {
807
+ record.dvmId = identity.dvmId;
808
+ changed = true;
809
+ }
810
+ if (record.attestedBuilderPubkey !== identity.builderPubkey) {
811
+ record.attestedBuilderPubkey = identity.builderPubkey;
812
+ changed = true;
813
+ }
814
+ }
815
+ if (endpoint !== record.endpoint && !dvmLocationOrigins(record).includes(endpoint)) {
816
+ record.endpointAliases = [...record.endpointAliases ?? [], endpoint];
817
+ changed = true;
818
+ }
819
+ return changed;
820
+ }
821
+ function dvmLocationOrigins(record) {
822
+ const aliases = Array.isArray(record.endpointAliases) ? record.endpointAliases.filter((endpoint) => typeof endpoint === "string") : [];
823
+ const primary = typeof record.endpoint === "string" ? [record.endpoint] : [];
824
+ return [...primary, ...aliases].map(normalizeCreditEndpoint);
825
+ }
826
+ function fundingReachableAt(funding, endpoint, credits) {
827
+ const at = normalizeCreditEndpoint(funding.endpoint);
828
+ const named = normalizeCreditEndpoint(endpoint);
829
+ if (at === named) return true;
830
+ const record = credits.find(
831
+ (c) => c.callerPubkey === funding.callerPubkey && c.creditId === funding.creditId
832
+ );
833
+ if (!record) return false;
834
+ const origins = dvmLocationOrigins(record);
835
+ return origins.includes(at) && origins.includes(named);
836
+ }
837
+ function loadCredit(endpoint, callerPubkey, identity) {
838
+ return withCreditLock(() => {
839
+ const key = normalizeCreditEndpoint(endpoint);
840
+ const store = loadStore();
841
+ const record = findDvmLocated(store.credits, {
842
+ endpoint: key,
843
+ ...identity ? { identity } : {},
844
+ where: (c) => c.callerPubkey === callerPubkey
845
+ });
846
+ if (record && attachDvmLocation(record, key, identity)) saveStore(store);
847
+ return record;
848
+ });
849
+ }
850
+ function listCredits() {
851
+ return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
852
+ }
853
+ function listPendingFundings(args = {}) {
854
+ const endpoint = args.endpoint ? normalizeCreditEndpoint(args.endpoint) : void 0;
855
+ return withCreditLock(() => {
856
+ const store = loadStore();
857
+ return store.pendingFundings.filter(
858
+ (funding) => (!endpoint || fundingReachableAt(funding, endpoint, store.credits)) && (!args.callerPubkey || funding.callerPubkey === args.callerPubkey) && (!args.creditId || funding.creditId === args.creditId)
859
+ ).sort((a, b) => a.requestedAt - b.requestedAt);
860
+ });
861
+ }
862
+ function loadPendingFunding(endpoint, callerPubkey, creditId) {
863
+ return listPendingFundings({ endpoint, callerPubkey, ...creditId ? { creditId } : {} })[0];
864
+ }
865
+ function recordPendingFunding(funding) {
866
+ withCreditLock(() => {
867
+ const store = loadStore();
868
+ const normalized = { ...funding, endpoint: normalizeCreditEndpoint(funding.endpoint) };
869
+ const index = store.pendingFundings.findIndex(
870
+ (entry) => entry.callerPubkey === normalized.callerPubkey && entry.creditId === normalized.creditId && entry.fundId === normalized.fundId && fundingReachableAt(entry, normalized.endpoint, store.credits)
871
+ );
872
+ if (index === -1) store.pendingFundings.push(normalized);
873
+ else
874
+ store.pendingFundings[index] = {
875
+ ...normalized,
876
+ endpoint: store.pendingFundings[index].endpoint
877
+ };
878
+ saveStore(store);
879
+ });
880
+ }
881
+ function clearPendingFunding(endpoint, callerPubkey, creditId, fundId) {
882
+ withCreditLock(() => {
883
+ const store = loadStore();
884
+ const normalized = normalizeCreditEndpoint(endpoint);
885
+ const remaining = store.pendingFundings.filter(
886
+ (entry) => !(entry.callerPubkey === callerPubkey && entry.creditId === creditId && entry.fundId === fundId && fundingReachableAt(entry, normalized, store.credits))
887
+ );
888
+ if (remaining.length === store.pendingFundings.length) return;
889
+ saveStore({ ...store, pendingFundings: remaining });
890
+ });
891
+ }
892
+ function recordFundResponse(args) {
893
+ const now = args.now ?? Date.now();
894
+ return withCreditLock(() => {
895
+ const store = loadStore();
896
+ const key = normalizeCreditEndpoint(args.endpoint);
897
+ let record = findDvmLocated(store.credits, {
898
+ endpoint: key,
899
+ ...args.identity ? { identity: args.identity } : {},
900
+ where: (c) => c.callerPubkey === args.callerPubkey
901
+ });
902
+ if (!record) {
903
+ record = {
904
+ endpoint: key,
905
+ ...identityFields(args.identity),
906
+ callerPubkey: args.callerPubkey,
907
+ creditId: args.credit.credit_id,
908
+ currency: args.credit.currency,
909
+ balanceMicro: 0,
910
+ remainingMicro: 0,
911
+ ledgerSeq: 0,
912
+ expiryMs: 0,
913
+ targetMicro: args.targetMicro,
914
+ status: "active",
915
+ openedAt: now,
916
+ updatedAt: now
917
+ };
918
+ store.credits.push(record);
919
+ }
920
+ attachDvmLocation(record, key, args.identity);
921
+ adoptCreditId(record, args.credit.credit_id);
922
+ record.currency = args.credit.currency;
923
+ record.balanceMicro = args.credit.balance_micro;
924
+ record.remainingMicro = args.credit.remaining_micro;
925
+ record.ledgerSeq = Math.max(record.ledgerSeq, args.credit.ledger_seq);
926
+ record.expiryMs = args.credit.expiry_ms;
927
+ record.targetMicro = args.targetMicro;
928
+ record.status = args.credit.status === "unbacked" ? "unbacked" : args.credit.expired ? "expired" : "active";
929
+ record.reconciliationPending = args.credit.reconciliation_pending;
930
+ record.updatedAt = now;
931
+ delete record.provisional;
932
+ if (args.sequenced === false) record.unsequencedBalance = true;
933
+ else delete record.unsequencedBalance;
934
+ if (args.menu) record.menu = args.menu;
935
+ if (args.rail) {
936
+ record.rail = args.rail;
937
+ delete record.refillUnavailableRail;
938
+ }
939
+ if (args.x402Instrument) record.x402Instrument = args.x402Instrument;
940
+ if (args.x402ChannelNetwork) record.x402ChannelNetwork = args.x402ChannelNetwork;
941
+ if (args.builderPubkey) record.builderPubkey = args.builderPubkey;
942
+ if (args.select) delete record.selectionCleared;
943
+ delete record.discrepancy;
944
+ saveStore(store);
945
+ return record;
946
+ });
947
+ }
948
+ function loadCreditX402Binding(endpoint, callerPubkey, creditId, identity) {
949
+ const record = loadCredit(endpoint, callerPubkey, identity);
950
+ if (record?.creditId !== creditId) return {};
951
+ if (record.status !== "active" && record.status !== "expired") return {};
952
+ return {
953
+ ...record.x402Instrument ? { instrument: record.x402Instrument } : {},
954
+ ...record.x402ChannelNetwork ? { channelNetwork: record.x402ChannelNetwork } : {}
955
+ };
956
+ }
957
+ function recordCreditX402Instrument(endpoint, callerPubkey, creditId, instrument, identity, channelNetwork) {
958
+ mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
959
+ if (record.creditId !== creditId) return "skip";
960
+ const instrumentChanged = record.x402Instrument !== instrument;
961
+ const channelNetworkChanged = instrument === "x402_channel" && channelNetwork !== void 0 && record.x402ChannelNetwork !== channelNetwork;
962
+ if (!instrumentChanged && !channelNetworkChanged) return "skip";
963
+ if (instrumentChanged) {
964
+ record.x402Instrument = instrument;
965
+ delete record.x402ChannelNetwork;
966
+ }
967
+ if (instrument === "x402_channel" && channelNetwork !== void 0) {
968
+ record.x402ChannelNetwork = channelNetwork;
969
+ }
970
+ return void 0;
971
+ });
972
+ }
973
+ function identityFields(identity) {
974
+ if (!identity) return {};
975
+ return { dvmId: identity.dvmId, attestedBuilderPubkey: identity.builderPubkey };
976
+ }
977
+ function mutationAt(endpoint, callerPubkey, identity) {
978
+ return { endpoint, callerPubkey, ...identity ? { identity } : {} };
979
+ }
980
+ function matchesDvmIdentity(record, identity) {
981
+ const stored = dvmLocationIdentity(record);
982
+ return stored?.dvmId === identity.dvmId && stored.builderPubkey === identity.builderPubkey;
983
+ }
984
+ function matchesDvmOrigin(record, endpoint, identity) {
985
+ if (identity) {
986
+ if (dvmLocationIdentity(record) !== void 0) return false;
987
+ if (record.dvmId !== void 0 && record.dvmId !== identity.dvmId) return false;
988
+ }
989
+ return dvmLocationOrigins(record).includes(endpoint);
990
+ }
991
+ function adoptCreditId(record, creditId) {
992
+ if (record.creditId === creditId) return;
993
+ if (record.status === "active" || record.status === "expired" || record.status === "unbacked") {
994
+ const prior = {
995
+ creditId: record.creditId,
996
+ status: record.status === "unbacked" ? "unbacked" : "active",
997
+ currency: record.currency,
998
+ balanceMicro: record.balanceMicro,
999
+ remainingMicro: record.remainingMicro,
1000
+ ledgerSeq: record.ledgerSeq,
1001
+ expiryMs: record.expiryMs,
1002
+ expired: record.status === "expired",
1003
+ drainable: record.status !== "unbacked" && record.balanceMicro > 0,
1004
+ ...record.reconciliationPending ? { reconciliationPending: true } : {},
1005
+ observedAt: record.updatedAt
1006
+ };
1007
+ record.siblingCredits = [
1008
+ ...(record.siblingCredits ?? []).filter(
1009
+ (sibling) => sibling.creditId !== prior.creditId && sibling.creditId !== creditId
1010
+ ),
1011
+ prior
1012
+ ];
1013
+ } else if (record.siblingCredits) {
1014
+ record.siblingCredits = record.siblingCredits.filter(
1015
+ (sibling) => sibling.creditId !== creditId
1016
+ );
1017
+ }
1018
+ record.creditId = creditId;
1019
+ record.ledgerSeq = 0;
1020
+ record.balanceMicro = 0;
1021
+ record.remainingMicro = 0;
1022
+ record.expiryMs = 0;
1023
+ delete record.unsequencedBalance;
1024
+ delete record.rail;
1025
+ delete record.x402Instrument;
1026
+ delete record.x402ChannelNetwork;
1027
+ delete record.refillUnavailableRail;
1028
+ delete record.refillDrainNoticed;
1029
+ delete record.discrepancy;
1030
+ delete record.lastReceiptJobId;
1031
+ }
1032
+ var MAX_LOST_IMPLICIT_CREDITS = 8;
1033
+ var MAX_DATE_MS = 864e13;
1034
+ function loadStore() {
1035
+ if (!existsSync2(CREDITS_FILE)) {
1036
+ return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
1037
+ }
1038
+ let raw;
1039
+ try {
1040
+ raw = readFileSync2(CREDITS_FILE, "utf-8");
1041
+ } catch (cause) {
1042
+ throw unreadableStore(cause);
1043
+ }
1044
+ let data;
1045
+ try {
1046
+ const parsed = JSON.parse(raw);
1047
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1048
+ throw new Error("the file parsed as JSON but is not a credit-store object");
1049
+ }
1050
+ data = parsed;
1051
+ } catch (cause) {
1052
+ throw quarantineCorruptStore(cause);
1053
+ }
1054
+ const merged = mergeDuplicateCredits(
1055
+ Array.isArray(data.credits) ? data.credits.filter((record) => isObjectRecord(record)).map(
1056
+ (record) => normalizeRailKey(normalizeEvidence(normalizeDrains(normalizeSiblingCredits(record))))
1057
+ ) : []
1058
+ );
1059
+ const snapshots = mergeDuplicateSnapshots(
1060
+ Array.isArray(data.fundingMenuSnapshots) ? data.fundingMenuSnapshots.filter(isFundingMenuSnapshot) : [],
1061
+ merged.credits
1062
+ );
1063
+ const fundings = dedupePendingFundings(
1064
+ Array.isArray(data.pendingFundings) ? data.pendingFundings.filter((funding) => isObjectRecord(funding)).map(normalizeRailKey) : []
1065
+ );
1066
+ const implicit = dedupeImplicitCredits(
1067
+ Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : []
1068
+ );
1069
+ const store = {
1070
+ credits: merged.credits,
1071
+ implicitCredits: implicit.credits,
1072
+ pendingFundings: fundings.fundings,
1073
+ fundingMenuSnapshots: snapshots.snapshots
1074
+ };
1075
+ if (merged.changed || snapshots.changed || fundings.changed || implicit.changed) saveStore(store);
1076
+ return store;
1077
+ }
1078
+ function isStoredImplicitCredit(value) {
1079
+ if (!isObjectRecord(value)) return false;
1080
+ return typeof value.endpoint === "string" && typeof value.callerPubkey === "string" && typeof value.creditId === "string" && value.creditId.startsWith("imp:") && typeof value.currency === "string" && typeof value.remainingMicro === "number" && Number.isFinite(value.remainingMicro) && typeof value.ledgerSeq === "number" && Number.isInteger(value.ledgerSeq) && typeof value.expiryMs === "number" && Number.isFinite(value.expiryMs) && (value.status === "active" || value.status === "lost") && typeof value.releasedByJobId === "string" && typeof value.releasedAt === "number" && Number.isFinite(value.releasedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt);
1081
+ }
1082
+ function dedupeImplicitCredits(credits) {
1083
+ const merged = [];
1084
+ let changed = false;
1085
+ for (const candidate of credits) {
1086
+ candidate.endpoint = normalizeCreditEndpoint(candidate.endpoint);
1087
+ const identity = dvmLocationIdentity(candidate);
1088
+ const existing = findDvmLocated(merged, {
1089
+ endpoint: candidate.endpoint,
1090
+ ...identity ? { identity } : {},
1091
+ where: (record) => record.callerPubkey === candidate.callerPubkey && record.creditId === candidate.creditId
1092
+ });
1093
+ if (!existing) {
1094
+ merged.push(candidate);
1095
+ continue;
1096
+ }
1097
+ changed = true;
1098
+ for (const origin of dvmLocationOrigins(candidate)) {
1099
+ attachDvmLocation(existing, origin, identity);
1100
+ }
1101
+ if (candidate.ledgerSeq > existing.ledgerSeq || candidate.ledgerSeq === existing.ledgerSeq && candidate.updatedAt > existing.updatedAt) {
1102
+ existing.currency = candidate.currency;
1103
+ existing.remainingMicro = candidate.remainingMicro;
1104
+ existing.ledgerSeq = candidate.ledgerSeq;
1105
+ existing.status = candidate.status;
1106
+ if (candidate.lossReason) existing.lossReason = candidate.lossReason;
1107
+ else delete existing.lossReason;
1108
+ existing.updatedAt = candidate.updatedAt;
1109
+ existing.lastReceiptJobId = candidate.lastReceiptJobId;
1110
+ existing.discrepancy = candidate.discrepancy;
1111
+ }
1112
+ existing.expiryMs = Math.min(existing.expiryMs, candidate.expiryMs);
1113
+ if (candidate.releasedAt < existing.releasedAt) {
1114
+ existing.releasedAt = candidate.releasedAt;
1115
+ existing.releasedByJobId = candidate.releasedByJobId;
1116
+ }
1117
+ }
1118
+ const capped = capLostImplicitCredits(merged);
1119
+ return { credits: capped.credits, changed: changed || capped.changed };
1120
+ }
1121
+ function capLostImplicitCredits(credits) {
1122
+ const lostByOwner = /* @__PURE__ */ new Map();
1123
+ for (const credit of credits) {
1124
+ if (credit.status !== "lost") continue;
1125
+ const identity = dvmLocationIdentity(credit);
1126
+ const dvmKey = identity ? `${identity.dvmId}\0${identity.builderPubkey}` : normalizeCreditEndpoint(credit.endpoint);
1127
+ const key = `${credit.callerPubkey}\0${dvmKey}`;
1128
+ const group = lostByOwner.get(key) ?? [];
1129
+ group.push(credit);
1130
+ lostByOwner.set(key, group);
1131
+ }
1132
+ const dropped = /* @__PURE__ */ new Set();
1133
+ for (const group of lostByOwner.values()) {
1134
+ group.sort((a, b) => b.updatedAt - a.updatedAt);
1135
+ for (const credit of group.slice(MAX_LOST_IMPLICIT_CREDITS)) dropped.add(credit);
1136
+ }
1137
+ return dropped.size === 0 ? { credits, changed: false } : { credits: credits.filter((credit) => !dropped.has(credit)), changed: true };
1138
+ }
1139
+ function mergeDuplicateSnapshots(snapshots, credits) {
1140
+ const identities = originIdentityMap(credits);
1141
+ const merged = [];
1142
+ const byIdentity = /* @__PURE__ */ new Map();
1143
+ let changed = false;
1144
+ for (const snapshot of snapshots) {
1145
+ const stamped = identities.get(`${snapshot.callerPubkey}\0${snapshot.endpoint}`);
1146
+ if (stamped !== void 0 && snapshot.dvmId === void 0) {
1147
+ snapshot.dvmId = stamped.dvmId;
1148
+ if (stamped.attestedBuilderPubkey !== void 0) {
1149
+ snapshot.attestedBuilderPubkey = stamped.attestedBuilderPubkey;
1150
+ }
1151
+ changed = true;
1152
+ }
1153
+ if (snapshot.dvmId === void 0) {
1154
+ merged.push(snapshot);
1155
+ continue;
1156
+ }
1157
+ const key = `${snapshot.callerPubkey}\0${snapshot.dvmId}\0${snapshot.attestedBuilderPubkey ?? ""}`;
1158
+ const index = byIdentity.get(key);
1159
+ if (index === void 0) {
1160
+ byIdentity.set(key, merged.length);
1161
+ merged.push(snapshot);
1162
+ continue;
1163
+ }
1164
+ merged[index] = mergeSnapshots(merged[index], snapshot);
1165
+ changed = true;
1166
+ }
1167
+ return { snapshots: merged, changed };
1168
+ }
1169
+ function mergeSnapshots(left, right) {
1170
+ const leftSeen = Number.isFinite(left.observedAt) ? left.observedAt : 0;
1171
+ const rightSeen = Number.isFinite(right.observedAt) ? right.observedAt : 0;
1172
+ const [newer, older] = rightSeen >= leftSeen ? [right, left] : [left, right];
1173
+ const snapshot = { ...newer };
1174
+ if (snapshot.builderPubkey === void 0 && older.builderPubkey !== void 0) {
1175
+ snapshot.builderPubkey = older.builderPubkey;
1176
+ }
1177
+ const origins = dvmLocationOrigins(newer).concat(dvmLocationOrigins(older));
1178
+ const aliases = [...new Set(origins)].filter((origin) => origin !== snapshot.endpoint);
1179
+ if (aliases.length > 0) snapshot.endpointAliases = aliases;
1180
+ else delete snapshot.endpointAliases;
1181
+ return snapshot;
1182
+ }
1183
+ function originIdentityMap(credits) {
1184
+ const identities = /* @__PURE__ */ new Map();
1185
+ const ambiguous = /* @__PURE__ */ new Set();
1186
+ for (const record of credits) {
1187
+ if (typeof record.dvmId !== "string" || typeof record.callerPubkey !== "string") continue;
1188
+ const identity = {
1189
+ dvmId: record.dvmId,
1190
+ ...record.attestedBuilderPubkey !== void 0 ? { attestedBuilderPubkey: record.attestedBuilderPubkey } : {}
1191
+ };
1192
+ for (const origin of dvmLocationOrigins(record)) {
1193
+ const key = `${record.callerPubkey}\0${origin}`;
1194
+ const seen = identities.get(key);
1195
+ if (seen !== void 0 && (seen.dvmId !== identity.dvmId || seen.attestedBuilderPubkey !== identity.attestedBuilderPubkey)) {
1196
+ ambiguous.add(key);
1197
+ continue;
1198
+ }
1199
+ identities.set(key, identity);
1200
+ }
1201
+ }
1202
+ for (const key of ambiguous) identities.delete(key);
1203
+ return identities;
1204
+ }
1205
+ function dedupePendingFundings(fundings) {
1206
+ const kept = [];
1207
+ const byIntent = /* @__PURE__ */ new Map();
1208
+ let changed = false;
1209
+ for (const funding of fundings) {
1210
+ if (typeof funding.callerPubkey !== "string" || typeof funding.creditId !== "string" || typeof funding.fundId !== "string") {
1211
+ kept.push(funding);
1212
+ continue;
1213
+ }
1214
+ const key = `${funding.callerPubkey}\0${funding.creditId}\0${funding.fundId}`;
1215
+ const index = byIntent.get(key);
1216
+ if (index === void 0) {
1217
+ byIntent.set(key, kept.length);
1218
+ kept.push(funding);
1219
+ continue;
1220
+ }
1221
+ const seen = kept[index];
1222
+ const seenAt = Number.isFinite(seen.requestedAt) ? seen.requestedAt : 0;
1223
+ const at = Number.isFinite(funding.requestedAt) ? funding.requestedAt : 0;
1224
+ if (at > seenAt) kept[index] = funding;
1225
+ changed = true;
1226
+ }
1227
+ return { fundings: kept, changed };
1228
+ }
1229
+ function isObjectRecord(value) {
1230
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1231
+ }
1232
+ function mergeDuplicateCredits(credits) {
1233
+ const merged = [];
1234
+ const byCredit = /* @__PURE__ */ new Map();
1235
+ let changed = false;
1236
+ for (const record of credits) {
1237
+ if (typeof record.creditId !== "string" || typeof record.callerPubkey !== "string") {
1238
+ merged.push(record);
1239
+ continue;
1240
+ }
1241
+ const key = `${record.callerPubkey}\0${record.creditId}`;
1242
+ const index = byCredit.get(key);
1243
+ if (index === void 0) {
1244
+ byCredit.set(key, merged.length);
1245
+ merged.push(record);
1246
+ continue;
1247
+ }
1248
+ merged[index] = mergeCreditRecords(merged[index], record);
1249
+ changed = true;
1250
+ }
1251
+ return { credits: merged, changed };
1252
+ }
1253
+ function mergeCreditRecords(left, right) {
1254
+ const leftUpdated = Number.isFinite(left.updatedAt) ? left.updatedAt : 0;
1255
+ const rightUpdated = Number.isFinite(right.updatedAt) ? right.updatedAt : 0;
1256
+ const [newer, older] = rightUpdated >= leftUpdated ? [right, left] : [left, right];
1257
+ const record = { ...newer };
1258
+ if (record.dvmId === void 0 && older.dvmId !== void 0) record.dvmId = older.dvmId;
1259
+ if (record.attestedBuilderPubkey === void 0 && older.attestedBuilderPubkey !== void 0 && record.dvmId === older.dvmId) {
1260
+ record.attestedBuilderPubkey = older.attestedBuilderPubkey;
1261
+ }
1262
+ if (record.rail === void 0 && older.rail !== void 0) record.rail = older.rail;
1263
+ if (record.x402Instrument === void 0 && older.x402Instrument !== void 0) {
1264
+ record.x402Instrument = older.x402Instrument;
1265
+ }
1266
+ if (record.x402ChannelNetwork === void 0 && older.x402ChannelNetwork !== void 0) {
1267
+ record.x402ChannelNetwork = older.x402ChannelNetwork;
1268
+ }
1269
+ if (record.menu === void 0 && older.menu !== void 0) record.menu = older.menu;
1270
+ if (record.builderPubkey === void 0 && older.builderPubkey !== void 0) {
1271
+ record.builderPubkey = older.builderPubkey;
1272
+ }
1273
+ const endpoints = dvmLocationOrigins(newer).concat(dvmLocationOrigins(older));
1274
+ const aliases = [...new Set(endpoints)].filter((endpoint) => endpoint !== record.endpoint);
1275
+ if (aliases.length > 0) record.endpointAliases = aliases;
1276
+ else delete record.endpointAliases;
1277
+ const pendingDrains = unionPendingDrains(newer.pendingDrains, older.pendingDrains);
1278
+ if (pendingDrains.length > 0) record.pendingDrains = pendingDrains;
1279
+ else delete record.pendingDrains;
1280
+ const drainEvidence = unionDrainEvidence(newer.drainEvidence, older.drainEvidence);
1281
+ if (drainEvidence.length > 0) record.drainEvidence = drainEvidence;
1282
+ else delete record.drainEvidence;
1283
+ const siblings = Array.isArray(newer.siblingCredits) ? newer.siblingCredits : older.siblingCredits;
1284
+ if (siblings) {
1285
+ record.siblingCredits = siblings.filter((sibling) => sibling.creditId !== record.creditId);
1286
+ } else {
1287
+ delete record.siblingCredits;
1288
+ }
1289
+ return record;
1290
+ }
1291
+ function unionPendingDrains(preferred, fallback) {
1292
+ const byDrain = /* @__PURE__ */ new Map();
1293
+ for (const drain of [...fallback ?? [], ...preferred ?? []]) {
1294
+ const key = `${drain.drainId}\0${drain.creditId ?? ""}`;
1295
+ byDrain.set(key, { ...byDrain.get(key) ?? {}, ...drain });
1296
+ }
1297
+ return [...byDrain.values()];
1298
+ }
1299
+ function unionDrainEvidence(preferred, fallback) {
1300
+ const byDrain = /* @__PURE__ */ new Map();
1301
+ for (const evidence of [...fallback ?? [], ...preferred ?? []]) {
1302
+ const key = `${evidence.drainId}\0${evidence.creditId}`;
1303
+ const existing = byDrain.get(key);
1304
+ if (!existing) {
1305
+ byDrain.set(key, evidence);
1306
+ continue;
1307
+ }
1308
+ const [newer, older] = evidence.collectedAt >= existing.collectedAt ? [evidence, existing] : [existing, evidence];
1309
+ const receipts = unionUnknown(newer.receipts, older.receipts);
1310
+ byDrain.set(key, {
1311
+ ...older,
1312
+ ...newer,
1313
+ receipts,
1314
+ collectedAt: Math.max(existing.collectedAt, evidence.collectedAt),
1315
+ ...existing.settledAt !== void 0 || evidence.settledAt !== void 0 ? { settledAt: Math.max(existing.settledAt ?? 0, evidence.settledAt ?? 0) } : {}
1316
+ });
1317
+ }
1318
+ return [...byDrain.values()];
1319
+ }
1320
+ function unionUnknown(preferred, fallback) {
1321
+ const seen = /* @__PURE__ */ new Set();
1322
+ const values = [];
1323
+ for (const value of [...preferred, ...fallback]) {
1324
+ let key;
1325
+ try {
1326
+ const serialized = JSON.stringify(value);
1327
+ key = typeof serialized === "string" ? serialized : String(value);
1328
+ } catch {
1329
+ key = String(value);
1330
+ }
1331
+ if (seen.has(key)) continue;
1332
+ seen.add(key);
1333
+ values.push(value);
1334
+ }
1335
+ return values;
1336
+ }
1337
+ function isFundingMenuSnapshot(value) {
1338
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1339
+ const snapshot = value;
1340
+ return typeof snapshot.endpoint === "string" && typeof snapshot.callerPubkey === "string" && typeof snapshot.observedAt === "number" && Number.isFinite(snapshot.observedAt) && Math.abs(snapshot.observedAt) <= MAX_DATE_MS && parseCreditMenu(snapshot.menu) !== void 0 && (snapshot.builderPubkey === void 0 || typeof snapshot.builderPubkey === "string");
1341
+ }
1342
+ function normalizeRailKey(record) {
1343
+ const loose = record;
1344
+ if (loose.rail === LEGACY_TEMPO_RAIL_KEY) loose.rail = "tempo";
1345
+ if (loose.refillUnavailableRail === LEGACY_TEMPO_RAIL_KEY) loose.refillUnavailableRail = "tempo";
1346
+ if (Array.isArray(loose.pendingDrains)) {
1347
+ for (const drain of loose.pendingDrains) {
1348
+ if (drain?.method === LEGACY_TEMPO_RAIL_KEY) drain.method = "tempo";
1349
+ }
1350
+ }
1351
+ return record;
1352
+ }
1353
+ var LEGACY_TEMPO_RAIL_KEY = "mpp";
1354
+ function normalizeSiblingCredits(record) {
1355
+ if (!Array.isArray(record.siblingCredits)) {
1356
+ delete record.siblingCredits;
1357
+ return record;
1358
+ }
1359
+ record.siblingCredits = record.siblingCredits.filter(
1360
+ (sibling) => isSiblingCredit(sibling) && sibling.creditId !== record.creditId
1361
+ );
1362
+ return record;
1363
+ }
1364
+ function isSiblingCredit(value) {
1365
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1366
+ const sibling = value;
1367
+ const status = value.status;
1368
+ return typeof sibling.creditId === "string" && typeof sibling.currency === "string" && Number.isSafeInteger(sibling.balanceMicro) && Number.isSafeInteger(sibling.remainingMicro) && Number.isSafeInteger(sibling.ledgerSeq) && Number.isFinite(sibling.expiryMs) && Math.abs(sibling.expiryMs) <= MAX_DATE_MS && typeof sibling.expired === "boolean" && typeof sibling.drainable === "boolean" && (status === void 0 || status === "active" || status === "unbacked") && (sibling.reconciliationPending === void 0 || typeof sibling.reconciliationPending === "boolean") && Number.isFinite(sibling.observedAt) && Math.abs(sibling.observedAt) <= MAX_DATE_MS;
1369
+ }
1370
+ function normalizeDrains(record) {
1371
+ const legacy = record;
1372
+ const drains = (Array.isArray(record.pendingDrains) ? record.pendingDrains : []).filter(
1373
+ isDrainish
1374
+ );
1375
+ if (isDrainish(legacy.pendingDrain)) drains.unshift(legacy.pendingDrain);
1376
+ delete legacy.pendingDrain;
1377
+ if (drains.length === 0) delete record.pendingDrains;
1378
+ else record.pendingDrains = drains;
1379
+ return record;
1380
+ }
1381
+ function normalizeEvidence(record) {
1382
+ if (!Array.isArray(record.drainEvidence)) {
1383
+ delete record.drainEvidence;
1384
+ return record;
1385
+ }
1386
+ const evidence = record.drainEvidence.filter(isEvidenceish).map((e) => {
1387
+ const bundle = {
1388
+ ...e,
1389
+ creditId: typeof e.creditId === "string" ? e.creditId : "",
1390
+ collectedAt: Number.isFinite(e.collectedAt) ? e.collectedAt : 0
1391
+ };
1392
+ if (!Number.isFinite(bundle.settledAt)) delete bundle.settledAt;
1393
+ return bundle;
1394
+ });
1395
+ if (evidence.length === 0) delete record.drainEvidence;
1396
+ else record.drainEvidence = evidence;
1397
+ return record;
1398
+ }
1399
+ function isEvidenceish(value) {
1400
+ return typeof value === "object" && value !== null && typeof value.drainId === "string" && Array.isArray(value.receipts);
1401
+ }
1402
+ function isDrainish(value) {
1403
+ return typeof value === "object" && value !== null && typeof value.drainId === "string";
1404
+ }
1405
+ function saveStore(store) {
1406
+ ensureConfigDir();
1407
+ const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
1408
+ writeFileSync2(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
1409
+ try {
1410
+ renameSync2(tmp, CREDITS_FILE);
1411
+ } catch (err) {
1412
+ try {
1413
+ unlinkSync2(tmp);
1414
+ } catch {
1415
+ }
1416
+ throw err;
1417
+ }
1418
+ }
1419
+ function unreadableStore(cause) {
1420
+ const detail = cause instanceof Error ? cause.message : String(cause);
1421
+ return new DvmError(
1422
+ "credit_store_unreadable",
1423
+ `The prepaid-credit record at ${CREDITS_FILE} exists but could not be opened: ${detail}`,
1424
+ `The file was left untouched. Check that you own it and can read it ('ls -l ${CREDITS_FILE}'), then retry \u2014 'chmod 600 ${CREDITS_FILE}' fixes the common case. Do not delete it: it may hold DVM-signed drain receipts for a refund that is still owed.`,
1425
+ { credits_file: CREDITS_FILE, cause: detail },
1426
+ `Your local record of prepaid credits could not be opened \u2014 this looks like a file-permissions problem on this machine, not damage to the record. Nothing was changed or deleted, so any signed proof of a refund you are owed is still there.`
1427
+ );
1428
+ }
1429
+ function quarantineCorruptStore(cause) {
1430
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1431
+ const quarantined = `${CREDITS_FILE}.corrupt-${stamp}-${String(process.pid)}`;
1432
+ let moved = false;
1433
+ try {
1434
+ renameSync2(CREDITS_FILE, quarantined);
1435
+ moved = true;
1436
+ } catch {
1437
+ }
1438
+ const kept = moved ? quarantined : CREDITS_FILE;
1439
+ return new DvmError(
1440
+ "credit_store_corrupt",
1441
+ moved ? `The prepaid-credit record at ${CREDITS_FILE} is damaged and could not be parsed; it was moved aside to ${quarantined}.` : `The prepaid-credit record at ${CREDITS_FILE} is damaged and could not be parsed, and could not be moved aside.`,
1442
+ `Do not delete ${kept} \u2014 it may hold DVM-signed drain receipts for a refund that is still owed. Balances themselves live on the DVM, so 'dvm credit balance <dvm>' re-reads them.`,
1443
+ {
1444
+ credits_file: CREDITS_FILE,
1445
+ ...moved ? { quarantined_path: quarantined } : {},
1446
+ cause: cause instanceof Error ? cause.message : String(cause)
1447
+ },
1448
+ `The local record of your prepaid credits is damaged. It was set aside at ${kept} rather than overwritten, so any signed proof of a refund you are owed is still in that file. Your credits are held by each DVM, not by this file, so re-reading a balance will restore what the CLI shows.`
1449
+ );
1450
+ }
1451
+ function mutateCredit(location, mutate) {
1452
+ withCreditLock(() => {
1453
+ const store = loadStore();
1454
+ const key = normalizeCreditEndpoint(location.endpoint);
1455
+ const record = findDvmLocated(store.credits, {
1456
+ endpoint: key,
1457
+ ...location.identity ? { identity: location.identity } : {},
1458
+ where: (c) => c.callerPubkey === location.callerPubkey
1459
+ });
1460
+ if (!record) return;
1461
+ const locationChanged = attachDvmLocation(record, key, location.identity);
1462
+ if (mutate(record) === "skip") {
1463
+ if (locationChanged) saveStore(store);
1464
+ return;
1465
+ }
1466
+ record.updatedAt = Date.now();
1467
+ saveStore(store);
1468
+ });
1469
+ }
1470
+ var DEFAULT_LOCK_STALE_MS = 1e4;
1471
+ var DEFAULT_LOCK_RETRIES = 600;
1472
+ var DEFAULT_LOCK_RETRY_INTERVAL_MS = 25;
1473
+ var lockDepth = 0;
1474
+ function acquireCreditLock(opts) {
1475
+ ensureConfigDir();
1476
+ const lockPath = `${CREDITS_FILE}.lock`;
1477
+ const stale = opts.stale ?? DEFAULT_LOCK_STALE_MS;
1478
+ const retries = opts.retries ?? DEFAULT_LOCK_RETRIES;
1479
+ const interval = opts.retryIntervalMs ?? DEFAULT_LOCK_RETRY_INTERVAL_MS;
1480
+ let lastError;
1481
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
1482
+ try {
1483
+ return lockfile2.lockSync(CREDITS_FILE, {
1484
+ lockfilePath: lockPath,
1485
+ stale,
1486
+ realpath: false
1487
+ });
1488
+ } catch (err) {
1489
+ lastError = err;
1490
+ if (err.code !== "ELOCKED") {
1491
+ const detail = err instanceof Error ? err.message : String(err);
1492
+ const configDir = dirname(lockPath);
1493
+ throw new DvmError(
1494
+ "credit_store_lock_failed",
1495
+ `Could not create the prepaid-credit lock at ${lockPath}: ${detail}`,
1496
+ `This is not contention \u2014 nothing is holding the lock. Check that ${configDir} exists and is writable by you ('ls -ld ${configDir}'), then retry.`,
1497
+ {
1498
+ lock_file: lockPath,
1499
+ config_dir: configDir,
1500
+ underlying: detail,
1501
+ errno_code: err.code
1502
+ },
1503
+ `The dvm CLI could not create the small lock file it uses to keep two commands from writing your credit record at once. Its folder looks unwritable \u2014 nothing about your credits is wrong, and nothing was changed.`
1504
+ );
1505
+ }
1506
+ if (attempt === retries) break;
1507
+ sleepSync(interval);
1508
+ }
1509
+ }
1510
+ throw new DvmError(
1511
+ "credit_store_locked",
1512
+ `Could not acquire the prepaid-credit lock at ${lockPath} within the retry budget.`,
1513
+ `Another dvm CLI may be writing to ${CREDITS_FILE}. Wait a moment and retry. If no other dvm CLI is running, remove ${lockPath} and try again.`,
1514
+ {
1515
+ lock_file: lockPath,
1516
+ underlying: lastError instanceof Error ? lastError.message : String(lastError)
1517
+ }
1518
+ );
1519
+ }
1520
+ function rejectAsyncBody(result) {
1521
+ if (typeof result?.then !== "function") {
1522
+ return result;
1523
+ }
1524
+ throw new DvmError(
1525
+ "credit_store_lock_misuse",
1526
+ "withCreditLock was called with an asynchronous body. The lock is released when the body returns, so a promise's writes would run unprotected.",
1527
+ "Make the locked body synchronous \u2014 the credit store's whole surface is sync \u2014 and do any awaiting outside the lock."
1528
+ );
1529
+ }
1530
+ function sleepSync(ms) {
1531
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1532
+ }
1533
+
1534
+ export {
1535
+ info,
1536
+ progress,
1537
+ maxAdvertisedMicro,
1538
+ withinAdvertisedTolerance,
1539
+ isValidEvmPrivateKey,
1540
+ authorizeProtectedRemoval,
1541
+ isCreditPosture,
1542
+ loadConfig,
1543
+ updateConfig,
1544
+ SUPPORTED_PROTOCOL_VERSION,
1545
+ parseCreditTerms,
1546
+ parseCreditMenu,
1547
+ normalizeCreditEndpoint,
1548
+ fundingReachableAt,
1549
+ listCredits,
1550
+ listPendingFundings,
1551
+ loadPendingFunding,
1552
+ recordPendingFunding,
1553
+ clearPendingFunding,
1554
+ recordFundResponse,
1555
+ loadCreditX402Binding,
1556
+ recordCreditX402Instrument
1557
+ };