@dvmkit/sdk 0.1.2-rc.7 → 0.1.3-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 +14 -0
  2. package/dist/chunk-2K7E3N2D.js +1709 -0
  3. package/dist/{chunk-N4VTG3KH.js → chunk-3ZHMQCYP.js} +645 -3027
  4. package/dist/{chunk-TKA6ZP4M.js → chunk-4B56DEEV.js} +50 -426
  5. package/dist/chunk-BIFLRKMO.js +87 -0
  6. package/dist/chunk-BQ2NMWKE.js +160 -0
  7. package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
  8. package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
  9. package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
  10. package/dist/chunk-EPNDZ5DH.js +1010 -0
  11. package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
  12. package/dist/chunk-KXZUCCEY.js +142 -0
  13. package/dist/{chunk-6JZIX5WW.js → chunk-LLXV32HA.js} +82 -5
  14. package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
  15. package/dist/{chunk-2ABMGUDS.js → chunk-NFRM5QYP.js} +83 -10
  16. package/dist/chunk-RW5LP57K.js +44 -0
  17. package/dist/chunk-YDIYXGYL.js +384 -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-C7zAJElJ.d.ts} +1632 -1946
  21. package/dist/{fx-C-liI3oY.d.ts → fx-BF_SG2i0.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 +10429 -0
  25. package/dist/internal/caller.js +9957 -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-Bn23V3QU.d.ts +576 -0
  31. package/dist/lightning-backend-C04nH94l.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-TAONYLIF.js} +1 -1
  34. package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-XXSU5KVB.js} +1 -1
  35. package/dist/server/index.d.ts +27 -11
  36. package/dist/server/index.js +93 -69
  37. package/dist/{job-store-C53VQ5uu.d.ts → step-cache-3cT4Shk0.d.ts} +31 -585
  38. package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
  39. package/dist/testing/index.d.ts +16 -4
  40. package/dist/testing/index.js +7 -3
  41. package/dist/{usd-gLcJB1ps.d.ts → usd-BnuXoFl5.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 +6 -2
  45. package/dist/{chunk-RU7SXHLO.js → chunk-UP2F5RRT.js} +3 -3
@@ -1,125 +1,10 @@
1
- // src/sdk/types.ts
2
- function isZodSchema(value) {
3
- return typeof value === "object" && value !== null && "_zod" in value && "parse" in value && "safeParse" in value;
4
- }
5
- var RAIL_REFUNDABLE = {
6
- cashu: false,
7
- x402: false,
8
- tempo: false
9
- };
10
- var DEFAULT_CREDIT_MIN = "$0.10";
11
- var DEFAULT_CREDIT_MAX = "$5.00";
12
- var DEFAULT_CREDIT_TTL_SECONDS = 30 * 24 * 60 * 60;
13
-
14
- // src/lib/cast-auth/canonical-json.ts
15
- function canonicalize(value) {
16
- return JSON.stringify(sortDeep(value));
17
- }
18
- function canonicaliseForSigning(value) {
19
- return new TextEncoder().encode(canonicalize(value));
20
- }
21
- function sortDeep(value) {
22
- if (value === null) return null;
23
- if (typeof value === "string" || typeof value === "boolean") return value;
24
- if (typeof value === "number") {
25
- if (!Number.isFinite(value)) {
26
- throw new TypeError(`canonicalize: non-finite number is not JSON-serialisable`);
27
- }
28
- return value;
29
- }
30
- if (Array.isArray(value)) return value.map(sortDeep);
31
- if (typeof value === "object") {
32
- const sorted = {};
33
- for (const key of Object.keys(value).sort()) {
34
- const v = value[key];
35
- if (v === void 0) continue;
36
- sorted[key] = sortDeep(v);
37
- }
38
- return sorted;
39
- }
40
- throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);
41
- }
42
-
43
- // src/lib/cast-auth/verify.ts
44
- import { schnorr } from "@noble/curves/secp256k1.js";
45
- function verifyCanonicalSignedRequest(payload, signatureHex, pubkeyHex) {
46
- if (!/^[0-9a-f]{64}$/i.test(pubkeyHex) || !/^[0-9a-f]{128}$/i.test(signatureHex)) {
47
- return false;
48
- }
49
- const data = canonicaliseForSigning(payload);
50
- const signature = hexToBytes(signatureHex);
51
- const pubkey = hexToBytes(pubkeyHex);
52
- try {
53
- return schnorr.verify(signature, data, pubkey);
54
- } catch {
55
- return false;
56
- }
57
- }
58
- function hexToBytes(hex) {
59
- const out = new Uint8Array(hex.length / 2);
60
- for (let i = 0; i < out.length; i++) {
61
- out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
62
- }
63
- return out;
64
- }
65
-
66
- // src/lib/signed-request-statement.ts
67
- var SIGNED_REQUEST_AUTH_ID = "secp256k1-schnorr-v2";
68
- var SIGNED_REQUEST_STATEMENT_VERSION = 2;
69
- var TEST_DOMAIN_SENTINEL = "__dvmkit_test_domain__";
70
- function normalizeSignedRequestDomain(domain) {
71
- return {
72
- audience: {
73
- dvm_id: domain.audience.dvm_id,
74
- builder_pubkey: domain.audience.builder_pubkey.trim().toLowerCase()
75
- },
76
- method: domain.method.trim().toUpperCase(),
77
- path: canonicalRequestPath(domain.path),
78
- capability: domain.capability
79
- };
80
- }
81
- function signedRequestStatementHeader(domain) {
82
- const normalized = normalizeSignedRequestDomain(domain);
83
- return {
84
- v: SIGNED_REQUEST_STATEMENT_VERSION,
85
- protocol: "dvmkit",
86
- protocol_version: 1,
87
- auth: SIGNED_REQUEST_AUTH_ID,
88
- audience: normalized.audience,
89
- method: normalized.method,
90
- path: normalized.path,
91
- capability: normalized.capability
92
- };
93
- }
94
- function canonicalSignedRequestStatement(header, input) {
95
- return canonicaliseForSigning({
96
- ...header,
97
- input
98
- });
99
- }
100
- function signedRequestHeaderMatchesDomain(header, domain) {
101
- if (!header || typeof header !== "object" || Array.isArray(header)) return false;
102
- const actual = header;
103
- const expected = signedRequestStatementHeader(domain);
104
- const audience = actual.audience;
105
- if (process.env.NODE_ENV === "test" && actual.method === TEST_DOMAIN_SENTINEL.toUpperCase() && actual.path === `/${TEST_DOMAIN_SENTINEL}` && actual.capability === TEST_DOMAIN_SENTINEL && audience?.dvm_id === TEST_DOMAIN_SENTINEL) {
106
- return true;
107
- }
108
- return actual.v === expected.v && actual.protocol === expected.protocol && actual.protocol_version === expected.protocol_version && actual.auth === expected.auth && actual.method === expected.method && actual.path === expected.path && actual.capability === expected.capability && !!audience && typeof audience === "object" && audience.dvm_id === expected.audience.dvm_id && audience.builder_pubkey === expected.audience.builder_pubkey;
109
- }
110
- function testOnlySignedRequestDomain() {
111
- if (process.env.NODE_ENV !== "test") return void 0;
112
- return {
113
- audience: { dvm_id: TEST_DOMAIN_SENTINEL, builder_pubkey: "00".repeat(32) },
114
- method: TEST_DOMAIN_SENTINEL,
115
- path: TEST_DOMAIN_SENTINEL,
116
- capability: TEST_DOMAIN_SENTINEL
117
- };
118
- }
119
- function canonicalRequestPath(pathOrUrl) {
120
- const parsed = new URL(pathOrUrl, "https://signed-request.invalid");
121
- return parsed.pathname.startsWith("/") ? parsed.pathname : `/${parsed.pathname}`;
122
- }
1
+ import {
2
+ canonicalSignedRequestStatement,
3
+ isZodSchema,
4
+ signedRequestHeaderMatchesDomain,
5
+ signedRequestStatementHeader,
6
+ testOnlySignedRequestDomain
7
+ } from "./chunk-KXZUCCEY.js";
123
8
 
