@dvmkit/sdk 0.4.0-rc.1 → 0.5.0-rc.1

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 (32) hide show
  1. package/dist/{chunk-TXX4GOUB.js → chunk-7ZUKL4IV.js} +1 -1
  2. package/dist/{chunk-KXZUCCEY.js → chunk-A4YFWZ7R.js} +4 -55
  3. package/dist/{chunk-2VGWYG4P.js → chunk-B2IROPQF.js} +446 -81
  4. package/dist/{chunk-K2YX2VRA.js → chunk-EYTHFFFU.js} +14 -12
  5. package/dist/{chunk-74PUEZJ4.js → chunk-GFY5FRX3.js} +1 -1
  6. package/dist/{chunk-W2EQGDF5.js → chunk-LE74UEV7.js} +49 -8
  7. package/dist/chunk-OFVV37XC.js +57 -0
  8. package/dist/{chunk-3MHE725Z.js → chunk-PAIM66YA.js} +90 -1
  9. package/dist/{chunk-5LR6P7WN.js → chunk-TUA7PYH7.js} +23 -103
  10. package/dist/{chunk-O3QDFPKM.js → chunk-TZ2WKCDD.js} +1 -1
  11. package/dist/{chunk-R4DQZFAL.js → chunk-UOCZXO2Z.js} +3 -3
  12. package/dist/{chunk-CE7XEHYC.js → chunk-Z4KLECAC.js} +1 -1
  13. package/dist/{credit-menu-B8_p_vUz.d.ts → credit-menu-CAfqe8IY.d.ts} +4 -4
  14. package/dist/{fx-DuaYHvVn.d.ts → fx-D9nsN3n4.d.ts} +1 -1
  15. package/dist/index.d.ts +5 -5
  16. package/dist/index.js +4 -3
  17. package/dist/internal/caller.d.ts +80 -13
  18. package/dist/internal/caller.js +36 -38
  19. package/dist/internal/index.js +1 -1
  20. package/dist/internal/server.d.ts +8 -8
  21. package/dist/internal/server.js +7 -6
  22. package/dist/{job-store-D6Rjj5Oi.d.ts → job-store-Bx-VFRjg.d.ts} +1 -1
  23. package/dist/{lightning-backend-BaeCPmx7.d.ts → lightning-backend-BAx2Y8pP.d.ts} +1 -1
  24. package/dist/server/index.d.ts +8 -8
  25. package/dist/server/index.js +6 -5
  26. package/dist/{step-cache-Bi43A0JN.d.ts → step-cache-QG_h5-3G.d.ts} +1 -1
  27. package/dist/{tempo-lifecycle-OTHV757Y.js → tempo-lifecycle-ZBBLFBMH.js} +5 -4
  28. package/dist/{tempo-wallet-32JWQZKQ.js → tempo-wallet-B5XXV3YH.js} +1 -1
  29. package/dist/testing/index.d.ts +2 -2
  30. package/dist/{usd-AZPVq4eU.d.ts → usd-EwRqCPF-.d.ts} +1 -1
  31. package/dist/{x402-CEDHGZDI.js → x402-R6HPBY5D.js} +5 -4
  32. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import {
2
- IMPLICIT_CREDIT_TTL_MS
3
- } from "./chunk-3MHE725Z.js";
2
+ IMPLICIT_CREDIT_TTL_MS,
3
+ isDrainReceipt
4
+ } from "./chunk-PAIM66YA.js";
4
5
  import {
5
6
  CREDITS_FILE,
6
7
  ensureConfigDir
@@ -9,42 +10,6 @@ import {
9
10
  DvmError
10
11
  } from "./chunk-LIRAHP7B.js";
11
12
 
12
- // src/lib/output.ts
13
- import { writeSync } from "fs";
14
- var _traceId = null;
15
- function setOutputTraceId(id) {
16
- _traceId = id;
17
- }
18
- function getOutputTraceId() {
19
- return _traceId;
20
- }
21
- function json(data) {
22
- const out = _traceId !== null && typeof data === "object" && data !== null && !Array.isArray(data) && !("trace_id" in data) ? { trace_id: _traceId, ...data } : data;
23
- writeAllSync(1, JSON.stringify(out) + "\n");
24
- }
25
- function info(msg) {
26
- writeAllSync(2, msg + "\n");
27
- }
28
- function raw(msg) {
29
- writeAllSync(1, msg + "\n");
30
- }
31
- function progress(event) {
32
- writeAllSync(2, JSON.stringify(event) + "\n");
33
- }
34
- function writeAllSync(fd, text) {
35
- const buf = Buffer.from(text, "utf8");
36
- let offset = 0;
37
- while (offset < buf.length) {
38
- try {
39
- offset += writeSync(fd, buf, offset, buf.length - offset);
40
- } catch (err) {
41
- const code = err.code;
42
- if (code === "EAGAIN" || code === "EINTR") continue;
43
- throw err;
44
- }
45
- }
46
- }
47
-
48
13
  // src/lib/payment-rails/types.ts
49
14
  var RAIL_FLAGS = ["cashu", "tempo", "x402"];
50
15
  var ADVERTISED_AMOUNT_TOLERANCE_BPS = 1000n;
@@ -57,9 +22,9 @@ function withinAdvertisedTolerance(advertisedMicro, localMicro) {
57
22
 
58
23
  // src/lib/transport/types.ts
59
24
  var SUPPORTED_PROTOCOL_VERSION = 1;
60
- function parseCreditTerms(raw2) {
61
- if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
62
- const credit = raw2;
25
+ function parseCreditTerms(raw) {
26
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
27
+ const credit = raw;
63
28
  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")
64
29
  return void 0;
65
30
  const tempo = credit.tempo === void 0 ? void 0 : parseCreditTempoTerms(credit.tempo);
@@ -79,10 +44,10 @@ function parseCreditTerms(raw2) {
79
44
  }
80
45
  };
81
46
  }
82
- function parseCreditMenu(raw2) {
83
- const terms = parseCreditTerms(raw2);
47
+ function parseCreditMenu(raw) {
48
+ const terms = parseCreditTerms(raw);
84
49
  if (!terms) return void 0;
85
- const credit = raw2;
50
+ const credit = raw;
86
51
  return {
87
52
  ...terms,
88
53
  ...typeof credit.credit_id === "string" && { credit_id: credit.credit_id },
@@ -93,9 +58,9 @@ function parseCreditMenu(raw2) {
93
58
  ...typeof credit.expiry_ms === "number" && { expiry_ms: credit.expiry_ms }
94
59
  };
95
60
  }
96
- function parseCreditTempoTerms(raw2) {
97
- if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
98
- const block = raw2;
61
+ function parseCreditTempoTerms(raw) {
62
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
63
+ const block = raw;
99
64
  if (!Array.isArray(block.methods) || !block.methods.every(isCreditTempoMethod)) return void 0;
100
65
  if (block.withheld !== void 0 && (!Array.isArray(block.withheld) || !block.withheld.every(isCreditTempoWithheldMethod)))
101
66
  return void 0;
@@ -113,15 +78,15 @@ function parseCreditTempoTerms(raw2) {
113
78
  }
114
79
  };
115
80
  }
116
- function isCreditTempoMethod(raw2) {
117
- return !!raw2 && typeof raw2 === "object" && typeof raw2.method === "string" && (raw2.intent === "charge" || raw2.intent === "session");
81
+ function isCreditTempoMethod(raw) {
82
+ return !!raw && typeof raw === "object" && typeof raw.method === "string" && (raw.intent === "charge" || raw.intent === "session");
118
83
  }
119
- function isCreditTempoWithheldMethod(raw2) {
120
- return isCreditTempoMethod(raw2) && typeof raw2.reason === "string";
84
+ function isCreditTempoWithheldMethod(raw) {
85
+ return isCreditTempoMethod(raw) && typeof raw.reason === "string";
121
86
  }
122
- function parseCreditX402Terms(raw2) {
123
- if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
124
- const schemes = raw2.schemes;
87
+ function parseCreditX402Terms(raw) {
88
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
89
+ const schemes = raw.schemes;
125
90
  if (!Array.isArray(schemes) || !schemes.every(
126
91
  (entry) => !!entry && typeof entry === "object" && typeof entry.scheme === "string" && typeof entry.network === "string"
127
92
  ))
@@ -135,9 +100,348 @@ function parseCreditX402Terms(raw2) {
135
100
  }
136
101
 
137
102
  // src/lib/credit-store.ts
138
- import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
103
+ import { existsSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
139
104
  import { dirname } from "path";
140
105
  import lockfile from "proper-lockfile";
106
+
107
+ // src/lib/credit-history.ts
108
+ import { createHash } from "crypto";
109
+ import {
110
+ closeSync,
111
+ fsyncSync,
112
+ openSync,
113
+ readFileSync,
114
+ writeSync,
115
+ ftruncateSync
116
+ } from "fs";
117
+ function historyPath() {
118
+ return `${CREDITS_FILE.slice(0, -5)}-history.jsonl`;
119
+ }
120
+ function hash(value) {
121
+ return createHash("sha256").update(value).digest("hex");
122
+ }
123
+ function fail(detail) {
124
+ throw new DvmError(
125
+ "credit_history_unavailable",
126
+ `Completed credit history could not be preserved: ${detail}`,
127
+ "Keep the credit store and history file intact, fix the file access or damage, and retry. Working evidence was not evicted."
128
+ );
129
+ }
130
+ var receiptKeys = /* @__PURE__ */ new Set([
131
+ "v",
132
+ "kind",
133
+ "credit_id",
134
+ "drain_id",
135
+ "event",
136
+ "method",
137
+ "amount_micro",
138
+ "balance_after_micro",
139
+ "ledger_seq",
140
+ "issued_at",
141
+ "caller_pubkey",
142
+ "dvm_id",
143
+ "dvm",
144
+ "receipt_pubkey",
145
+ "signature"
146
+ ]);
147
+ var attestationKeys = /* @__PURE__ */ new Set([
148
+ "dvm_id",
149
+ "slug",
150
+ "capabilities_hash",
151
+ "builder_pubkey",
152
+ "receipt_pubkey",
153
+ "deployed_at"
154
+ ]);
155
+ function safeReceipts(bundle) {
156
+ const receipts = [];
157
+ for (const receipt of bundle.receipts) {
158
+ if (!isDrainReceipt(receipt))
159
+ fail("a completed bundle contains an unrecognized receipt");
160
+ if (Object.keys(receipt).some((key) => !receiptKeys.has(key)))
161
+ fail("a receipt contains unsupported fields");
162
+ if (Object.values(receipt).some(
163
+ (value) => typeof value !== "string" && typeof value !== "number" || typeof value === "number" && (!Number.isSafeInteger(value) || value < 0)
164
+ ))
165
+ fail("a receipt contains unsupported values");
166
+ receipts.push(receipt);
167
+ }
168
+ return receipts;
169
+ }
170
+ function safeAnchor(anchor) {
171
+ if (!anchor) return void 0;
172
+ if (typeof anchor.pubkey !== "string" || typeof anchor.signature !== "string" || !anchor.attestation || Object.keys(anchor.attestation).some((key) => !attestationKeys.has(key)) || Object.values(anchor.attestation).some(
173
+ (value) => typeof value !== "string" && typeof value !== "number"
174
+ ))
175
+ fail("an advertisement contains unsupported fields");
176
+ return {
177
+ pubkey: anchor.pubkey,
178
+ signature: anchor.signature,
179
+ attestation: anchor.attestation
180
+ };
181
+ }
182
+ function hasAttestedIdentity(record) {
183
+ return Boolean(record.dvmId && record.attestedBuilderPubkey);
184
+ }
185
+ function compatibleLocations(left, right) {
186
+ if (hasAttestedIdentity(left) && hasAttestedIdentity(right))
187
+ return left.dvmId === right.dvmId && left.attestedBuilderPubkey === right.attestedBuilderPubkey;
188
+ return [left.endpoint, ...left.endpointAliases].some(
189
+ (origin) => [right.endpoint, ...right.endpointAliases].includes(origin)
190
+ );
191
+ }
192
+ function base(record, identity, occurredAt) {
193
+ return {
194
+ v: 1,
195
+ id: hash(
196
+ JSON.stringify([
197
+ record.callerPubkey,
198
+ hasAttestedIdentity(record) ? record.dvmId : record.endpoint,
199
+ hasAttestedIdentity(record) ? record.attestedBuilderPubkey : null,
200
+ record.creditId,
201
+ identity
202
+ ])
203
+ ),
204
+ endpoint: record.endpoint,
205
+ endpointAliases: record.endpointAliases ?? [],
206
+ dvmId: record.dvmId ?? null,
207
+ attestedBuilderPubkey: record.attestedBuilderPubkey ?? null,
208
+ callerPubkey: record.callerPubkey,
209
+ creditId: record.creditId,
210
+ occurredAt,
211
+ rail: null,
212
+ currency: null,
213
+ amountMicro: null,
214
+ feeMicro: null
215
+ };
216
+ }
217
+ function completedRefund(record, bundle) {
218
+ const receipts = safeReceipts(bundle);
219
+ const anchor = safeAnchor(bundle.anchor);
220
+ const receipt = receipts.find(
221
+ (r) => r.drain_id === bundle.drainId && r.credit_id === bundle.creditId
222
+ );
223
+ const metadata = bundle.settlement;
224
+ if (metadata && (metadata.rail !== null && typeof metadata.rail !== "string" || metadata.currency !== null && typeof metadata.currency !== "string" || [metadata.amountMicro, metadata.feeMicro].some(
225
+ (value) => value !== null && (!Number.isSafeInteger(value) || value < 0)
226
+ )))
227
+ fail("invalid settlement metadata");
228
+ return {
229
+ ...base(
230
+ { ...record, creditId: bundle.creditId },
231
+ `refund:${bundle.drainId}`,
232
+ bundle.settledAt
233
+ ),
234
+ kind: "completed_refund",
235
+ outcome: "refunded",
236
+ drainId: bundle.drainId,
237
+ settledAt: bundle.settledAt,
238
+ collectedAt: bundle.collectedAt === 0 ? null : bundle.collectedAt,
239
+ rail: metadata?.rail ?? receipt?.method ?? null,
240
+ currency: metadata?.currency ?? null,
241
+ amountMicro: metadata?.amountMicro ?? receipt?.amount_micro ?? null,
242
+ feeMicro: metadata?.feeMicro ?? null,
243
+ receipts,
244
+ ...anchor ? { anchor } : {},
245
+ provenance: {
246
+ settlement: "local_observation",
247
+ receipts: "unverified_observations",
248
+ advertisement: anchor ? "present" : "absent"
249
+ }
250
+ };
251
+ }
252
+ function completedLoss(record) {
253
+ return {
254
+ ...base(record, "loss", record.updatedAt),
255
+ kind: "expired_credit_loss",
256
+ outcome: "lost",
257
+ rail: record.creditId.startsWith("imp:") ? record.creditId.split(":")[1] || null : null,
258
+ currency: record.currency,
259
+ amountMicro: record.remainingMicro,
260
+ reason: record.lossReason ?? "expired",
261
+ releasedAt: record.releasedAt,
262
+ expiryMs: record.expiryMs,
263
+ releasedByJobId: record.releasedByJobId
264
+ };
265
+ }
266
+ function validateHistoryRecord(row) {
267
+ if (!row || row.v !== 1 || typeof row.id !== "string" || !Number.isSafeInteger(row.occurredAt) || typeof row.endpoint !== "string" || typeof row.callerPubkey !== "string" || typeof row.creditId !== "string" || !Array.isArray(row.endpointAliases) || row.endpointAliases.some((value) => typeof value !== "string") || [row.dvmId, row.attestedBuilderPubkey, row.currency, row.rail].some(
268
+ (value) => value !== null && typeof value !== "string"
269
+ ) || [row.amountMicro, row.feeMicro].some(
270
+ (value) => value !== null && (!Number.isSafeInteger(value) || value < 0)
271
+ ))
272
+ fail("invalid complete journal entry");
273
+ if (row.kind === "completed_refund") {
274
+ if (row.outcome !== "refunded" || typeof row.drainId !== "string" || !Number.isSafeInteger(row.settledAt) || row.collectedAt !== null && !Number.isSafeInteger(row.collectedAt) || !Array.isArray(row.receipts) || row.provenance?.settlement !== "local_observation" || row.provenance.receipts !== "unverified_observations" || !["present", "absent"].includes(row.provenance.advertisement))
275
+ fail("invalid refund entry");
276
+ safeReceipts({
277
+ drainId: row.drainId,
278
+ creditId: row.creditId,
279
+ receipts: row.receipts,
280
+ collectedAt: row.collectedAt ?? 0
281
+ });
282
+ safeAnchor(row.anchor);
283
+ } else if (row.kind === "expired_credit_loss") {
284
+ if (row.outcome !== "lost" || !["expired", "not_found"].includes(row.reason) || !Number.isSafeInteger(row.releasedAt) || !Number.isSafeInteger(row.expiryMs) || typeof row.releasedByJobId !== "string")
285
+ fail("invalid loss entry");
286
+ } else fail("invalid complete journal entry");
287
+ }
288
+ function journal() {
289
+ let bytes;
290
+ try {
291
+ bytes = readFileSync(historyPath());
292
+ } catch (error) {
293
+ if (error.code === "ENOENT")
294
+ return { bytes: Buffer.alloc(0), complete: 0, rows: [] };
295
+ fail(error instanceof Error ? error.message : String(error));
296
+ }
297
+ const complete = bytes.lastIndexOf(10) + 1;
298
+ const rows = bytes.subarray(0, complete).toString("utf8").split("\n").filter(Boolean).map((line) => {
299
+ let row;
300
+ try {
301
+ row = JSON.parse(line);
302
+ } catch {
303
+ fail("damaged complete journal line");
304
+ }
305
+ validateHistoryRecord(row);
306
+ return row;
307
+ });
308
+ return { bytes, complete, rows };
309
+ }
310
+ function archiveCompletedCreditRecords(records) {
311
+ if (!records.length) return;
312
+ try {
313
+ const current = journal();
314
+ const latest = new Map(
315
+ current.rows.map((record) => [record.id, JSON.stringify(record)])
316
+ );
317
+ const observed = new Map(current.rows.map((record) => [record.id, record]));
318
+ const additions = [];
319
+ for (const incoming of records) {
320
+ const prior = [...observed.values()].reverse().find(
321
+ (row) => row.kind === incoming.kind && row.callerPubkey === incoming.callerPubkey && row.creditId === incoming.creditId && (row.kind !== "completed_refund" || incoming.kind === "completed_refund" && row.drainId === incoming.drainId) && compatibleLocations(row, incoming)
322
+ );
323
+ const newerPrior = prior && (incoming.kind === "completed_refund" && prior.kind === "completed_refund" ? (prior.collectedAt ?? 0) > (incoming.collectedAt ?? 0) : prior.occurredAt > incoming.occurredAt);
324
+ const facts = newerPrior ? prior : incoming;
325
+ const identity = prior && hasAttestedIdentity(prior) ? prior : hasAttestedIdentity(incoming) ? incoming : newerPrior || !incoming.dvmId && !incoming.attestedBuilderPubkey ? prior ?? incoming : incoming;
326
+ const record = prior ? {
327
+ ...facts,
328
+ id: prior.id,
329
+ dvmId: identity.dvmId,
330
+ attestedBuilderPubkey: identity.attestedBuilderPubkey,
331
+ endpointAliases: [
332
+ .../* @__PURE__ */ new Set([
333
+ prior.endpoint,
334
+ ...prior.endpointAliases,
335
+ incoming.endpoint,
336
+ ...incoming.endpointAliases
337
+ ])
338
+ ].filter((origin) => origin !== facts.endpoint)
339
+ } : incoming;
340
+ validateHistoryRecord(record);
341
+ const encoded = JSON.stringify(record);
342
+ if (latest.get(record.id) === encoded) continue;
343
+ additions.push(encoded + "\n");
344
+ latest.set(record.id, encoded);
345
+ observed.set(record.id, record);
346
+ }
347
+ const fd = openSync(historyPath(), "a", 384);
348
+ try {
349
+ if (current.complete !== current.bytes.length)
350
+ ftruncateSync(fd, current.complete);
351
+ const bytes = Buffer.from(additions.join(""));
352
+ let written = 0;
353
+ while (written < bytes.length) {
354
+ const count = writeSync(fd, bytes, written, bytes.length - written);
355
+ if (!count) fail("zero-length append");
356
+ written += count;
357
+ }
358
+ fsyncSync(fd);
359
+ } finally {
360
+ closeSync(fd);
361
+ }
362
+ {
363
+ const directory = openSync(
364
+ historyPath().slice(0, historyPath().lastIndexOf("/")),
365
+ "r"
366
+ );
367
+ try {
368
+ fsyncSync(directory);
369
+ } finally {
370
+ closeSync(directory);
371
+ }
372
+ }
373
+ } catch (error) {
374
+ fail(error instanceof Error ? error.message : String(error));
375
+ }
376
+ }
377
+ function readCreditHistoryPage(query) {
378
+ const limit = query.limit ?? 50;
379
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
380
+ throw new DvmError(
381
+ "invalid_credit_history_query",
382
+ "History page limit must be an integer from 1 through 100."
383
+ );
384
+ const order = query.order ?? "asc";
385
+ if (order !== "asc" && order !== "desc")
386
+ throw new DvmError(
387
+ "invalid_credit_history_query",
388
+ "History order must be asc or desc."
389
+ );
390
+ const direction = order === "asc" ? 1 : -1;
391
+ const filter = JSON.stringify([
392
+ order,
393
+ query.endpoint ? new URL(query.endpoint).origin : null,
394
+ query.dvmId ?? null,
395
+ query.callerPubkey ?? null
396
+ ]);
397
+ const current = journal();
398
+ let end = current.complete;
399
+ let after;
400
+ if (query.cursor) {
401
+ try {
402
+ const cursor = JSON.parse(
403
+ Buffer.from(query.cursor, "base64url").toString("utf8")
404
+ );
405
+ if (cursor.v !== 1 || !Number.isSafeInteger(cursor.end) || cursor.end < 0 || cursor.end > current.complete || cursor.end > 0 && current.bytes[cursor.end - 1] !== 10 || cursor.filter !== filter || cursor.prefix !== hash(current.bytes.subarray(0, cursor.end).toString("utf8")) || !Array.isArray(cursor.after) || !Number.isFinite(cursor.after[0]) || typeof cursor.after[1] !== "string")
406
+ throw new Error("cursor mismatch");
407
+ end = cursor.end;
408
+ after = cursor.after;
409
+ } catch {
410
+ throw new DvmError(
411
+ "invalid_credit_history_cursor",
412
+ "History cursor is invalid, stale, or belongs to different filters."
413
+ );
414
+ }
415
+ }
416
+ const rows = current.bytes.subarray(0, end).toString("utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
417
+ const endpoint = query.endpoint ? new URL(query.endpoint).origin : void 0;
418
+ const ordered = [...new Map(rows.map((row) => [row.id, row])).values()].filter(
419
+ (row) => (!endpoint || row.endpoint === endpoint || row.endpointAliases.includes(endpoint)) && (!query.dvmId || row.dvmId === query.dvmId) && (!query.callerPubkey || row.callerPubkey === query.callerPubkey)
420
+ ).sort(
421
+ (a, b) => direction * (a.occurredAt - b.occurredAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
422
+ );
423
+ const remaining = ordered.filter(
424
+ (row) => !after || direction * (row.occurredAt - after[0]) > 0 || row.occurredAt === after[0] && (direction === 1 ? row.id > after[1] : row.id < after[1])
425
+ );
426
+ const records = remaining.slice(0, limit);
427
+ const last = records.at(-1);
428
+ return {
429
+ records,
430
+ ...remaining.length > limit && last ? {
431
+ nextCursor: Buffer.from(
432
+ JSON.stringify({
433
+ v: 1,
434
+ end,
435
+ prefix: hash(current.bytes.subarray(0, end).toString("utf8")),
436
+ filter,
437
+ after: [last.occurredAt, last.id]
438
+ })
439
+ ).toString("base64url")
440
+ } : {}
441
+ };
442
+ }
443
+
444
+ // src/lib/credit-store.ts
141
445
  function withCreditLock(fn, opts = {}) {
142
446
  if (lockDepth > 0) {
143
447
  lockDepth += 1;
@@ -260,6 +564,12 @@ function clearCreditSelection(endpoint, callerPubkey, creditId, identity) {
260
564
  return structuredClone(record);
261
565
  });
262
566
  }
567
+ function readCompletedCreditHistory(query = {}) {
568
+ return withCreditLock(() => {
569
+ loadStore(true);
570
+ return readCreditHistoryPage(query);
571
+ });
572
+ }
263
573
  function listCredits() {
264
574
  return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
265
575
  }
@@ -310,7 +620,13 @@ function markImplicitCreditLost(args) {
310
620
  where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.creditId
311
621
  });
312
622
  if (!record) return void 0;
313
- attachDvmLocation(record, key, args.identity);
623
+ const locationChanged = attachDvmLocation(record, key, args.identity);
624
+ if (record.status === "lost" && !archiveOrKeep(
625
+ () => archiveCompletedCreditRecords([completedLoss(record)])
626
+ )) {
627
+ if (locationChanged) saveStore(store);
628
+ return record;
629
+ }
314
630
  record.status = "lost";
315
631
  record.lossReason = args.reason;
316
632
  record.updatedAt = args.now ?? Date.now();
@@ -579,7 +895,13 @@ function applyImplicitReceiptCredit(args) {
579
895
  saveStore(store);
580
896
  return store.implicitCredits.includes(record) ? { credit: record } : {};
581
897
  }
582
- attachDvmLocation(record, key, args.identity);
898
+ const locationChanged = attachDvmLocation(record, key, args.identity);
899
+ if (record.status === "lost" && !archiveOrKeep(
900
+ () => archiveCompletedCreditRecords([completedLoss(record)])
901
+ )) {
902
+ if (locationChanged) saveStore(store);
903
+ return { credit: record };
904
+ }
583
905
  let mismatch;
584
906
  const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
585
907
  if (sequential && args.outcome === "completed" && record.status === "active") {
@@ -756,6 +1078,9 @@ function forgetCredit(endpoint, callerPubkey, identity) {
756
1078
  where: (c) => c.callerPubkey === callerPubkey
757
1079
  });
758
1080
  if (!record) return;
1081
+ archiveCompletedCreditRecords(
1082
+ (record.drainEvidence ?? []).filter((bundle) => bundle.settledAt !== void 0).map((bundle) => completedRefund(record, bundle))
1083
+ );
759
1084
  saveStore({ ...store, credits: store.credits.filter((c) => c !== record) });
760
1085
  });
761
1086
  }
@@ -798,6 +1123,11 @@ function recordDrainReceipts(args) {
798
1123
  mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
799
1124
  const evidence = record.drainEvidence ??= [];
800
1125
  let bundle = evidence.find((e) => e.drainId === drainId);
1126
+ if (bundle?.settledAt !== void 0 && !archiveOrKeep(
1127
+ () => archiveCompletedCreditRecords([completedRefund(record, bundle)])
1128
+ )) {
1129
+ return "skip";
1130
+ }
801
1131
  if (!bundle) {
802
1132
  bundle = { drainId, creditId, receipts: [], collectedAt: now };
803
1133
  evidence.push(bundle);
@@ -805,9 +1135,15 @@ function recordDrainReceipts(args) {
805
1135
  bundle.creditId = creditId;
806
1136
  bundle.receipts = receipts;
807
1137
  bundle.collectedAt = now;
1138
+ bundle.settlement ??= {
1139
+ rail: null,
1140
+ currency: record.creditId === creditId ? record.currency : null,
1141
+ amountMicro: null,
1142
+ feeMicro: null
1143
+ };
808
1144
  if (read.anchor) bundle.anchor = read.anchor;
809
1145
  else if (read.reachable) delete bundle.anchor;
810
- record.drainEvidence = capDrainEvidence(evidence, endpoint);
1146
+ record.drainEvidence = evidence;
811
1147
  return void 0;
812
1148
  });
813
1149
  }
@@ -818,7 +1154,8 @@ function settleDrainEvidence(args) {
818
1154
  const bundle = evidence.find((e) => e.drainId === drainId);
819
1155
  if (!bundle || bundle.settledAt !== void 0) return "skip";
820
1156
  bundle.settledAt = now;
821
- record.drainEvidence = capDrainEvidence(evidence, endpoint);
1157
+ if (args.settlement) bundle.settlement = args.settlement;
1158
+ record.drainEvidence = evidence;
822
1159
  return void 0;
823
1160
  });
824
1161
  }
@@ -884,19 +1221,19 @@ function adoptCreditId(record, creditId) {
884
1221
  var MAX_SETTLED_DRAIN_EVIDENCE = 8;
885
1222
  var MAX_LOST_IMPLICIT_CREDITS = 8;
886
1223
  var MAX_DATE_MS = 864e13;
887
- function loadStore() {
1224
+ function loadStore(requireArchive = false) {
888
1225
  if (!existsSync(CREDITS_FILE)) {
889
1226
  return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
890
1227
  }
891
- let raw2;
1228
+ let raw;
892
1229
  try {
893
- raw2 = readFileSync(CREDITS_FILE, "utf-8");
1230
+ raw = readFileSync2(CREDITS_FILE, "utf-8");
894
1231
  } catch (cause) {
895
1232
  throw unreadableStore(cause);
896
1233
  }
897
1234
  let data;
898
1235
  try {
899
- const parsed = JSON.parse(raw2);
1236
+ const parsed = JSON.parse(raw);
900
1237
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
901
1238
  throw new Error("the file parsed as JSON but is not a credit-store object");
902
1239
  }
@@ -917,7 +1254,8 @@ function loadStore() {
917
1254
  Array.isArray(data.pendingFundings) ? data.pendingFundings.filter((funding) => isObjectRecord(funding)).map(normalizeRailKey) : []
918
1255
  );
919
1256
  const implicit = dedupeImplicitCredits(
920
- Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : []
1257
+ Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : [],
1258
+ requireArchive
921
1259
  );
922
1260
  const store = {
923
1261
  credits: merged.credits,
@@ -925,14 +1263,16 @@ function loadStore() {
925
1263
  pendingFundings: fundings.fundings,
926
1264
  fundingMenuSnapshots: snapshots.snapshots
927
1265
  };
1266
+ if (requireArchive) archiveStore(store);
928
1267
  if (merged.changed || snapshots.changed || fundings.changed || implicit.changed) saveStore(store);
1268
+ else if (!requireArchive) archiveOrKeep(() => archiveStore(store));
929
1269
  return store;
930
1270
  }
931
1271
  function isStoredImplicitCredit(value) {
932
1272
  if (!isObjectRecord(value)) return false;
933
1273
  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);
934
1274
  }
935
- function dedupeImplicitCredits(credits) {
1275
+ function dedupeImplicitCredits(credits, requireArchive = false) {
936
1276
  const merged = [];
937
1277
  let changed = false;
938
1278
  for (const candidate of credits) {
@@ -947,6 +1287,9 @@ function dedupeImplicitCredits(credits) {
947
1287
  merged.push(candidate);
948
1288
  continue;
949
1289
  }
1290
+ if (!archiveOrKeep(() => archiveCompletedCreditRecords(
1291
+ [existing, candidate].filter((record) => record.status === "lost").map(completedLoss)
1292
+ ), requireArchive)) return { credits, changed: false };
950
1293
  changed = true;
951
1294
  for (const origin of dvmLocationOrigins(candidate)) {
952
1295
  attachDvmLocation(existing, origin, identity);
@@ -968,7 +1311,7 @@ function dedupeImplicitCredits(credits) {
968
1311
  existing.releasedByJobId = candidate.releasedByJobId;
969
1312
  }
970
1313
  }
971
- const capped = capLostImplicitCredits(merged);
1314
+ const capped = capLostImplicitCredits(merged, requireArchive);
972
1315
  return { credits: capped.credits, changed: changed || capped.changed };
973
1316
  }
974
1317
  function ageImplicitCredits(credits, now) {
@@ -983,7 +1326,14 @@ function ageImplicitCredits(credits, now) {
983
1326
  }
984
1327
  return changed;
985
1328
  }
986
- function capLostImplicitCredits(credits) {
1329
+ function capLostImplicitCredits(credits, requireArchive = false) {
1330
+ const archived = archiveOrKeep(
1331
+ () => archiveCompletedCreditRecords(
1332
+ credits.filter((credit) => credit.status === "lost").map(completedLoss)
1333
+ ),
1334
+ requireArchive
1335
+ );
1336
+ if (!archived) return { credits, changed: false };
987
1337
  const lostByOwner = /* @__PURE__ */ new Map();
988
1338
  for (const credit of credits) {
989
1339
  if (credit.status !== "lost") continue;
@@ -1286,19 +1636,39 @@ function isEvidenceish(value) {
1286
1636
  function isDrainish(value) {
1287
1637
  return typeof value === "object" && value !== null && typeof value.drainId === "string";
1288
1638
  }
1289
- function capDrainEvidence(evidence, endpoint) {
1290
- const settled = evidence.filter((e) => e.settledAt !== void 0).sort((a, b) => (a.settledAt ?? 0) - (b.settledAt ?? 0));
1291
- if (settled.length <= MAX_SETTLED_DRAIN_EVIDENCE) return evidence;
1292
- const dropped = new Set(settled.slice(0, settled.length - MAX_SETTLED_DRAIN_EVIDENCE));
1293
- for (const e of dropped) {
1294
- info(
1295
- `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'.`
1296
- );
1639
+ function archiveOrKeep(action, requireArchive = false) {
1640
+ try {
1641
+ action();
1642
+ return true;
1643
+ } catch (error) {
1644
+ if (requireArchive || !(error instanceof DvmError) || error.code !== "credit_history_unavailable") {
1645
+ throw error;
1646
+ }
1647
+ return false;
1297
1648
  }
1649
+ }
1650
+ function archiveStore(store) {
1651
+ archiveCompletedCreditRecords([
1652
+ ...store.credits.flatMap(
1653
+ (credit) => (credit.drainEvidence ?? []).filter((bundle) => bundle.settledAt !== void 0).map((bundle) => completedRefund(credit, bundle))
1654
+ ),
1655
+ ...store.implicitCredits.filter((credit) => credit.status === "lost").map(completedLoss)
1656
+ ]);
1657
+ }
1658
+ function capDrainEvidence(evidence) {
1659
+ const settled = evidence.filter((e) => e.settledAt !== void 0).sort((a, b) => a.settledAt - b.settledAt);
1660
+ const dropped = new Set(
1661
+ settled.slice(0, Math.max(0, settled.length - MAX_SETTLED_DRAIN_EVIDENCE))
1662
+ );
1298
1663
  return evidence.filter((e) => !dropped.has(e));
1299
1664
  }
1300
1665
  function saveStore(store) {
1301
1666
  ensureConfigDir();
1667
+ if (archiveOrKeep(() => archiveStore(store))) {
1668
+ for (const credit of store.credits) {
1669
+ if (credit.drainEvidence) credit.drainEvidence = capDrainEvidence(credit.drainEvidence);
1670
+ }
1671
+ }
1302
1672
  const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
1303
1673
  writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
1304
1674
  try {
@@ -1430,12 +1800,6 @@ function sleepSync(ms) {
1430
1800
  }
1431
1801
 
1432
1802
  export {
1433
- setOutputTraceId,
1434
- getOutputTraceId,
1435
- json,
1436
- info,
1437
- raw,
1438
- progress,
1439
1803
  RAIL_FLAGS,
1440
1804
  maxAdvertisedMicro,
1441
1805
  withinAdvertisedTolerance,
@@ -1449,6 +1813,7 @@ export {
1449
1813
  pendingDrainById,
1450
1814
  loadCredit,
1451
1815
  clearCreditSelection,
1816
+ readCompletedCreditHistory,
1452
1817
  listCredits,
1453
1818
  listImplicitCredits,
1454
1819
  loadImplicitCredits,