@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.
- package/README.md +107 -8
- package/dist/{chunk-YDIYXGYL.js → chunk-E4EVGPDX.js} +16 -9
- package/dist/{chunk-EPNDZ5DH.js → chunk-EDDYHZ6W.js} +4 -4
- package/dist/{chunk-NFRM5QYP.js → chunk-EVBK675R.js} +5 -0
- package/dist/{chunk-3ZHMQCYP.js → chunk-QK3VJNCK.js} +329 -28
- package/dist/{chunk-LLXV32HA.js → chunk-SSSZUVWM.js} +96 -3
- package/dist/{chunk-4B56DEEV.js → chunk-U6M3ATSG.js} +7 -1
- package/dist/{chunk-2K7E3N2D.js → chunk-VRQDX5P4.js} +47 -14
- package/dist/{credit-menu-C7zAJElJ.d.ts → credit-menu-C1ezIFlJ.d.ts} +76 -4
- package/dist/{fx-BF_SG2i0.d.ts → fx-C6dl2LVI.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/internal/caller.d.ts +29 -17
- package/dist/internal/caller.js +150 -29
- package/dist/internal/server.d.ts +8 -8
- package/dist/internal/server.js +5 -5
- package/dist/{job-store-Bn23V3QU.d.ts → job-store-DHnW4Cg_.d.ts} +16 -1
- package/dist/{lightning-backend-C04nH94l.d.ts → lightning-backend-Ci1nogk_.d.ts} +1 -1
- package/dist/{postgres-job-store-TAONYLIF.js → postgres-job-store-3RAXMNSY.js} +1 -1
- package/dist/{revenue-reporter-XXSU5KVB.js → revenue-reporter-ASZ7SHHH.js} +1 -1
- package/dist/server/index.d.ts +22 -8
- package/dist/server/index.js +7 -7
- package/dist/{step-cache-3cT4Shk0.d.ts → step-cache-BLPZNizw.d.ts} +148 -3
- package/dist/testing/index.d.ts +13 -3
- package/dist/testing/index.js +31 -2
- package/dist/{usd-BnuXoFl5.d.ts → usd-BgOfZlk6.d.ts} +1 -1
- package/package.json +6 -4
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
localMessage,
|
|
36
36
|
providerMessage,
|
|
37
37
|
toJobRecord
|
|
38
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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))
|
|
9577
|
-
|
|
9578
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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)
|
|
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 =
|
|
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 =
|
|
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)
|
|
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)
|
|
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
|
|
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.
|
|
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.
|
|
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") {
|