@dvmkit/sdk 0.3.4-rc.10 → 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 (56) hide show
  1. package/dist/{chunk-2KGG5ORW.js → chunk-7ZUKL4IV.js} +3 -3
  2. package/dist/{chunk-KXZUCCEY.js → chunk-A4YFWZ7R.js} +4 -55
  3. package/dist/{chunk-C2AGARLM.js → chunk-B2IROPQF.js} +450 -83
  4. package/dist/{chunk-MKI6OVW4.js → chunk-CZULM42S.js} +0 -51
  5. package/dist/{chunk-UZC4A4DM.js → chunk-EYTHFFFU.js} +2489 -213
  6. package/dist/{chunk-M4QDXD2A.js → chunk-FYRR2UEZ.js} +1 -1
  7. package/dist/{chunk-4UZN2GP5.js → chunk-GAOAYHZA.js} +209 -33
  8. package/dist/{chunk-YYQG4R7T.js → chunk-GFY5FRX3.js} +6 -4
  9. package/dist/{chunk-Z4BNLUZF.js → chunk-IWQHUSMO.js} +3 -105
  10. package/dist/{chunk-4J32DTLT.js → chunk-IXR5RFK7.js} +82 -4
  11. package/dist/{chunk-2SD2W2BG.js → chunk-JH3SX3LY.js} +252 -26
  12. package/dist/chunk-KA6VN5FK.js +245 -0
  13. package/dist/{chunk-YRDA7EWN.js → chunk-LE74UEV7.js} +61 -16
  14. package/dist/chunk-LIRAHP7B.js +53 -0
  15. package/dist/{chunk-MG67KXU7.js → chunk-MZRJR2CW.js} +4 -2
  16. package/dist/chunk-OFVV37XC.js +57 -0
  17. package/dist/{chunk-A4CM2BD3.js → chunk-PAIM66YA.js} +91 -2
  18. package/dist/{chunk-JDFLRNAZ.js → chunk-PB4FYZET.js} +67 -7
  19. package/dist/{chunk-BQ2NMWKE.js → chunk-PLG4XER6.js} +1 -1
  20. package/dist/chunk-RLTSCKRI.js +109 -0
  21. package/dist/{chunk-VAMSJRJT.js → chunk-TUA7PYH7.js} +27 -105
  22. package/dist/{chunk-O3QDFPKM.js → chunk-TZ2WKCDD.js} +1 -1
  23. package/dist/{chunk-Z3EHBW2G.js → chunk-UOCZXO2Z.js} +7 -5
  24. package/dist/{chunk-EAMZZ2B4.js → chunk-X7EZ5CLI.js} +59 -0
  25. package/dist/{chunk-CE7XEHYC.js → chunk-Z4KLECAC.js} +1 -1
  26. package/dist/{chunk-7X6UDVCQ.js → chunk-Z4ZT5LNT.js} +66 -2
  27. package/dist/{credit-ledger-A35WH2TI.js → credit-ledger-MI2H7NEC.js} +3 -1
  28. package/dist/{credit-menu-0-UtJ3hh.d.ts → credit-menu-CAfqe8IY.d.ts} +133 -7
  29. package/dist/{fx-DhyT1Cjh.d.ts → fx-D9nsN3n4.d.ts} +1 -1
  30. package/dist/index.d.ts +6 -6
  31. package/dist/index.js +4 -3
  32. package/dist/internal/caller.d.ts +80 -13
  33. package/dist/internal/caller.js +56 -54
  34. package/dist/internal/index.js +1 -1
  35. package/dist/internal/server.d.ts +11 -85
  36. package/dist/internal/server.js +21 -18
  37. package/dist/{job-store-D3CcZp_g.d.ts → job-store-Bx-VFRjg.d.ts} +1 -1
  38. package/dist/{lightning-backend-BhsyrBLa.d.ts → lightning-backend-BAx2Y8pP.d.ts} +1 -1
  39. package/dist/{memory-credit-ledger-2LBZVMKJ.js → memory-credit-ledger-W2BAUYJP.js} +4 -2
  40. package/dist/{mpp-setup-7CGBKNUP.js → mpp-setup-RF5FXGGV.js} +5 -1
  41. package/dist/{payout-reporter-ZMOA37GN.js → payout-reporter-3Y2U55ZX.js} +1 -1
  42. package/dist/{postgres-job-store-MSD2HZOI.js → postgres-job-store-VLXLWXUQ.js} +1 -1
  43. package/dist/{revenue-reporter-EKJOGQYR.js → revenue-reporter-MAERPYGK.js} +1 -1
  44. package/dist/server/index.d.ts +14 -10
  45. package/dist/server/index.js +57 -19
  46. package/dist/{step-cache-v2yhyhSZ.d.ts → step-cache-QG_h5-3G.d.ts} +1769 -1549
  47. package/dist/{tempo-charge-store-4WX7FPOV.js → tempo-charge-store-Q3D7KYMH.js} +1 -1
  48. package/dist/tempo-lifecycle-ZBBLFBMH.js +15 -0
  49. package/dist/{tempo-session-store-LCSDHB7G.js → tempo-session-store-ZRNCOX7P.js} +1 -1
  50. package/dist/{tempo-wallet-RCGBBAJH.js → tempo-wallet-B5XXV3YH.js} +4 -3
  51. package/dist/testing/index.d.ts +3 -3
  52. package/dist/{usd-Cu9G6xv2.d.ts → usd-EwRqCPF-.d.ts} +1 -1
  53. package/dist/{x402-S47Q77UE.js → x402-R6HPBY5D.js} +18 -14
  54. package/package.json +1 -1
  55. package/dist/chunk-YHN3WMMF.js +0 -122
  56. package/dist/tempo-lifecycle-TJ62A7UX.js +0 -13
