@dvmkit/sdk 0.1.2-rc.7 → 0.1.4-rc.7

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 (45) hide show
  1. package/README.md +121 -8
  2. package/dist/chunk-BIFLRKMO.js +87 -0
  3. package/dist/chunk-BQ2NMWKE.js +160 -0
  4. package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
  5. package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
  6. package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
  7. package/dist/chunk-E4EVGPDX.js +391 -0
  8. package/dist/chunk-EDDYHZ6W.js +1010 -0
  9. package/dist/{chunk-2ABMGUDS.js → chunk-EVBK675R.js} +88 -10
  10. package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
  11. package/dist/chunk-KXZUCCEY.js +142 -0
  12. package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
  13. package/dist/{chunk-N4VTG3KH.js → chunk-QK3VJNCK.js} +930 -3011
  14. package/dist/chunk-RW5LP57K.js +44 -0
  15. package/dist/{chunk-6JZIX5WW.js → chunk-SSSZUVWM.js} +178 -8
  16. package/dist/{chunk-TKA6ZP4M.js → chunk-U6M3ATSG.js} +56 -426
  17. package/dist/chunk-VRQDX5P4.js +1742 -0
  18. package/dist/{chunk-JGGI65I3.js → chunk-Z4BNLUZF.js} +1 -150
  19. package/dist/{credit-ledger-ED6JXKVD.js → credit-ledger-2DFQHNLB.js} +2 -2
  20. package/dist/{credit-menu-BM4qCD5U.d.ts → credit-menu-C1ezIFlJ.d.ts} +1699 -1941
  21. package/dist/{fx-C-liI3oY.d.ts → fx-C6dl2LVI.d.ts} +1 -1
  22. package/dist/index.d.ts +7 -6
  23. package/dist/index.js +8 -4
  24. package/dist/internal/caller.d.ts +10441 -0
  25. package/dist/internal/caller.js +10078 -0
  26. package/dist/internal/index.d.ts +2 -10816
  27. package/dist/internal/index.js +2 -10130
  28. package/dist/internal/server.d.ts +404 -0
  29. package/dist/internal/server.js +202 -0
  30. package/dist/job-store-DHnW4Cg_.d.ts +591 -0
  31. package/dist/lightning-backend-Ci1nogk_.d.ts +367 -0
  32. package/dist/{memory-credit-ledger-XJ5VQEVP.js → memory-credit-ledger-OP24Z2KO.js} +3 -3
  33. package/dist/{postgres-job-store-J5F4GUWU.js → postgres-job-store-3RAXMNSY.js} +1 -1
  34. package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-ASZ7SHHH.js} +1 -1
  35. package/dist/server/index.d.ts +41 -11
  36. package/dist/server/index.js +93 -69
  37. package/dist/{job-store-C53VQ5uu.d.ts → step-cache-BLPZNizw.d.ts} +176 -585
  38. package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
  39. package/dist/testing/index.d.ts +25 -3
  40. package/dist/testing/index.js +36 -3
  41. package/dist/{usd-gLcJB1ps.d.ts → usd-BgOfZlk6.d.ts} +1 -1
  42. package/dist/wallet-CJC8lwxx.d.ts +29 -0
  43. package/dist/{x402-FTG2GRAQ.js → x402-T2C5MX3T.js} +6 -3
  44. package/package.json +11 -5
  45. package/dist/{chunk-RU7SXHLO.js → chunk-UP2F5RRT.js} +3 -3