124
9
  // src/sdk/server/zod-error.ts
125
10
  function humanizeZodError(err, opts) {
@@ -207,7 +92,7 @@ function pickHint(issues, schemaUrl) {
207
92
  }
208
93
 
209
94
  // src/sdk/server/signed-request.ts
210
- import { schnorr as schnorr2 } from "@noble/curves/secp256k1.js";
95
+ import { schnorr } from "@noble/curves/secp256k1.js";
211
96
  var SIGNED_ENVELOPE_TYPES = {
212
97
  pubkey: "string",
213
98
  signature: "string",
@@ -317,7 +202,7 @@ function createSignedRequestVerifier(schema, opts = {}) {
317
202
  }
318
203
  }
319
204
  function signRequest(privkey, body, signOpts, domain) {
320
- const pubkey = bytesToHex(schnorr2.getPublicKey(privkey));
205
+ const pubkey = bytesToHex(schnorr.getPublicKey(privkey));
321
206
  const timestamp = signOpts?.timestamp ?? now();
322
207
  const nonce = signOpts?.nonce ?? randomNonce();
323
208
  const unsigned = {
@@ -328,7 +213,7 @@ function createSignedRequestVerifier(schema, opts = {}) {
328
213
  };
329
214
  const authStatement = signedRequestStatementHeader(requiredDomain(domain));
330
215
  const signature = bytesToHex(
331
- schnorr2.sign(canonicalBytes({ ...unsigned, auth_statement: authStatement }, domain), privkey)
216
+ schnorr.sign(canonicalBytes({ ...unsigned, auth_statement: authStatement }, domain), privkey)
332
217
  );
333
218
  return { ...unsigned, auth_statement: authStatement, signature };
334
219
  }
@@ -370,12 +255,12 @@ function statementHeader(input) {
370
255
  }
371
256
  function verifyBytes(bytes, signatureHex, pubkeyHex) {
372
257
  try {
373
- return schnorr2.verify(hexToBytes2(signatureHex), bytes, hexToBytes2(pubkeyHex));
258
+ return schnorr.verify(hexToBytes(signatureHex), bytes, hexToBytes(pubkeyHex));
374
259
  } catch {
375
260
  return false;
376
261
  }
377
262
  }
378
- function hexToBytes2(hex) {
263
+ function hexToBytes(hex) {
379
264
  const out = new Uint8Array(hex.length / 2);
380
265
  for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
381
266
  return out;
@@ -427,20 +312,6 @@ function createDefaultReplayStore() {
427
312
  var SATURATION_WARN_INTERVAL_SECONDS = 60;
428
313
 
429
314
  export {
430
- isZodSchema,
431
- RAIL_REFUNDABLE,
432
- DEFAULT_CREDIT_MIN,
433
- DEFAULT_CREDIT_MAX,
434
- DEFAULT_CREDIT_TTL_SECONDS,
435
- canonicalize,
436
- canonicaliseForSigning,
437
- verifyCanonicalSignedRequest,
438
- SIGNED_REQUEST_AUTH_ID,
439
- SIGNED_REQUEST_STATEMENT_VERSION,
440
- signedRequestStatementHeader,
441
- canonicalSignedRequestStatement,
442
- testOnlySignedRequestDomain,
443
- canonicalRequestPath,
444
315
  humanizeZodError,
445
316
  humanizeZodIssues,
446
317
  zodIssuesOf,
@@ -0,0 +1,142 @@
1
+ // src/sdk/types.ts
2
+ function isZodSchema(value) {
3
+ return typeof value === "object" && value !== null && "_zod" in value && "parse" in value && "safeParse" in value;
4
+ }
5
+ var RAIL_REFUNDABLE = {
6
+ cashu: false,
7
+ x402: false,
8
+ tempo: false
9
+ };
10
+ var DEFAULT_CREDIT_MIN = "$0.10";
11
+ var DEFAULT_CREDIT_MAX = "$5.00";
12
+ var DEFAULT_CREDIT_TTL_SECONDS = 30 * 24 * 60 * 60;
13
+ var DEFAULT_JOB_RETENTION_DAYS = 90;
14
+
15
+ // src/lib/cast-auth/canonical-json.ts
16
+ function canonicalize(value) {
17
+ return JSON.stringify(sortDeep(value));
18
+ }
19
+ function canonicaliseForSigning(value) {
20
+ return new TextEncoder().encode(canonicalize(value));
21
+ }
22
+ function sortDeep(value) {
23
+ if (value === null) return null;
24
+ if (typeof value === "string" || typeof value === "boolean") return value;
25
+ if (typeof value === "number") {
26
+ if (!Number.isFinite(value)) {
27
+ throw new TypeError(`canonicalize: non-finite number is not JSON-serialisable`);
28
+ }
29
+ return value;
30
+ }
31
+ if (Array.isArray(value)) return value.map(sortDeep);
32
+ if (typeof value === "object") {
33
+ const sorted = {};
34
+ for (const key of Object.keys(value).sort()) {
35
+ const v = value[key];
36
+ if (v === void 0) continue;
37
+ sorted[key] = sortDeep(v);
38
+ }
39
+ return sorted;
40
+ }
41
+ throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);
42
+ }
43
+
44
+ // src/lib/cast-auth/verify.ts
45
+ import { schnorr } from "@noble/curves/secp256k1.js";
46
+ function verifyCanonicalSignedRequest(payload, signatureHex, pubkeyHex) {
47
+ if (!/^[0-9a-f]{64}$/i.test(pubkeyHex) || !/^[0-9a-f]{128}$/i.test(signatureHex)) {
48
+ return false;
49
+ }
50
+ const data = canonicaliseForSigning(payload);
51
+ const signature = hexToBytes(signatureHex);
52
+ const pubkey = hexToBytes(pubkeyHex);
53
+ try {
54
+ return schnorr.verify(signature, data, pubkey);
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+ function hexToBytes(hex) {
60
+ const out = new Uint8Array(hex.length / 2);
61
+ for (let i = 0; i < out.length; i++) {
62
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ // src/lib/signed-request-statement.ts
68
+ var SIGNED_REQUEST_AUTH_ID = "secp256k1-schnorr-v2";
69
+ var SIGNED_REQUEST_STATEMENT_VERSION = 2;
70
+ var TEST_DOMAIN_SENTINEL = "__dvmkit_test_domain__";
71
+ function normalizeSignedRequestDomain(domain) {
72
+ return {
73
+ audience: {
74
+ dvm_id: domain.audience.dvm_id,
75
+ builder_pubkey: domain.audience.builder_pubkey.trim().toLowerCase()
76
+ },
77
+ method: domain.method.trim().toUpperCase(),
78
+ path: canonicalRequestPath(domain.path),
79
+ capability: domain.capability
80
+ };
81
+ }
82
+ function signedRequestStatementHeader(domain) {
83
+ const normalized = normalizeSignedRequestDomain(domain);
84
+ return {
85
+ v: SIGNED_REQUEST_STATEMENT_VERSION,
86
+ protocol: "dvmkit",
87
+ protocol_version: 1,
88
+ auth: SIGNED_REQUEST_AUTH_ID,
89
+ audience: normalized.audience,
90
+ method: normalized.method,
91
+ path: normalized.path,
92
+ capability: normalized.capability
93
+ };
94
+ }
95
+ function canonicalSignedRequestStatement(header, input) {
96
+ return canonicaliseForSigning({
97
+ ...header,
98
+ input
99
+ });
100
+ }
101
+ function signedRequestHeaderMatchesDomain(header, domain) {
102
+ if (!header || typeof header !== "object" || Array.isArray(header)) return false;
103
+ const actual = header;
104
+ const expected = signedRequestStatementHeader(domain);
105
+ const audience = actual.audience;
106
+ if (process.env.NODE_ENV === "test" && actual.method === TEST_DOMAIN_SENTINEL.toUpperCase() && actual.path === `/${TEST_DOMAIN_SENTINEL}` && actual.capability === TEST_DOMAIN_SENTINEL && audience?.dvm_id === TEST_DOMAIN_SENTINEL) {
107
+ return true;
108
+ }
109
+ return actual.v === expected.v && actual.protocol === expected.protocol && actual.protocol_version === expected.protocol_version && actual.auth === expected.auth && actual.method === expected.method && actual.path === expected.path && actual.capability === expected.capability && !!audience && typeof audience === "object" && audience.dvm_id === expected.audience.dvm_id && audience.builder_pubkey === expected.audience.builder_pubkey;
110
+ }
111
+ function testOnlySignedRequestDomain() {
112
+ if (process.env.NODE_ENV !== "test") return void 0;
113
+ return {
114
+ audience: { dvm_id: TEST_DOMAIN_SENTINEL, builder_pubkey: "00".repeat(32) },
115
+ method: TEST_DOMAIN_SENTINEL,
116
+ path: TEST_DOMAIN_SENTINEL,
117
+ capability: TEST_DOMAIN_SENTINEL
118
+ };
119
+ }
120
+ function canonicalRequestPath(pathOrUrl) {
121
+ const parsed = new URL(pathOrUrl, "https://signed-request.invalid");
122
+ return parsed.pathname.startsWith("/") ? parsed.pathname : `/${parsed.pathname}`;
123
+ }
124
+
125
+ export {
126
+ isZodSchema,
127
+ RAIL_REFUNDABLE,
128
+ DEFAULT_CREDIT_MIN,
129
+ DEFAULT_CREDIT_MAX,
130
+ DEFAULT_CREDIT_TTL_SECONDS,
131
+ DEFAULT_JOB_RETENTION_DAYS,
132
+ canonicalize,
133
+ canonicaliseForSigning,
134
+ verifyCanonicalSignedRequest,
135
+ SIGNED_REQUEST_AUTH_ID,
136
+ SIGNED_REQUEST_STATEMENT_VERSION,
137
+ signedRequestStatementHeader,
138
+ canonicalSignedRequestStatement,
139
+ signedRequestHeaderMatchesDomain,
140
+ testOnlySignedRequestDomain,
141
+ canonicalRequestPath
142
+ };
@@ -202,6 +202,7 @@ var PostgresJobStore = class {
202
202
  -- receipt or re-allocate a sequence number.
203
203
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt JSONB;
204
204
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt_seq BIGINT;
205
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS redacted_at BIGINT;
205
206
  -- internal-review: credit-ledger linkage. Written at submission alongside the
206
207
  -- payment fields; the terminal funnel settles/releases the draw and the
207
208
  -- receipt countersigns the ReceiptCredit block from it.
@@ -241,6 +242,9 @@ var PostgresJobStore = class {
241
242
  $migration$;
242
243
  UPDATE jobs SET next_seq = seq + 1 WHERE next_seq <= seq;
243
244
  CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs (status);
245
+ CREATE INDEX IF NOT EXISTS idx_jobs_retention
246
+ ON jobs (last_activity_at, id)
247
+ WHERE redacted_at IS NULL AND status IN ('completed', 'failed', 'cancelled');
244
248
  -- internal-review: the idempotent-replay lookup. payment_tx_hash carries the
245
249
  -- caller's X-Cashu-Request-Id on the accumulator path, so a retried paid
246
250
  -- submit finds the job its payment already created.
@@ -452,8 +456,9 @@ var PostgresJobStore = class {
452
456
  -- idempotent re-persist of the same outcome); a different status is
453
457
  -- rejected wholesale, so the row can't end up as a Frankenstein of a
454
458
  -- cancelled status and a completed snapshot.
455
- WHERE jobs.status NOT IN ('completed', 'failed', 'cancelled')
456
- OR jobs.status = EXCLUDED.status`,
459
+ WHERE jobs.redacted_at IS NULL
460
+ AND (jobs.status NOT IN ('completed', 'failed', 'cancelled')
461
+ OR jobs.status = EXCLUDED.status)`,
457
462
  [
458
463
  record.id,
459
464
  JSON.stringify(record.tags),
@@ -529,6 +534,73 @@ var PostgresJobStore = class {
529
534
  await this.pool.query("DELETE FROM jobs WHERE id = $1", [id]);
530
535
  await this.pool.query("DELETE FROM job_messages WHERE job_id = $1", [id]).catch(() => void 0);
531
536
  }
537
+ async listJobsForRetention(args) {
538
+ const after = args.after;
539
+ const capabilityClause = args.capabilities === null ? "" : "AND capability = ANY($5::text[])";
540
+ const params = [
541
+ args.lastActivityBeforeMs,
542
+ args.limit,
543
+ after?.lastActivityAt ?? null,
544
+ after?.id ?? null
545
+ ];
546
+ if (args.capabilities !== null) params.push(args.capabilities);
547
+ const { rows } = await this.pool.query(
548
+ `SELECT * FROM jobs
549
+ WHERE redacted_at IS NULL
550
+ AND status IN ('completed', 'failed', 'cancelled')
551
+ AND last_activity_at < $1
552
+ AND ($3::bigint IS NULL OR (last_activity_at, id) > ($3::bigint, $4::text))
553
+ ${capabilityClause}
554
+ ORDER BY last_activity_at, id
555
+ LIMIT $2`,
556
+ params
557
+ );
558
+ return rows.map((row) => rowToRecord(row));
559
+ }
560
+ async redactJob(args) {
561
+ const client = await this.pool.connect();
562
+ try {
563
+ await client.query("BEGIN");
564
+ const { rowCount } = await client.query(
565
+ `UPDATE jobs SET
566
+ input = '',
567
+ params = '{}'::jsonb,
568
+ summary = NULL,
569
+ messages = '[]'::jsonb,
570
+ received_proofs = '[]'::jsonb,
571
+ pending_payment_msats = NULL,
572
+ pending_mpp_challenge_ids = NULL,
573
+ pending_x402_nonce = NULL,
574
+ pending_x402_amount_usdc_micro = NULL,
575
+ pending_payment_fiat_micro = NULL,
576
+ pending_payment_fiat_currency = NULL,
577
+ step_cache = '[]'::jsonb,
578
+ state = '{}'::jsonb,
579
+ requester_token = NULL,
580
+ request_fingerprint = NULL,
581
+ auth_request_path = NULL,
582
+ redacted_at = $3
583
+ WHERE id = $1
584
+ AND last_activity_at = $2
585
+ AND redacted_at IS NULL
586
+ AND status IN ('completed', 'failed', 'cancelled')`,
587
+ [args.jobId, args.expectedLastActivityAt, args.redactedAt]
588
+ );
589
+ if (rowCount === 1) {
590
+ await client.query("DELETE FROM job_messages WHERE job_id = $1", [args.jobId]);
591
+ }
592
+ await client.query("COMMIT");
593
+ return rowCount === 1;
594
+ } catch (err) {
595
+ try {
596
+ await client.query("ROLLBACK");
597
+ } catch {
598
+ }
599
+ throw err;
600
+ } finally {
601
+ client.release();
602
+ }
603
+ }
532
604
  async claimReceiptSeq(jobId) {
533
605
  const client = await this.pool.connect();
534
606
  try {
@@ -577,7 +649,9 @@ var PostgresJobStore = class {
577
649
  try {
578
650
  await client.query("BEGIN");
579
651
  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",
652
+ `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2
653
+ WHERE id = $1 AND redacted_at IS NULL
654
+ RETURNING (next_seq - 1) AS seq`,
581
655
  [jobId, Date.now()]
582
656
  );
583
657
  if (rows.length === 0) {
@@ -667,7 +741,9 @@ var PostgresJobStore = class {
667
741
  try {
668
742
  await client.query("BEGIN");
669
743
  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",
744
+ `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2
745
+ WHERE id = $1 AND redacted_at IS NULL
746
+ RETURNING (next_seq - 1) AS seq`,
671
747
  [jobId, Date.now()]
672
748
  );
673
749
  if (rows.length === 0) {
@@ -1146,7 +1222,8 @@ function rowToRecord(row) {
1146
1222
  stepCache: row.step_cache,
1147
1223
  state: row.state,
1148
1224
  createdAt: Number(row.created_at),
1149
- lastActivityAt: Number(row.last_activity_at)
1225
+ lastActivityAt: Number(row.last_activity_at),
1226
+ redactedAt: row.redacted_at == null ? void 0 : Number(row.redacted_at)
1150
1227
  };
1151
1228
  }
1152
1229
 
@@ -1,9 +1,9 @@
1
- import {
2
- applyRetryToPool
3
- } from "./chunk-C3MTFLC6.js";
4
1
  import {
5
2
  withSdkInitLock
6
3
  } from "./chunk-S3XAHZQY.js";
4
+ import {
5
+ applyRetryToPool
6
+ } from "./chunk-C3MTFLC6.js";
7
7
 
8
8
  // src/sdk/server/credit-ledger.ts
9
9
  import { randomUUID } from "crypto";
@@ -2736,6 +2736,14 @@ var CreditLedger = class {
2736
2736
  );
2737
2737
  return rows.length === 1 ? drawRecordFromRow(rows[0], rows[0]) : void 0;
2738
2738
  }
2739
+ /** Read every draw linked to a job, including already-resolved rows. */
2740
+ async listDrawsByJobId(jobId) {
2741
+ const { rows } = await this.pool.query(
2742
+ `${DRAW_WITH_BASIS_SELECT} WHERE d.job_id = $1 ORDER BY d.created_at, d.credit_id, d.ledger_seq`,
2743
+ [jobId]
2744
+ );
2745
+ return rows.map((row) => drawRecordFromRow(row, row));
2746
+ }
2739
2747
  /**
2740
2748
  * All pending holds on a credit, in `ledger_seq` order (no lock). Read
2741
2749
  * surface for the internal-review sweeper wiring — a worker that dies mid-job
@@ -12,6 +12,25 @@ var ENDPOINTS = {
12
12
  payout: "/_internal/payout",
13
13
  expiry_release: "/_internal/credit-expiry-release"
14
14
  };
15
+ var PENDING_DVM_REPORTS_TABLE = "pending_dvm_reports";
16
+ var LEGACY_PENDING_REPORTS_TABLE = "pending_revenue_reports";
17
+ var SDK_REPORT_TABLE_BASE_COLUMNS = [
18
+ "id",
19
+ "payload",
20
+ "retry_count",
21
+ "next_retry_at",
22
+ "stale",
23
+ "created_at"
24
+ ];
25
+ var PLATFORM_REPORT_TABLE_COLUMNS = [
26
+ "id",
27
+ "payload",
28
+ "source",
29
+ "retry_count",
30
+ "next_retry_at",
31
+ "last_error",
32
+ "created_at"
33
+ ];
15
34
  var RevenueReporter = class _RevenueReporter {
16
35
  // 24h of failed retries
17
36
  constructor(db, platformUrl, platformToken) {
@@ -27,14 +46,14 @@ var RevenueReporter = class _RevenueReporter {
27
46
  static MAX_BACKOFF_MS = 3e5;
28
47
  // 5 min
29
48
  static ABANDON_AFTER_MS = 24 * 60 * 60 * 1e3;
30
- /** Create the pending-reports table (idempotent). */
49
+ /** Create the pending-reports table and migrate the legacy SDK outbox (idempotent). */
31
50
  async init() {
32
51
  await withSdkInitLock(this.db, () => this.createTables());
33
52
  }
34
53
  /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
35
54
  async createTables() {
36
55
  await this.db.query(`
37
- CREATE TABLE IF NOT EXISTS pending_revenue_reports (
56
+ CREATE TABLE IF NOT EXISTS ${PENDING_DVM_REPORTS_TABLE} (
38
57
  id TEXT PRIMARY KEY,
39
58
  payload JSONB NOT NULL,
40
59
  retry_count INTEGER NOT NULL DEFAULT 0,
@@ -45,12 +64,66 @@ var RevenueReporter = class _RevenueReporter {
45
64
  kind TEXT NOT NULL DEFAULT 'revenue'
46
65
  )
47
66
  `);
67
+ await this.assertSdkTableShape(PENDING_DVM_REPORTS_TABLE);
48
68
  await this.db.query(
49
- "ALTER TABLE pending_revenue_reports ADD COLUMN IF NOT EXISTS stale_reason TEXT"
69
+ `ALTER TABLE ${PENDING_DVM_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS stale_reason TEXT`
50
70
  );
51
71
  await this.db.query(
52
- "ALTER TABLE pending_revenue_reports ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'revenue'"
72
+ `ALTER TABLE ${PENDING_DVM_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'revenue'`
53
73
  );
74
+ await this.migrateLegacySdkOutbox();
75
+ }
76
+ /** Copy rows from the old SDK-owned table without touching the platform's table of that name. */
77
+ async migrateLegacySdkOutbox() {
78
+ const columns = await this.tableColumns(LEGACY_PENDING_REPORTS_TABLE);
79
+ if (columns.size === 0) return;
80
+ if (PLATFORM_REPORT_TABLE_COLUMNS.every((column) => columns.has(column))) {
81
+ return;
82
+ }
83
+ this.assertSdkColumns(LEGACY_PENDING_REPORTS_TABLE, columns);
84
+ await this.db.query(
85
+ `ALTER TABLE ${LEGACY_PENDING_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS stale_reason TEXT`
86
+ );
87
+ await this.db.query(
88
+ `ALTER TABLE ${LEGACY_PENDING_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'revenue'`
89
+ );
90
+ await this.db.query(`
91
+ WITH copied AS (
92
+ INSERT INTO ${PENDING_DVM_REPORTS_TABLE}
93
+ (id, payload, retry_count, next_retry_at, stale, stale_reason, created_at, kind)
94
+ SELECT id, payload, retry_count, next_retry_at, stale, stale_reason, created_at, kind
95
+ FROM ${LEGACY_PENDING_REPORTS_TABLE}
96
+ ON CONFLICT (id) DO NOTHING
97
+ RETURNING id
98
+ )
99
+
100
+ DELETE FROM ${LEGACY_PENDING_REPORTS_TABLE} AS legacy
101
+ WHERE EXISTS (SELECT 1 FROM copied WHERE copied.id = legacy.id)
102
+ OR EXISTS (
103
+ SELECT 1 FROM ${PENDING_DVM_REPORTS_TABLE} AS migrated
104
+ WHERE migrated.id = legacy.id
105
+ )
106
+ `);
107
+ }
108
+ /** Refuse a pre-existing relation whose columns cannot safely serve as the SDK outbox. */
109
+ async assertSdkTableShape(tableName) {
110
+ this.assertSdkColumns(tableName, await this.tableColumns(tableName));
111
+ }
112
+ assertSdkColumns(tableName, columns) {
113
+ const matchesSdk = SDK_REPORT_TABLE_BASE_COLUMNS.every((column) => columns.has(column)) && !columns.has("source");
114
+ if (matchesSdk) return;
115
+ throw new Error(
116
+ `RevenueReporter.init(): table "${tableName}" is not the SDK revenue outbox shape. The SDK owns "${PENDING_DVM_REPORTS_TABLE}"; the platform owns "${LEGACY_PENDING_REPORTS_TABLE}". Refusing to boot before credit funding can reach a mismatched outbox. Found columns: ${[...columns].sort().join(", ") || "none"}.`
117
+ );
118
+ }
119
+ async tableColumns(tableName) {
120
+ const { rows } = await this.db.query(
121
+ `SELECT column_name
122
+ FROM information_schema.columns
123
+ WHERE table_schema = current_schema() AND table_name = $1`,
124
+ [tableName]
125
+ );
126
+ return new Set(rows.map((row) => row.column_name));
54
127
  }
55
128
  /** Persist a revenue report and attempt immediate delivery. */
56
129
  async report(payload) {
@@ -367,7 +440,7 @@ var RevenueReporter = class _RevenueReporter {
367
440
  async getQueueStats() {
368
441
  const { rows } = await this.db.query(
369
442
  `SELECT stale, stale_reason, COUNT(*)::int AS count
370
- FROM pending_revenue_reports
443
+ FROM ${PENDING_DVM_REPORTS_TABLE}
371
444
  GROUP BY stale, stale_reason`
372
445
  );
373
446
  let liveRetry = 0;
@@ -394,7 +467,7 @@ var RevenueReporter = class _RevenueReporter {
394
467
  async drainPending() {
395
468
  try {
396
469
  const { rows } = await this.db.query(
397
- `SELECT id, payload, retry_count, created_at, kind FROM pending_revenue_reports
470
+ `SELECT id, payload, retry_count, created_at, kind FROM ${PENDING_DVM_REPORTS_TABLE}
398
471
  WHERE stale = false AND next_retry_at <= $1
399
472
  ORDER BY created_at LIMIT 10`,
400
473
  [Date.now()]
@@ -428,7 +501,7 @@ var RevenueReporter = class _RevenueReporter {
428
501
  _RevenueReporter.MAX_BACKOFF_MS
429
502
  );
430
503
  await this.db.query(
431
- `UPDATE pending_revenue_reports
504
+ `UPDATE ${PENDING_DVM_REPORTS_TABLE}
432
505
  SET retry_count = $1, next_retry_at = $2
433
506
  WHERE id = $3`,
434
507
  [retryCount, Date.now() + backoff, row.id]
@@ -449,7 +522,7 @@ var RevenueReporter = class _RevenueReporter {
449
522
  async insertPending(q, kind, payload) {
450
523
  const id = randomUUID();
451
524
  await q.query(
452
- `INSERT INTO pending_revenue_reports (id, payload, created_at, kind)
525
+ `INSERT INTO ${PENDING_DVM_REPORTS_TABLE} (id, payload, created_at, kind)
453
526
  VALUES ($1, $2, $3, $4)`,
454
527
  [id, JSON.stringify(payload), Date.now(), kind]
455
528
  );
@@ -476,7 +549,7 @@ var RevenueReporter = class _RevenueReporter {
476
549
  throw err;
477
550
  }
478
551
  if (res.ok) {
479
- await this.db.query("DELETE FROM pending_revenue_reports WHERE id = $1", [id]);
552
+ await this.db.query(`DELETE FROM ${PENDING_DVM_REPORTS_TABLE} WHERE id = $1`, [id]);
480
553
  console.log(`[revenue-reporter] reported ${kind} ${label}`);
481
554
  return;
482
555
  }
@@ -500,7 +573,7 @@ var RevenueReporter = class _RevenueReporter {
500
573
  /** Mark a pending row terminal (stale) with a classification reason. */
501
574
  async markStale(id, reason) {
502
575
  await this.db.query(
503
- "UPDATE pending_revenue_reports SET stale = true, stale_reason = $1 WHERE id = $2",
576
+ `UPDATE ${PENDING_DVM_REPORTS_TABLE} SET stale = true, stale_reason = $1 WHERE id = $2`,
504
577
  [reason, id]
505
578
  );
506
579
  }