@@ -1,47 +1,14 @@
1
1
  import {
2
- IMPLICIT_CREDIT_TTL_MS
3
- } from "./chunk-A4CM2BD3.js";
2
+ IMPLICIT_CREDIT_TTL_MS,
3
+ isDrainReceipt
4
+ } from "./chunk-PAIM66YA.js";
4
5
  import {
5
6
  CREDITS_FILE,
6
- DvmError,
7
7
  ensureConfigDir
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
- }
8
+ } from "./chunk-CZULM42S.js";
9
+ import {
10
+ DvmError
11
+ } from "./chunk-LIRAHP7B.js";
45
12
 
46
13
  // src/lib/payment-rails/types.ts
47
14
  var RAIL_FLAGS = ["cashu", "tempo", "x402"];
@@ -55,9 +22,9 @@ function withinAdvertisedTolerance(advertisedMicro, localMicro) {
55
22
 
56
23
  // src/lib/transport/types.ts
57
24
  var SUPPORTED_PROTOCOL_VERSION = 1;
58
- function parseCreditTerms(raw2) {
59
- if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
60
- const credit = raw2;
25
+ function parseCreditTerms(raw) {
26
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
27
+ const credit = raw;
61
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")
62
29
  return void 0;
63
30
  const tempo = credit.tempo === void 0 ? void 0 : parseCreditTempoTerms(credit.tempo);
@@ -77,10 +44,10 @@ function parseCreditTerms(raw2) {
77
44
  }
78
45
  };
79
46
  }
80
- function parseCreditMenu(raw2) {
81
- const terms = parseCreditTerms(raw2);
47
+ function parseCreditMenu(raw) {
48
+ const terms = parseCreditTerms(raw);
82
49
  if (!terms) return void 0;
83
- const credit = raw2;
50
+ const credit = raw;
84
51
  return {
85
52
  ...terms,
86
53
  ...typeof credit.credit_id === "string" && { credit_id: credit.credit_id },
@@ -91,9 +58,9 @@ function parseCreditMenu(raw2) {
91
58
  ...typeof credit.expiry_ms === "number" && { expiry_ms: credit.expiry_ms }
92
59
  };
93
60
  }
94
- function parseCreditTempoTerms(raw2) {
95
- if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
96
- const block = raw2;
61
+ function parseCreditTempoTerms(raw) {
62
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
63
+ const block = raw;
97
64
  if (!Array.isArray(block.methods) || !block.methods.every(isCreditTempoMethod)) return void 0;
98
65
  if (block.withheld !== void 0 && (!Array.isArray(block.withheld) || !block.withheld.every(isCreditTempoWithheldMethod)))
99
66
  return void 0;
@@ -111,15 +78,15 @@ function parseCreditTempoTerms(raw2) {
111
78
  }
112
79
  };
113
80
  }
114
- function isCreditTempoMethod(raw2) {
115
- 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");
116
83
  }
117
- function isCreditTempoWithheldMethod(raw2) {
118
- return isCreditTempoMethod(raw2) && typeof raw2.reason === "string";
84
+ function isCreditTempoWithheldMethod(raw) {
85
+ return isCreditTempoMethod(raw) && typeof raw.reason === "string";
119
86
  }
120
- function parseCreditX402Terms(raw2) {
121
- if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return void 0;
122
- 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;
123
90
  if (!Array.isArray(schemes) || !schemes.every(
124
91
  (entry) => !!entry && typeof entry === "object" && typeof entry.scheme === "string" && typeof entry.network === "string"
125
92
  ))
@@ -133,9 +100,348 @@ function parseCreditX402Terms(raw2) {
133
100
  }
134
101
 
135
102
  // src/lib/credit-store.ts
136
- import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
103
+ import { existsSync, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
137
104
  import { dirname } from "path";
138
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
139
445
  function withCreditLock(fn, opts = {}) {
140
446
  if (lockDepth > 0) {
141
447
  lockDepth += 1;
@@ -258,6 +564,12 @@ function clearCreditSelection(endpoint, callerPubkey, creditId, identity) {
258
564
  return structuredClone(record);
259
565
  });
260
566
  }
567
+ function readCompletedCreditHistory(query = {}) {
568
+ return withCreditLock(() => {
569
+ loadStore(true);
570
+ return readCreditHistoryPage(query);
571
+ });
572
+ }
261
573
  function listCredits() {
262
574
  return withCreditLock(() => [...loadStore().credits].sort((a, b) => b.updatedAt - a.updatedAt));
263
575
  }
@@ -308,7 +620,13 @@ function markImplicitCreditLost(args) {
308
620
  where: (candidate) => candidate.callerPubkey === args.callerPubkey && candidate.creditId === args.creditId
309
621
  });
310
622
  if (!record) return void 0;
311
- 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
+ }
312
630
  record.status = "lost";
313
631
  record.lossReason = args.reason;
314
632
  record.updatedAt = args.now ?? Date.now();
@@ -577,7 +895,13 @@ function applyImplicitReceiptCredit(args) {
577
895
  saveStore(store);
578
896
  return store.implicitCredits.includes(record) ? { credit: record } : {};
579
897
  }
580
- 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
+ }
581
905
  let mismatch;
582
906
  const sequential = args.credit.ledger_seq === record.ledgerSeq + 1;
583
907
  if (sequential && args.outcome === "completed" && record.status === "active") {
@@ -754,6 +1078,9 @@ function forgetCredit(endpoint, callerPubkey, identity) {
754
1078
  where: (c) => c.callerPubkey === callerPubkey
755
1079
  });
756
1080
  if (!record) return;
1081
+ archiveCompletedCreditRecords(
1082
+ (record.drainEvidence ?? []).filter((bundle) => bundle.settledAt !== void 0).map((bundle) => completedRefund(record, bundle))
1083
+ );
757
1084
  saveStore({ ...store, credits: store.credits.filter((c) => c !== record) });
758
1085
  });
759
1086
  }