@@ -0,0 +1,44 @@
1
+ // src/sdk/store.ts
2
+ var MemoryKVStore = class {
3
+ data = /* @__PURE__ */ new Map();
4
+ /** Retrieve a value by key. Returns undefined if not found or expired. */
5
+ get(key) {
6
+ const entry = this.data.get(key);
7
+ if (!entry) return Promise.resolve(void 0);
8
+ if (entry.expiry !== void 0 && Date.now() > entry.expiry) {
9
+ this.data.delete(key);
10
+ return Promise.resolve(void 0);
11
+ }
12
+ return Promise.resolve(entry.value);
13
+ }
14
+ /** Store a value. Optionally set a TTL in seconds. */
15
+ set(key, value, opts) {
16
+ const expiry = opts?.ttl !== void 0 ? Date.now() + opts.ttl * 1e3 : void 0;
17
+ this.data.set(key, { value, expiry });
18
+ return Promise.resolve();
19
+ }
20
+ /** Delete a key. */
21
+ delete(key) {
22
+ this.data.delete(key);
23
+ return Promise.resolve();
24
+ }
25
+ /** List keys, optionally filtered by prefix. Excludes expired entries. */
26
+ list(prefix) {
27
+ const now = Date.now();
28
+ const keys = [];
29
+ for (const [key, entry] of this.data) {
30
+ if (entry.expiry !== void 0 && now > entry.expiry) {
31
+ this.data.delete(key);
32
+ continue;
33
+ }
34
+ if (prefix === void 0 || key.startsWith(prefix)) {
35
+ keys.push(key);
36
+ }
37
+ }
38
+ return Promise.resolve(keys);
39
+ }
40
+ };
41
+
42
+ export {
43
+ MemoryKVStore
44
+ };
@@ -125,6 +125,7 @@ var ListenChannel = class {
125
125
 
126
126
  // src/sdk/server/postgres-job-store.ts
127
127
  var JOB_MESSAGES_CHANNEL = "job_messages";
128
+ var JOB_DVM_ID = /* @__PURE__ */ Symbol("jobDvmId");
128
129
  var PostgresJobStore = class {
129
130
  pool;
130
131
  listenChannel = null;
@@ -177,6 +178,18 @@ var PostgresJobStore = class {
177
178
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS native_amount DOUBLE PRECISION;
178
179
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS native_asset TEXT;
179
180
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS cashu_flow TEXT;
181
+ -- internal-review: what the job cost the BUILDER to serve, declared by the
182
+ -- handler through ctx.cost(). NULL is "unreported", deliberately
183
+ -- distinguishable from a declared zero.
184
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS cost_amount_micro BIGINT;
185
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS cost_currency TEXT;
186
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS cost_revision BIGINT;
187
+ -- Cost-only reports are queued after the terminal row lands. Keep the
188
+ -- owning DVM and the last durably-enqueued revision on that row so a
189
+ -- restarted (or sibling) process can close that crash window without
190
+ -- replaying another mount's jobs.
191
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS dvm_id TEXT;
192
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS cost_reported_revision BIGINT;
180
193
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS required_msats BIGINT;
181
194
  -- next_seq is intentionally absent from isolate_jobs (platform/db.ts).
182
195
  -- It powers StreamableJobStore cross-machine seq allocation; isolate
@@ -202,6 +215,7 @@ var PostgresJobStore = class {
202
215
  -- receipt or re-allocate a sequence number.
203
216
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt JSONB;
204
217
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt_seq BIGINT;
218
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS redacted_at BIGINT;
205
219
  -- internal-review: credit-ledger linkage. Written at submission alongside the
206
220
  -- payment fields; the terminal funnel settles/releases the draw and the
207
221
  -- receipt countersigns the ReceiptCredit block from it.
@@ -241,6 +255,13 @@ var PostgresJobStore = class {
241
255
  $migration$;
242
256
  UPDATE jobs SET next_seq = seq + 1 WHERE next_seq <= seq;
243
257
  CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs (status);
258
+ CREATE INDEX IF NOT EXISTS idx_jobs_retention
259
+ ON jobs (last_activity_at, id)
260
+ WHERE redacted_at IS NULL AND status IN ('completed', 'failed', 'cancelled');
261
+ CREATE INDEX IF NOT EXISTS idx_jobs_pending_cost_report
262
+ ON jobs (dvm_id, id)
263
+ WHERE cost_revision IS NOT NULL
264
+ AND status IN ('completed', 'failed', 'cancelled');
244
265
  -- internal-review: the idempotent-replay lookup. payment_tx_hash carries the
245
266
  -- caller's X-Cashu-Request-Id on the accumulator path, so a retried paid
246
267
  -- submit finds the job its payment already created.
@@ -409,18 +430,32 @@ var PostgresJobStore = class {
409
430
  held.client.release();
410
431
  }
411
432
  async save(record) {
433
+ await this.saveRecord(record, record[JOB_DVM_ID]);
434
+ }
435
+ /** Persist a job while retaining its mount identity for cost-report recovery. */
436
+ async saveForDvm(record, dvmId) {
437
+ const scoped = record;
438
+ scoped[JOB_DVM_ID] = dvmId;
439
+ try {
440
+ await this.save(scoped);
441
+ } finally {
442
+ delete scoped[JOB_DVM_ID];
443
+ }
444
+ }
445
+ async saveRecord(record, dvmId) {
412
446
  const { rowCount } = await this.pool.query(
413
447
  `INSERT INTO jobs (
414
448
  id, tags, capability, input, params, requester_id, status, summary,
415
449
  messages, seq, paid_msats, payment_mint, payment_rail,
416
450
  payment_tx_hash, payment_transaction_hash, native_amount, native_asset, cashu_flow,
451
+ cost_amount_micro, cost_currency, cost_revision,
417
452
  received_proofs, pending_payment_msats, pending_mpp_challenge_ids,
418
453
  pending_x402_nonce, pending_x402_amount_usdc_micro, required_msats, step_cache, state, created_at,
419
454
  last_activity_at, next_seq, requester_token_hash, requester_token,
420
455
  request_fingerprint, requester_pubkey, request_id, credit_id, draw_id,
421
456
  pending_payment_fiat_micro, pending_payment_fiat_currency, auth_request_path,
422
- funding_receipt, funding_credit
423
- ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41)
457
+ funding_receipt, funding_credit, dvm_id
458
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,$45)
424
459
  ON CONFLICT (id) DO UPDATE SET
425
460
  status = EXCLUDED.status,
426
461
  summary = EXCLUDED.summary,
@@ -436,6 +471,29 @@ var PostgresJobStore = class {
436
471
  native_amount = EXCLUDED.native_amount,
437
472
  native_asset = EXCLUDED.native_asset,
438
473
  cashu_flow = EXCLUDED.cashu_flow,
474
+ -- Cost declarations are revisioned because a terminal snapshot can be
475
+ -- re-saved by a different machine. Never let a stale persisted prefix
476
+ -- replace the handler owner's later cumulative declaration; a higher
477
+ -- revision with NULL amount/currency is an intentional invalidation.
478
+ cost_amount_micro = CASE
479
+ WHEN EXCLUDED.cost_revision IS NOT NULL
480
+ AND (jobs.cost_revision IS NULL OR EXCLUDED.cost_revision >= jobs.cost_revision)
481
+ THEN EXCLUDED.cost_amount_micro
482
+ ELSE jobs.cost_amount_micro
483
+ END,
484
+ cost_currency = CASE
485
+ WHEN EXCLUDED.cost_revision IS NOT NULL
486
+ AND (jobs.cost_revision IS NULL OR EXCLUDED.cost_revision >= jobs.cost_revision)
487
+ THEN EXCLUDED.cost_currency
488
+ ELSE jobs.cost_currency
489
+ END,
490
+ cost_revision = CASE
491
+ WHEN EXCLUDED.cost_revision IS NOT NULL
492
+ AND (jobs.cost_revision IS NULL OR EXCLUDED.cost_revision >= jobs.cost_revision)
493
+ THEN EXCLUDED.cost_revision
494
+ ELSE jobs.cost_revision
495
+ END,
496
+ dvm_id = COALESCE(jobs.dvm_id, EXCLUDED.dvm_id),
439
497
  received_proofs = EXCLUDED.received_proofs,
440
498
  pending_mpp_challenge_ids = EXCLUDED.pending_mpp_challenge_ids,
441
499
  pending_x402_nonce = EXCLUDED.pending_x402_nonce,
@@ -452,8 +510,9 @@ var PostgresJobStore = class {
452
510
  -- idempotent re-persist of the same outcome); a different status is
453
511
  -- rejected wholesale, so the row can't end up as a Frankenstein of a
454
512
  -- cancelled status and a completed snapshot.
455
- WHERE jobs.status NOT IN ('completed', 'failed', 'cancelled')
456
- OR jobs.status = EXCLUDED.status`,
513
+ WHERE jobs.redacted_at IS NULL
514
+ AND (jobs.status NOT IN ('completed', 'failed', 'cancelled')
515
+ OR jobs.status = EXCLUDED.status)`,
457
516
  [
458
517
  record.id,
459
518
  JSON.stringify(record.tags),
@@ -473,6 +532,9 @@ var PostgresJobStore = class {
473
532
  record.nativeAmount ?? null,
474
533
  record.nativeAsset ?? null,
475
534
  record.cashuFlow ?? null,
535
+ record.costAmountMicro ?? null,
536
+ record.costCurrency ?? null,
537
+ record.costRevision ?? null,
476
538
  JSON.stringify(record.receivedProofs),
477
539
  record.pendingPaymentMsats ?? null,
478
540
  record.pendingMppChallengeIds ? JSON.stringify(record.pendingMppChallengeIds) : null,
@@ -508,7 +570,8 @@ var PostgresJobStore = class {
508
570
  // whose live gate accepted this proof.
509
571
  record.authRequestPath ?? null,
510
572
  record.fundingReceipt ? JSON.stringify(record.fundingReceipt) : null,
511
- record.fundingCredit ? JSON.stringify(record.fundingCredit) : null
573
+ record.fundingCredit ? JSON.stringify(record.fundingCredit) : null,
574
+ dvmId ?? null
512
575
  ]
513
576
  );
514
577
  if (rowCount === 0) {
@@ -525,10 +588,109 @@ var PostgresJobStore = class {
525
588
  await this.pool.query("DELETE FROM job_messages WHERE job_id = $1", [record.id]).catch(() => void 0);
526
589
  }
527
590
  }
591
+ /**
592
+ * Read terminal zero-revenue jobs whose latest declared-cost revision has
593
+ * not yet reached the durable reporter outbox.
594
+ */
595
+ async listUnreportedTerminalCosts(args) {
596
+ const { rows } = await this.pool.query(
597
+ `SELECT *
598
+ FROM jobs
599
+ WHERE dvm_id = $1
600
+ AND cost_revision IS NOT NULL
601
+ AND (cost_reported_revision IS NULL OR cost_reported_revision < cost_revision)
602
+ AND (
603
+ status IN ('failed', 'cancelled')
604
+ OR (status = 'completed' AND paid_msats = 0)
605
+ )
606
+ AND ($2::text IS NULL OR id > $2)
607
+ ORDER BY id
608
+ LIMIT $3`,
609
+ [args.dvmId, args.afterJobId ?? null, args.limit]
610
+ );
611
+ return rows.map(rowToRecord);
612
+ }
613
+ /** Mark one revision only after its report has reached durable enqueue. */
614
+ async markTerminalCostReported(jobId, costRevision) {
615
+ const { rowCount } = await this.pool.query(
616
+ `UPDATE jobs
617
+ SET cost_reported_revision = GREATEST(COALESCE(cost_reported_revision, -1), $2)
618
+ WHERE id = $1 AND cost_revision = $2`,
619
+ [jobId, costRevision]
620
+ );
621
+ return rowCount === 1;
622
+ }
528
623
  async delete(id) {
529
624
  await this.pool.query("DELETE FROM jobs WHERE id = $1", [id]);
530
625
  await this.pool.query("DELETE FROM job_messages WHERE job_id = $1", [id]).catch(() => void 0);
531
626
  }
627
+ async listJobsForRetention(args) {
628
+ const after = args.after;
629
+ const capabilityClause = args.capabilities === null ? "" : "AND capability = ANY($5::text[])";
630
+ const params = [
631
+ args.lastActivityBeforeMs,
632
+ args.limit,
633
+ after?.lastActivityAt ?? null,
634
+ after?.id ?? null
635
+ ];
636
+ if (args.capabilities !== null) params.push(args.capabilities);
637
+ const { rows } = await this.pool.query(
638
+ `SELECT * FROM jobs
639
+ WHERE redacted_at IS NULL
640
+ AND status IN ('completed', 'failed', 'cancelled')
641
+ AND last_activity_at < $1
642
+ AND ($3::bigint IS NULL OR (last_activity_at, id) > ($3::bigint, $4::text))
643
+ ${capabilityClause}
644
+ ORDER BY last_activity_at, id
645
+ LIMIT $2`,
646
+ params
647
+ );
648
+ return rows.map((row) => rowToRecord(row));
649
+ }
650
+ async redactJob(args) {
651
+ const client = await this.pool.connect();
652
+ try {
653
+ await client.query("BEGIN");
654
+ const { rowCount } = await client.query(
655
+ `UPDATE jobs SET
656
+ input = '',
657
+ params = '{}'::jsonb,
658
+ summary = NULL,
659
+ messages = '[]'::jsonb,
660
+ received_proofs = '[]'::jsonb,
661
+ pending_payment_msats = NULL,
662
+ pending_mpp_challenge_ids = NULL,
663
+ pending_x402_nonce = NULL,
664
+ pending_x402_amount_usdc_micro = NULL,
665
+ pending_payment_fiat_micro = NULL,
666
+ pending_payment_fiat_currency = NULL,
667
+ step_cache = '[]'::jsonb,
668
+ state = '{}'::jsonb,
669
+ requester_token = NULL,
670
+ request_fingerprint = NULL,
671
+ auth_request_path = NULL,
672
+ redacted_at = $3
673
+ WHERE id = $1
674
+ AND last_activity_at = $2
675
+ AND redacted_at IS NULL
676
+ AND status IN ('completed', 'failed', 'cancelled')`,
677
+ [args.jobId, args.expectedLastActivityAt, args.redactedAt]
678
+ );
679
+ if (rowCount === 1) {
680
+ await client.query("DELETE FROM job_messages WHERE job_id = $1", [args.jobId]);
681
+ }
682
+ await client.query("COMMIT");
683
+ return rowCount === 1;
684
+ } catch (err) {
685
+ try {
686
+ await client.query("ROLLBACK");
687
+ } catch {
688
+ }
689
+ throw err;
690
+ } finally {
691
+ client.release();
692
+ }
693
+ }
532
694
  async claimReceiptSeq(jobId) {
533
695
  const client = await this.pool.connect();
534
696
  try {
@@ -577,7 +739,9 @@ var PostgresJobStore = class {
577
739
  try {
578
740
  await client.query("BEGIN");
579
741
  const { rows } = await client.query(
580
- "UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2 WHERE id = $1 RETURNING (next_seq - 1) AS seq",
742
+ `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2
743
+ WHERE id = $1 AND redacted_at IS NULL
744
+ RETURNING (next_seq - 1) AS seq`,
581
745
  [jobId, Date.now()]
582
746
  );
583
747
  if (rows.length === 0) {
@@ -667,7 +831,9 @@ var PostgresJobStore = class {
667
831
  try {
668
832
  await client.query("BEGIN");
669
833
  const { rows } = await client.query(
670
- "UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2 WHERE id = $1 RETURNING (next_seq - 1) AS seq",
834
+ `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2
835
+ WHERE id = $1 AND redacted_at IS NULL
836
+ RETURNING (next_seq - 1) AS seq`,
671
837
  [jobId, Date.now()]
672
838
  );
673
839
  if (rows.length === 0) {
@@ -1127,6 +1293,9 @@ function rowToRecord(row) {
1127
1293
  nativeAmount: row.native_amount,
1128
1294
  nativeAsset: row.native_asset,
1129
1295
  cashuFlow: row.cashu_flow,
1296
+ costAmountMicro: row.cost_amount_micro == null ? void 0 : Number(row.cost_amount_micro),
1297
+ costCurrency: row.cost_currency ?? void 0,
1298
+ costRevision: row.cost_revision == null ? void 0 : Number(row.cost_revision),
1130
1299
  creditId: row.credit_id ?? void 0,
1131
1300
  drawId: row.draw_id ?? void 0,
1132
1301
  fundingReceipt: row.funding_receipt ?? void 0,
@@ -1146,7 +1315,8 @@ function rowToRecord(row) {
1146
1315
  stepCache: row.step_cache,
1147
1316
  state: row.state,
1148
1317
  createdAt: Number(row.created_at),
1149
- lastActivityAt: Number(row.last_activity_at)
1318
+ lastActivityAt: Number(row.last_activity_at),
1319
+ redactedAt: row.redacted_at == null ? void 0 : Number(row.redacted_at)
1150
1320
  };
1151
1321
  }
1152
1322