@dvmkit/sdk 0.4.0-rc.1 → 0.6.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 (43) hide show
  1. package/README.md +100 -0
  2. package/dist/{chunk-TXX4GOUB.js → chunk-7ZUKL4IV.js} +1 -1
  3. package/dist/chunk-AFV77S4C.js +258 -0
  4. package/dist/{chunk-2VGWYG4P.js → chunk-B2IROPQF.js} +446 -81
  5. package/dist/chunk-BQJIFRAZ.js +36 -0
  6. package/dist/{chunk-PB4FYZET.js → chunk-EQLA4FFY.js} +54 -4
  7. package/dist/{chunk-74PUEZJ4.js → chunk-GFY5FRX3.js} +1 -1
  8. package/dist/{chunk-O3QDFPKM.js → chunk-HCLVYZUZ.js} +2 -2
  9. package/dist/{chunk-K2YX2VRA.js → chunk-HHBDBMEO.js} +170 -43
  10. package/dist/{chunk-O5AT6S5T.js → chunk-IV6Q7NRW.js} +25 -195
  11. package/dist/{chunk-W2EQGDF5.js → chunk-LE74UEV7.js} +49 -8
  12. package/dist/{chunk-CE7XEHYC.js → chunk-N5RZ5HVB.js} +1 -1
  13. package/dist/chunk-OFVV37XC.js +57 -0
  14. package/dist/{chunk-5LR6P7WN.js → chunk-OQA7IHI4.js} +133 -213
  15. package/dist/{chunk-3MHE725Z.js → chunk-PAIM66YA.js} +90 -1
  16. package/dist/{chunk-6DOGTAHA.js → chunk-QR2EF2NI.js} +9 -43
  17. package/dist/{chunk-KXZUCCEY.js → chunk-RCM3P2HE.js} +5 -56
  18. package/dist/{chunk-R4DQZFAL.js → chunk-UOCZXO2Z.js} +3 -3
  19. package/dist/content-retention-BTGSQRfP.d.ts +78 -0
  20. package/dist/{credit-menu-B8_p_vUz.d.ts → credit-menu-BAsrRu3z.d.ts} +98 -5
  21. package/dist/{fx-DuaYHvVn.d.ts → fx-BgDmyTdd.d.ts} +1 -1
  22. package/dist/index.d.ts +5 -5
  23. package/dist/index.js +4 -3
  24. package/dist/internal/caller.d.ts +90 -22
  25. package/dist/internal/caller.js +40 -40
  26. package/dist/internal/index.js +1 -1
  27. package/dist/internal/server.d.ts +8 -99
  28. package/dist/internal/server.js +15 -11
  29. package/dist/{job-store-D6Rjj5Oi.d.ts → job-store-BVeF5_Jq.d.ts} +1 -1
  30. package/dist/{lightning-backend-BaeCPmx7.d.ts → lightning-backend-40fIzz1W.d.ts} +1 -1
  31. package/dist/{postgres-consumed-credential-store-CUQJKRSL.js → postgres-consumed-credential-store-BUVCAPLS.js} +13 -0
  32. package/dist/{postgres-job-store-VLXLWXUQ.js → postgres-job-store-VHLQBJZC.js} +3 -1
  33. package/dist/server/index.d.ts +28 -10
  34. package/dist/server/index.js +24 -10
  35. package/dist/{step-cache-Bi43A0JN.d.ts → step-cache-CPdH1hei.d.ts} +3 -3
  36. package/dist/{tempo-lifecycle-OTHV757Y.js → tempo-lifecycle-ZBBLFBMH.js} +5 -4
  37. package/dist/{tempo-wallet-32JWQZKQ.js → tempo-wallet-B5XXV3YH.js} +1 -1
  38. package/dist/testing/index.d.ts +9 -2
  39. package/dist/testing/index.js +6 -3
  40. package/dist/{usd-AZPVq4eU.d.ts → usd-AWlrwlM5.d.ts} +1 -1
  41. package/dist/{x402-CEDHGZDI.js → x402-R6HPBY5D.js} +5 -4
  42. package/package.json +3 -2
  43. package/recipes/postgres/financial-export-v1.sql +137 -0
@@ -0,0 +1,36 @@
1
+ // src/sdk/step-cache.ts
2
+ var StepCache = class _StepCache {
3
+ cache = /* @__PURE__ */ new Map();
4
+ has(id) {
5
+ return this.cache.has(id);
6
+ }
7
+ get(id) {
8
+ return this.getRecord(id).value;
9
+ }
10
+ getRecord(id) {
11
+ const record = this.cache.get(id);
12
+ if (record === void 0) throw new Error(`StepCache: no cached result for step "${id}"`);
13
+ return record;
14
+ }
15
+ set(id, value, costs) {
16
+ this.cache.set(id, {
17
+ id,
18
+ value,
19
+ ...costs !== void 0 && costs.length > 0 ? { costs: costs.map(({ amount, currency }) => ({ amount, currency })) } : {}
20
+ });
21
+ }
22
+ serialize() {
23
+ return [...this.cache.values()];
24
+ }
25
+ static deserialize(records) {
26
+ const cache = new _StepCache();
27
+ for (const { id, value, costs } of records) {
28
+ cache.set(id, value, costs);
29
+ }
30
+ return cache;
31
+ }
32
+ };
33
+
34
+ export {
35
+ StepCache
36
+ };
@@ -1,3 +1,7 @@
1
+ import {
2
+ ContentHoldError,
3
+ applyContentHoldMutation
4
+ } from "./chunk-AFV77S4C.js";
1
5
  import {
2
6
  withSdkInitLock
3
7
  } from "./chunk-COYKJRMM.js";