@@ -796,6 +1123,11 @@ function recordDrainReceipts(args) {
796
1123
  mutateCredit(mutationAt(endpoint, callerPubkey, identity), (record) => {
797
1124
  const evidence = record.drainEvidence ??= [];
798
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
+ }
799
1131
  if (!bundle) {
800
1132
  bundle = { drainId, creditId, receipts: [], collectedAt: now };
801
1133
  evidence.push(bundle);
@@ -803,9 +1135,15 @@ function recordDrainReceipts(args) {
803
1135
  bundle.creditId = creditId;
804
1136
  bundle.receipts = receipts;
805
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
+ };
806
1144
  if (read.anchor) bundle.anchor = read.anchor;
807
1145
  else if (read.reachable) delete bundle.anchor;
808
- record.drainEvidence = capDrainEvidence(evidence, endpoint);
1146
+ record.drainEvidence = evidence;
809
1147
  return void 0;
810
1148
  });
811
1149
  }
@@ -816,7 +1154,8 @@ function settleDrainEvidence(args) {
816
1154
  const bundle = evidence.find((e) => e.drainId === drainId);
817
1155
  if (!bundle || bundle.settledAt !== void 0) return "skip";
818
1156
  bundle.settledAt = now;
819
- record.drainEvidence = capDrainEvidence(evidence, endpoint);
1157
+ if (args.settlement) bundle.settlement = args.settlement;
1158
+ record.drainEvidence = evidence;
820
1159
  return void 0;
821
1160
  });
822
1161
  }
