@dvmkit/sdk 0.1.0-rc.5 → 0.1.0-rc.6

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.
@@ -1,14 +1,50 @@
1
1
  import {
2
- CONFIG_AUDIT_FILE,
3
- CONFIG_BACKUP_FILE,
4
- CONFIG_FILE,
5
- CONFIG_PINNED_FILE,
2
+ IMPLICIT_CREDIT_TTL_MS
3
+ } from "./chunk-5URG56JJ.js";
4
+ import {
6
5
  CREDITS_FILE,
7
6
  DvmError,
8
7
  ensureConfigDir
9
- } from "./chunk-F2L6KIMD.js";
8
+ } from "./chunk-MKI6OVW4.js";
9
+
10
+ // src/lib/output.ts
11
+ import { writeSync } from "fs";
12
+ var _traceId = null;
13
+ function setOutputTraceId(id) {
14
+ _traceId = id;
15
+ }
16
+ function getOutputTraceId() {
17
+ return _traceId;
18
+ }
19
+ function json(data) {
20
+ const out = _traceId !== null && typeof data === "object" && data !== null && !Array.isArray(data) && !("trace_id" in data) ? { trace_id: _traceId, ...data } : data;
21
+ writeAllSync(1, JSON.stringify(out) + "\n");
22
+ }
23
+ function info(msg) {
24
+ writeAllSync(2, msg + "\n");
25
+ }
26
+ function raw(msg) {
27
+ writeAllSync(1, msg + "\n");
28
+ }
29
+ function progress(event) {
30
+ writeAllSync(2, JSON.stringify(event) + "\n");
31
+ }
32
+ function writeAllSync(fd, text) {
33
+ const buf = Buffer.from(text, "utf8");
34
+ let offset = 0;
35
+ while (offset < buf.length) {
36
+ try {
37
+ offset += writeSync(fd, buf, offset, buf.length - offset);
38
+ } catch (err) {
39
+ const code = err.code;
40
+ if (code === "EAGAIN" || code === "EINTR") continue;
41
+ throw err;
42
+ }
43
+ }
44
+ }
10
45
 
11
46
  // src/lib/payment-rails/types.ts
47
+ var RAIL_FLAGS = ["cashu", "tempo", "x402"];
12
48
  var ADVERTISED_AMOUNT_TOLERANCE_BPS = 1000n;