@@ -228,6 +232,18 @@ var PostgresJobStore = class {
228
232
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt JSONB;
229
233
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt_seq BIGINT;
230
234
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS redacted_at BIGINT;
235
+ CREATE TABLE IF NOT EXISTS job_content_holds (
236
+ id TEXT PRIMARY KEY, job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
237
+ data JSONB NOT NULL
238
+ );
239
+ CREATE INDEX IF NOT EXISTS job_content_holds_job_idx ON job_content_holds(job_id);
240
+ CREATE TABLE IF NOT EXISTS job_content_hold_events (
241
+ event_position BIGSERIAL UNIQUE,
242
+ id TEXT PRIMARY KEY, job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
243
+ data JSONB NOT NULL
244
+ );
245
+ CREATE INDEX IF NOT EXISTS job_content_hold_events_job_idx ON job_content_hold_events(job_id);
246
+
231
247
  -- credit-ledger linkage. Written at submission alongside the
232
248
  -- payment fields; the terminal funnel settles/releases the draw and the
233
249
  -- receipt countersigns the ReceiptCredit block from it.
@@ -536,7 +552,7 @@ var PostgresJobStore = class {
536
552
  required_msats = EXCLUDED.required_msats,
537
553
  step_cache = EXCLUDED.step_cache,
538
554
  state = EXCLUDED.state,
539
- last_activity_at = EXCLUDED.last_activity_at
555
+ last_activity_at = CASE WHEN jobs.status IN ('completed', 'failed', 'cancelled') THEN jobs.last_activity_at ELSE EXCLUDED.last_activity_at END
540
556
  -- terminal is terminal. On a multi-machine DVM a DELETE can
541
557
  -- land on a machine that isn't running the handler, which cancels the
542
558
  -- job in the store only; the handler's machine never learns of it and
@@ -737,6 +753,7 @@ var PostgresJobStore = class {
737
753
  const client = await this.pool.connect();
738
754
  try {
739
755
  await client.query("BEGIN");
756
+ await client.query("SELECT id FROM jobs WHERE id = $1 FOR UPDATE", [args.jobId]);
740
757
  const { rowCount } = await client.query(
741
758
  `UPDATE jobs SET
742
759
  input = '',
@@ -760,6 +777,7 @@ var PostgresJobStore = class {
760
777
  AND last_activity_at = $2
761
778
  AND redacted_at IS NULL
762
779
  AND status IN ('completed', 'failed', 'cancelled')
780
+ AND NOT EXISTS (SELECT 1 FROM job_content_holds h WHERE h.job_id=jobs.id AND h.data->'cleared_at' = 'null'::jsonb)
763
781
  AND ($4::boolean = false OR NOT EXISTS (
764
782
  SELECT 1 FROM job_messages m WHERE m.job_id=jobs.id
765
783
  AND m.data->>'type'='payment'
@@ -781,6 +799,38 @@ var PostgresJobStore = class {
781
799
  client.release();
782
800
  }
783
801
  }
802
+ async getContentSnapshot(jobId, dvmId) {
803
+ const { rows } = await this.pool.query(
804
+ `SELECT j.*, COALESCE((SELECT jsonb_agg(h.data ORDER BY h.data->>'created_at', h.id) FROM job_content_holds h WHERE h.job_id=j.id), '[]'::jsonb) AS content_holds,
805
+ COALESCE((SELECT jsonb_agg(e.data ORDER BY e.event_position) FROM job_content_hold_events e WHERE e.job_id=j.id), '[]'::jsonb) AS content_hold_events
806
+ FROM jobs j WHERE j.id=$1 AND ($2::text IS NULL OR j.dvm_id IS NULL OR j.dvm_id=$2)`,
807
+ [jobId, dvmId ?? null]
808
+ );
809
+ if (!rows[0]) return void 0;
810
+ return { job: rowToRecord(rows[0]), history: { holds: rows[0].content_holds, events: rows[0].content_hold_events } };
811
+ }
812
+ async mutateContentHold(jobId, mutation, dvmId) {
813
+ const client = await this.pool.connect();
814
+ try {
815
+ await client.query("BEGIN");
816
+ const { rows } = await client.query("SELECT * FROM jobs WHERE id=$1 AND ($2::text IS NULL OR dvm_id IS NULL OR dvm_id=$2) FOR UPDATE", [jobId, dvmId ?? null]);
817
+ if (!rows[0]) throw new ContentHoldError("job_not_found");
818
+ const holds = await client.query("SELECT data FROM job_content_holds WHERE job_id=$1 ORDER BY data->>'created_at', id", [jobId]);
819
+ const events = await client.query("SELECT data FROM job_content_hold_events WHERE job_id=$1 ORDER BY event_position", [jobId]);
820
+ const history = applyContentHoldMutation(rowToRecord(rows[0]), { holds: holds.rows.map((row) => row.data), events: events.rows.map((row) => row.data) }, mutation);
821
+ const event = history.events.at(-1);
822
+ const hold = history.holds.find((item) => item.id === event.hold_id);
823
+ await client.query(`INSERT INTO job_content_holds(id,job_id,data) VALUES($1,$2,$3) ON CONFLICT(id) DO UPDATE SET data=EXCLUDED.data`, [hold.id, jobId, JSON.stringify(hold)]);
824
+ await client.query("INSERT INTO job_content_hold_events(id,job_id,data) VALUES($1,$2,$3)", [event.id, jobId, JSON.stringify(event)]);
825
+ await client.query("COMMIT");
826
+ return history;
827
+ } catch (error) {
828
+ await client.query("ROLLBACK");
829
+ throw error;
830
+ } finally {
831
+ client.release();
832
+ }
833
+ }
784
834
  async claimReceiptSeq(jobId) {
785
835
  const client = await this.pool.connect();
786
836
  try {
@@ -828,7 +878,7 @@ var PostgresJobStore = class {
828
878
  try {
829
879
  await client.query("BEGIN");
830
880
  const { rows } = await client.query(
831
- `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2,
881
+ `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = CASE WHEN status IN ('completed','failed','cancelled') THEN last_activity_at ELSE $2 END,
832
882
  awaiting_input_at_ms = CASE WHEN $3::boolean AND status NOT IN ('completed', 'failed', 'cancelled')
833
883
  THEN COALESCE(awaiting_input_at_ms, $2) ELSE awaiting_input_at_ms END,
834
884
  status = CASE WHEN $3::boolean AND status NOT IN ('completed', 'failed', 'cancelled')
@@ -925,7 +975,7 @@ var PostgresJobStore = class {
925
975
  await client.query("BEGIN");
926
976
  if (message.type === "payment") await this.hostingCleanupFence?.assertNewIntent(client);
927
977
  const { rows } = await client.query(
928
- `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2
978
+ `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = CASE WHEN status IN ('completed','failed','cancelled') THEN last_activity_at ELSE $2 END
929
979
  WHERE id = $1 AND redacted_at IS NULL
930
980
  RETURNING (next_seq - 1) AS seq`,
931
981
  [jobId, Date.now()]
@@ -1024,7 +1074,7 @@ var PostgresJobStore = class {
1024
1074
  cashu_flow = COALESCE($9, cashu_flow),
1025
1075
  pending_x402_nonce = CASE WHEN $11::boolean THEN NULL ELSE pending_x402_nonce END,
1026
1076
  pending_x402_amount_usdc_micro = CASE WHEN $11::boolean THEN NULL ELSE pending_x402_amount_usdc_micro END,
1027
- last_activity_at = $12
1077
+ last_activity_at = CASE WHEN status IN ('completed','failed','cancelled') THEN last_activity_at ELSE $12 END
1028
1078
  WHERE id = $1
1029
1079
  RETURNING status, paid_msats, pending_payment_msats, credit_id, draw_id`,
1030
1080
  params
@@ -103,7 +103,7 @@ async function generateTempoKey(opts = {}) {
103
103
  );
104
104
  }
105
105
  if (existing && opts.force) {
106
- const { assertTempoWalletIdle } = await import("./tempo-lifecycle-OTHV757Y.js");
106
+ const { assertTempoWalletIdle } = await import("./tempo-lifecycle-ZBBLFBMH.js");
107
107
  await assertTempoWalletIdle("replace");
108
108
  }
109
109
  const { privateKeyToAccount, generatePrivateKey } = await import("viem/accounts");
@@ -4,7 +4,7 @@ import {
4
4
  DEFAULT_CREDIT_TTL_SECONDS,
5
5
  DEFAULT_JOB_RETENTION_DAYS,
6
6
  isZodSchema
7
- } from "./chunk-KXZUCCEY.js";
7
+ } from "./chunk-RCM3P2HE.js";
8
8
  import {
9
9
  InvalidCurrencyError,
10
10
  validateCurrency
@@ -47,7 +47,7 @@ function configureDVM(config) {
47
47
  }
48
48
  function resolveJobRetentionDays(raw) {
49
49
  const days = raw ?? DEFAULT_JOB_RETENTION_DAYS;
50
- if (!Number.isSafeInteger(days) || days < 0) {
50
+ if (!Number.isSafeInteger(days) || days < 0 || days > Math.floor((Number.MAX_SAFE_INTEGER - Date.now()) / 864e5)) {
51
51
  throw new Error(
52
52
  `configureDVM: jobRetentionDays must be a non-negative whole number of days; received ${JSON.stringify(raw)}`
53
53
  );
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-FYRR2UEZ.js";
15
15
  import {
16
16
  PostgresJobStore
17
- } from "./chunk-PB4FYZET.js";
17
+ } from "./chunk-EQLA4FFY.js";
18
18
  import {
19
19
  parseStoreValue
20
20
  } from "./chunk-YG7G4DPZ.js";
@@ -25,24 +25,30 @@ import {
25
25
  humanizeZodIssues,
26
26
  signedRequestInput,
27
27
  zodIssuesOf
28
- } from "./chunk-CE7XEHYC.js";
28
+ } from "./chunk-N5RZ5HVB.js";
29
29
  import {
30
- JobCancelledError,
31
30
  MemoryJobStore,
32
31
  UNCAPPABLE_ASK_TOTAL,
33
- abortJob,
34
32
  accumulateAskedTopUp,
35
33
  createInstrumentedFetch,
36
- fromJobRecord,
37
34
  isJobRetentionStore,
38
35
  isReceiptIssuingStore,
39
- isStreamableJobStore,
36
+ isStreamableJobStore
37
+ } from "./chunk-IV6Q7NRW.js";
38
+ import {
39
+ ContentHoldError,
40
+ JobCancelledError,
41
+ abortJob,
42
+ baselineContentExpiry,
43
+ contentMetadata,
44
+ fromJobRecord,
45
+ isContentHoldStore,
40
46
  isTerminal,
41
47
  isYieldMessage,
42
48
  localMessage,
43
49
  providerMessage,
44
50
  toJobRecord
45
- } from "./chunk-O5AT6S5T.js";
51
+ } from "./chunk-AFV77S4C.js";
46
52
  import {
47
53
  ADMIN_AUTH_CLOCK_SKEW_SECONDS,
48
54
  ADMIN_AUTH_NONCE_TTL_SECONDS,
@@ -52,7 +58,6 @@ import {
52
58
  attestedDvmIdentity,
53
59
  buildAdminChallenge,
54
60
  checkMintHealth,
55
- computeResultHash,
56
61
  createMonotonicClock,
57
62
  getWallet,
58
63
  isPublicRequestId,
@@ -61,13 +66,10 @@ import {
61
66
  normaliseMintUrl,
62
67
  parseTraceparent,
63
68
  receiptPubkeyFromSecret,
64
- signDrainReceipt,
65
- signFundingReceipt,
66
- signReceipt,
67
69
  toLockPubkey,
68
70
  verifyCashuToken,
69
71
  verifyChallenge
70
- } from "./chunk-5LR6P7WN.js";
72
+ } from "./chunk-OQA7IHI4.js";
71
73
  import {
72
74
  X402_BATCH_SETTLEMENT_SCHEME,
73
75
  X402_DEFAULT_FACILITATOR,
@@ -85,28 +87,36 @@ import {
85
87
  } from "./chunk-IWQHUSMO.js";
86
88
  import {
87
89
  IMPLICIT_CREDIT_TTL_MS,
90
+ computeResultHash,
88
91
  formatStaleness,
89
92
  msatsToUsd,
90
93
  msatsToUsdc,
94
+ signDrainReceipt,
95
+ signFundingReceipt,
96
+ signReceipt,
91
97
  usdcToMsats
92
- } from "./chunk-3MHE725Z.js";
98
+ } from "./chunk-PAIM66YA.js";
93
99
  import {
94
100
  resolveRpcOverride
95
101
  } from "./chunk-CZULM42S.js";
96
102
  import {
97
103
  RAIL_REFUNDABLE,
98
104
  canonicalRequestPath,
99
- canonicalize,
100
105
  isZodSchema
101
- } from "./chunk-KXZUCCEY.js";
106
+ } from "./chunk-RCM3P2HE.js";
107
+ import {
108
+ canonicalize
109
+ } from "./chunk-OFVV37XC.js";
102
110
  import {
103
- StepCache,
104
111
  createConsoleLogger,
105
112
  loggers,
106
113
  pinDimension,
107
114
  withSpan,
108
115
  withTraceContext
109
- } from "./chunk-6DOGTAHA.js";
116
+ } from "./chunk-QR2EF2NI.js";
117
+ import {
118
+ StepCache
119
+ } from "./chunk-BQJIFRAZ.js";
110
120
  import {
111
121
  FxRateUnavailableError,
112
122
  InvalidCurrencyError,
@@ -766,6 +776,15 @@ var MemoryConsumedCredentialStore = class {
766
776
  }
767
777
  return Promise.resolve(true);
768
778
  }
779
+ consume(realm, challengeId, ttlSeconds) {
780
+ const key = `${realm}:${challengeId}`;
781
+ const now = Date.now();
782
+ if ((this.data.get(key) ?? 0) > now) return Promise.resolve(false);
783
+ for (const [entry, expiry] of this.data) if (expiry <= now) this.data.delete(entry);
784
+ if (this.data.size >= this.maxEntries) return Promise.reject(new Error("Nonce store capacity exhausted"));
785
+ this.data.set(key, now + ttlSeconds * 1e3);
786
+ return Promise.resolve(true);
787
+ }
769
788
  mark(realm, challengeId, ttlSeconds) {
770
789
  const key = `${realm}:${challengeId}`;
771
790
  if (this.data.has(key)) this.data.delete(key);
@@ -3777,7 +3796,7 @@ async function verifyUpfrontPayment(opts) {
3777
3796
  x402Requirements
3778
3797
  });
3779
3798
  }
3780
- const { verifyX402Payment } = await import("./x402-CEDHGZDI.js");
3799
+ const { verifyX402Payment } = await import("./x402-R6HPBY5D.js");
3781
3800
  const receipt = await verifyX402Payment(
3782
3801
  x402Payment,
3783
3802
  x402Config,
@@ -4752,7 +4771,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4752
4771
  snapshot
4753
4772
  });
4754
4773
  }
4755
- const { verifyX402Payment } = await import("./x402-CEDHGZDI.js");
4774
+ const { verifyX402Payment } = await import("./x402-R6HPBY5D.js");
4756
4775
  const receipt = await verifyX402Payment(
4757
4776
  body.content.x402_payment,
4758
4777
  opts.x402Config,
@@ -5177,7 +5196,7 @@ async function processIncomingPayment(job, body, opts) {
5177
5196
  try {
5178
5197
  const requiredMsats = job.pendingPaymentMsats ?? 0;
5179
5198
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
5180
- const { verifyX402Payment } = await import("./x402-CEDHGZDI.js");
5199
+ const { verifyX402Payment } = await import("./x402-R6HPBY5D.js");
5181
5200
  const receipt = await verifyX402Payment(
5182
5201
  body.content.x402_payment,
5183
5202
  opts.x402Config,
@@ -8705,10 +8724,14 @@ async function verifyAdminAuth(opts) {
8705
8724
  nonceHex,
8706
8725
  unixSeconds: unixStr
8707
8726
  });
8708
- const matched = opts.acceptedPubkeys.some((pk) => verifyChallenge(pk, digest, sigHex));
8727
+ const matched = opts.acceptedPubkeys.find((pk) => verifyChallenge(pk, digest, sigHex));
8709
8728
  if (!matched) return { ok: false, reason: "bad_signature" };
8710
- await opts.nonceStore.mark(NONCE_REALM, nonceHex, NONCE_TTL_SECONDS);
8711
- return { ok: true };
8729
+ if (opts.nonceStore.consume) {
8730
+ if (!await opts.nonceStore.consume(NONCE_REALM, nonceHex, NONCE_TTL_SECONDS)) return { ok: false, reason: "replay" };
8731
+ } else {
8732
+ await opts.nonceStore.mark(NONCE_REALM, nonceHex, NONCE_TTL_SECONDS);
8733
+ }
8734
+ return { ok: true, actor: matched };
8712
8735
  }
8713
8736
  function installAdminCashuRoutes(app, opts) {
8714
8737
  app.get("/admin/cashu/pending-batches", (c) => handlePendingBatches(c, opts));
@@ -8973,6 +8996,69 @@ async function handleRotateLockPubkey(c, opts) {
8973
8996
  });
8974
8997
  }
8975
8998
 
8999
+ // src/sdk/server/admin-content.ts
9000
+ function installAdminContentRoutes(app, opts) {
9001
+ app.get("/admin/jobs/:jobId/content-holds", (c) => handle(c, opts));
9002
+ app.post("/admin/jobs/:jobId/content-holds", (c) => handle(c, opts, "placed"));
9003
+ app.post("/admin/jobs/:jobId/content-holds/:holdId/renew", (c) => handle(c, opts, "renewed"));
9004
+ app.post("/admin/jobs/:jobId/content-holds/:holdId/clear", (c) => handle(c, opts, "cleared"));
9005
+ }
9006
+ async function handle(c, opts, action) {
9007
+ const loaded = await loadAdminLockPubkeyState(c, opts);
9008
+ if ("response" in loaded) return loaded.response;
9009
+ const auth = await verifyAdminAuth({
9010
+ authHeader: c.req.header("Authorization"),
9011
+ method: c.req.method,
9012
+ path: c.req.path,
9013
+ dvmId: opts.dvmId,
9014
+ acceptedPubkeys: [loaded.state.current, ...loaded.state.retired.map((item) => item.pubkey)],
9015
+ nonceStore: opts.nonceStore,
9016
+ now: opts.now
9017
+ });
9018
+ if (!auth.ok) return c.json({ error: "unauthorized", reason: auth.reason }, 401);
9019
+ if (!isContentHoldStore(opts.store) || !opts.nonceStore.consume) {
9020
+ return c.json({ error: "content_holds_unavailable", message: "This service requires a content-hold store and atomic admin nonce store." }, 503);
9021
+ }
9022
+ const jobId = c.req.param("jobId") ?? "";
9023
+ const at = opts.now?.() ?? Date.now();
9024
+ try {
9025
+ if (action) {
9026
+ let body;
9027
+ try {
9028
+ body = await c.req.json();
9029
+ } catch {
9030
+ return c.json({ error: "invalid_body", message: "Send a JSON object." }, 400);
9031
+ }
9032
+ if (!body || typeof body !== "object" || Array.isArray(body)) return c.json({ error: "invalid_body" }, 400);
9033
+ const allowed = action === "placed" ? ["purpose", "reason", "review_due_at"] : action === "renewed" ? ["reason", "review_due_at"] : ["reason"];
9034
+ if (Object.keys(body).some((key) => !allowed.includes(key))) return c.json({ error: "invalid_body", message: "Only purpose, reason and review_due_at are accepted as applicable; identity and times are server-derived." }, 400);
9035
+ if (typeof body.reason !== "string" || body.reason.trim().length === 0 || body.reason.length > 1e3) return c.json({ error: "invalid_reason", message: "reason must contain 1..1000 characters." }, 400);
9036
+ if (action !== "cleared" && (!Number.isSafeInteger(body.review_due_at) || body.review_due_at <= at)) return c.json({ error: "invalid_review_due_at", message: "review_due_at must be a future epoch millisecond integer." }, 400);
9037
+ const base2 = { reason: body.reason.trim(), actor: auth.actor, at };
9038
+ let mutation;
9039
+ if (action === "placed") {
9040
+ if (body.purpose !== "support" && body.purpose !== "dispute") return c.json({ error: "invalid_purpose" }, 400);
9041
+ mutation = { ...base2, action, purpose: body.purpose, review_due_at: body.review_due_at };
9042
+ } else if (action === "renewed") {
9043
+ mutation = { ...base2, action, hold_id: c.req.param("holdId") ?? "", review_due_at: body.review_due_at };
9044
+ } else mutation = { ...base2, action, hold_id: c.req.param("holdId") ?? "" };
9045
+ await opts.store.mutateContentHold(jobId, mutation, opts.dvmId);
9046
+ }
9047
+ const snapshot = await opts.store.getContentSnapshot(jobId, opts.dvmId);
9048
+ if (!snapshot) return c.json({ error: "job_not_found" }, 404);
9049
+ return c.json({
9050
+ job_id: jobId,
9051
+ ...contentMetadata(snapshot.job, opts.retentionDays, snapshot.history.holds),
9052
+ baseline_content_expires_at: baselineContentExpiry(snapshot.job, opts.retentionDays),
9053
+ holds: snapshot.history.holds.map((hold) => ({ ...hold, review_overdue: hold.cleared_at === null && hold.review_due_at <= at })),
9054
+ events: snapshot.history.events
9055
+ }, action === "placed" ? 201 : 200);
9056
+ } catch (error) {
9057
+ if (error instanceof ContentHoldError) return c.json({ error: error.code }, error.code === "job_not_found" ? 404 : 409);
9058
+ throw error;
9059
+ }
9060
+ }
9061
+
8976
9062
  // src/sdk/server/x402-exact-settlement.ts
8977
9063
  import { createHash as createHash4 } from "crypto";
8978
9064
  var X402ExactSettlementServer = class {
@@ -19942,6 +20028,13 @@ var DVMServer = class {
19942
20028
  ...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
19943
20029
  });
19944
20030
  }
20031
+ installAdminContentRoutes(this.app, {
20032
+ dvmId,
20033
+ store: this.jobManager.store,
20034
+ retentionDays: this.descriptor.jobRetentionDays,
20035
+ getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
20036
+ nonceStore: this.adminCashuNonceStore
20037
+ });
19945
20038
  installAdminCreditRoutes(this.app, {
19946
20039
  db,
19947
20040
  dvmId,
@@ -20248,12 +20341,12 @@ var DVMServer = class {
20248
20341
  ownerDisplayBlock() {
20249
20342
  const owner = this.opts.owner;
20250
20343
  const env = this.opts.env;
20251
- const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE;
20252
- if (!handle) return null;
20344
+ const handle2 = owner?.handle ?? env.DVMKIT_OWNER_HANDLE;
20345
+ if (!handle2) return null;
20253
20346
  const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? "builder";
20254
20347
  const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
20255
20348
  const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? null;
20256
- return { handle, display_name: displayName, avatar_url: avatarUrl, type };
20349
+ return { handle: handle2, display_name: displayName, avatar_url: avatarUrl, type };
20257
20350
  }
20258
20351
  defaultPaymentMethods() {
20259
20352
  const methods = [];
@@ -21138,7 +21231,7 @@ var DVMServer = class {
21138
21231
  if (prior?.requestId !== requestId || prior.requesterPubkey !== authPubkey || prior.requestFingerprint !== fingerprint || prior.requesterId !== c.get("requesterId")) {
21139
21232
  return requestResultNotFound(c, requestId);
21140
21233
  }
21141
- return buildRequestResultResponse(c, requestId, prior);
21234
+ return buildRequestResultResponse(c, requestId, prior, await this.contentFields(prior));
21142
21235
  }
21143
21236
  async handleCreateJob(c, resumeOnly = false) {
21144
21237
  if (!resumeOnly) this.maybeSweepTempoSessions();
@@ -21268,7 +21361,7 @@ var DVMServer = class {
21268
21361
  const prior = priorId ? await this.jobManager.store.get(priorId) : void 0;
21269
21362
  if (prior && prior.requestFingerprint === fingerprint && prior.requesterId === c.get("requesterId") && (!this.descriptor.auth || prior.requesterPubkey === authPubkey)) {
21270
21363
  await this.jobManager.ensureReceipt(prior);
21271
- return buildSubmitResponse(c, prior, prior.requesterToken);
21364
+ return await this.submitResponse(c, prior, prior.requesterToken);
21272
21365
  }
21273
21366
  }
21274
21367
  if (!resumeOnly && this.opts.hostingControlInstance && creditEnvelope && authPubkey) {
@@ -21310,7 +21403,7 @@ var DVMServer = class {
21310
21403
  if (publicRequestId && authPubkey && recoveryStore) {
21311
21404
  const prior = await recoveryStore.findJobByRequestId(publicRequestId, authPubkey);
21312
21405
  if (prior) {
21313
- return resumeOnly ? buildSubmitResponse(c, prior, prior.requesterToken) : requestIdExists(c, publicRequestId);
21406
+ return resumeOnly ? await this.submitResponse(c, prior, prior.requesterToken) : requestIdExists(c, publicRequestId);
21314
21407
  }
21315
21408
  }
21316
21409
  let jobId = publicRequestClaimResult?.jobId ?? this.jobManager.allocateJobId();
@@ -21526,7 +21619,7 @@ var DVMServer = class {
21526
21619
  exactAdmission.intentId
21527
21620
  );
21528
21621
  await this.jobManager.ensureReceipt(prior);
21529
- return buildSubmitResponse(c, prior, prior.requesterToken);
21622
+ return await this.submitResponse(c, prior, prior.requesterToken);
21530
21623
  }
21531
21624
  }
21532
21625
  if (this.descriptor.auth && authEnvelope) {
@@ -21587,7 +21680,7 @@ var DVMServer = class {
21587
21680
  await jm.finalizeStoredTerminal(jobId);
21588
21681
  }
21589
21682
  const terminal = await store.get(jobId);
21590
- if (terminal) return buildSubmitResponse(c, terminal, jobToken);
21683
+ if (terminal) return await this.submitResponse(c, terminal, jobToken);
21591
21684
  }
21592
21685
  if (exactAdmission) {
21593
21686
  await this.x402ExactSettlement?.completeAdmission(
@@ -21602,7 +21695,7 @@ var DVMServer = class {
21602
21695
  setTimeout(resolve, 0);
21603
21696
  });
21604
21697
  await awaitTerminalWork(job);
21605
- let response = buildSubmitResponse(c, job, jobToken);
21698
+ let response = await this.submitResponse(c, job, jobToken);
21606
21699
  if (payment.x402SettleResponseHeader) {
21607
21700
  response = new Response(response.body, response);
21608
21701
  response.headers.set("PAYMENT-RESPONSE", payment.x402SettleResponseHeader);
@@ -21690,7 +21783,7 @@ var DVMServer = class {
21690
21783
  return void 0;
21691
21784
  }
21692
21785
  await this.jobManager.ensureReceipt(prior);
21693
- return buildSubmitResponse(c, prior, prior.requesterToken);
21786
+ return await this.submitResponse(c, prior, prior.requesterToken);
21694
21787
  }
21695
21788
  /**
21696
21789
  * Resolve the recorded draw's job under the same fingerprint/pubkey/requester
@@ -21735,7 +21828,7 @@ var DVMServer = class {
21735
21828
  return void 0;
21736
21829
  }
21737
21830
  await this.jobManager.ensureReceipt(prior);
21738
- return buildSubmitResponse(c, prior, prior.requesterToken);
21831
+ return await this.submitResponse(c, prior, prior.requesterToken);
21739
21832
  }
21740
21833
  /**
21741
21834
  * Resume the gap between a durable x402/Tempo marker+draw commit and the
@@ -21884,6 +21977,23 @@ var DVMServer = class {
21884
21977
  };
21885
21978
  }
21886
21979
  }
21980
+ async submitResponse(c, job, jobToken) {
21981
+ const snapshot = { ...job, messages: [...job.messages] };
21982
+ return buildSubmitResponse(c, snapshot, jobToken, await this.contentFields(snapshot));
21983
+ }
21984
+ async contentFields(job) {
21985
+ const store = this.jobManager.store;
21986
+ const responseStatus = job.status;
21987
+ if (isContentHoldStore(store)) {
21988
+ const snapshot = await store.getContentSnapshot(job.id);
21989
+ if (snapshot) return { ...contentMetadata({ ...snapshot.job, status: responseStatus }, this.descriptor.jobRetentionDays, snapshot.history.holds) };
21990
+ }
21991
+ return isContentHoldStore(store) ? { ...contentMetadata(job, this.descriptor.jobRetentionDays) } : {};
21992
+ }
21993
+ async messageFields(job, messages) {
21994
+ const fields = await this.contentFields(job);
21995
+ return { messages: fields.content_available === false ? [] : messages, ...fields };
21996
+ }
21887
21997
  async handleGetJob(c) {
21888
21998
  if (!jobCredential.presents(c.req)) return c.json(jobCredential.body(), 401);
21889
21999
  const id = c.req.param("id") ?? "";
@@ -21892,7 +22002,8 @@ var DVMServer = class {
21892
22002
  if (!isJobOwner(c, job)) return c.json({ error: "Job not found" }, 404);
21893
22003
  await this.jobManager.ensureReceipt(job);
21894
22004
  const res2 = { job_id: job.id, status: job.status };
21895
- Object.assign(res2, terminalResponseFields(job));
22005
+ Object.assign(res2, terminalResponseFields(job), await this.contentFields(job));
22006
+ removeUnavailableContent(res2);
21896
22007
  return c.json(res2);
21897
22008
  }
21898
22009
  const record = await this.jobManager.store.get(id);
@@ -21900,7 +22011,8 @@ var DVMServer = class {
21900
22011
  if (!isJobOwner(c, record)) return c.json({ error: "Job not found" }, 404);
21901
22012
  await this.jobManager.ensureReceipt(record);
21902
22013
  const res = { job_id: record.id, status: record.status };
21903
- Object.assign(res, terminalResponseFields(record));
22014
+ Object.assign(res, terminalResponseFields(record), await this.contentFields(record));
22015
+ removeUnavailableContent(res);
21904
22016
  return c.json(res);
21905
22017
  }
21906
22018
  async handleGetMessages(c) {
@@ -21971,7 +22083,9 @@ var DVMServer = class {
21971
22083
  });
21972
22084
  });
21973
22085
  }
22086
+ const fields = await this.contentFields(record);
21974
22087
  return streamSSE(c, async (stream) => {
22088
+ if (fields.content_available === false) return;
21975
22089
  for (const msg of filtered) {
21976
22090
  await stream.writeSSE({ data: JSON.stringify(msg), event: "message" });
21977
22091
  }
@@ -21979,13 +22093,15 @@ var DVMServer = class {
21979
22093
  }
21980
22094
  if (isStreamableJobStore(store) && !isTerminal(record.status)) {
21981
22095
  const msgs = await store.getMessages(id, after);
21982
- if (msgs.length > 0) return c.json({ messages: msgs });
22096
+ if (msgs.length > 0) return c.json(await this.messageFields(record, msgs));
21983
22097
  }
21984
- return c.json({ messages: filtered });
22098
+ return c.json(await this.messageFields(record, filtered));
21985
22099
  }
21986
22100
  if (!isJobOwner(c, job)) return c.json({ error: "Job not found" }, 404);
21987
22101
  if (accept.includes("text/event-stream")) {
22102
+ const fields = await this.contentFields(job);
21988
22103
  return streamSSE(c, async (stream) => {
22104
+ if (fields.content_available === false) return;
21989
22105
  const existing = job.messages.filter((m) => m.seq > after);
21990
22106
  let yielded = false;
21991
22107
  for (const msg of existing) {
@@ -22014,7 +22130,7 @@ var DVMServer = class {
22014
22130
  });
22015
22131
  });
22016
22132
  }
22017
- return c.json({ messages: job.messages.filter((m) => m.seq > after) });
22133
+ return c.json(await this.messageFields(job, job.messages.filter((m) => m.seq > after)));
22018
22134
  }
22019
22135
  jobNotActiveResponse(c) {
22020
22136
  return c.json(
@@ -22258,12 +22374,13 @@ function timingSafeEqualHex(a, b) {
22258
22374
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
22259
22375
  return diff === 0;
22260
22376
  }
22261
- function buildSubmitResponse(c, job, jobToken) {
22377
+ function buildSubmitResponse(c, job, jobToken, contentFields) {
22262
22378
  const res = { job_id: job.id };
22263
22379
  if (jobToken) res.job_token = jobToken;
22264
22380
  if (job.status === "awaiting-input") res.status = "awaiting-input";
22265
22381
  else if (job.status === "completed") res.status = "completed";
22266
- Object.assign(res, terminalResponseFields(job));
22382
+ Object.assign(res, terminalResponseFields(job), contentFields);
22383
+ removeUnavailableContent(res);
22267
22384
  return c.json(res, job.status === "completed" ? 200 : 202);
22268
22385
  }
22269
22386
  function requestRecoveryStore(store) {
@@ -22319,7 +22436,7 @@ function requestNotAccepted(c, requestId) {
22319
22436
  409
22320
22437
  );
22321
22438
  }
22322
- function buildRequestResultResponse(c, requestId, job) {
22439
+ function buildRequestResultResponse(c, requestId, job, contentFields) {
22323
22440
  const status = job.status === "working" ? "processing" : job.status;
22324
22441
  const res = {
22325
22442
  request_id: requestId,
@@ -22328,7 +22445,8 @@ function buildRequestResultResponse(c, requestId, job) {
22328
22445
  };
22329
22446
  if (job.requesterToken) res.job_token = job.requesterToken;
22330
22447
  if (job.paymentTransactionHash) res.transaction_hash = job.paymentTransactionHash;
22331
- Object.assign(res, terminalResponseFields(job));
22448
+ Object.assign(res, terminalResponseFields(job), contentFields);
22449
+ removeUnavailableContent(res);
22332
22450
  return c.json(res, isTerminal(job.status) ? 200 : 202);
22333
22451
  }
22334
22452
  function requestResultNotFound(c, requestId) {
@@ -22356,6 +22474,14 @@ async function awaitTerminalWork(job) {
22356
22474
  ]);
22357
22475
  clearTimeout(timer);
22358
22476
  }
22477
+ function removeUnavailableContent(fields) {
22478
+ if (fields.content_available === false) {
22479
+ delete fields.summary;
22480
+ fields.has_artifacts = false;
22481
+ delete fields.artifact_count;
22482
+ delete fields.payload_in;
22483
+ }
22484
+ }
22359
22485
  function terminalResponseFields(job) {
22360
22486
  const fields = {};
22361
22487
  if (job.status === "completed") {
@@ -22748,6 +22874,7 @@ export {
22748
22874
  handleCreditRequest,
22749
22875
  installAdminCashuRoutes,
22750
22876
  createAdminCashuNonceStore,
22877
+ installAdminContentRoutes,
22751
22878
  X402ExactSettlementServer,
22752
22879
  X402ExactSettlementEvidenceMissingError,
22753
22880
  HostedSettlementReportObligations,