@dvmkit/sdk 0.1.3-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.
@@ -35,7 +35,7 @@ import {
35
35
  localMessage,
36
36
  providerMessage,
37
37
  toJobRecord
38
- } from "./chunk-4B56DEEV.js";
38
+ } from "./chunk-U6M3ATSG.js";
39
39
  import {
40
40
  ADMIN_AUTH_CLOCK_SKEW_SECONDS,
41
41
  ADMIN_AUTH_NONCE_TTL_SECONDS,
@@ -59,7 +59,7 @@ import {
59
59
  toLockPubkey,
60
60
  verifyCashuToken,
61
61
  verifyChallenge
62
- } from "./chunk-2K7E3N2D.js";
62
+ } from "./chunk-VRQDX5P4.js";
63
63
  import {
64
64
  X402_BATCH_SETTLEMENT_SCHEME,
65
65
  X402_DEFAULT_FACILITATOR,
@@ -99,7 +99,7 @@ import {
99
99
  pinDimension,
100
100
  withSpan,
101
101
  withTraceContext
102
- } from "./chunk-YDIYXGYL.js";
102
+ } from "./chunk-E4EVGPDX.js";
103
103
  import {
104
104
  FxRateUnavailableError,
105
105
  InvalidCurrencyError,
@@ -6360,8 +6360,8 @@ function authErrorBody(err, opts = {}) {
6360
6360
  hint: "Regenerate the request with the current timestamp + a fresh nonce; sign with secp256k1+BIP-340 Schnorr over the canonical JSON form of the input."
6361
6361
  };
6362
6362
  }