13
49
  function maxAdvertisedMicro(localMicro) {
14
50
  return localMicro * (10000n + ADVERTISED_AMOUNT_TOLERANCE_BPS) / 10000n;
@@ -19,9 +55,9 @@ function withinAdvertisedTolerance(advertisedMicro, localMicro) {
19
55
 
20
56
  // src/lib/transport/types.ts
21
57
  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;
58
+ function parseCreditTerms(raw2) {
59
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
60
+ const credit = raw2;
25
61
  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
62
  return void 0;
27
63
  const tempo = credit.tempo === void 0 ? void 0 : parseCreditTempoTerms(credit.tempo);
@@ -41,10 +77,10 @@ function parseCreditTerms(raw) {
41
77
  }
42
78
  };
43
79
  }
44
- function parseCreditMenu(raw) {
45
- const terms = parseCreditTerms(raw);
80
+ function parseCreditMenu(raw2) {
81
+ const terms = parseCreditTerms(raw2);
46
82
  if (!terms) return void 0;
47
- const credit = raw;
83
+ const credit = raw2;
48
84
  return {
49
85
  ...terms,
50
86
  ...typeof credit.credit_id === "string" && { credit_id: credit.credit_id },
@@ -55,9 +91,9 @@ function parseCreditMenu(raw) {
55
91
  ...typeof credit.expiry_ms === "number" && { expiry_ms: credit.expiry_ms }
56
92
  };
57
93
  }
58
- function parseCreditTempoTerms(raw) {
59
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
60
- const block = raw;
94
+ function parseCreditTempoTerms(raw2) {
95
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
96
+ const block = raw2;
61
97
  if (!Array.isArray(block.methods) || !block.methods.every(isCreditTempoMethod)) return void 0;
62
98
  if (block.withheld !== void 0 && (!Array.isArray(block.withheld) || !block.withheld.every(isCreditTempoWithheldMethod)))
63
99
  return void 0;
@@ -75,15 +111,15 @@ function parseCreditTempoTerms(raw) {
75
111
  }
76
112
  };
77
113
  }
78
- function isCreditTempoMethod(raw) {
79
- return !!raw && typeof raw === "object" && typeof raw.method === "string" && (raw.intent === "charge" || raw.intent === "session");
114
+ function isCreditTempoMethod(raw2) {
115
+ return !!raw2 && typeof raw2 === "object" && typeof raw2.method === "string" && (raw2.intent === "charge" || raw2.intent === "session");
80
116
  }
81
- function isCreditTempoWithheldMethod(raw) {
82
- return isCreditTempoMethod(raw) && typeof raw.reason === "string";
117
+ function isCreditTempoWithheldMethod(raw2) {
118
+ return isCreditTempoMethod(raw2) && typeof raw2.reason === "string";
83
119
  }
84
- function parseCreditX402Terms(raw) {
85
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
86
- const schemes = raw.schemes;
120
+ function parseCreditX402Terms(raw2) {
121
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
122
+ const schemes = raw2.schemes;
87
123
  if (!Array.isArray(schemes) || !schemes.every(
88
124
  (entry) => !!entry && typeof entry === "object" && typeof entry.scheme === "string" && typeof entry.network === "string"
89
125
  ))
@@ -100,30 +136,6 @@ function parseCreditX402Terms(raw) {
100
136
  import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
101
137
  import { dirname } from "path";
102
138
  import lockfile from "proper-lockfile";
103
-
104
- // src/lib/output.ts
105
- import { writeSync } from "fs";
106
- function info(msg) {
107
- writeAllSync(2, msg + "\n");
108
- }
109
- function progress(event) {
110
- writeAllSync(2, JSON.stringify(event) + "\n");
111
- }
112
- function writeAllSync(fd, text) {
113
- const buf = Buffer.from(text, "utf8");
114
- let offset = 0;
115
- while (offset < buf.length) {
116
- try {
117
- offset += writeSync(fd, buf, offset, buf.length - offset);
118
- } catch (err) {
119
- const code = err.code;
120
- if (code === "EAGAIN" || code === "EINTR") continue;
121
- throw err;
122
- }
123
- }
124
- }
125
-
126
- // src/lib/credit-store.ts
127
139
  function withCreditLock(fn, opts = {}) {
128
140
  if (lockDepth > 0) {
129
141
  lockDepth += 1;
@@ -208,6 +220,9 @@ function fundingReachableAt(funding, endpoint, credits) {
208
220
  function pendingDrainFor(record, creditId) {
209
221
  return record?.pendingDrains?.find((d) => d.creditId === creditId);
210
222
  }
223
+ function pendingDrainById(record, drainId) {
224
+ return record?.pendingDrains?.find((d) => d.drainId === drainId);
225
+ }
211
226
  function loadCredit(endpoint, callerPubkey, identity) {
212
227
  return withCreditLock(() => {
213
228
  const key = normalizeCreditEndpoint(endpoint);
@@ -221,9 +236,124 @@ function loadCredit(endpoint, callerPubkey, identity) {
221
236
  return record;
222
237
  });
223
238
  }
239
+ function clearCreditSelection(endpoint, callerPubkey, creditId, identity) {
240
+ return withCreditLock(() => {
241
+ const key = normalizeCreditEndpoint(endpoint);
242
+ const store = loadStore();
243
+ const record = findDvmLocated(store.credits, {
244
+ endpoint: key,
245
+ ...identity ? { identity } : {},
246
+ where: (candidate) => candidate.callerPubkey === callerPubkey && candidate.creditId === creditId
247
+ });
248
+ if (!record) {
249
+ throw new DvmError(
250
+ "credit_not_found",
251
+ `Credit ${creditId} is not the selected local credit at ${key}.`
252
+ );
253
+ }
254
+ attachDvmLocation(record, key, identity);
255
+ record.selectionCleared = true;
256
+ record.updatedAt = Date.now();
257
+ saveStore(store);
258
+ return structuredClone(record);
259
+ });
260
+ }
224
261
  function listCredits() {
225
262
  return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
226
263
  }
264
+ function listImplicitCredits(now = Date.now()) {
265
+ return withCreditLock(() => {
266
+ const store = loadStore();
267
+ const aged = ageImplicitCredits(store.implicitCredits, now);
268
+ const capped = capLostImplicitCredits(store.implicitCredits);
269
+ if (aged || capped.changed) {
270
+ store.implicitCredits = capped.credits;
271
+ saveStore(store);
272
+ }
273
+ return [...store.implicitCredits].sort(compareImplicitCredits);
274
+ });
275
+ }
276
+ function loadImplicitCredits(endpoint, callerPubkey, identity, now = Date.now()) {
277
+ return withCreditLock(() => {
278
+ const store = loadStore();
279
+ const key = normalizeCreditEndpoint(endpoint);
280
+ let changed = ageImplicitCredits(store.implicitCredits, now);
281
+ const matches = store.implicitCredits.filter(
282
+ (record) => Boolean(
283
+ findDvmLocated([record], {
284
+ endpoint: key,
285
+ ...identity ? { identity } : {},
286
+ where: (candidate) => candidate.callerPubkey === callerPubkey
287
+ })
288
+ )
289
+ );
290
+ for (const record of matches) {
291
+ changed = attachDvmLocation(record, key, identity) || changed;
292
+ }
293
+ const capped = capLostImplicitCredits(store.implicitCredits);
294
+ if (changed || capped.changed) {
295
+ store.implicitCredits = capped.credits;
296
+ saveStore(store);
297
+ }
298
+ return matches.filter((record) => store.implicitCredits.includes(record)).sort(compareImplicitCredits);
299
+ });
300
+ }
301
+ function markImplicitCreditLost(args) {
302
+ return withCreditLock(() => {
303
+ const store = loadStore();
304
+ const key = normalizeCreditEndpoint(args.endpoint);
305
+ const record = findDvmLocated(store.implicitCredits, {
306
+ endpoint: key,
307
+ ...args.identity ? { identity: args.identity } : {},
308
+ where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.creditId
309
+ });
310
+ if (!record) return void 0;
311
+ attachDvmLocation(record, key, args.identity);
312
+ record.status = "lost";
313
+ record.lossReason = args.reason;
314
+ record.updatedAt = args.now ?? Date.now();
315
+ const capped = capLostImplicitCredits(store.implicitCredits);
316
+ store.implicitCredits = capped.credits;
317
+ saveStore(store);
318
+ return store.implicitCredits.includes(record) ? record : void 0;
319
+ });
320
+ }
321
+ function listFundingMenuSnapshots() {
322
+ return withCreditLock(
323
+ () => [...loadStore().fundingMenuSnapshots].sort((a, b) => b.observedAt - a.observedAt)
324
+ );
325
+ }
326
+ function recordFundingMenuSnapshot(args) {
327
+ const menu = parseCreditMenu(args.menu);
328
+ if (!menu) return;
329
+ withCreditLock(() => {
330
+ const store = loadStore();
331
+ const key = normalizeCreditEndpoint(args.endpoint);
332
+ const previous = findDvmLocated(store.fundingMenuSnapshots, {
333
+ endpoint: key,
334
+ ...args.identity ? { identity: args.identity } : {},
335
+ where: (entry) => entry.callerPubkey === args.callerPubkey
336
+ });
337
+ const index = previous ? store.fundingMenuSnapshots.indexOf(previous) : -1;
338
+ const builderPubkey = args.builderPubkey ?? previous?.builderPubkey;
339
+ const snapshot = {
340
+ // The row keeps the origin it was opened at and learns the new spelling
341
+ // as an alias, so a caller who alternates hostnames still reads one menu.
342
+ endpoint: previous?.endpoint ?? key,
343
+ ...previous?.endpointAliases ? { endpointAliases: [...previous.endpointAliases] } : {},
344
+ ...previous?.dvmId ? { dvmId: previous.dvmId } : {},
345
+ ...previous?.attestedBuilderPubkey ? { attestedBuilderPubkey: previous.attestedBuilderPubkey } : {},
346
+ callerPubkey: args.callerPubkey,
347
+ menu,
348
+ observedAt: args.now ?? Date.now(),
349
+ ...builderPubkey ? { builderPubkey } : {}
350
+ };
351
+ attachDvmLocation(snapshot, key, args.identity);
352
+ if (index === -1) store.fundingMenuSnapshots.push(snapshot);
353
+ else store.fundingMenuSnapshots[index] = snapshot;
354
+ saveStore(store);
355
+ });
356
+ }
227
357
  function listPendingFundings(args = {}) {
228
358
  const endpoint = args.endpoint ? normalizeCreditEndpoint(args.endpoint) : void 0;
229
359
  return withCreditLock(() => {
@@ -319,6 +449,177 @@ function recordFundResponse(args) {
319
449
  return record;
320
450
  });
321
451
  }
452
+ function recordBalanceResponse(args) {
453
+ const now = args.now ?? Date.now();
454
+ return withCreditLock(() => {
455
+ const existing = loadCredit(args.endpoint, args.callerPubkey, args.identity);
456
+ const prepaidCredits = args.credits.filter((credit) => !credit.credit_id.startsWith("imp:"));
457
+ const selectedPrimary = args.primary && !args.primary.credit_id.startsWith("imp:") ? args.primary : void 0;
458
+ const primary = selectedPrimary ?? prepaidCredits.find((credit) => credit.credit_id === existing?.creditId);
459
+ if (!primary) {
460
+ if (existing) {
461
+ mutateCredit(
462
+ {
463
+ endpoint: args.endpoint,
464
+ callerPubkey: args.callerPubkey,
465
+ ...args.identity ? { identity: args.identity } : {}
466
+ },
467
+ (record2) => {
468
+ record2.siblingCredits = [];
469
+ return void 0;
470
+ }
471
+ );
472
+ }
473
+ return existing;
474
+ }
475
+ const record = recordFundResponse({
476
+ endpoint: args.endpoint,
477
+ ...args.identity ? { identity: args.identity } : {},
478
+ callerPubkey: args.callerPubkey,
479
+ credit: primary,
480
+ targetMicro: args.targetMicro ?? existing?.targetMicro ?? primary.remaining_micro,
481
+ ...args.menu ? { menu: args.menu } : {},
482
+ ...args.builderPubkey ? { builderPubkey: args.builderPubkey } : {},
483
+ ...args.selectPrimary ? { select: true } : {},
484
+ ...args.sequenced === false ? { sequenced: false } : {},
485
+ now
486
+ });
487
+ mutateCredit(
488
+ {
489
+ endpoint: args.endpoint,
490
+ callerPubkey: args.callerPubkey,
491
+ ...args.identity ? { identity: args.identity } : {}
492
+ },
493
+ (current) => {
494
+ current.siblingCredits = prepaidCredits.filter((credit) => credit.credit_id !== current.creditId).map((credit) => siblingFromWire(credit, now));
495
+ return void 0;
496
+ }
497
+ );
498
+ return loadCredit(args.endpoint, args.callerPubkey, args.identity) ?? record;
499
+ });
500
+ }
501
+ function applyReceiptCredit(args) {
502
+ const now = args.now ?? Date.now();
503
+ return withCreditLock(() => {
504
+ const store = loadStore();
505
+ const key = normalizeCreditEndpoint(args.endpoint);
506
+ const record = findDvmLocated(store.credits, {
507
+ endpoint: key,
508
+ ...args.identity ? { identity: args.identity } : {},
509
+ where: (c) => c.callerPubkey === args.callerPubkey && c.creditId === args.credit.credit_id
510
+ });
511
+ if (!record) return {};
512
+ attachDvmLocation(record, key, args.identity);
513
+ let mismatch;
514
+ const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
515
+ if (sequential && args.outcome === "completed" && record.status === "active" && !record.provisional && !record.unsequencedBalance) {
516
+ const expected = record.remainingMicro - args.credit.amount;
517
+ if (args.credit.balance_after !== expected) {
518
+ mismatch = {
519
+ atMs: now,
520
+ expectedMicro: expected,
521
+ reportedMicro: args.credit.balance_after,
522
+ drawId: args.credit.draw_id,
523
+ ledgerSeq: args.credit.ledger_seq,
524
+ ...record.lastReceiptJobId ? { priorJobId: record.lastReceiptJobId } : {},
525
+ jobId: args.jobId
526
+ };
527
+ record.discrepancy = mismatch;
528
+ }
529
+ }
530
+ if (args.credit.ledger_seq >= record.ledgerSeq) {
531
+ record.remainingMicro = args.credit.balance_after;
532
+ record.ledgerSeq = args.credit.ledger_seq;
533
+ delete record.unsequencedBalance;
534
+ }
535
+ delete record.provisional;
536
+ if (args.outcome === "completed") record.lastPriceMicro = args.credit.amount;
537
+ if (!mismatch && record.discrepancy && args.credit.ledger_seq > record.discrepancy.ledgerSeq) {
538
+ delete record.discrepancy;
539
+ }
540
+ record.lastReceiptJobId = args.jobId;
541
+ record.updatedAt = now;
542
+ saveStore(store);
543
+ return { credit: record, ...mismatch ? { mismatch } : {} };
544
+ });
545
+ }
546
+ function applyImplicitReceiptCredit(args) {
547
+ if (!args.credit.credit_id.startsWith("imp:")) return {};
548
+ const now = args.now ?? Date.now();
549
+ return withCreditLock(() => {
550
+ const store = loadStore();
551
+ const key = normalizeCreditEndpoint(args.endpoint);
552
+ let record = findDvmLocated(store.implicitCredits, {
553
+ endpoint: key,
554
+ ...args.identity ? { identity: args.identity } : {},
555
+ where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.credit.credit_id
556
+ });
557
+ if (!record && (args.outcome === "completed" || args.credit.balance_after <= 0)) return {};
558
+ if (!record) {
559
+ record = {
560
+ endpoint: key,
561
+ ...identityFields(args.identity),
562
+ callerPubkey: args.callerPubkey,
563
+ creditId: args.credit.credit_id,
564
+ currency: args.currency,
565
+ remainingMicro: args.credit.balance_after,
566
+ ledgerSeq: args.credit.ledger_seq,
567
+ expiryMs: args.issuedAtMs + IMPLICIT_CREDIT_TTL_MS,
568
+ status: args.issuedAtMs + IMPLICIT_CREDIT_TTL_MS <= now ? "lost" : "active",
569
+ releasedByJobId: args.jobId,
570
+ releasedAt: args.issuedAtMs,
571
+ updatedAt: now,
572
+ lastReceiptJobId: args.jobId
573
+ };
574
+ store.implicitCredits.push(record);
575
+ const capped2 = capLostImplicitCredits(store.implicitCredits);
576
+ store.implicitCredits = capped2.credits;
577
+ saveStore(store);
578
+ return store.implicitCredits.includes(record) ? { credit: record } : {};
579
+ }
580
+ attachDvmLocation(record, key, args.identity);
581
+ let mismatch;
582
+ const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
583
+ if (sequential && args.outcome === "completed" && record.status === "active") {
584
+ const expected = record.remainingMicro - args.credit.amount;
585
+ if (args.credit.balance_after !== expected) {
586
+ mismatch = {
587
+ atMs: now,
588
+ expectedMicro: expected,
589
+ reportedMicro: args.credit.balance_after,
590
+ drawId: args.credit.draw_id,
591
+ ledgerSeq: args.credit.ledger_seq,
592
+ ...record.lastReceiptJobId ? { priorJobId: record.lastReceiptJobId } : {},
593
+ jobId: args.jobId
594
+ };
595
+ record.discrepancy = mismatch;
596
+ }
597
+ }
598
+ if (args.credit.ledger_seq >= record.ledgerSeq) {
599
+ record.remainingMicro = args.credit.balance_after;
600
+ record.ledgerSeq = args.credit.ledger_seq;
601
+ }
602
+ if (!mismatch && record.discrepancy && args.credit.ledger_seq > record.discrepancy.ledgerSeq) {
603
+ delete record.discrepancy;
604
+ }
605
+ record.currency = args.currency;
606
+ record.lastReceiptJobId = args.jobId;
607
+ record.updatedAt = now;
608
+ if (record.remainingMicro <= 0) {
609
+ store.implicitCredits = store.implicitCredits.filter((candidate) => candidate !== record);
610
+ saveStore(store);
611
+ return { ...mismatch ? { mismatch } : {}, pruned: true };
612
+ }
613
+ if (record.expiryMs <= now) record.status = "lost";
614
+ const capped = capLostImplicitCredits(store.implicitCredits);
615
+ store.implicitCredits = capped.credits;
616
+ saveStore(store);
617
+ return {
618
+ ...store.implicitCredits.includes(record) ? { credit: record } : {},
619
+ ...mismatch ? { mismatch } : {}
620
+ };
621
+ });
622
+ }
322
623
  function ensureProvisionalCredit(args) {
323
624
  const now = args.now ?? Date.now();
324
625
  withCreditLock(() => {
@@ -358,6 +659,21 @@ function ensureProvisionalCredit(args) {
358
659
  saveStore(store);
359
660
  });
360
661
  }
662
+ function markCredit(endpoint, callerPubkey, status, opts = {}) {
663
+ mutateCredit(
664
+ { endpoint, callerPubkey, ...opts.identity ? { identity: opts.identity } : {} },
665
+ (record) => {
666
+ if (opts.ifCreditId !== void 0 && record.creditId !== opts.ifCreditId) return "skip";
667
+ record.status = status;
668
+ if (status !== "active") {
669
+ delete record.refillUnavailableRail;
670
+ delete record.refillDrainNoticed;
671
+ }
672
+ if (status === "drained") record.remainingMicro = 0;
673
+ return void 0;
674
+ }
675
+ );
676
+ }
361
677
  function loadCreditX402Binding(endpoint, callerPubkey, creditId, identity) {
362
678
  const record = loadCredit(endpoint, callerPubkey, identity);
363
679
  if (record?.creditId !== creditId) return {};
@@ -383,6 +699,64 @@ function recordCreditX402Instrument(endpoint, callerPubkey, creditId, instrument
383
699
  return void 0;
384
700
  });
385
701
  }
702
+ function recordRefusedDrawBalance(args) {
703
+ const remaining = safeMicro(args.remainingMicro);
704
+ if (remaining === void 0) return;
705
+ const balance = safeMicro(args.balanceMicro);
706
+ mutateCredit(mutationAt(args.endpoint, args.callerPubkey, args.identity), (record) => {
707
+ if (record.creditId !== args.creditId) return "skip";
708
+ const unchanged = record.remainingMicro === remaining && (balance === void 0 || record.balanceMicro === balance) && record.unsequencedBalance === true && !record.provisional;
709
+ if (unchanged) return "skip";
710
+ record.remainingMicro = remaining;
711
+ if (balance !== void 0) record.balanceMicro = balance;
712
+ record.unsequencedBalance = true;
713
+ delete record.provisional;
714
+ return void 0;
715
+ });
716
+ }
717
+ function markRefillRailUnavailable(endpoint, callerPubkey, creditId, rail, identity) {
718
+ let firstObservation = false;
719
+ mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
720
+ if (record.creditId !== creditId) return "skip";
721
+ if (record.refillUnavailableRail === rail) return "skip";
722
+ record.refillUnavailableRail = rail;
723
+ firstObservation = true;
724
+ return void 0;
725
+ });
726
+ return firstObservation;
727
+ }
728
+ function markRefillDrainNoticed(endpoint, callerPubkey, creditId, drainId, identity) {
729
+ let firstObservation = false;
730
+ mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
731
+ if (record.creditId !== creditId) return "skip";
732
+ if (record.refillDrainNoticed === drainId) return "skip";
733
+ record.refillDrainNoticed = drainId;
734
+ firstObservation = true;
735
+ return void 0;
736
+ });
737
+ return firstObservation;
738
+ }
739
+ function clearRefillRailUnavailable(endpoint, callerPubkey, creditId, identity) {
740
+ mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
741
+ if (record.creditId !== creditId) return "skip";
742
+ if (record.refillUnavailableRail === void 0) return "skip";
743
+ delete record.refillUnavailableRail;
744
+ return void 0;
745
+ });
746
+ }
747
+ function forgetCredit(endpoint, callerPubkey, identity) {
748
+ withCreditLock(() => {
749
+ const store = loadStore();
750
+ const key = normalizeCreditEndpoint(endpoint);
751
+ const record = findDvmLocated(store.credits, {
752
+ endpoint: key,
753
+ ...identity ? { identity } : {},
754
+ where: (c) => c.callerPubkey === callerPubkey
755
+ });
756
+ if (!record) return;
757
+ saveStore({ ...store, credits: store.credits.filter((c) => c !== record) });
758
+ });
759
+ }
386
760
  function recordPendingDrain(endpoint, callerPubkey, drain, identity) {
387
761
  mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
388
762
  const drains = record.pendingDrains ?? [];
@@ -407,6 +781,45 @@ function clearPendingDrain(endpoint, callerPubkey, drain, identity) {
407
781
  return void 0;
408
782
  });
409
783
  }
784
+ function recordDrainReceipts(args) {
785
+ const {
786
+ endpoint,
787
+ identity,
788
+ callerPubkey,
789
+ drainId,
790
+ creditId,
791
+ receipts,
792
+ read,
793
+ now = Date.now()
794
+ } = args;
795
+ if (receipts.length === 0) return;
796
+ mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
797
+ const evidence = record.drainEvidence ??= [];
798
+ let bundle = evidence.find((e) => e.drainId === drainId);
799
+ if (!bundle) {
800
+ bundle = { drainId, creditId, receipts: [], collectedAt: now };
801
+ evidence.push(bundle);
802
+ }
803
+ bundle.creditId = creditId;
804
+ bundle.receipts = receipts;
805
+ bundle.collectedAt = now;
806
+ if (read.anchor) bundle.anchor = read.anchor;
807
+ else if (read.reachable) delete bundle.anchor;
808
+ record.drainEvidence = capDrainEvidence(evidence, endpoint);
809
+ return void 0;
810
+ });
811
+ }
812
+ function settleDrainEvidence(args) {
813
+ const { endpoint, identity, callerPubkey, drainId, now = Date.now() } = args;
814
+ mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
815
+ const evidence = record.drainEvidence ?? [];
816
+ const bundle = evidence.find((e) => e.drainId === drainId);
817
+ if (!bundle || bundle.settledAt !== void 0) return "skip";
818
+ bundle.settledAt = now;
819
+ record.drainEvidence = capDrainEvidence(evidence, endpoint);
820
+ return void 0;
821
+ });
822
+ }
410
823
  function identityFields(identity) {
411
824
  if (!identity) return {};
412
825
  return { dvmId: identity.dvmId, attestedBuilderPubkey: identity.builderPubkey };
@@ -467,21 +880,22 @@ function adoptCreditId(record, creditId) {
467
880
  delete record.lastReceiptJobId;
468
881
  }
469
882
  var MAX_PENDING_DRAINS = 8;
883
+ var MAX_SETTLED_DRAIN_EVIDENCE = 8;
470
884
  var MAX_LOST_IMPLICIT_CREDITS = 8;
471
885
  var MAX_DATE_MS = 864e13;
472
886
  function loadStore() {
473
887
  if (!existsSync(CREDITS_FILE)) {
474
888
  return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
475
889
  }
476
- let raw;
890
+ let raw2;
477
891
  try {
478
- raw = readFileSync(CREDITS_FILE, "utf-8");
892
+ raw2 = readFileSync(CREDITS_FILE, "utf-8");
479
893
  } catch (cause) {
480
894
  throw unreadableStore(cause);
481
895
  }
482
896
  let data;
483
897
  try {
484
- const parsed = JSON.parse(raw);
898
+ const parsed = JSON.parse(raw2);
485
899
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
486
900
  throw new Error("the file parsed as JSON but is not a credit-store object");
487
901
  }
@@ -556,6 +970,18 @@ function dedupeImplicitCredits(credits) {
556
970
  const capped = capLostImplicitCredits(merged);
557
971
  return { credits: capped.credits, changed: changed || capped.changed };
558
972
  }
973
+ function ageImplicitCredits(credits, now) {
974
+ let changed = false;
975
+ for (const credit of credits) {
976
+ if (credit.status === "active" && credit.expiryMs <= now) {
977
+ credit.status = "lost";
978
+ credit.lossReason = "expired";
979
+ credit.updatedAt = now;
980
+ changed = true;
981
+ }
982
+ }
983
+ return changed;
984
+ }
559
985
  function capLostImplicitCredits(credits) {
560
986
  const lostByOwner = /* @__PURE__ */ new Map();
561
987
  for (const credit of credits) {
@@ -574,6 +1000,10 @@ function capLostImplicitCredits(credits) {
574
1000
  }
575
1001
  return dropped.size === 0 ? { credits, changed: false } : { credits: credits.filter((credit) => !dropped.has(credit)), changed: true };
576
1002
  }
1003
+ function compareImplicitCredits(a, b) {
1004
+ if (a.status !== b.status) return a.status === "active" ? -1 : 1;
1005
+ return a.status === "active" ? a.expiryMs - b.expiryMs : b.updatedAt - a.updatedAt;
1006
+ }
577
1007
  function mergeDuplicateSnapshots(snapshots, credits) {
578
1008
  const identities = originIdentityMap(credits);
579
1009
  const merged = [];
@@ -777,6 +1207,21 @@ function isFundingMenuSnapshot(value) {
777
1207
  const snapshot = value;
778
1208
  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");
779
1209
  }
1210
+ function siblingFromWire(credit, observedAt) {
1211
+ return {
1212
+ creditId: credit.credit_id,
1213
+ status: credit.status ?? "active",
1214
+ currency: credit.currency,
1215
+ balanceMicro: credit.balance_micro,
1216
+ remainingMicro: credit.remaining_micro,
1217
+ ledgerSeq: credit.ledger_seq,
1218
+ expiryMs: credit.expiry_ms,
1219
+ expired: credit.expired,
1220
+ drainable: credit.drainable,
1221
+ ...credit.reconciliation_pending ? { reconciliationPending: true } : {},
1222
+ observedAt
1223
+ };
1224
+ }
780
1225
  function normalizeRailKey(record) {
781
1226
  const loose = record;
782
1227
  if (loose.rail === LEGACY_TEMPO_RAIL_KEY) loose.rail = "tempo";
@@ -851,6 +1296,17 @@ function capPendingDrains(drains, creditId) {
851
1296
  }
852
1297
  return drains.filter((d) => !dropped.has(d));
853
1298
  }
1299
+ function capDrainEvidence(evidence, endpoint) {
1300
+ const settled = evidence.filter((e) => e.settledAt !== void 0).sort((a, b) => (a.settledAt ?? 0) - (b.settledAt ?? 0));
1301
+ if (settled.length <= MAX_SETTLED_DRAIN_EVIDENCE) return evidence;
1302
+ const dropped = new Set(settled.slice(0, settled.length - MAX_SETTLED_DRAIN_EVIDENCE));
1303
+ for (const e of dropped) {
1304
+ info(
1305
+ `Dropping the oldest completed reclaim's receipts (drain ${e.drainId}${e.creditId ? ` on credit ${e.creditId}` : ""}) \u2014 ${String(MAX_SETTLED_DRAIN_EVIDENCE)} is the most this DVM's record archives. Export the ones still held before they age out too: 'dvm credit receipts ${endpoint} --export'.`
1306
+ );
1307
+ }
1308
+ return evidence.filter((e) => !dropped.has(e));
1309
+ }
854
1310
  function saveStore(store) {
855
1311
  ensureConfigDir();
856
1312
  const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
@@ -897,6 +1353,9 @@ function quarantineCorruptStore(cause) {
897
1353
  `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.`
898
1354
  );
899
1355
  }
1356
+ function safeMicro(value) {
1357
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1358
+ }
900
1359
  function mutateCredit(location, mutate) {
901
1360
  withCreditLock(() => {
902
1361
  const store = loadStore();
@@ -980,663 +1439,51 @@ function sleepSync(ms) {
980
1439
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
981
1440
  }
982
1441
 
983
- // src/lib/evm-private-key.ts
984
- import { secp256k1 } from "@noble/curves/secp256k1.js";
985
- import { hexToBytes } from "@noble/hashes/utils.js";
986
- var EVM_PRIVATE_KEY_RE = /^0x[0-9a-fA-F]{64}$/;
987
- function isValidEvmPrivateKey(value) {
988
- return EVM_PRIVATE_KEY_RE.test(value) && secp256k1.utils.isValidSecretKey(hexToBytes(value.slice(2)));
989
- }
990
-
991
- // src/lib/config-mutation.ts
992
- import {
993
- appendFileSync,
994
- chmodSync,
995
- existsSync as existsSync2,
996
- readFileSync as readFileSync2,
997
- renameSync as renameSync2,
998
- statSync,
999
- unlinkSync as unlinkSync2,
1000
- writeFileSync as writeFileSync2
1001
- } from "fs";
1002
- var PROTECTED_CONFIG_FIELDS = ["nwcUri", "defaultIdentity", "tempo", "x402"];
1003
- var ProtectedFieldAuthorization = class {
1004
- /** Targets this authorization covers. A field also covers its own leaves. */
1005
- targets;
1006
- /** Prefer {@link authorizeProtectedRemoval}; this exists for it to call. */
1007
- constructor(targets) {
1008
- this.targets = [...targets];
1009
- }
1010
- /**
1011
- * True when `target` is one this authorization names. Naming a field covers
1012
- * the leaves under it; naming a leaf covers only that leaf, so a command
1013
- * entitled to clear `tempo.apiKey` still cannot take the whole `tempo` block.
1014
- */
1015
- covers(target) {
1016
- if (this.targets.includes(target)) return true;
1017
- const field = target.split(".")[0];
1018
- return target !== field && this.targets.includes(field);
1019
- }
1020
- };
1021
- function authorizeProtectedRemoval(...targets) {
1022
- return new ProtectedFieldAuthorization(targets);
1023
- }
1024
- function applyConfigPatch(base, patch) {
1025
- if (!isPlainObject(patch))
1026
- throw patchShapeError("A change must be an object with `set` and/or `unset`.");
1027
- if (patch.set !== void 0 && !isPlainObject(patch.set)) {
1028
- throw patchShapeError("A change's `set` must be an object of fields to write.");
1029
- }
1030
- if (patch.unset !== void 0 && !Array.isArray(patch.unset)) {
1031
- throw patchShapeError("A change's `unset` must be an array of field names.");
1032
- }
1033
- const set = patch.set ?? {};
1034
- const unset = patch.unset ?? [];
1035
- for (const [field, value] of Object.entries(set)) {
1036
- if (value !== void 0) continue;
1037
- throw new DvmError(
1038
- "config_patch_invalid",
1039
- `Config field '${field}' was declared with no value.`,
1040
- "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."
1041
- );
1042
- }
1043
- for (const field of unset) {
1044
- if (typeof field !== "string" || field.length === 0) {
1045
- throw patchShapeError("Every entry in a change's `unset` must be a field name.");
1046
- }
1047
- if (!Object.hasOwn(set, field)) continue;
1048
- throw new DvmError(
1049
- "config_patch_invalid",
1050
- `Config field '${field}' is both written and removed by the same change.`,
1051
- "Declare each field once: in `set` with its new value, or in `unset` to remove it."
1052
- );
1053
- }
1054
- const merged = { ...base, ...set };
1055
- const removing = new Set(unset);
1056
- return Object.fromEntries(
1057
- Object.entries(merged).filter(([field]) => !removing.has(field))
1058
- );
1059
- }
1060
- function deepFreezeConfig(config) {
1061
- freezeDeep(config);
1062
- return config;
1063
- }
1064
- function protectedRemovals(before, after) {
1065
- const previous = before;
1066
- const next = after;
1067
- const removed = [];
1068
- for (const field of PROTECTED_CONFIG_FIELDS) {
1069
- const held = previous[field];
1070
- if (!isPresentValue(held)) continue;
1071
- const kept = next[field];
1072
- if (!isPresentValue(kept)) {
1073
- removed.push(field);
1074
- continue;
1075
- }
1076
- const spec = PROTECTED_FIELDS[field];
1077
- if (spec.credentialLeaves.length === 0) continue;
1078
- if (!isPlainObject(held)) continue;
1079
- if (!isPlainObject(kept)) {
1080
- removed.push(field);
1081
- continue;
1082
- }
1083
- for (const leaf of spec.credentialLeaves) {
1084
- if (!isPresentValue(held[leaf]) || isPresentValue(kept[leaf])) continue;
1085
- removed.push(`${field}.${leaf}`);
1086
- }
1087
- }
1088
- return removed;
1089
- }
1090
- function supersededProtectedSecrets(before, after) {
1091
- const previous = before;
1092
- const next = after;
1093
- const superseded = [];
1094
- for (const field of PROTECTED_CONFIG_FIELDS) {
1095
- const spec = PROTECTED_FIELDS[field];
1096
- if (!spec.secret) continue;
1097
- const held = previous[field];
1098
- const kept = next[field];
1099
- if (spec.credentialLeaves.length === 0) {
1100
- if (isPresentValue(held) && isPresentValue(kept) && held !== kept) superseded.push(field);
1101
- continue;
1102
- }
1103
- if (!isPlainObject(held) || !isPlainObject(kept)) continue;
1104
- for (const leaf of spec.credentialLeaves) {
1105
- const was = held[leaf];
1106
- const now = kept[leaf];
1107
- if (!isPresentValue(was) || !isPresentValue(now) || was === now) continue;
1108
- superseded.push(`${field}.${leaf}`);
1109
- }
1110
- }
1111
- return superseded;
1112
- }
1113
- function changedConfigPaths(before, after) {
1114
- const found = /* @__PURE__ */ new Set();
1115
- collectChangedPaths(
1116
- before,
1117
- after,
1118
- CONFIG_PATH_SHAPE,
1119
- "",
1120
- found
1121
- );
1122
- return [...found].sort();
1123
- }
1124
- function assertValidConfig(config, fields) {
1125
- if (!isPlainObject(config)) {
1126
- throw new DvmError(
1127
- "config_invalid",
1128
- "The caller configuration is not a JSON object.",
1129
- `Expected an object at ${CONFIG_FILE}.${rollbackSuffix()}`
1130
- );
1131
- }
1132
- const record = config;
1133
- const scope = fields ? new Set(fields) : null;
1134
- for (const [field, kind] of Object.entries(CONFIG_FIELD_TYPES)) {
1135
- if (scope && !scope.has(field)) continue;
1136
- const value = record[field];
1137
- if (value === void 0) continue;
1138
- if (!matchesKind(value, kind)) throw invalidFieldError(field, kind);
1139
- }
1140
- for (const [path, kind] of Object.entries(CONFIG_LEAF_TYPES)) {
1141
- const [field, leaf] = path.split(".");
1142
- if (scope && !scope.has(field)) continue;
1143
- const owner = record[field];
1144
- if (!isPlainObject(owner)) continue;
1145
- const value = owner[leaf];
1146
- if (value === void 0 || !matchesKind(value, kind)) throw invalidFieldError(path, kind);
1147
- }
1148
- }
1149
- function readRawConfig() {
1150
- if (!existsSync2(CONFIG_FILE)) return null;
1151
- try {
1152
- return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
1153
- } catch {
1154
- throw new DvmError(
1155
- "config_corrupt",
1156
- "Config file is corrupt.",
1157
- // Not "delete it": those bytes are the only copy on this machine of the
1158
- // NWC connection string and both private keys, however badly they parse.
1159
- `Run 'dvm config rollback' to restore a stored copy, or move ${CONFIG_FILE} aside yourself and run 'dvm init'.${rollbackSuffix()}`
1160
- );
1161
- }
1162
- }
1163
- function writeConfigFile(config) {
1164
- writeJsonAtomic(CONFIG_FILE, config);
1165
- }
1166
- var CONFIG_SNAPSHOT_SLOTS = ["backup", "pinned"];
1167
- function configSnapshotPath(slot) {
1168
- return slot === "pinned" ? CONFIG_PINNED_FILE : CONFIG_BACKUP_FILE;
1169
- }
1170
- function describeConfigBackup(slot = "backup") {
1171
- const path = configSnapshotPath(slot);
1172
- try {
1173
- if (!existsSync2(path)) return null;
1174
- const stat = statSync(path);
1175
- const posix = process.platform !== "win32";
1176
- const mode = stat.mode & 511;
1177
- return {
1178
- path,
1179
- slot,
1180
- bytes: stat.size,
1181
- modifiedAt: stat.mtime.toISOString(),
1182
- modeOctal: posix ? toOctal(mode) : null,
1183
- ownerOnly: !posix || (mode & 63) === 0
1184
- };
1185
- } catch {
1186
- return null;
1187
- }
1188
- }
1189
- function readConfigBackup(slot = "backup") {
1190
- const path = configSnapshotPath(slot);
1191
- if (!existsSync2(path)) return null;
1192
- try {
1193
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
1194
- return isPlainObject(parsed) ? parsed : null;
1195
- } catch {
1196
- return null;
1197
- }
1198
- }
1199
- function retainConfigBackup(slot = "backup") {
1200
- if (!existsSync2(CONFIG_FILE)) return false;
1201
- ensureConfigDir();
1202
- const path = configSnapshotPath(slot);
1203
- const tmp = `${path}.tmp`;
1204
- writeFileSync2(tmp, readFileSync2(CONFIG_FILE), { mode: 384 });
1205
- chmodSync(tmp, 384);
1206
- commitTmp(tmp, path);
1207
- return true;
1208
- }
1209
- function writeConfigBackup(config, slot = "backup") {
1210
- writeJsonAtomic(configSnapshotPath(slot), config);
1211
- }
1212
- function removeConfigBackup(slot = "backup") {
1213
- const path = configSnapshotPath(slot);
1214
- if (!existsSync2(path)) return false;
1215
- unlinkSync2(path);
1216
- return true;
1217
- }
1218
- function protectedTargetValue(config, target) {
1219
- const [field, named] = target.split(".");
1220
- const held = config[field];
1221
- const leaves = PROTECTED_FIELDS[field].credentialLeaves;
1222
- const leaf = named ?? (leaves.length > 0 ? leaves[0] : void 0);
1223
- if (leaf === void 0) return held;
1224
- return isPlainObject(held) ? held[leaf] : void 0;
1225
- }
1226
- function protectedValuesMatch(target, left, right) {
1227
- const normalized = comparableProtectedValue(target, left);
1228
- return normalized !== void 0 && normalized === comparableProtectedValue(target, right);
1229
- }
1230
- function isSecretProtectedTarget(target) {
1231
- return PROTECTED_FIELDS[target.split(".")[0]].secret;
1232
- }
1233
- function withoutProtectedTargets(config, targets) {
1234
- const dropping = /* @__PURE__ */ new Set();
1235
- for (const target of targets) {
1236
- const field = target.split(".")[0];
1237
- dropping.add(field);
1238
- for (const alias of PROTECTED_FIELDS[field].aliases) dropping.add(alias);
1239
- }
1240
- return Object.fromEntries(
1241
- Object.entries(config).filter(([field]) => !dropping.has(field))
1242
- );
1243
- }
1244
- function appendConfigAudit(record) {
1245
- try {
1246
- ensureConfigDir();
1247
- appendFileSync(CONFIG_AUDIT_FILE, JSON.stringify(record) + "\n", { mode: 384 });
1248
- chmodSync(CONFIG_AUDIT_FILE, 384);
1249
- } catch {
1250
- }
1251
- }
1252
- function protectedRemovalError(targets, reason) {
1253
- const named = targets.join(", ");
1254
- const one = targets.length === 1;
1255
- return new DvmError(
1256
- "config_protected_field",
1257
- `The '${reason}' change would remove ${named} from the local configuration.`,
1258
- `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.`,
1259
- { fields: targets }
1260
- );
1261
- }
1262
- var PROTECTED_FIELDS = {
1263
- nwcUri: { aliases: [], credentialLeaves: [], secret: true, hexCredential: false },
1264
- // A pointer at a key, not a key: worth protecting from removal, and never
1265
- // worth stripping out of a stored copy — that is recovery lost for nothing.
1266
- defaultIdentity: { aliases: [], credentialLeaves: [], secret: false, hexCredential: false },
1267
- // `mpp` is the pre-internal-review name; a caller who has not run a mutating command
1268
- // since the rename still has their private key under it.
1269
- tempo: { aliases: ["mpp"], credentialLeaves: ["apiKey"], secret: true, hexCredential: true },
1270
- x402: { aliases: [], credentialLeaves: ["privateKey"], secret: true, hexCredential: true }
1271
- };
1272
- var CONFIG_PATH_SHAPE = {
1273
- float: { statedBudget: {}, connectedAt: {}, alias: {}, methods: {} },
1274
- x402: { privateKey: {}, network: {} },
1275
- tempo: { method: {}, accountId: {}, apiKey: {} },
1276
- credit: {
1277
- targetJobs: {},
1278
- posture: {},
1279
- tier0Cap: {},
1280
- tier1Cap: {},
1281
- pocketTarget: {},
1282
- fundingRail: {},
1283
- perDvm: { "*": { targetJobs: {}, posture: {}, fundingRail: {}, dvmId: {}, builderPubkey: {} } },
1284
- trustOverrides: { "*": {} }
1285
- },
1286
- feedback: { posture: {} }
1287
- };
1288
- var CONFIG_FIELD_TYPES = {
1289
- nwcUri: "string",
1290
- float: "object",
1291
- defaultBudget: "string",
1292
- autoPayThreshold: "string",
1293
- defaultMaxPayments: "number",
1294
- cashuMints: "string[]",
1295
- x402: "object",
1296
- tempo: "object",
1297
- defaultRail: "string",
1298
- defaultIdentity: "string",
1299
- credit: "object",
1300
- feedback: "object"
1301
- };
1302
- var CONFIG_LEAF_TYPES = {
1303
- "float.connectedAt": "string",
1304
- "x402.privateKey": "string",
1305
- "tempo.method": "string"
1306
- };
1307
- function collectChangedPaths(before, after, shape, prefix, found) {
1308
- for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
1309
- const held = before[key];
1310
- const kept = after[key];
1311
- if (deepEqual(held, kept)) continue;
1312
- const known = shape !== void 0 && Object.hasOwn(shape, key);
1313
- const wildcard = !known && shape !== void 0 && Object.hasOwn(shape, "*");
1314
- const child = known ? shape[key] : wildcard ? shape["*"] : void 0;
1315
- const path = prefix ? `${prefix}.${wildcard ? "*" : key}` : key;
1316
- const descendable = child !== void 0 && Object.keys(child).length > 0 && (held === void 0 || isPlainObject(held)) && (kept === void 0 || isPlainObject(kept));
1317
- if (descendable) {
1318
- collectChangedPaths(
1319
- isPlainObject(held) ? held : {},
1320
- isPlainObject(kept) ? kept : {},
1321
- child,
1322
- path,
1323
- found
1324
- );
1325
- } else {
1326
- found.add(path);
1327
- }
1328
- }
1329
- }
1330
- function invalidFieldError(path, kind) {
1331
- return new DvmError(
1332
- "config_invalid",
1333
- `Config field '${path}' must be ${describeKind(kind)}.`,
1334
- `Nothing was written. Correct the field in ${CONFIG_FILE}, or run 'dvm doctor' for the full picture.${rollbackSuffix()}`
1335
- );
1336
- }
1337
- function describeKind(kind) {
1338
- switch (kind) {
1339
- case "string":
1340
- return "a string";
1341
- case "number":
1342
- return "a number";
1343
- case "object":
1344
- return "an object";
1345
- case "string[]":
1346
- return "an array of strings";
1347
- }
1348
- }
1349
- function matchesKind(value, kind) {
1350
- switch (kind) {
1351
- case "string":
1352
- return typeof value === "string";
1353
- case "number":
1354
- return typeof value === "number" && Number.isFinite(value);
1355
- case "object":
1356
- return isPlainObject(value);
1357
- case "string[]":
1358
- return Array.isArray(value) && value.every((item) => typeof item === "string");
1359
- }
1360
- }
1361
- function rollbackSuffix() {
1362
- const stored = describeConfigBackup() ?? describeConfigBackup("pinned");
1363
- return stored === null ? "" : ` A stored copy of the previous configuration is at ${stored.path}.`;
1364
- }
1365
- function writeJsonAtomic(path, value) {
1366
- ensureConfigDir();
1367
- const tmp = `${path}.tmp`;
1368
- writeFileSync2(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
1369
- chmodSync(tmp, 384);
1370
- commitTmp(tmp, path);
1371
- }
1372
- function commitTmp(tmp, path) {
1373
- try {
1374
- renameSync2(tmp, path);
1375
- } catch (err) {
1376
- try {
1377
- unlinkSync2(tmp);
1378
- } catch {
1379
- }
1380
- throw err;
1381
- }
1382
- }
1383
- function deepEqual(a, b) {
1384
- if (a === b) return true;
1385
- if (Array.isArray(a) || Array.isArray(b)) {
1386
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
1387
- return a.every((item, index) => deepEqual(item, b[index]));
1388
- }
1389
- if (!isPlainObject(a) || !isPlainObject(b)) return false;
1390
- for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
1391
- if (!deepEqual(a[key], b[key])) return false;
1392
- }
1393
- return true;
1394
- }
1395
- function isPresentValue(value) {
1396
- if (value === void 0 || value === null) return false;
1397
- return typeof value !== "string" || value.trim().length > 0;
1398
- }
1399
- function comparableProtectedValue(target, value) {
1400
- if (typeof value !== "string") return void 0;
1401
- const trimmed = value.trim();
1402
- if (trimmed.length === 0) return void 0;
1403
- const field = target.split(".")[0];
1404
- return PROTECTED_FIELDS[field].hexCredential ? trimmed.toLowerCase() : trimmed;
1405
- }
1406
- function patchShapeError(message) {
1407
- return new DvmError(
1408
- "config_patch_invalid",
1409
- message,
1410
- "A change declares `set` (fields to write) and `unset` (field names to remove). Anything else cannot be applied, and nothing was written."
1411
- );
1412
- }
1413
- function freezeDeep(value) {
1414
- if (typeof value !== "object" || value === null || Object.isFrozen(value)) return;
1415
- Object.freeze(value);
1416
- for (const nested of Object.values(value)) freezeDeep(nested);
1417
- }
1418
- function isPlainObject(value) {
1419
- return typeof value === "object" && value !== null && !Array.isArray(value);
1420
- }
1421
- function toOctal(mode) {
1422
- return "0" + mode.toString(8).padStart(3, "0");
1423
- }
1424
-
1425
- // src/lib/identity.ts
1426
- import lockfile2 from "proper-lockfile";
1427
- var CREDIT_POSTURES = ["suggest", "auto", "off"];
1428
- function isCreditPosture(value) {
1429
- return typeof value === "string" && CREDIT_POSTURES.includes(value);
1430
- }
1431
- function migrateCreditConfig(config) {
1432
- const credit = config.credit;
1433
- if (!credit) return config;
1434
- const migrateLayer = (layer) => {
1435
- const railed = migrateFundingRail(layer);
1436
- if (!("disabled" in railed)) return railed;
1437
- const { disabled, ...rest } = railed;
1438
- const migrated = { ...rest };
1439
- if (migrated.posture === void 0 && disabled === true) migrated.posture = "off";
1440
- return migrated;
1441
- };
1442
- const perDvm = credit.perDvm;
1443
- return {
1444
- ...config,
1445
- credit: {
1446
- ...migrateLayer(credit),
1447
- ...perDvm ? {
1448
- perDvm: Object.fromEntries(
1449
- Object.entries(perDvm).map(([endpoint, per]) => [endpoint, migrateLayer(per)])
1450
- )
1451
- } : {}
1452
- }
1453
- };
1454
- }
1455
- function migrateFundingRail(layer) {
1456
- const loose = layer;
1457
- if (loose.fundingRail !== "mpp") return layer;
1458
- return { ...layer, fundingRail: "tempo" };
1459
- }
1460
- function migrateTempoWalletKey(config) {
1461
- const loose = config;
1462
- if (!loose.mpp || loose.tempo) return config;
1463
- const { mpp, ...rest } = loose;
1464
- return { ...rest, tempo: mpp };
1465
- }
1466
- function migrateConfig(config) {
1467
- return migrateCreditConfig(migrateTempoWalletKey(config));
1468
- }
1469
- function loadConfig() {
1470
- const raw = readRawConfig();
1471
- return raw === null ? null : migrateConfig(raw);
1472
- }
1473
- async function updateConfig(update) {
1474
- return withConfigLock(async () => {
1475
- const raw = readRawConfig();
1476
- const snapshot = raw === null ? {} : structuredClone(raw);
1477
- const base = raw === null ? null : migrateConfig(structuredClone(raw));
1478
- const handed = raw === null ? null : deepFreezeConfig(migrateConfig(structuredClone(raw)));
1479
- const next = applyConfigPatch(base ?? {}, await runPatch(update, handed));
1480
- const changed = changedConfigPaths(snapshot, next);
1481
- if (changed.length === 0) {
1482
- return {
1483
- config: base ?? {},
1484
- previous: base,
1485
- changed: [],
1486
- written: false,
1487
- backup: describeConfigBackup()
1488
- };
1489
- }
1490
- const removals = protectedRemovals(base ?? {}, next);
1491
- const unauthorized = removals.filter((target) => update.authorize?.covers(target) !== true);
1492
- if (unauthorized.length > 0) throw protectedRemovalError(unauthorized, update.reason);
1493
- assertValidConfig(
1494
- next,
1495
- changed.map((path) => path.split(".")[0])
1496
- );
1497
- const superseded = supersededProtectedSecrets(base ?? {}, next);
1498
- const stripped = [
1499
- .../* @__PURE__ */ new Set([...removals, ...superseded, ...update.sanitizeBackup ?? []])
1500
- ].filter(isSecretProtectedTarget);
1501
- const sweeping = update.sweepSlots ?? CONFIG_SNAPSHOT_SLOTS;
1502
- const swept = [];
1503
- if (raw !== null) {
1504
- if (stripped.length > 0) {
1505
- writeConfigBackup(withoutProtectedTargets(raw, stripped));
1506
- swept.push("backup");
1507
- } else {
1508
- retainConfigBackup();
1509
- }
1510
- }
1511
- if (stripped.length > 0 && sweeping.includes("pinned")) {
1512
- if (sweepPinnedSnapshot(stripped, base ?? {}, update.reason)) swept.push("pinned");
1513
- }
1514
- const backup = describeConfigBackup();
1515
- writeConfigFile(next);
1516
- appendConfigAudit({
1517
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1518
- event: "update",
1519
- reason: update.reason,
1520
- fields: changed,
1521
- ...removals.length > 0 ? { removed: removals } : {},
1522
- ...superseded.length > 0 ? { superseded } : {},
1523
- ...swept.length > 0 ? { sweptSlots: swept } : {},
1524
- backup: backup !== null,
1525
- pid: process.pid
1526
- });
1527
- return { config: next, previous: base, changed, written: true, backup };
1528
- });
1529
- }
1530
- async function withConfigLock(fn) {
1531
- ensureConfigDir();
1532
- let release;
1533
- try {
1534
- release = await lockfile2.lock(CONFIG_FILE, {
1535
- lockfilePath: `${CONFIG_FILE}.lock`,
1536
- realpath: false,
1537
- stale: 3e4,
1538
- retries: { retries: 10, factor: 1, minTimeout: 100, maxTimeout: 100 }
1539
- });
1540
- } catch {
1541
- throw new DvmError(
1542
- "config_locked",
1543
- "Another dvm process is updating the local configuration.",
1544
- "Wait a moment and retry."
1545
- );
1546
- }
1547
- try {
1548
- return await fn();
1549
- } finally {
1550
- await release().catch(() => void 0);
1551
- }
1552
- }
1553
- async function runPatch(update, current) {
1554
- try {
1555
- return await update.patch(current);
1556
- } catch (err) {
1557
- if (err instanceof TypeError && /read only|not extensible|frozen|cannot delete property|cannot add property/i.test(
1558
- err.message
1559
- )) {
1560
- throw new DvmError(
1561
- "config_patch_invalid",
1562
- `The '${update.reason}' change tried to modify the configuration it was handed.`,
1563
- "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."
1564
- );
1565
- }
1566
- throw err;
1567
- }
1568
- }
1569
- function sweepPinnedSnapshot(targets, departing, reason) {
1570
- if (describeConfigBackup("pinned") === null) return false;
1571
- const pinned = readConfigBackup("pinned");
1572
- if (pinned === null) {
1573
- return retireSlot(`${reason}:unreadable_pin`, "pinned");
1574
- }
1575
- const migrated = migrateConfig(structuredClone(pinned));
1576
- const matching = targets.filter(
1577
- (target) => protectedValuesMatch(
1578
- target,
1579
- protectedTargetValue(departing, target),
1580
- protectedTargetValue(migrated, target)
1581
- )
1582
- );
1583
- if (matching.length === 0) return false;
1584
- const sanitized = withoutProtectedTargets(pinned, matching);
1585
- const changed = changedConfigPaths(pinned, sanitized);
1586
- if (changed.length === 0) return false;
1587
- writeConfigBackup(sanitized, "pinned");
1588
- appendConfigAudit({
1589
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1590
- event: "backup_sanitized",
1591
- reason,
1592
- fields: changed,
1593
- removed: matching,
1594
- slot: "pinned",
1595
- backup: describeConfigBackup() !== null,
1596
- pid: process.pid
1597
- });
1598
- return true;
1599
- }
1600
- function retireSlot(reason, slot) {
1601
- if (!removeConfigBackup(slot)) return false;
1602
- appendConfigAudit({
1603
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1604
- event: "backup_retired",
1605
- reason,
1606
- fields: [],
1607
- slot,
1608
- backup: describeConfigBackup() !== null,
1609
- pid: process.pid
1610
- });
1611
- return true;
1612
- }
1613
-
1614
1442
  export {
1443
+ setOutputTraceId,
1444
+ getOutputTraceId,
1445
+ json,
1615
1446
  info,
1447
+ raw,
1616
1448
  progress,
1449
+ RAIL_FLAGS,
1617
1450
  maxAdvertisedMicro,
1618
1451
  withinAdvertisedTolerance,
1619
- isValidEvmPrivateKey,
1620
- authorizeProtectedRemoval,
1621
- isCreditPosture,
1622
- loadConfig,
1623
- updateConfig,
1624
1452
  SUPPORTED_PROTOCOL_VERSION,
1625
1453
  parseCreditTerms,
1626
1454
  parseCreditMenu,
1627
1455
  normalizeCreditEndpoint,
1456
+ dvmLocationIdentity,
1628
1457
  fundingReachableAt,
1629
1458
  pendingDrainFor,
1459
+ pendingDrainById,
1630
1460
  loadCredit,
1461
+ clearCreditSelection,
1631
1462
  listCredits,
1463
+ listImplicitCredits,
1464
+ loadImplicitCredits,
1465
+ markImplicitCreditLost,
1466
+ listFundingMenuSnapshots,
1467
+ recordFundingMenuSnapshot,
1632
1468
  listPendingFundings,
1633
1469
  loadPendingFunding,
1634
1470
  recordPendingFunding,
1635
1471
  clearPendingFunding,
1636
1472
  recordFundResponse,
1473
+ recordBalanceResponse,
1474
+ applyReceiptCredit,
1475
+ applyImplicitReceiptCredit,
1637
1476
  ensureProvisionalCredit,
1477
+ markCredit,
1638
1478
  loadCreditX402Binding,
1639
1479
  recordCreditX402Instrument,
1480
+ recordRefusedDrawBalance,
1481
+ markRefillRailUnavailable,
1482
+ markRefillDrainNoticed,
1483
+ clearRefillRailUnavailable,
1484
+ forgetCredit,
1640
1485
  recordPendingDrain,
1641
- clearPendingDrain
1486
+ clearPendingDrain,
1487
+ recordDrainReceipts,
1488
+ settleDrainEvidence
1642
1489
  };