@@ -882,19 +1221,19 @@ function adoptCreditId(record, creditId) {
882
1221
  var MAX_SETTLED_DRAIN_EVIDENCE = 8;
883
1222
  var MAX_LOST_IMPLICIT_CREDITS = 8;
884
1223
  var MAX_DATE_MS = 864e13;
885
- function loadStore() {
1224
+ function loadStore(requireArchive = false) {
886
1225
  if (!existsSync(CREDITS_FILE)) {
887
1226
  return { credits: [], implicitCredits: [], pendingFundings: [], fundingMenuSnapshots: [] };
888
1227
  }
889
- let raw2;
1228
+ let raw;
890
1229
  try {
891
- raw2 = readFileSync(CREDITS_FILE, "utf-8");
1230
+ raw = readFileSync2(CREDITS_FILE, "utf-8");
892
1231
  } catch (cause) {
893
1232
  throw unreadableStore(cause);
894
1233
  }
895
1234
  let data;
896
1235
  try {
897
- const parsed = JSON.parse(raw2);
1236
+ const parsed = JSON.parse(raw);
898
1237
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
899
1238
  throw new Error("the file parsed as JSON but is not a credit-store object");
900
1239
  }
@@ -915,7 +1254,8 @@ function loadStore() {
915
1254
  Array.isArray(data.pendingFundings) ? data.pendingFundings.filter((funding) => isObjectRecord(funding)).map(normalizeRailKey) : []
916
1255
  );
917
1256
  const implicit = dedupeImplicitCredits(
918
- Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : []
1257
+ Array.isArray(data.implicitCredits) ? data.implicitCredits.filter(isStoredImplicitCredit) : [],
1258
+ requireArchive
919
1259
  );
920
1260
  const store = {
921
1261
  credits: merged.credits,
@@ -923,14 +1263,16 @@ function loadStore() {
923
1263
  pendingFundings: fundings.fundings,
924
1264
  fundingMenuSnapshots: snapshots.snapshots
925
1265
  };
1266
+ if (requireArchive) archiveStore(store);
926
1267
  if (merged.changed || snapshots.changed || fundings.changed || implicit.changed) saveStore(store);
1268
+ else if (!requireArchive) archiveOrKeep(() => archiveStore(store));
927
1269
  return store;
928
1270
  }
929
1271
  function isStoredImplicitCredit(value) {
930
1272
  if (!isObjectRecord(value)) return false;
931
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);
932
1274
  }
933
- function dedupeImplicitCredits(credits) {
1275
+ function dedupeImplicitCredits(credits, requireArchive = false) {
934
1276
  const merged = [];
935
1277
  let changed = false;
936
1278
  for (const candidate of credits) {
@@ -945,6 +1287,9 @@ function dedupeImplicitCredits(credits) {
945
1287
  merged.push(candidate);
946
1288
  continue;
947
1289
  }
1290
+ if (!archiveOrKeep(() => archiveCompletedCreditRecords(
1291
+ [existing, candidate].filter((record) => record.status === "lost").map(completedLoss)
1292
+ ), requireArchive)) return { credits, changed: false };
948
1293
  changed = true;
949
1294
  for (const origin of dvmLocationOrigins(candidate)) {
950
1295
  attachDvmLocation(existing, origin, identity);
@@ -966,7 +1311,7 @@ function dedupeImplicitCredits(credits) {
966
1311
  existing.releasedByJobId = candidate.releasedByJobId;
967
1312
  }
968
1313
  }
969
- const capped = capLostImplicitCredits(merged);
1314
+ const capped = capLostImplicitCredits(merged, requireArchive);
970
1315
  return { credits: capped.credits, changed: changed || capped.changed };
971
1316
  }
972
1317
  function ageImplicitCredits(credits, now) {
@@ -981,7 +1326,14 @@ function ageImplicitCredits(credits, now) {
981
1326
  }
982
1327
  return changed;
983
1328
  }
984
- 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 };
985
1337
  const lostByOwner = /* @__PURE__ */ new Map();
986
1338
  for (const credit of credits) {
987
1339
  if (credit.status !== "lost") continue;
@@ -1284,19 +1636,39 @@ function isEvidenceish(value) {
1284
1636
  function isDrainish(value) {
1285
1637
  return typeof value === "object" && value !== null && typeof value.drainId === "string";
1286
1638
  }
1287
- function capDrainEvidence(evidence, endpoint) {
1288
- const settled = evidence.filter((e) => e.settledAt !== void 0).sort((a, b) => (a.settledAt ?? 0) - (b.settledAt ?? 0));
1289
- if (settled.length <= MAX_SETTLED_DRAIN_EVIDENCE) return evidence;
1290
- const dropped = new Set(settled.slice(0, settled.length - MAX_SETTLED_DRAIN_EVIDENCE));
1291
- for (const e of dropped) {
1292
- info(
1293
- `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'.`
1294
- );
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;
1295
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
+ );
1296
1663
  return evidence.filter((e) => !dropped.has(e));
1297
1664
  }
1298
1665
  function saveStore(store) {
1299
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
+ }
1300
1672
  const tmp = `${CREDITS_FILE}.${String(process.pid)}.tmp`;
1301
1673
  writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
1302
1674
  try {
@@ -1428,12 +1800,6 @@ function sleepSync(ms) {
1428
1800
  }
1429
1801
 
1430
1802
  export {
1431
- setOutputTraceId,
1432
- getOutputTraceId,
1433
- json,
1434
- info,
1435
- raw,
1436
- progress,
1437
1803
  RAIL_FLAGS,
1438
1804
  maxAdvertisedMicro,
1439
1805
  withinAdvertisedTolerance,
@@ -1447,6 +1813,7 @@ export {
1447
1813
  pendingDrainById,
1448
1814
  loadCredit,
1449
1815
  clearCreditSelection,
1816
+ readCompletedCreditHistory,
1450
1817
  listCredits,
1451
1818
  listImplicitCredits,
1452
1819
  loadImplicitCredits,