6363
- function infoSchemaUrl(requestUrl) {
6364
- return `${new URL(requestUrl).origin}/v1/info`;
6363
+ function infoSchemaUrl(requestUrl, publicOrigin) {
6364
+ return `${publicOrigin ?? new URL(requestUrl).origin}/v1/info`;
6365
6365
  }
6366
6366
  function callerInputIssues(issues) {
6367
6367
  if (!issues) return [];
@@ -9118,6 +9118,7 @@ function isRecord2(value) {
9118
9118
  import { randomBytes as randomBytes4 } from "crypto";
9119
9119
 
9120
9120
  // src/sdk/server/context.ts
9121
+ import { AsyncLocalStorage } from "async_hooks";
9121
9122
  import { randomBytes as randomBytes3 } from "crypto";
9122
9123
 
9123
9124
  // src/sdk/server/instrumented-kv.ts
@@ -9160,6 +9161,7 @@ function instrumentKVStore(inner) {
9160
9161
  }
9161
9162
 
9162
9163
  // src/sdk/server/context.ts
9164
+ var stepCostScope = new AsyncLocalStorage();
9163
9165
  function buildContext(job, opts) {
9164
9166
  const log = createConsoleLogger(job.id);
9165
9167
  const instrumentedStore = instrumentKVStore(opts.store);
@@ -9180,6 +9182,45 @@ function buildContext(job, opts) {
9180
9182
  let creditedToHandlerMsats = 0;
9181
9183
  let creditedToHandlerMicro = 0;
9182
9184
  let poolBasis = false;
9185
+ let declaredCostAmount = 0;
9186
+ let declaredCostCurrency;
9187
+ let declaredCostRevision = 0;
9188
+ let costPoisoned = false;
9189
+ const stepCostOwner = {};
9190
+ const declareCost = (declared) => {
9191
+ if (terminated("cost")) return;
9192
+ const currency = validateCurrency(declared.currency);
9193
+ const total = declaredCostAmount + declared.amount;
9194
+ const micro = Math.round(total * 1e6);
9195
+ if (!Number.isFinite(declared.amount) || declared.amount < 0 || !Number.isSafeInteger(micro)) {
9196
+ throw new RangeError(
9197
+ `ctx.cost amounts must each be a finite, non-negative number of currency units, and must total no more than ${Math.floor(Number.MAX_SAFE_INTEGER / 1e6)}; got ${String(declared.amount)}`
9198
+ );
9199
+ }
9200
+ declaredCostRevision += 1;
9201
+ job.costRevision = declaredCostRevision;
9202
+ const replayable = { amount: declared.amount, currency };
9203
+ const scope = stepCostScope.getStore();
9204
+ if (scope?.owner === stepCostOwner) {
9205
+ for (const frame of scope.frames) frame.push(replayable);
9206
+ }
9207
+ if (declaredCostCurrency !== void 0 && declaredCostCurrency !== currency) {
9208
+ costPoisoned = true;
9209
+ log.error("ctx.cost() currency switched mid-job \u2014 this job reports no cost", {
9210
+ declared: currency,
9211
+ pinned: declaredCostCurrency
9212
+ });
9213
+ }
9214
+ declaredCostCurrency ??= currency;
9215
+ if (costPoisoned) {
9216
+ job.costAmountMicro = void 0;
9217
+ job.costCurrency = void 0;
9218
+ return;
9219
+ }
9220
+ declaredCostAmount = total;
9221
+ job.costAmountMicro = micro;
9222
+ job.costCurrency = currency;
9223
+ };
9183
9224
  return {
9184
9225
  jobId: job.id,
9185
9226
  tags: job.tags,
@@ -9572,10 +9613,21 @@ function buildContext(job, opts) {
9572
9613
  providerMessage(job, "cancel", { reason: error });
9573
9614
  runTerminal();
9574
9615
  },
9616
+ cost: declareCost,
9575
9617
  step: async (id, fn) => {
9576
- if (job.stepCache.has(id)) return job.stepCache.get(id);
9577
- const result = await fn();
9578
- job.stepCache.set(id, result);
9618
+ if (job.stepCache.has(id)) {
9619
+ const record = job.stepCache.getRecord(id);
9620
+ for (const declared of record.costs ?? []) declareCost(declared);
9621
+ return record.value;
9622
+ }
9623
+ const costs = [];
9624
+ const scope = stepCostScope.getStore();
9625
+ const parentFrames = scope?.owner === stepCostOwner ? scope.frames : [];
9626
+ const result = await stepCostScope.run(
9627
+ { owner: stepCostOwner, frames: [...parentFrames, costs] },
9628
+ fn
9629
+ );
9630
+ job.stepCache.set(id, result, costs);
9579
9631
  return result;
9580
9632
  },
9581
9633
  state: job.state,
@@ -9941,6 +9993,9 @@ async function reapExpiredKeysetRows(opts, db) {
9941
9993
  // src/sdk/server/job-manager.ts
9942
9994
  var STALE_JOB_WATCHDOG_HEADROOM_MS = 9e4;
9943
9995
  var DEFAULT_PROCESSING_WATCHDOG_MS = 5 * 6e4;
9996
+ var TERMINAL_COST_RECOVERY_BATCH = 100;
9997
+ var TERMINAL_COST_RECOVERY_MAX_PAGES = 10;
9998
+ var TERMINAL_COST_RECOVERY_INTERVAL_MS = 3e4;
9944
9999
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
9945
10000
  var DEFAULT_REACTIVATION_CLAIM_GRACE_MS = 150;
9946
10001
  var REACTIVATION_CLAIM_POLL_MS = 25;
@@ -9959,7 +10014,12 @@ var ORPHAN_SWEEP_CLAIMS = /* @__PURE__ */ new WeakSet();
9959
10014
  var ATTACH_RAILS = ["cashu", "x402", "tempo"];
9960
10015
  var CREDIT_INBOUND_ATTEMPTS = 3;
9961
10016
  var CREDIT_INBOUND_BACKOFF_MS = 50;
10017
+ var TERMINAL_COST_HANDOFF_RETRY_INITIAL_MS = 100;
10018
+ var TERMINAL_COST_HANDOFF_RETRY_MAX_MS = 3e4;
9962
10019
  var CREDIT_INBOUND_READ_ATTEMPTS = 2;
10020
+ function isTerminalCostRecoveryStore(store) {
10021
+ return "saveForDvm" in store && "listUnreportedTerminalCosts" in store && "markTerminalCostReported" in store;
10022
+ }
9963
10023
  var JobManager = class {
9964
10024
  descriptor;
9965
10025
  opts;
@@ -9967,6 +10027,8 @@ var JobManager = class {
9967
10027
  jobStore;
9968
10028
  idleTimers = /* @__PURE__ */ new Map();
9969
10029
  cleanupTimers = /* @__PURE__ */ new Map();
10030
+ /** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
10031
+ terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
9970
10032
  /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
9971
10033
  statusWatchers = /* @__PURE__ */ new Map();
9972
10034
  /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
@@ -9990,6 +10052,7 @@ var JobManager = class {
9990
10052
  staleSweepTimer;
9991
10053
  heartbeatTimer;
9992
10054
  orphanDrawSweepTimer;
10055
+ terminalCostRecoveryTimer;
9993
10056
  jobRetentionSweepTimer;
9994
10057
  jobRetentionBootTimer;
9995
10058
  /** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
@@ -10007,6 +10070,8 @@ var JobManager = class {
10007
10070
  monotonicNowMs = createMonotonicClock();
10008
10071
  sweepInFlight = false;
10009
10072
  orphanSweepInFlight = false;
10073
+ terminalCostRecoveryInFlight = false;
10074
+ terminalCostRecoveryResumeAfter;
10010
10075
  jobRetentionSweepInFlight = false;
10011
10076
  heartbeatInFlight = false;
10012
10077
  /**
@@ -10132,6 +10197,19 @@ var JobManager = class {
10132
10197
  }
10133
10198
  }
10134
10199
  }
10200
+ if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
10201
+ this.terminalCostRecoveryTimer = setInterval(() => {
10202
+ void this.recoverTerminalCosts().catch((err) => {
10203
+ console.error(
10204
+ JSON.stringify({
10205
+ level: "terminal_cost_recovery_failed",
10206
+ error: err instanceof Error ? err.message : String(err)
10207
+ })
10208
+ );
10209
+ });
10210
+ }, TERMINAL_COST_RECOVERY_INTERVAL_MS);
10211
+ this.terminalCostRecoveryTimer.unref();
10212
+ }
10135
10213
  }
10136
10214
  /** Active in-memory jobs. */
10137
10215
  get activeJobs() {
@@ -10431,7 +10509,11 @@ var JobManager = class {
10431
10509
  record.seq = Math.max(record.seq, highSeq);
10432
10510
  }
10433
10511
  }
10434
- await this.jobStore.save(record);
10512
+ if (this.opts.dvmId && isTerminalCostRecoveryStore(this.jobStore)) {
10513
+ await this.jobStore.saveForDvm(record, this.opts.dvmId);
10514
+ } else {
10515
+ await this.jobStore.save(record);
10516
+ }
10435
10517
  }
10436
10518
  /**
10437
10519
  * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
@@ -10472,6 +10554,7 @@ var JobManager = class {
10472
10554
  job.pendingPrompts.clear();
10473
10555
  job.pendingPayment = null;
10474
10556
  abortJob(job, reason);
10557
+ const terminalCostPersisted = durable.status === "completed" ? true : await this.tryPersistAdoptedTerminalCost(job, 1);
10475
10558
  if (durable.status !== "completed") {
10476
10559
  localMessage(job, "provider", "cancel", { reason });
10477
10560
  }
@@ -10486,8 +10569,78 @@ var JobManager = class {
10486
10569
  this.clearIdleTimer(job.id);
10487
10570
  void this.issueReceiptForStoredJob(job.id);
10488
10571
  this.scheduleCleanup(job);
10572
+ if (durable.status !== "completed" && terminalCostPersisted) {
10573
+ await this.reportJobCost(job);
10574
+ }
10489
10575
  return true;
10490
10576
  }
10577
+ /**
10578
+ * Persist an adopted terminal cost, retaining the in-memory revision for a
10579
+ * later retry when the durable store is transiently unavailable.
10580
+ */
10581
+ async tryPersistAdoptedTerminalCost(job, attempt) {
10582
+ try {
10583
+ await this.persistAdoptedTerminalCost(job);
10584
+ return true;
10585
+ } catch (err) {
10586
+ this.scheduleTerminalCostHandoffRetry(job, attempt, err);
10587
+ return false;
10588
+ }
10589
+ }
10590
+ /** Retry outside the status watcher, which correctly stops once the job is terminal. */
10591
+ scheduleTerminalCostHandoffRetry(job, attempt, err) {
10592
+ if (this.terminalCostHandoffRetryTimers.has(job.id)) return;
10593
+ const delayMs = Math.min(
10594
+ TERMINAL_COST_HANDOFF_RETRY_INITIAL_MS * 2 ** Math.min(attempt - 1, 8),
10595
+ TERMINAL_COST_HANDOFF_RETRY_MAX_MS
10596
+ );
10597
+ console.error(
10598
+ JSON.stringify({
10599
+ level: "terminal_cost_handoff_retry_scheduled",
10600
+ jobId: job.id,
10601
+ attempt,
10602
+ delayMs,
10603
+ error: err instanceof Error ? err.message : String(err)
10604
+ })
10605
+ );
10606
+ const timer = setTimeout(() => {
10607
+ this.terminalCostHandoffRetryTimers.delete(job.id);
10608
+ void this.retryTerminalCostHandoff(job, attempt + 1);
10609
+ }, delayMs);
10610
+ timer.unref();
10611
+ this.terminalCostHandoffRetryTimers.set(job.id, timer);
10612
+ }
10613
+ async retryTerminalCostHandoff(job, attempt) {
10614
+ if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
10615
+ await this.reportJobCost(job);
10616
+ }
10617
+ /** Durably merge a terminal handler owner's latest cost before reporting it. */
10618
+ async persistAdoptedTerminalCost(job) {
10619
+ const dvmId = this.opts.dvmId;
10620
+ const localRevision = job.costRevision;
10621
+ if (localRevision === void 0 || !dvmId || !isTerminalCostRecoveryStore(this.jobStore)) {
10622
+ return;
10623
+ }
10624
+ const durable = await this.jobStore.get(job.id);
10625
+ if (!durable || durable.status !== job.status) {
10626
+ throw new Error(`Terminal cost merge lost durable job ${job.id}`);
10627
+ }
10628
+ if ((durable.costRevision ?? -1) > localRevision) {
10629
+ job.costAmountMicro = durable.costAmountMicro;
10630
+ job.costCurrency = durable.costCurrency;
10631
+ job.costRevision = durable.costRevision;
10632
+ return;
10633
+ }
10634
+ await this.jobStore.saveForDvm(
10635
+ {
10636
+ ...durable,
10637
+ costAmountMicro: job.costAmountMicro,
10638
+ costCurrency: job.costCurrency,
10639
+ costRevision: localRevision
10640
+ },
10641
+ dvmId
10642
+ );
10643
+ }
10491
10644
  /**
10492
10645
  * Watch the durable status of a locally-active job (internal-review).
10493
10646
  *
@@ -10763,7 +10916,7 @@ var JobManager = class {
10763
10916
  );
10764
10917
  if (claimed) {
10765
10918
  swept++;
10766
- await this.issueReceiptForStoredJob(record.id);
10919
+ await this.finalizeStoredTerminal(record.id);
10767
10920
  console.warn(
10768
10921
  JSON.stringify({
10769
10922
  level: awaiting ? "stale_job_cancelled" : "stale_job_failed",
@@ -12135,6 +12288,7 @@ var JobManager = class {
12135
12288
  async finalizeTerminal(job) {
12136
12289
  await this.attachReceipt(job);
12137
12290
  await this.persistJob(job).catch(() => void 0);
12291
+ await this.reportJobCost(job);
12138
12292
  }
12139
12293
  /**
12140
12294
  * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
@@ -12151,13 +12305,117 @@ var JobManager = class {
12151
12305
  if (active && receipt) active.receipt = receipt;
12152
12306
  return receipt;
12153
12307
  }
12308
+ /**
12309
+ * Finish accounting for a terminal written directly to the store — the
12310
+ * stale reaper and a cancel handled on a different machine (internal-review).
12311
+ */
12312
+ async finalizeStoredTerminal(jobId) {
12313
+ const receipt = await this.issueReceiptForStoredJob(jobId);
12314
+ const record = await this.jobStore.get(jobId).catch(() => void 0);
12315
+ if (record && isTerminal(record.status)) await this.reportJobCost(record);
12316
+ return receipt;
12317
+ }
12154
12318
  /** Handle job reaching terminal state (completed, failed, cancelled). */
12155
12319
  async handleJobTerminal(job) {
12156
12320
  this.clearIdleTimer(job.id);
12157
12321
  await this.attachReceipt(job);
12158
- await this.bookRevenue(job);
12322
+ const booked = await this.bookRevenue(job);
12323
+ if (!booked) await this.reportJobCost(job);
12159
12324
  this.scheduleCleanup(job);
12160
12325
  }
12326
+ /**
12327
+ * Re-enqueue terminal cost-only rows left between the job commit and outbox
12328
+ * insert. The marker advances only after `onJobCost` resolves, which for the
12329
+ * SDK reporter means the local durable insert has committed; a crash after
12330
+ * that insert but before the marker can duplicate a payload, but the
12331
+ * revisioned platform contract makes that replay harmless.
12332
+ */
12333
+ async recoverTerminalCosts() {
12334
+ const store = this.jobStore;
12335
+ const dvmId = this.opts.dvmId;
12336
+ if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
12337
+ return { examined: 0, queued: 0, complete: true };
12338
+ }
12339
+ if (this.terminalCostRecoveryInFlight) {
12340
+ return { examined: 0, queued: 0, complete: false };
12341
+ }
12342
+ this.terminalCostRecoveryInFlight = true;
12343
+ let examined = 0;
12344
+ let queued = 0;
12345
+ let cursor = this.terminalCostRecoveryResumeAfter;
12346
+ try {
12347
+ for (let page = 0; page < TERMINAL_COST_RECOVERY_MAX_PAGES; page++) {
12348
+ const candidates = await store.listUnreportedTerminalCosts({
12349
+ dvmId,
12350
+ limit: TERMINAL_COST_RECOVERY_BATCH,
12351
+ ...cursor ? { afterJobId: cursor } : {}
12352
+ });
12353
+ for (const record of candidates) {
12354
+ examined++;
12355
+ cursor = record.id;
12356
+ if (await this.reportJobCost(record)) queued++;
12357
+ }
12358
+ if (candidates.length < TERMINAL_COST_RECOVERY_BATCH) {
12359
+ this.terminalCostRecoveryResumeAfter = void 0;
12360
+ if (examined > 0) {
12361
+ console.log(
12362
+ JSON.stringify({
12363
+ level: "info",
12364
+ event: "terminal_cost_recovery_complete",
12365
+ dvmId,
12366
+ examined,
12367
+ queued
12368
+ })
12369
+ );
12370
+ }
12371
+ return { examined, queued, complete: true };
12372
+ }
12373
+ }
12374
+ this.terminalCostRecoveryResumeAfter = cursor;
12375
+ console.warn(
12376
+ JSON.stringify({
12377
+ level: "terminal_cost_recovery_truncated",
12378
+ dvmId,
12379
+ examined,
12380
+ queued,
12381
+ resumeAfterJobId: cursor
12382
+ })
12383
+ );
12384
+ return { examined, queued, complete: false };
12385
+ } finally {
12386
+ this.terminalCostRecoveryInFlight = false;
12387
+ }
12388
+ }
12389
+ /** Report the latest declared-cost state only when no revenue event carried it. */
12390
+ async reportJobCost(job) {
12391
+ const onJobCost = this.opts.onJobCost;
12392
+ if (!onJobCost || job.status !== "completed" && job.status !== "failed" && job.status !== "cancelled") {
12393
+ return false;
12394
+ }
12395
+ const costRevision = job.costRevision ?? (job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? 0 : void 0);
12396
+ if (costRevision === void 0) return false;
12397
+ const payload = {
12398
+ dvmId: this.opts.dvmId ?? "",
12399
+ jobId: job.id,
12400
+ ...job.capability ? { capability: job.capability } : {},
12401
+ outcome: job.status,
12402
+ costRevision,
12403
+ cost: job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { amountMicro: job.costAmountMicro, currency: job.costCurrency } : null
12404
+ };
12405
+ try {
12406
+ await onJobCost(payload);
12407
+ if (isTerminalCostRecoveryStore(this.jobStore)) {
12408
+ const marked = await this.jobStore.markTerminalCostReported(job.id, costRevision);
12409
+ if (!marked) {
12410
+ return false;
12411
+ }
12412
+ }
12413
+ } catch (err) {
12414
+ console.error(`[${job.id}] Job cost reporting failed:`, err);
12415
+ return false;
12416
+ }
12417
+ return true;
12418
+ }
12161
12419
  /**
12162
12420
  * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
12163
12421
  * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
@@ -12296,6 +12554,9 @@ var JobManager = class {
12296
12554
  onJobCompleted({
12297
12555
  dvmId: this.opts.dvmId ?? "",
12298
12556
  jobId: job.id,
12557
+ // Legacy Postgres rows surface a missing capability as `""`. Keep
12558
+ // that absence distinct from every real capability name on the wire.
12559
+ ...job.capability ? { capability: job.capability } : {},
12299
12560
  paidMsats: bookedMsats,
12300
12561
  paymentMint: job.paymentMint,
12301
12562
  rail: job.paymentRail,
@@ -12303,6 +12564,11 @@ var JobManager = class {
12303
12564
  nativeAmount: job.nativeAmount,
12304
12565
  nativeAsset: job.nativeAsset,
12305
12566
  cashuFlow: job.cashuFlow,
12567
+ // Absent unless the handler declared one, and absent is a fact: the
12568
+ // platform shows "no costs reported" rather than a zero cost and a
12569
+ // 100% margin (internal-review). Both halves or neither — a bare amount
12570
+ // in an unnamed currency is not a cost.
12571
+ ...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
12306
12572
  ...credit
12307
12573
  })
12308
12574
  ).catch((err) => {
@@ -12329,6 +12595,10 @@ var JobManager = class {
12329
12595
  clearTimeout(timer);
12330
12596
  this.cleanupTimers.delete(id);
12331
12597
  }
12598
+ for (const [id, timer] of this.terminalCostHandoffRetryTimers) {
12599
+ clearTimeout(timer);
12600
+ this.terminalCostHandoffRetryTimers.delete(id);
12601
+ }
12332
12602
  for (const id of [...this.statusWatchers.keys()]) {
12333
12603
  this.unwatchDurableStatus(id);
12334
12604
  }
@@ -12348,6 +12618,10 @@ var JobManager = class {
12348
12618
  clearInterval(this.orphanDrawSweepTimer);
12349
12619
  this.orphanDrawSweepTimer = void 0;
12350
12620
  }
12621
+ if (this.terminalCostRecoveryTimer) {
12622
+ clearInterval(this.terminalCostRecoveryTimer);
12623
+ this.terminalCostRecoveryTimer = void 0;
12624
+ }
12351
12625
  if (this.jobRetentionBootTimer) {
12352
12626
  clearTimeout(this.jobRetentionBootTimer);
12353
12627
  this.jobRetentionBootTimer = void 0;
@@ -15293,6 +15567,23 @@ function parseSerializedDleq(raw) {
15293
15567
  return { e: dleq.e, s: dleq.s, ...dleq.r !== void 0 ? { r: dleq.r } : {} };
15294
15568
  }
15295
15569
 
15570
+ // src/sdk/server/caller-facing-url.ts
15571
+ function resolvePublicOrigin(value) {
15572
+ if (!value) return void 0;
15573
+ try {
15574
+ const url = new URL(value);
15575
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
15576
+ return url.origin;
15577
+ } catch {
15578
+ return void 0;
15579
+ }
15580
+ }
15581
+ function callerFacingRequestUrl(requestUrl, publicOrigin) {
15582
+ if (!publicOrigin) return requestUrl;
15583
+ const request = new URL(requestUrl);
15584
+ return `${publicOrigin}${request.pathname}${request.search}`;
15585
+ }
15586
+
15296
15587
  // src/sdk/server/credit-expiry-sweep.ts
15297
15588
  function startCreditExpirySweep(ledger, opts = {}) {
15298
15589
  const sweepExpiredCredits = ledger.sweepExpiredCredits?.bind(ledger);
@@ -16323,7 +16614,7 @@ async function createDVMServer(descriptor, opts) {
16323
16614
  let revenueReporter;
16324
16615
  let payoutReporter;
16325
16616
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
16326
- const { RevenueReporter } = await import("./revenue-reporter-XXSU5KVB.js");
16617
+ const { RevenueReporter } = await import("./revenue-reporter-ASZ7SHHH.js");
16327
16618
  const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
16328
16619
  await reporter.init();
16329
16620
  reporter.startRetryLoop();
@@ -16349,6 +16640,7 @@ async function createDVMServer(descriptor, opts) {
16349
16640
  hasPgPool: !!opts.db
16350
16641
  });
16351
16642
  const effectiveOnJobCompleted = opts.onJobCompleted ?? (revenueReporter ? (info) => revenueReporter.report(info) : void 0);
16643
+ const effectiveOnJobCost = opts.onJobCost ?? (revenueReporter ? (info) => revenueReporter.reportCost(info) : void 0);
16352
16644
  const effectiveOnPaidJobDeath = opts.onPaidJobDeath ?? (revenueReporter ? (info) => revenueReporter.reportPaidJobDeath(info) : void 0);
16353
16645
  const effectiveOnRevenueSkippedNoRail = opts.onRevenueSkippedNoRail ?? (revenueReporter ? (info) => revenueReporter.reportRevenueSkippedNoRail(info) : void 0);
16354
16646
  const enqueueCreditDeposit = !opts.onCreditFunded && revenueReporter ? revenueReporter.enqueueDeposit.bind(revenueReporter) : void 0;
@@ -16468,6 +16760,7 @@ async function createDVMServer(descriptor, opts) {
16468
16760
  creditLedger,
16469
16761
  processedPayments,
16470
16762
  onJobCompleted: effectiveOnJobCompleted,
16763
+ onJobCost: effectiveOnJobCost,
16471
16764
  onPaidJobDeath: effectiveOnPaidJobDeath,
16472
16765
  onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
16473
16766
  onCreditFunded: opts.onCreditFunded,
@@ -16479,6 +16772,7 @@ async function createDVMServer(descriptor, opts) {
16479
16772
  x402ExactSettlement,
16480
16773
  payoutReporter
16481
16774
  });
16775
+ await server.jobManager.recoverTerminalCosts();
16482
16776
  return {
16483
16777
  app: server.app,
16484
16778
  authAudience: server.authAudience,
@@ -16507,6 +16801,8 @@ var DVMServer = class {
16507
16801
  mintHealthTracker;
16508
16802
  descriptor;
16509
16803
  opts;
16804
+ /** Validated `DVMKIT_PUBLIC_URL` origin, or request-derived URLs when absent. */
16805
+ publicOrigin;
16510
16806
  consumedCredentialStore;
16511
16807
  adminCashuNonceStore;
16512
16808
  ownsMintHealthTracker;
@@ -16550,6 +16846,7 @@ var DVMServer = class {
16550
16846
  this.descriptor = descriptor;
16551
16847
  this.pricingCurrency = descriptor.currency ?? "usd";
16552
16848
  this.opts = opts;
16849
+ this.publicOrigin = resolvePublicOrigin(opts.env.DVMKIT_PUBLIC_URL);
16553
16850
  this.authAudience = this.resolveAuthAudience();
16554
16851
  this.x402BatchSettlement = opts.x402BatchSettlement;
16555
16852
  this.x402ExactSettlement = opts.x402ExactSettlement;
@@ -16631,6 +16928,7 @@ var DVMServer = class {
16631
16928
  pricingCurrency: this.pricingCurrency,
16632
16929
  onCreditFunded: opts.onCreditFunded,
16633
16930
  onJobCompleted: opts.onJobCompleted,
16931
+ onJobCost: opts.onJobCost,
16634
16932
  onPaidJobDeath: opts.onPaidJobDeath,
16635
16933
  onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
16636
16934
  onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
@@ -17368,7 +17666,7 @@ var DVMServer = class {
17368
17666
  x402ExactSettlement: this.x402ExactSettlement,
17369
17667
  x402ExactVersions: x402Support?.exact,
17370
17668
  x402BatchSettlement,
17371
- x402Resource: c.req.url,
17669
+ x402Resource: callerFacingRequestUrl(c.req.url, this.publicOrigin),
17372
17670
  mppCredential,
17373
17671
  mpp: this.opts.mpp,
17374
17672
  resourcePath: c.req.path,
@@ -17698,7 +17996,8 @@ var DVMServer = class {
17698
17996
  500
17699
17997
  );
17700
17998
  }
17701
- if (err instanceof SignedRequestError) return buildAuthErrorResponse(c, err);
17999
+ if (err instanceof SignedRequestError)
18000
+ return buildAuthErrorResponse(c, err, this.publicOrigin);
17702
18001
  throw err;
17703
18002
  }
17704
18003
  }
@@ -17723,7 +18022,7 @@ var DVMServer = class {
17723
18022
  { onError: () => ({ "error.code": "invalid_quote_data" }) }
17724
18023
  );
17725
18024
  } catch (err) {
17726
- const schemaUrl = infoSchemaUrl(c.req.url);
18025
+ const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
17727
18026
  const { message, hint } = err instanceof z2.ZodError ? humanizeZodError(err, { schemaUrl }) : {
17728
18027
  message: "Quote-input validation failed",
17729
18028
  hint: `Inspect ${schemaUrl} for the expected input schema.`
@@ -17837,7 +18136,7 @@ var DVMServer = class {
17837
18136
  */
17838
18137
  requireCapabilityFromBody(c, capability) {
17839
18138
  const known = this.jobManager.capabilityNames();
17840
- const infoUrl = `${new URL(c.req.url).origin}/v1/info`;
18139
+ const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
17841
18140
  if (typeof capability !== "string" || capability.length === 0) {
17842
18141
  return {
17843
18142
  error: c.json(
@@ -17878,7 +18177,7 @@ var DVMServer = class {
17878
18177
  try {
17879
18178
  return { body: await c.req.json() };
17880
18179
  } catch {
17881
- const infoUrl = `${new URL(c.req.url).origin}/v1/info`;
18180
+ const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
17882
18181
  return {
17883
18182
  error: c.json(
17884
18183
  {
@@ -17913,7 +18212,7 @@ var DVMServer = class {
17913
18212
  * relayed to a human don't change with the status.
17914
18213
  */
17915
18214
  invalidInputResponse(c, err) {
17916
- const schemaUrl = infoSchemaUrl(c.req.url);
18215
+ const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
17917
18216
  const issues = zodIssuesOf(err);
17918
18217
  if (issues) {
17919
18218
  const inputIssues = callerInputIssues(issues);
@@ -18027,7 +18326,8 @@ var DVMServer = class {
18027
18326
  500
18028
18327
  );
18029
18328
  }
18030
- if (error instanceof SignedRequestError) return buildAuthErrorResponse(c, error);
18329
+ if (error instanceof SignedRequestError)
18330
+ return buildAuthErrorResponse(c, error, this.publicOrigin);
18031
18331
  throw error;
18032
18332
  }
18033
18333
  const signedRequestId = requestIdFromEnvelope(body.data);
@@ -18081,6 +18381,7 @@ var DVMServer = class {
18081
18381
  const capabilityResult = this.requireCapabilityFromBody(c, body.capability);
18082
18382
  if ("error" in capabilityResult) return capabilityResult.error;
18083
18383
  const capabilityName = capabilityResult.name;
18384
+ pinDimension("capability", capabilityName);
18084
18385
  const cap = this.descriptor.capabilities[capabilityName];
18085
18386
  let parsedInput;
18086
18387
  let deferredInputError;
@@ -18145,7 +18446,8 @@ var DVMServer = class {
18145
18446
  500
18146
18447
  );
18147
18448
  }
18148
- if (err instanceof SignedRequestError) return buildAuthErrorResponse(c, err);
18449
+ if (err instanceof SignedRequestError)
18450
+ return buildAuthErrorResponse(c, err, this.publicOrigin);
18149
18451
  throw err;
18150
18452
  }
18151
18453
  }
@@ -18378,7 +18680,7 @@ var DVMServer = class {
18378
18680
  } catch (err) {
18379
18681
  if (err instanceof SignedRequestError && err.sub_reason === "replay_detected" && exactAdmission?.replayed) {
18380
18682
  } else if (err instanceof SignedRequestError) {
18381
- return buildAuthErrorResponse(c, err);
18683
+ return buildAuthErrorResponse(c, err, this.publicOrigin);
18382
18684
  } else {
18383
18685
  throw err;
18384
18686
  }
@@ -18936,8 +19238,7 @@ var DVMServer = class {
18936
19238
  summary,
18937
19239
  lastActivityAt: Date.now()
18938
19240
  });
18939
- const baseUrl = new URL(c.req.url);
18940
- const infoUrl = `${baseUrl.origin}/v1/info`;
19241
+ const infoUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
18941
19242
  return c.json(
18942
19243
  {
18943
19244
  error: "unknown_capability",
@@ -18960,7 +19261,7 @@ var DVMServer = class {
18960
19261
  summary: "Persisted auth envelope failed signature re-verification on reactivation",
18961
19262
  lastActivityAt: Date.now()
18962
19263
  }).catch(() => void 0);
18963
- return buildAuthErrorResponse(c, err);
19264
+ return buildAuthErrorResponse(c, err, this.publicOrigin);
18964
19265
  }
18965
19266
  if (err instanceof ReactivationAuthSchemaError) {
18966
19267
  return c.json(
@@ -19077,13 +19378,13 @@ var DVMServer = class {
19077
19378
  if (isStreamableJobStore(store)) {
19078
19379
  const cancelled = await store.cancelJob(id, reason ?? "Job cancelled");
19079
19380
  if (!cancelled) return this.jobNotActiveResponse(c);
19080
- await jm.issueReceiptForStoredJob(id);
19381
+ await jm.finalizeStoredTerminal(id);
19081
19382
  return c.json({ status: "cancelled" });
19082
19383
  }
19083
19384
  record.status = "cancelled";
19084
19385
  record.lastActivityAt = Date.now();
19085
19386
  await jm.store.save(record);
19086
- await jm.issueReceiptForStoredJob(id);
19387
+ await jm.finalizeStoredTerminal(id);
19087
19388
  return c.json({ status: "cancelled" });
19088
19389
  }
19089
19390
  if (!isJobOwner(c, job)) return c.json({ error: "Job not found" }, 404);
@@ -19350,8 +19651,8 @@ var PaymentValidationError = class extends Error {
19350
19651
  }
19351
19652
  detail;
19352
19653
  };
19353
- function buildAuthErrorResponse(c, err) {
19354
- return c.json(authErrorBody(err, { schemaUrl: infoSchemaUrl(c.req.url) }), 401);
19654
+ function buildAuthErrorResponse(c, err, publicOrigin) {
19655
+ return c.json(authErrorBody(err, { schemaUrl: infoSchemaUrl(c.req.url, publicOrigin) }), 401);
19355
19656
  }
19356
19657
  function validateMessageContent(type, content, devMode) {
19357
19658
  if (type === "response") {