@dvmkit/sdk 0.1.5-rc.8 → 0.2.0-rc.9
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 +12 -0
- package/dist/{chunk-BIP6G74V.js → chunk-2ATUAUAO.js} +8 -8
- package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
- package/dist/{chunk-EVBK675R.js → chunk-6GRIKOFB.js} +28 -23
- package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
- package/dist/{chunk-L4OYF4DQ.js → chunk-FT6HTUM4.js} +1 -1
- package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
- package/dist/{chunk-U6M3ATSG.js → chunk-JDT5LCJC.js} +40 -6
- package/dist/{chunk-FROTD5XQ.js → chunk-JLXYOV4Y.js} +1 -2
- package/dist/{chunk-M7LHFJ5K.js → chunk-KMZXTBLA.js} +2 -2
- package/dist/{chunk-6BQM7TOW.js → chunk-L67WTZX2.js} +3 -7
- package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
- package/dist/{chunk-JZWELPFH.js → chunk-MRAGS5VP.js} +1 -1
- package/dist/{chunk-2UUXIIOC.js → chunk-O2X2CCKH.js} +3 -3
- package/dist/{chunk-TQWGQCNV.js → chunk-OMIQMMME.js} +3 -3
- package/dist/{chunk-UB5FZ43T.js → chunk-PCUQZDZA.js} +475 -366
- package/dist/{chunk-KVEHHC7W.js → chunk-PHHAYRQV.js} +7 -9
- package/dist/{chunk-SSSZUVWM.js → chunk-QP53RWAD.js} +88 -38
- package/dist/{chunk-DMNLFNTW.js → chunk-QT4ONTST.js} +1 -1
- package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
- package/dist/{chunk-MLRCSJYX.js → chunk-V7EVFLAK.js} +87 -90
- package/dist/{chunk-E4EVGPDX.js → chunk-XQXJKJ3P.js} +0 -2
- package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-5ZEJRI46.js} +1 -1
- package/dist/{credit-menu-s5HmGCqx.d.ts → credit-menu-D4Gcdgc4.d.ts} +488 -644
- package/dist/{fx-D860pZvP.d.ts → fx-B0SLBe5x.d.ts} +38 -82
- package/dist/index.d.ts +11 -14
- package/dist/index.js +2 -2
- package/dist/internal/caller.d.ts +618 -1525
- package/dist/internal/caller.js +28 -60
- package/dist/internal/server.d.ts +36 -61
- package/dist/internal/server.js +11 -11
- package/dist/{job-store-BUGqvCfL.d.ts → job-store-B2uZvga4.d.ts} +70 -59
- package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-CQBnQgsT.d.ts} +19 -27
- package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-ZOH6C3N4.js} +2 -2
- package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-IOJBF7DB.js} +1 -1
- package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-5PIRYFVQ.js} +1 -1
- package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-ISRHBMOU.js} +1 -1
- package/dist/{postgres-job-store-3RAXMNSY.js → postgres-job-store-OGQ6IT4U.js} +1 -1
- package/dist/{postgres-kv-store-JFBDP5IP.js → postgres-kv-store-D5E2EZ24.js} +1 -1
- package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-IZFLTTAC.js} +1 -1
- package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
- package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-FIDI3RNH.js} +1 -1
- package/dist/{revenue-reporter-ASZ7SHHH.js → revenue-reporter-NNCNRY4C.js} +1 -1
- package/dist/server/index.d.ts +53 -59
- package/dist/server/index.js +38 -37
- package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
- package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-CXg7ziML.d.ts} +389 -551
- package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-76TDAF34.js} +1 -1
- package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-DXM7QXJQ.js} +3 -3
- package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-O67H5M4N.js} +2 -2
- package/dist/testing/index.d.ts +5 -15
- package/dist/testing/index.js +4 -11
- package/dist/{usd-DoRuAckA.d.ts → usd-BNDg1715.d.ts} +14 -16
- package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
- package/dist/{x402-5H27DCBE.js → x402-7S2EFINY.js} +2 -2
- package/package.json +2 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MemoryProcessedPaymentStore,
|
|
3
3
|
ProcessedPaymentReplayError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QT4ONTST.js";
|
|
5
5
|
import {
|
|
6
6
|
createX402FacilitatorAuthHeaders,
|
|
7
7
|
createX402FacilitatorClient,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./chunk-BQ2NMWKE.js";
|
|
11
11
|
import {
|
|
12
12
|
MemoryCreditLedger
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-PHHAYRQV.js";
|
|
14
14
|
import {
|
|
15
15
|
SIGNED_ENVELOPE_FIELDS,
|
|
16
16
|
SignedRequestError,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
localMessage,
|
|
36
36
|
providerMessage,
|
|
37
37
|
toJobRecord
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-JDT5LCJC.js";
|
|
39
39
|
import {
|
|
40
40
|
ADMIN_AUTH_CLOCK_SKEW_SECONDS,
|
|
41
41
|
ADMIN_AUTH_NONCE_TTL_SECONDS,
|
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
pinDimension,
|
|
101
101
|
withSpan,
|
|
102
102
|
withTraceContext
|
|
103
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-XQXJKJ3P.js";
|
|
104
104
|
import {
|
|
105
105
|
FxRateUnavailableError,
|
|
106
106
|
InvalidCurrencyError,
|
|
@@ -109,7 +109,7 @@ import {
|
|
|
109
109
|
fxRateFor,
|
|
110
110
|
resolveFxSourceFromEnv,
|
|
111
111
|
validateCurrency
|
|
112
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-4A2RAKCW.js";
|
|
113
113
|
import {
|
|
114
114
|
fiatToSatsCeil,
|
|
115
115
|
parseUsdPrice,
|
|
@@ -130,7 +130,7 @@ import {
|
|
|
130
130
|
isNonChannelBitcoinRail,
|
|
131
131
|
netOwedSats,
|
|
132
132
|
x402SettlementPending
|
|
133
|
-
} from "./chunk-
|
|
133
|
+
} from "./chunk-V7EVFLAK.js";
|
|
134
134
|
import {
|
|
135
135
|
withSdkInitLock
|
|
136
136
|
} from "./chunk-S3XAHZQY.js";
|
|
@@ -141,7 +141,7 @@ import {
|
|
|
141
141
|
advertisedMethods,
|
|
142
142
|
challengeMeta,
|
|
143
143
|
isTempoInsufficientFundsError
|
|
144
|
-
} from "./chunk-
|
|
144
|
+
} from "./chunk-KMZXTBLA.js";
|
|
145
145
|
|
|
146
146
|
// src/sdk/server/lock-pubkey-store.ts
|
|
147
147
|
async function initLockPubkeyTable(db) {
|
|
@@ -883,7 +883,7 @@ async function buildCreditMenu(args) {
|
|
|
883
883
|
funding,
|
|
884
884
|
// Rendered when either half has something to say: a DVM configured for
|
|
885
885
|
// sessions only would otherwise lose the whole block the moment the gate
|
|
886
|
-
// closed, taking its capability signal with it
|
|
886
|
+
// closed, taking its capability signal with it.
|
|
887
887
|
...(tempoMethods.length > 0 || tempoWithheld.length > 0) && {
|
|
888
888
|
tempo: {
|
|
889
889
|
methods: tempoMethods,
|
|
@@ -896,7 +896,7 @@ async function buildCreditMenu(args) {
|
|
|
896
896
|
// are the same misadvertisement in the other direction.
|
|
897
897
|
...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } }
|
|
898
898
|
};
|
|
899
|
-
if (
|
|
899
|
+
if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
|
|
900
900
|
try {
|
|
901
901
|
const fx = await args.fxFetcher.fetch();
|
|
902
902
|
menu.lightning_min_micro = lightningFloorMicro(args.lightningFundingMinSats, fx, currency);
|
|
@@ -905,7 +905,7 @@ async function buildCreditMenu(args) {
|
|
|
905
905
|
"credit_menu_lightning_floor_unavailable",
|
|
906
906
|
`credit menu degraded: could not express the Lightning funding floor in ${currency}, so the lightning rail is omitted from the funding menu until a rate is available.`
|
|
907
907
|
);
|
|
908
|
-
menu.funding =
|
|
908
|
+
menu.funding = menu.funding.filter((rail) => rail !== "lightning");
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
911
|
const echo = await selectPrimaryCredit({
|
|
@@ -1950,7 +1950,7 @@ async function createX402BatchSettlementServer(opts) {
|
|
|
1950
1950
|
...opts.config.autoSettlement === false ? {} : opts.config.autoSettlement,
|
|
1951
1951
|
// Every claim this fleet builds is capped at earned draws — the claim
|
|
1952
1952
|
// timer, `claimAndSettle()`, and the withdraw-race rush claim all route
|
|
1953
|
-
// through this selector
|
|
1953
|
+
// through this selector. A builder-supplied
|
|
1954
1954
|
// `autoSettlement.selectClaimChannels` cannot override it: the ceiling is
|
|
1955
1955
|
// a money invariant, not a policy knob. Upstream's refund job builds its
|
|
1956
1956
|
// bundled claim from storage instead and never consults a selector, which
|
|
@@ -2071,14 +2071,13 @@ async function createX402BatchSettlementServer(opts) {
|
|
|
2071
2071
|
// move money. The voucher base does not: a cooperatively drained and
|
|
2072
2072
|
// reused channel can present the same cumulative maximum again under a
|
|
2073
2073
|
// fresh authorization, which made the second legitimate deposit
|
|
2074
|
-
// collide with the first row
|
|
2074
|
+
// collide with the first row. Conversely, keying by the
|
|
2075
2075
|
// credit effect would let one signed authorization open several rows.
|
|
2076
2076
|
//
|
|
2077
2077
|
// A cooperative refund belongs to one caller drain: after an operator
|
|
2078
2078
|
// writes a failed drain off, the unchanged channel can legitimately
|
|
2079
2079
|
// present the same voucher base under a fresh drain id. Keep that new
|
|
2080
2080
|
// effect out of the old row's `(operation, payment_id)` replay slot
|
|
2081
|
-
// (internal-review).
|
|
2082
2081
|
paymentId: isRefund ? `${paymentBase}:${settlementId}` : depositPaymentId ?? paymentBase,
|
|
2083
2082
|
amount: matched.amount
|
|
2084
2083
|
};
|
|
@@ -3049,6 +3048,26 @@ function msatsToMethodAmount(msats, methodName, btcUsd) {
|
|
|
3049
3048
|
const usd = msatsToUsd(msats, btcUsd);
|
|
3050
3049
|
return usd.toFixed(decimalsFor(methodName));
|
|
3051
3050
|
}
|
|
3051
|
+
function fiatMicroToMethodAmount(amountMicro, methodName) {
|
|
3052
|
+
if (!Number.isSafeInteger(amountMicro) || amountMicro < 0) {
|
|
3053
|
+
throw new Error("mpp-units: fiat amount must be a non-negative safe integer");
|
|
3054
|
+
}
|
|
3055
|
+
const decimals = decimalsFor(methodName);
|
|
3056
|
+
const microDecimals = 6;
|
|
3057
|
+
let atomic = BigInt(amountMicro);
|
|
3058
|
+
if (decimals < microDecimals) {
|
|
3059
|
+
const divisor = 10n ** BigInt(microDecimals - decimals);
|
|
3060
|
+
if (atomic % divisor !== 0n) {
|
|
3061
|
+
throw new Error(
|
|
3062
|
+
`mpp-units: fiat microunits cannot be represented exactly with ${decimals} decimals`
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
atomic /= divisor;
|
|
3066
|
+
} else if (decimals > microDecimals) {
|
|
3067
|
+
atomic *= 10n ** BigInt(decimals - microDecimals);
|
|
3068
|
+
}
|
|
3069
|
+
return methodNativeAmountToDecimal(atomic, methodName);
|
|
3070
|
+
}
|
|
3052
3071
|
function methodNativeAsset(methodName) {
|
|
3053
3072
|
switch (methodName) {
|
|
3054
3073
|
case "tempo":
|
|
@@ -3287,7 +3306,7 @@ var PAYMENT_PROOF_KEYS = ["cashu_token", "x402_payment", "tempo_credential"];
|
|
|
3287
3306
|
function hasPaymentProof(content) {
|
|
3288
3307
|
return PAYMENT_PROOF_KEYS.some((key) => Boolean(content[key]));
|
|
3289
3308
|
}
|
|
3290
|
-
async function issueUpfrontChallenges(mpp, requiredMsats, resourcePath, btcUsdRate, sessionIssuable = true, allowOneShotStablecoin = false) {
|
|
3309
|
+
async function issueUpfrontChallenges(mpp, requiredMsats, resourcePath, btcUsdRate, sessionIssuable = true, allowOneShotStablecoin = false, priceFiat) {
|
|
3291
3310
|
return loggers.tempo.span(
|
|
3292
3311
|
"tempo.issue_challenges",
|
|
3293
3312
|
{
|
|
@@ -3305,7 +3324,7 @@ async function issueUpfrontChallenges(mpp, requiredMsats, resourcePath, btcUsdRa
|
|
|
3305
3324
|
if (creditSurface && meta.intent === "charge" && !stablecoinCreditInstrumentAllowed(allowOneShotStablecoin, "tempo_charge")) {
|
|
3306
3325
|
continue;
|
|
3307
3326
|
}
|
|
3308
|
-
const amount = msatsToMethodAmount(requiredMsats, meta.name, btcUsdRate);
|
|
3327
|
+
const amount = priceFiat?.currency === "usd" ? fiatMicroToMethodAmount(priceFiat.amountMicro, meta.name) : msatsToMethodAmount(requiredMsats, meta.name, btcUsdRate);
|
|
3309
3328
|
const ch = await mpp.issueChallenge(meta.name, meta.intent, {
|
|
3310
3329
|
amount,
|
|
3311
3330
|
...meta.intent === "session" && { suggestedDeposit: amount, unitType: "request" },
|
|
@@ -3611,7 +3630,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3611
3630
|
// Nothing settled, so the EIP-3009 nonce is unspent and the same
|
|
3612
3631
|
// header is still payable — unlike the post-settlement branch
|
|
3613
3632
|
// below, where saying this would be telling the caller to pay
|
|
3614
|
-
// twice
|
|
3633
|
+
// twice.
|
|
3615
3634
|
hint: `Nothing was settled \u2014 re-sign this request with fund.amount_micro set to expected_micro (${predicted}), re-attach this same X-PAYMENT header, and resubmit.`
|
|
3616
3635
|
}
|
|
3617
3636
|
);
|
|
@@ -3632,7 +3651,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3632
3651
|
x402Requirements
|
|
3633
3652
|
});
|
|
3634
3653
|
}
|
|
3635
|
-
const { verifyX402Payment } = await import("./x402-
|
|
3654
|
+
const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
|
|
3636
3655
|
const receipt = await verifyX402Payment(
|
|
3637
3656
|
x402Payment,
|
|
3638
3657
|
x402Config,
|
|
@@ -3669,7 +3688,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3669
3688
|
`fund.amount_micro (${creditEnv.fund.amountMicro}) does not match the settled payment's value at the quoted price (${fundedMicro}).`,
|
|
3670
3689
|
// `expected_micro` states the valuation, but carries no corrective
|
|
3671
3690
|
// hint here: the facilitator already settled, so re-presenting
|
|
3672
|
-
// this credential would be a second payment
|
|
3691
|
+
// this credential would be a second payment.
|
|
3673
3692
|
{ extra: { expected_micro: fundedMicro } }
|
|
3674
3693
|
);
|
|
3675
3694
|
}
|
|
@@ -3783,13 +3802,14 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3783
3802
|
resourcePath ?? "/v1/job",
|
|
3784
3803
|
rate,
|
|
3785
3804
|
sessionIssuable,
|
|
3786
|
-
opts.fundOnly?.allowOneShotStablecoin
|
|
3805
|
+
opts.fundOnly?.allowOneShotStablecoin,
|
|
3806
|
+
opts.priceFiat
|
|
3787
3807
|
)
|
|
3788
3808
|
}
|
|
3789
3809
|
};
|
|
3790
3810
|
}
|
|
3791
3811
|
}
|
|
3792
|
-
const expectedAmount = msatsToMethodAmount(requiredMsats, cred.challenge.method, rate);
|
|
3812
|
+
const expectedAmount = opts.priceFiat?.currency === "usd" ? fiatMicroToMethodAmount(opts.priceFiat.amountMicro, cred.challenge.method) : msatsToMethodAmount(requiredMsats, cred.challenge.method, rate);
|
|
3793
3813
|
const sessionBasis = mppSessionBasis(cred);
|
|
3794
3814
|
let tempoReceipt;
|
|
3795
3815
|
const broadcast = async () => {
|
|
@@ -3862,7 +3882,8 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3862
3882
|
resourcePath ?? "/v1/job",
|
|
3863
3883
|
rate,
|
|
3864
3884
|
sessionIssuable,
|
|
3865
|
-
opts.fundOnly?.allowOneShotStablecoin
|
|
3885
|
+
opts.fundOnly?.allowOneShotStablecoin,
|
|
3886
|
+
opts.priceFiat
|
|
3866
3887
|
);
|
|
3867
3888
|
return {
|
|
3868
3889
|
paidMsats: 0,
|
|
@@ -3886,7 +3907,8 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3886
3907
|
resourcePath ?? "/v1/job",
|
|
3887
3908
|
rate,
|
|
3888
3909
|
sessionIssuable,
|
|
3889
|
-
opts.fundOnly?.allowOneShotStablecoin
|
|
3910
|
+
opts.fundOnly?.allowOneShotStablecoin,
|
|
3911
|
+
opts.priceFiat
|
|
3890
3912
|
);
|
|
3891
3913
|
return {
|
|
3892
3914
|
paidMsats: 0,
|
|
@@ -3952,7 +3974,8 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3952
3974
|
resourcePath ?? "/v1/job",
|
|
3953
3975
|
rate,
|
|
3954
3976
|
sessionIssuable,
|
|
3955
|
-
opts.fundOnly?.allowOneShotStablecoin
|
|
3977
|
+
opts.fundOnly?.allowOneShotStablecoin,
|
|
3978
|
+
opts.priceFiat
|
|
3956
3979
|
);
|
|
3957
3980
|
return {
|
|
3958
3981
|
paidMsats: 0,
|
|
@@ -4015,7 +4038,8 @@ async function verifyUpfrontPayment(opts) {
|
|
|
4015
4038
|
resourcePath ?? "/v1/job",
|
|
4016
4039
|
rate,
|
|
4017
4040
|
sessionIssuable,
|
|
4018
|
-
opts.fundOnly?.allowOneShotStablecoin
|
|
4041
|
+
opts.fundOnly?.allowOneShotStablecoin,
|
|
4042
|
+
opts.priceFiat
|
|
4019
4043
|
) : void 0;
|
|
4020
4044
|
return {
|
|
4021
4045
|
paidMsats: 0,
|
|
@@ -4170,16 +4194,16 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4170
4194
|
// Only the Cashu accumulator emits this code, and it evaluates each call's
|
|
4171
4195
|
// own proofs — nothing sums a partial with a later one, on any path. So the
|
|
4172
4196
|
// retry owes the full amount: implicit upfront the shortfall was forfeited
|
|
4173
|
-
//
|
|
4174
|
-
//
|
|
4175
|
-
// instead
|
|
4197
|
+
// mid-job it sits on a credit the per-site hint names
|
|
4198
|
+
// Explicit upfront short deposits use `insufficient_credit`
|
|
4199
|
+
// instead. Telling a caller to "send the difference" produced a
|
|
4176
4200
|
// second short pay either way.
|
|
4177
4201
|
payment_insufficient: {
|
|
4178
4202
|
display: "The payment was less than the amount required, so it didn't cover the request. The full amount is still owed.",
|
|
4179
4203
|
hint: "Retry with a single payment of required_msats \u2014 a partial payment does not reduce what is owed.",
|
|
4180
4204
|
retryable: true
|
|
4181
4205
|
},
|
|
4182
|
-
//
|
|
4206
|
+
// the rail and the ledger leg commit together, but the job row is
|
|
4183
4207
|
// written by a *second* transaction (`verifyAndCredit`). When that one can't
|
|
4184
4208
|
// be made to land, the money is real and the caller's — it just isn't on the
|
|
4185
4209
|
// job. Say so rather than answering a bare 500: the per-site body names the
|
|
@@ -4226,7 +4250,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4226
4250
|
hint: "Use a different prepaid balance or pay per call. To reclaim this balance after running jobs finish, inspect it with 'dvm wallet tempo-exit <dvm>'.",
|
|
4227
4251
|
retryable: false
|
|
4228
4252
|
},
|
|
4229
|
-
//
|
|
4253
|
+
// a refund on this credit's payment channel reached the chain and
|
|
4230
4254
|
// never finished booking, so the escrow behind the balance may already have
|
|
4231
4255
|
// been paid back. Spending it would buy work the service can never collect
|
|
4232
4256
|
// for. The copy must not promise the money is gone *or* that it is safe to
|
|
@@ -4258,7 +4282,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4258
4282
|
hint: "Fund at least min_micro (in the credit block of any quote) and try again.",
|
|
4259
4283
|
retryable: true
|
|
4260
4284
|
},
|
|
4261
|
-
//
|
|
4285
|
+
// a per-rail floor, not the menu-wide minimum — today only the
|
|
4262
4286
|
// lightning rail carries one (the receive channel's routing floor). Refused
|
|
4263
4287
|
// before any invoice is minted, so nothing is ever paid against this code;
|
|
4264
4288
|
// `retryable: false` because re-sending the identical request can never
|
|
@@ -4288,7 +4312,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4288
4312
|
hint: "The balance is zero or fully held by in-flight jobs. Wait for running jobs to finish, then request the drain again with a fresh drain_id.",
|
|
4289
4313
|
retryable: true
|
|
4290
4314
|
},
|
|
4291
|
-
//
|
|
4315
|
+
// Distinct from `nothing_to_drain` because the balance is real and
|
|
4292
4316
|
// waiting changes nothing: a reusable Bitcoin balance is a deposit the
|
|
4293
4317
|
// service owes back in the asset it received, and what is left here converts
|
|
4294
4318
|
// to less than one satoshi at the rate its own funding landed at. There is
|
|
@@ -4320,7 +4344,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4320
4344
|
hint: "Run 'dvm wallet tempo-exit <dvm>' to inspect the channel, request its on-chain exit if needed, and withdraw after the 15-minute grace period.",
|
|
4321
4345
|
retryable: false
|
|
4322
4346
|
},
|
|
4323
|
-
//
|
|
4347
|
+
// The voucher verified and the ledger leg rolled back with the
|
|
4324
4348
|
// failed submission, so nothing was charged and nothing about the payment is
|
|
4325
4349
|
// wrong — which is exactly what the `payment_invalid` this replaced got
|
|
4326
4350
|
// backwards, telling an agent to discard a credential that is still good. The
|
|
@@ -4397,7 +4421,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4397
4421
|
hint: "The token's proofs are pending at the mint; retry in a few seconds.",
|
|
4398
4422
|
retryable: true
|
|
4399
4423
|
},
|
|
4400
|
-
//
|
|
4424
|
+
// Not failures — the two `lightning_*` states below are the normal
|
|
4401
4425
|
// shape of an interactive funding leg, which is why both are retryable and
|
|
4402
4426
|
// neither `display` implies anything went wrong.
|
|
4403
4427
|
lightning_settlement_pending: {
|
|
@@ -4417,7 +4441,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4417
4441
|
},
|
|
4418
4442
|
replay_rejected: {
|
|
4419
4443
|
display: "This payment was already submitted, so it wasn't charged again.",
|
|
4420
|
-
//
|
|
4444
|
+
// this hint reaches a caller whose money already landed — the
|
|
4421
4445
|
// accumulator holds these proofs under this request id. It used to read
|
|
4422
4446
|
// "use a fresh token and a new request id for the next payment", which an
|
|
4423
4447
|
// agent could follow straight into paying twice for the same charge.
|
|
@@ -4561,7 +4585,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
|
|
|
4561
4585
|
snapshot
|
|
4562
4586
|
});
|
|
4563
4587
|
}
|
|
4564
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4588
|
+
const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
|
|
4565
4589
|
const receipt = await verifyX402Payment(
|
|
4566
4590
|
body.content.x402_payment,
|
|
4567
4591
|
opts.x402Config,
|
|
@@ -4696,7 +4720,10 @@ async function verifyIncomingPayment(body, opts, snapshot) {
|
|
|
4696
4720
|
}
|
|
4697
4721
|
};
|
|
4698
4722
|
}
|
|
4699
|
-
const expectedAmount =
|
|
4723
|
+
const expectedAmount = snapshot.pendingPaymentFiatCurrency === "usd" && snapshot.pendingPaymentFiatMicro !== void 0 ? fiatMicroToMethodAmount(
|
|
4724
|
+
snapshot.pendingPaymentFiatMicro,
|
|
4725
|
+
credential.challenge.method
|
|
4726
|
+
) : msatsToMethodAmount(expectedMsats, credential.challenge.method, rate);
|
|
4700
4727
|
const expectedMeta = challengeMeta(credential.challenge);
|
|
4701
4728
|
const creditNative = methodNativeAmountFromDecimal(
|
|
4702
4729
|
expectedAmount,
|
|
@@ -4975,7 +5002,7 @@ async function processIncomingPayment(job, body, opts) {
|
|
|
4975
5002
|
try {
|
|
4976
5003
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
4977
5004
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
4978
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5005
|
+
const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
|
|
4979
5006
|
const receipt = await verifyX402Payment(
|
|
4980
5007
|
body.content.x402_payment,
|
|
4981
5008
|
opts.x402Config,
|
|
@@ -5041,7 +5068,7 @@ async function processIncomingPayment(job, body, opts) {
|
|
|
5041
5068
|
status: 402
|
|
5042
5069
|
};
|
|
5043
5070
|
}
|
|
5044
|
-
const expectedAmount = msatsToMethodAmount(expectedMsats, credential.challenge.method, rate);
|
|
5071
|
+
const expectedAmount = job.pendingPaymentFiatCurrency === "usd" && job.pendingPaymentFiatMicro !== void 0 ? fiatMicroToMethodAmount(job.pendingPaymentFiatMicro, credential.challenge.method) : msatsToMethodAmount(expectedMsats, credential.challenge.method, rate);
|
|
5045
5072
|
const expectedMeta = challengeMeta(credential.challenge);
|
|
5046
5073
|
await loggers.tempo.span(
|
|
5047
5074
|
"tempo.verify_credential",
|
|
@@ -5279,7 +5306,7 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5279
5306
|
// requestId is non-null here because acceptAccumulatorPayment returns
|
|
5280
5307
|
// kind:"rejected" with code "missing_request_id" when unset (handled by
|
|
5281
5308
|
// the rejected branch above). Thread it as the per-call settlement
|
|
5282
|
-
// reference for revenue_events.tx_hash
|
|
5309
|
+
// reference for revenue_events.tx_hash. Unique per
|
|
5283
5310
|
// (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint, so
|
|
5284
5311
|
// it serves as a stable id even though it isn't an on-chain hash.
|
|
5285
5312
|
paymentTxHash: opts.requestId,
|
|
@@ -5428,7 +5455,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
|
|
|
5428
5455
|
amount_micro: priceFiat.amountMicro,
|
|
5429
5456
|
// Which rung degraded: the whole snapshot fell back to `lastKnown()`, or
|
|
5430
5457
|
// the live fetch landed without this currency and it was carried forward
|
|
5431
|
-
// from an earlier one
|
|
5458
|
+
// from an earlier one. The first is the rate source down, the
|
|
5432
5459
|
// second is the rate source having quietly stopped carrying a currency
|
|
5433
5460
|
// this DVM advertises — a config problem, and a different repair.
|
|
5434
5461
|
...denomination.carriedForward === true && { rate_carried_forward: true },
|
|
@@ -5659,7 +5686,7 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
5659
5686
|
creditId: creditId2,
|
|
5660
5687
|
drawId: drawId2,
|
|
5661
5688
|
addAmountMicro: fundedMicro,
|
|
5662
|
-
//
|
|
5689
|
+
// never grow the draw past what the job cumulatively asked.
|
|
5663
5690
|
// The available-balance guard inside `growDraw` cannot do this — the
|
|
5664
5691
|
// `fund` above raised the balance by exactly this amount — so a second
|
|
5665
5692
|
// payment for one ask would otherwise be absorbed and settle as revenue.
|
|
@@ -5772,7 +5799,7 @@ function creditDepositPayload(dvmId, funding) {
|
|
|
5772
5799
|
creditCurrency: funding.currency,
|
|
5773
5800
|
fundedAt: funding.fundedAt,
|
|
5774
5801
|
expiryMs: funding.expiryMs,
|
|
5775
|
-
//
|
|
5802
|
+
// label a session-funded deposit as one, whether or not the chain
|
|
5776
5803
|
// binding below could be read. `rail: "tempo"` alone does not say — a
|
|
5777
5804
|
// `tempo/charge` one-shot funds a credit with no channel at all — so this is
|
|
5778
5805
|
// what lets the platform tell a routine charge from a session whose binding
|
|
@@ -5780,7 +5807,7 @@ function creditDepositPayload(dvmId, funding) {
|
|
|
5780
5807
|
...funding.tempoSession && {
|
|
5781
5808
|
tempoSession: { channelId: funding.tempoSession.channelId.toLowerCase() }
|
|
5782
5809
|
},
|
|
5783
|
-
//
|
|
5810
|
+
// the channel registration rides the deposit report, so it
|
|
5784
5811
|
// commits with the voucher acceptance that created the channel's liability.
|
|
5785
5812
|
...funding.tempoChannel && funding.tempoSession && {
|
|
5786
5813
|
tempoChannel: {
|
|
@@ -6047,7 +6074,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
|
|
|
6047
6074
|
receivedProofs: [],
|
|
6048
6075
|
error: {
|
|
6049
6076
|
// A fixed message plus the reason enum, never `err.message`: that string
|
|
6050
|
-
// is upstream's
|
|
6077
|
+
// is upstream's and may include the relay lock holder's
|
|
6051
6078
|
// backend pid and `client_addr`. The detail is logged where the throw is.
|
|
6052
6079
|
body: paymentErrorBody("x402_settlement_submission_failed", {
|
|
6053
6080
|
message: "x402 batch settlement submission failed",
|
|
@@ -6646,7 +6673,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
|
|
|
6646
6673
|
/** Client-generated idempotency key — a retry with the same id credits once. */
|
|
6647
6674
|
fund_id: z.string().min(1),
|
|
6648
6675
|
/**
|
|
6649
|
-
* SHA-256 hex over the raw funding artifact (
|
|
6676
|
+
* SHA-256 hex over the raw funding artifact (the funding-commitment rule).
|
|
6650
6677
|
* Required for every attach rail; absent on `method: "lightning"`, which
|
|
6651
6678
|
* carries no caller-supplied artifact to hash — see {@link CreditRequest}.
|
|
6652
6679
|
*/
|
|
@@ -6670,7 +6697,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
|
|
|
6670
6697
|
* rail this build does not carry is answered by `validateDrainTarget`
|
|
6671
6698
|
* with a typed `drain_method_unsupported` naming the rails that do
|
|
6672
6699
|
* work — not by a Zod parse failure that says only "invalid enum". The
|
|
6673
|
-
* caller of a removed rail (`lightning
|
|
6700
|
+
* caller of a removed rail (`lightning`) and the caller of a
|
|
6674
6701
|
* typo deserve the same actionable refusal.
|
|
6675
6702
|
*/
|
|
6676
6703
|
method: z.string().min(1).optional(),
|
|
@@ -6798,12 +6825,12 @@ async function handleBalance(c, deps, args) {
|
|
|
6798
6825
|
return c.json({
|
|
6799
6826
|
op: "balance",
|
|
6800
6827
|
// Expired credits are included: expiry ends spending, never ownership
|
|
6801
|
-
// (
|
|
6828
|
+
// (the caller-ownership contract), so stranded value must stay visible to whoever owns it.
|
|
6802
6829
|
credits: all.map(
|
|
6803
6830
|
(credit) => creditBody(credit, pendingChannels.has(credit.tempoChannelId ?? ""))
|
|
6804
6831
|
),
|
|
6805
6832
|
...primary ? { credit: creditBody(primary, pendingChannels.has(primary.tempoChannelId ?? "")) } : {},
|
|
6806
|
-
//
|
|
6833
|
+
// the funding menu the quote and 402 surfaces already advertise,
|
|
6807
6834
|
// echoed here so one read answers "what do I hold, and what may I add".
|
|
6808
6835
|
// Under the key `menu`, not `credit` — that one is the credit projection on
|
|
6809
6836
|
// this route. Carried without the caller echo fields: `credits[]` above says
|
|
@@ -7088,7 +7115,7 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7088
7115
|
expectedChannelId,
|
|
7089
7116
|
// The close intent is re-asserted from inside the verifier, once the
|
|
7090
7117
|
// credential is bound to this credit's own channel and before anything
|
|
7091
|
-
// broadcasts
|
|
7118
|
+
// broadcasts. Run above the call instead and a credential
|
|
7092
7119
|
// signed for another channel touches the reservation on its way to a
|
|
7093
7120
|
// refusal that was never going to close anything.
|
|
7094
7121
|
onBound: async () => {
|
|
@@ -7394,7 +7421,7 @@ async function handleX402ChannelDrain(c, deps, args) {
|
|
|
7394
7421
|
effectId: `${args.credit.creditId}:${args.drainId}`,
|
|
7395
7422
|
// The voucher the caller presents is what actually gets revoked and paid,
|
|
7396
7423
|
// so the settlement layer refuses one signed for any other channel before
|
|
7397
|
-
// it touches anything
|
|
7424
|
+
// it touches anything.
|
|
7398
7425
|
expectedChannelId: channelId,
|
|
7399
7426
|
refundBoundNative,
|
|
7400
7427
|
commit: async (_funding, tx) => {
|
|
@@ -7613,7 +7640,7 @@ function drainBody(record) {
|
|
|
7613
7640
|
status: record.status,
|
|
7614
7641
|
amount_micro: record.amountMicro,
|
|
7615
7642
|
currency: record.currency,
|
|
7616
|
-
// The in-kind obligation, where there is one
|
|
7643
|
+
// The in-kind obligation, where there is one: a Bitcoin
|
|
7617
7644
|
// reclaim returns the deposited sats pro rata, at the rate each funding
|
|
7618
7645
|
// landed at, so this figure was fixed when the reclaim was requested and
|
|
7619
7646
|
// does not move with the market. Published beside the fiat one rather than
|
|
@@ -7636,7 +7663,7 @@ function drainCopy(record) {
|
|
|
7636
7663
|
// Named because it is the obligation, and because it is
|
|
7637
7664
|
// fixed: a Bitcoin refund returns the share of the Bitcoin you
|
|
7638
7665
|
// deposited that you never spent, so it does not move if the
|
|
7639
|
-
// price does before you collect it
|
|
7666
|
+
// price does before you collect it. The mint's own
|
|
7640
7667
|
// redemption charge is named rather than glossed — it is a sat
|
|
7641
7668
|
// or two at a fee-charging mint and nothing at a free one, and
|
|
7642
7669
|
// a figure an agent relays to a human should not be a promise
|
|
@@ -8298,7 +8325,7 @@ var DRAIN_CONFLICT_COPY = {
|
|
|
8298
8325
|
},
|
|
8299
8326
|
// Deliberately rail-neutral: `refundFloorEvidence` is one floor over both
|
|
8300
8327
|
// channel rails, and two doors with two answers about one shortfall is the
|
|
8301
|
-
// drift consolidating it was meant to prevent
|
|
8328
|
+
// drift consolidating it was meant to prevent. Tempo answers its
|
|
8302
8329
|
// own through `tempoFloorRefusal`, which reaches the same conclusion a step
|
|
8303
8330
|
// earlier and adds the one rail-specific fact — its channel is already closed.
|
|
8304
8331
|
refund_floor: {
|
|
@@ -8309,13 +8336,13 @@ var DRAIN_CONFLICT_COPY = {
|
|
|
8309
8336
|
retryable: false,
|
|
8310
8337
|
// True even though the ledger transaction that would have written the drain
|
|
8311
8338
|
// row rolled back around this refusal. The wedged settlement row is recorded
|
|
8312
|
-
// outside that transaction
|
|
8339
|
+
// outside that transaction and holds this drain id as its
|
|
8313
8340
|
// `effect_id`, which is the id `X402SettlementRepair.reconcile` books the
|
|
8314
8341
|
// drain under — so the caller has to keep it. Dropping it mints a fresh one
|
|
8315
8342
|
// that the wedged-channel gate refuses as `settlement_pending` anyway, and
|
|
8316
8343
|
// re-presenting *this* id is that gate's own documented exemption. Tempo
|
|
8317
8344
|
// reaches the same `true` one artifact earlier, off a `credit_drains` row
|
|
8318
|
-
// its close-intent reservation already committed
|
|
8345
|
+
// its close-intent reservation already committed.
|
|
8319
8346
|
drainRegistered: true
|
|
8320
8347
|
}
|
|
8321
8348
|
};
|
|
@@ -8413,7 +8440,7 @@ function creditBody(credit, reconciliationPending = false) {
|
|
|
8413
8440
|
remaining_micro: credit.availableMicro,
|
|
8414
8441
|
expiry_ms: credit.expiryMs,
|
|
8415
8442
|
expired: credit.expired,
|
|
8416
|
-
// Reclaimable via op "drain"
|
|
8443
|
+
// Reclaimable via op "drain" — true whenever spendable-or-
|
|
8417
8444
|
// expired value remains. Expiry ends spending, never ownership.
|
|
8418
8445
|
drainable: credit.status === "active" && credit.availableMicro > 0,
|
|
8419
8446
|
...reconciliationPending && { reconciliation_pending: true },
|
|
@@ -9287,13 +9314,13 @@ function buildContext(job, opts) {
|
|
|
9287
9314
|
if (terminated("prompt")) {
|
|
9288
9315
|
return Promise.reject(new JobCancelledError(`Job already ${job.status}`));
|
|
9289
9316
|
}
|
|
9317
|
+
job.status = "awaiting-input";
|
|
9290
9318
|
providerMessage(job, "prompt", {
|
|
9291
9319
|
id,
|
|
9292
9320
|
text,
|
|
9293
9321
|
...promptOpts?.options && { options: promptOpts.options },
|
|
9294
9322
|
...promptOpts?.schema && { schema: promptOpts.schema }
|
|
9295
9323
|
});
|
|
9296
|
-
job.status = "awaiting-input";
|
|
9297
9324
|
if (job.replayHighSeq !== null) {
|
|
9298
9325
|
const response = job.messages.find(
|
|
9299
9326
|
(m) => m.type === "response" && m.from === "requester" && m.content.prompt_id === id
|
|
@@ -9407,7 +9434,7 @@ function buildContext(job, opts) {
|
|
|
9407
9434
|
poolBasis: pool.basis,
|
|
9408
9435
|
askCurrency,
|
|
9409
9436
|
// `draw` here means the pool read fine and either the ASK wouldn't pin
|
|
9410
|
-
//
|
|
9437
|
+
// an fx outage on a bare-msats ask, or a sub-micro amount — or it
|
|
9411
9438
|
// pinned in a currency this pool isn't denominated in.
|
|
9412
9439
|
...pool.basis === "draw" && { poolCurrency: pool.currency }
|
|
9413
9440
|
});
|
|
@@ -9494,7 +9521,7 @@ function buildContext(job, opts) {
|
|
|
9494
9521
|
for (const m of opts.mpp.methods) {
|
|
9495
9522
|
const meta = m;
|
|
9496
9523
|
if (meta.intent === "session") continue;
|
|
9497
|
-
const amount2 = msatsToMethodAmount(amountMsats, meta.name, rate);
|
|
9524
|
+
const amount2 = pendingFiat?.currency === "usd" ? fiatMicroToMethodAmount(pendingFiat.amountMicro, meta.name) : msatsToMethodAmount(amountMsats, meta.name, rate);
|
|
9498
9525
|
const ch = await opts.mpp.issueChallenge(meta.name, meta.intent, {
|
|
9499
9526
|
amount: amount2,
|
|
9500
9527
|
description: reason
|
|
@@ -9542,8 +9569,8 @@ function buildContext(job, opts) {
|
|
|
9542
9569
|
ask_msats: amountMsats
|
|
9543
9570
|
});
|
|
9544
9571
|
}
|
|
9545
|
-
providerMessage(job, "payment-request", paymentRequestContent);
|
|
9546
9572
|
job.status = "awaiting-input";
|
|
9573
|
+
providerMessage(job, "payment-request", paymentRequestContent);
|
|
9547
9574
|
const paidMsatsAtYield = job.paidMsats;
|
|
9548
9575
|
const p = new Promise((resolve, reject) => {
|
|
9549
9576
|
let resolved = false;
|
|
@@ -9552,12 +9579,24 @@ function buildContext(job, opts) {
|
|
|
9552
9579
|
unsubscribeNotify?.();
|
|
9553
9580
|
unsubscribeNotify = void 0;
|
|
9554
9581
|
};
|
|
9555
|
-
const doResolve = (pc) => {
|
|
9582
|
+
const doResolve = async (pc) => {
|
|
9556
9583
|
if (resolved) return;
|
|
9557
9584
|
resolved = true;
|
|
9558
9585
|
cleanup();
|
|
9559
9586
|
creditedToHandlerMsats += amountMsats;
|
|
9560
9587
|
creditedToHandlerMicro += pendingFiat?.amountMicro ?? 0;
|
|
9588
|
+
if (job.pendingPayment?.resolve === doResolve) {
|
|
9589
|
+
job.pendingPayment = null;
|
|
9590
|
+
job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
|
|
9591
|
+
if (job.status === "awaiting-input" && opts.onYield) {
|
|
9592
|
+
try {
|
|
9593
|
+
await opts.onYield();
|
|
9594
|
+
} catch (err) {
|
|
9595
|
+
reject(err);
|
|
9596
|
+
return;
|
|
9597
|
+
}
|
|
9598
|
+
}
|
|
9599
|
+
}
|
|
9561
9600
|
resolve(pc);
|
|
9562
9601
|
};
|
|
9563
9602
|
const doReject = (err) => {
|
|
@@ -9574,11 +9613,17 @@ function buildContext(job, opts) {
|
|
|
9574
9613
|
if (resolved) return;
|
|
9575
9614
|
try {
|
|
9576
9615
|
const counters = await getCounters();
|
|
9577
|
-
if (!counters) return;
|
|
9616
|
+
if (!counters || resolved) return;
|
|
9578
9617
|
const satisfied = poolBasis ? counters.paidMsats - paidMsatsAtYield >= amountMsats : counters.paidMsats - creditedToHandlerMsats >= amountMsats;
|
|
9579
9618
|
if (satisfied) {
|
|
9619
|
+
const resume = async () => {
|
|
9620
|
+
job.paidMsats = counters.paidMsats;
|
|
9621
|
+
job.pendingPaymentMsats = counters.pendingPaymentMsats;
|
|
9622
|
+
await doResolve({ amount_msats: amountMsats });
|
|
9623
|
+
};
|
|
9580
9624
|
if (opts.claimForProcessing) {
|
|
9581
|
-
const won = await opts.claimForProcessing();
|
|
9625
|
+
const won = await opts.claimForProcessing(resume);
|
|
9626
|
+
if (resolved) return;
|
|
9582
9627
|
if (!won) {
|
|
9583
9628
|
resolved = true;
|
|
9584
9629
|
cleanup();
|
|
@@ -9586,10 +9631,7 @@ function buildContext(job, opts) {
|
|
|
9586
9631
|
return;
|
|
9587
9632
|
}
|
|
9588
9633
|
}
|
|
9589
|
-
|
|
9590
|
-
job.pendingPaymentMsats = counters.pendingPaymentMsats;
|
|
9591
|
-
if (job.status === "awaiting-input") job.status = "processing";
|
|
9592
|
-
doResolve({ amount_msats: amountMsats });
|
|
9634
|
+
await resume();
|
|
9593
9635
|
}
|
|
9594
9636
|
} catch (err) {
|
|
9595
9637
|
log.error("requestPayment NOTIFY check failed", { error: String(err) });
|
|
@@ -9640,12 +9682,12 @@ function buildContext(job, opts) {
|
|
|
9640
9682
|
runTerminal();
|
|
9641
9683
|
},
|
|
9642
9684
|
// `failOpts.refund` is a caller-fault annotation and nothing more
|
|
9643
|
-
//
|
|
9685
|
+
// No rail hands value backwards: a failed job's debit is the
|
|
9644
9686
|
// credit draw the terminal funnel releases, and the caller reclaims that
|
|
9645
|
-
// balance through the
|
|
9687
|
+
// balance through the `drain` op. The Cashu branch that used to
|
|
9646
9688
|
// send proofs back here was dead — `job.receivedProofs` has been empty on
|
|
9647
9689
|
// every path since the accumulator became the commit boundary for inbound
|
|
9648
|
-
// proofs
|
|
9690
|
+
// proofs — and the accumulator-debit rewrite that would
|
|
9649
9691
|
// have revived it, was cancelled as superseded by the ledger.
|
|
9650
9692
|
fail: (error) => {
|
|
9651
9693
|
if (terminated("fail")) return;
|
|
@@ -9715,13 +9757,17 @@ var ReceiptIssuer = class {
|
|
|
9715
9757
|
}
|
|
9716
9758
|
/**
|
|
9717
9759
|
* Build and sign the receipt for a terminal job. `seq` comes from
|
|
9718
|
-
* `JobStore.claimReceiptSeq
|
|
9760
|
+
* `JobStore.claimReceiptSeq`; `issuedAt` is epoch seconds and defaults to now.
|
|
9719
9761
|
*
|
|
9720
9762
|
* Free jobs get receipts too, with `paid.msats: 0` — they still consume a
|
|
9721
9763
|
* sequence number and still attest an outcome. Whether feedback is
|
|
9722
9764
|
* paid-only is the feedback layer's call, not this one's.
|
|
9723
9765
|
*
|
|
9724
|
-
*
|
|
9766
|
+
* Timing and attribution come from the committed terminal row, not signing
|
|
9767
|
+
* time or handler input. Legacy rows without those durable inputs omit the
|
|
9768
|
+
* optional blocks rather than inventing historical evidence.
|
|
9769
|
+
*
|
|
9770
|
+
* `credit` is the resolved draw block — present on every job
|
|
9725
9771
|
* whose payment funded/drew the ledger, absent otherwise. Signature-
|
|
9726
9772
|
* compatible either way: `canonicalize` omits absent keys.
|
|
9727
9773
|
*/
|
|
@@ -9734,6 +9780,14 @@ var ReceiptIssuer = class {
|
|
|
9734
9780
|
capability: record.capability,
|
|
9735
9781
|
outcome: outcomeFor(record.status),
|
|
9736
9782
|
reason: terminalReason(record),
|
|
9783
|
+
...record.timingKnown !== false && record.terminalAtMs !== void 0 && {
|
|
9784
|
+
timing: {
|
|
9785
|
+
accepted_at_ms: record.createdAt,
|
|
9786
|
+
terminal_at_ms: record.terminalAtMs,
|
|
9787
|
+
caller_wait_ms: record.callerWaitMs ?? 0
|
|
9788
|
+
}
|
|
9789
|
+
},
|
|
9790
|
+
...record.timingKnown !== false && record.status !== "completed" && record.endedBy && { ended_by: record.endedBy },
|
|
9737
9791
|
seq,
|
|
9738
9792
|
issued_at: issuedAt ?? Math.floor(Date.now() / 1e3),
|
|
9739
9793
|
requester_pubkey: record.requesterPubkey ?? null,
|
|
@@ -9752,7 +9806,7 @@ var ReceiptIssuer = class {
|
|
|
9752
9806
|
return signReceipt(unsigned, this.secret);
|
|
9753
9807
|
}
|
|
9754
9808
|
/**
|
|
9755
|
-
* Countersign one reclaim event
|
|
9809
|
+
* Countersign one reclaim event. Unlike job receipts there is
|
|
9756
9810
|
* no store-allocated sequence — the drain's own `ledger_seq` (taken under
|
|
9757
9811
|
* the credit row lock, shared with draws) already orders it in the
|
|
9758
9812
|
* per-credit evidence chain.
|
|
@@ -10069,12 +10123,16 @@ var JobManager = class {
|
|
|
10069
10123
|
cleanupTimers = /* @__PURE__ */ new Map();
|
|
10070
10124
|
/** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
|
|
10071
10125
|
terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
|
|
10072
|
-
/** Per-job NOTIFY unsubscribe handles for the durable-status watch
|
|
10126
|
+
/** Per-job NOTIFY unsubscribe handles for the durable-status watch. */
|
|
10073
10127
|
statusWatchers = /* @__PURE__ */ new Map();
|
|
10074
|
-
/** Job ids whose durable-status re-read is in flight — coalesces notify storms
|
|
10128
|
+
/** Job ids whose durable-status re-read is in flight — coalesces notify storms. */
|
|
10075
10129
|
statusChecksInFlight = /* @__PURE__ */ new Set();
|
|
10076
|
-
/** Job ids that were notified mid-re-read and must be re-checked
|
|
10130
|
+
/** Job ids that were notified mid-re-read and must be re-checked. */
|
|
10077
10131
|
statusChecksQueued = /* @__PURE__ */ new Set();
|
|
10132
|
+
// Every local input resume holds this queue through its durable wait update.
|
|
10133
|
+
// A prompt response and a payment NOTIFY must not mistake each other for a
|
|
10134
|
+
// different machine's successful claim.
|
|
10135
|
+
inputResumeTails = /* @__PURE__ */ new WeakMap();
|
|
10078
10136
|
sessionId = randomBytes4(3).toString("hex");
|
|
10079
10137
|
nextJobId = 1;
|
|
10080
10138
|
accumulatorMonitor;
|
|
@@ -10082,7 +10140,7 @@ var JobManager = class {
|
|
|
10082
10140
|
processingWatchdogMs;
|
|
10083
10141
|
reactivationClaimGraceMs;
|
|
10084
10142
|
/**
|
|
10085
|
-
* Worker-heartbeat cadence
|
|
10143
|
+
* Worker-heartbeat cadence. Also floors the clock
|
|
10086
10144
|
* guard's compensation on the processing arm — see
|
|
10087
10145
|
* {@link staleSweepCompensationCapMs}.
|
|
10088
10146
|
*/
|
|
@@ -10102,7 +10160,7 @@ var JobManager = class {
|
|
|
10102
10160
|
jobRetentionResumeFrom;
|
|
10103
10161
|
/**
|
|
10104
10162
|
* A `Date.now()` reading that cannot regress within this manager's lifetime
|
|
10105
|
-
*
|
|
10163
|
+
* Anchored at construction, so a manager already running when
|
|
10106
10164
|
* the clock stepped is covered. The stale-job sweep's two cutoffs, the
|
|
10107
10165
|
* orphan-draw age gate and the reactivation claim grace all read it; see
|
|
10108
10166
|
* {@link createMonotonicClock} for what it costs and where it is clamped.
|
|
@@ -10115,7 +10173,7 @@ var JobManager = class {
|
|
|
10115
10173
|
jobRetentionSweepInFlight = false;
|
|
10116
10174
|
heartbeatInFlight = false;
|
|
10117
10175
|
/**
|
|
10118
|
-
* The denomination of every credit this DVM opens
|
|
10176
|
+
* The denomination of every credit this DVM opens. Resolved once,
|
|
10119
10177
|
* with the same boundary fallback `DVMServer` applies for direct JavaScript
|
|
10120
10178
|
* callers that force an incomplete value past the descriptor contract.
|
|
10121
10179
|
*/
|
|
@@ -10130,7 +10188,7 @@ var JobManager = class {
|
|
|
10130
10188
|
this.jobStore = new MemoryJobStore();
|
|
10131
10189
|
} else {
|
|
10132
10190
|
throw new Error(
|
|
10133
|
-
"JobManager requires a persistent jobStore in production. Pass `jobStore` explicitly, set `devMode: true`, or construct via createDVMHost (which boots a PostgresJobStore from DATABASE_URL).
|
|
10191
|
+
"JobManager requires a persistent jobStore in production. Pass `jobStore` explicitly, set `devMode: true`, or construct via createDVMHost (which boots a PostgresJobStore from DATABASE_URL)."
|
|
10134
10192
|
);
|
|
10135
10193
|
}
|
|
10136
10194
|
if (opts.receiptIssuer && !isReceiptIssuingStore(this.jobStore)) {
|
|
@@ -10138,7 +10196,7 @@ var JobManager = class {
|
|
|
10138
10196
|
JSON.stringify({
|
|
10139
10197
|
level: "warn",
|
|
10140
10198
|
event: "receipts_disabled_store_unsupported",
|
|
10141
|
-
message: "DVMKIT_RECEIPT_KEY is set but the configured jobStore does not implement ReceiptIssuingStore (claimReceiptSeq / saveReceipt), so no receipts will be issued and /v1/info will not advertise them. Use PostgresJobStore, MemoryJobStore, or implement both methods.
|
|
10199
|
+
message: "DVMKIT_RECEIPT_KEY is set but the configured jobStore does not implement ReceiptIssuingStore (claimReceiptSeq / saveReceipt), so no receipts will be issued and /v1/info will not advertise them. Use PostgresJobStore, MemoryJobStore, or implement both methods."
|
|
10142
10200
|
})
|
|
10143
10201
|
);
|
|
10144
10202
|
}
|
|
@@ -10237,7 +10295,7 @@ var JobManager = class {
|
|
|
10237
10295
|
}
|
|
10238
10296
|
}
|
|
10239
10297
|
}
|
|
10240
|
-
if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10298
|
+
if (opts.dvmId && (opts.onJobCost || opts.onJobTerminal) && isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10241
10299
|
this.terminalCostRecoveryTimer = setInterval(() => {
|
|
10242
10300
|
void this.recoverTerminalCosts().catch((err) => {
|
|
10243
10301
|
console.error(
|
|
@@ -10251,16 +10309,14 @@ var JobManager = class {
|
|
|
10251
10309
|
this.terminalCostRecoveryTimer.unref();
|
|
10252
10310
|
}
|
|
10253
10311
|
}
|
|
10254
|
-
/** Active in-memory jobs. */
|
|
10255
10312
|
get activeJobs() {
|
|
10256
10313
|
return this._activeJobs;
|
|
10257
10314
|
}
|
|
10258
|
-
/** The backing job store. */
|
|
10259
10315
|
get store() {
|
|
10260
10316
|
return this.jobStore;
|
|
10261
10317
|
}
|
|
10262
10318
|
/**
|
|
10263
|
-
* True when this DVM will actually issue receipts
|
|
10319
|
+
* True when this DVM will actually issue receipts — a key *and*
|
|
10264
10320
|
* a store that can allocate sequence numbers and persist bytes write-once.
|
|
10265
10321
|
* `/v1/info#receipts` reads this rather than the key alone, so the flag can
|
|
10266
10322
|
* never promise something `issueReceipt` silently declines to do.
|
|
@@ -10277,7 +10333,7 @@ var JobManager = class {
|
|
|
10277
10333
|
return Object.keys(this.descriptor.capabilities);
|
|
10278
10334
|
}
|
|
10279
10335
|
/**
|
|
10280
|
-
* Resolve a capability's static `price` to msats
|
|
10336
|
+
* Resolve a capability's static `price` to msats.
|
|
10281
10337
|
*
|
|
10282
10338
|
* `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
|
|
10283
10339
|
* Returns `undefined` for dynamic-priced capabilities (those that declare
|
|
@@ -10290,7 +10346,7 @@ var JobManager = class {
|
|
|
10290
10346
|
}
|
|
10291
10347
|
/**
|
|
10292
10348
|
* Translate a capability's static `price` into the fiat envelope used by
|
|
10293
|
-
* the per-capability `pricing.max` advertised on `/v1/info
|
|
10349
|
+
* the per-capability `pricing.max` advertised on `/v1/info`.
|
|
10294
10350
|
* Returns `undefined` for dynamic-priced capabilities and for unknown
|
|
10295
10351
|
* names (the route handler validates the name before invoking this).
|
|
10296
10352
|
*/
|
|
@@ -10300,24 +10356,24 @@ var JobManager = class {
|
|
|
10300
10356
|
return resolvePricingMax(cap.price);
|
|
10301
10357
|
}
|
|
10302
10358
|
/**
|
|
10303
|
-
* Parse a request body through a capability's input schema
|
|
10359
|
+
* Parse a request body through a capability's input schema.
|
|
10304
10360
|
*
|
|
10305
10361
|
* Resolution order when a capability declares an `input` schema:
|
|
10306
10362
|
* 1. `body.data` is preferred — agents pass structured fields directly
|
|
10307
|
-
*
|
|
10363
|
+
* (CLI builds this from `--param k=v`).
|
|
10308
10364
|
* 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
|
|
10309
|
-
*
|
|
10365
|
+
* JSON-string contract.
|
|
10310
10366
|
* 3. Neither usable → throw `MissingStructuredInputError` so the route can
|
|
10311
|
-
*
|
|
10367
|
+
* return `invalid_input` with a hint pointing at `/v1/info`.
|
|
10312
10368
|
*
|
|
10313
10369
|
* When the capability has no `input` schema, returns `body.input` raw
|
|
10314
10370
|
* (primitive path).
|
|
10315
10371
|
*
|
|
10316
|
-
* The
|
|
10317
|
-
* branches
|
|
10372
|
+
* The credit envelope is removed before the schema runs, on both
|
|
10373
|
+
* branches — it rides the signed body but is protocol-level, so a
|
|
10318
10374
|
* top-level `.strict()` capability schema would otherwise reject every
|
|
10319
10375
|
* explicit draw as an unknown key, before the envelope was even extracted.
|
|
10320
|
-
* The persisted `record.input` the
|
|
10376
|
+
* The persisted `record.input` the reactivation path re-reads is the
|
|
10321
10377
|
* pre-Zod wire form, so it comes through the second branch carrying them too.
|
|
10322
10378
|
*/
|
|
10323
10379
|
parseInput(body, capability) {
|
|
@@ -10335,7 +10391,7 @@ var JobManager = class {
|
|
|
10335
10391
|
throw new MissingStructuredInputError();
|
|
10336
10392
|
}
|
|
10337
10393
|
/**
|
|
10338
|
-
* Look up the per-capability descriptor by name
|
|
10394
|
+
* Look up the per-capability descriptor by name. Throws when the
|
|
10339
10395
|
* name doesn't exist — the route layer validates body.capability first, so
|
|
10340
10396
|
* reaching this with an unknown name is a programmer error.
|
|
10341
10397
|
*/
|
|
@@ -10361,7 +10417,7 @@ var JobManager = class {
|
|
|
10361
10417
|
return this.descriptor.capabilities[name];
|
|
10362
10418
|
}
|
|
10363
10419
|
/**
|
|
10364
|
-
* Allocate the id the next job will be created under
|
|
10420
|
+
* Allocate the id the next job will be created under. The
|
|
10365
10421
|
* submit path calls this BEFORE payment verification so the ledger draw
|
|
10366
10422
|
* commits with its job linkage, then passes the id back through
|
|
10367
10423
|
* `createJob`'s provenance. Ids allocated for requests whose payment is
|
|
@@ -10372,7 +10428,7 @@ var JobManager = class {
|
|
|
10372
10428
|
return `job-${this.sessionId}-${this.nextJobId++}`;
|
|
10373
10429
|
}
|
|
10374
10430
|
/**
|
|
10375
|
-
* Create a new job from a request body. `provenance`
|
|
10431
|
+
* Create a new job from a request body. `provenance` is what the
|
|
10376
10432
|
* idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
|
|
10377
10433
|
* to re-issue, and the fingerprint + caller pubkey a retry must reproduce
|
|
10378
10434
|
* to be given it.
|
|
@@ -10428,27 +10484,23 @@ var JobManager = class {
|
|
|
10428
10484
|
this.watchDurableStatus(job);
|
|
10429
10485
|
return job;
|
|
10430
10486
|
}
|
|
10431
|
-
/** Build an SDKJobContext and attach it to the job. */
|
|
10432
10487
|
buildAndAttachContext(job, parsedInput) {
|
|
10433
10488
|
const auth = deriveAuthFromParsedInput(this.descriptor, parsedInput);
|
|
10434
10489
|
const sdkCtx = buildContext(job, { ...this.buildContextOpts(job), parsedInput, auth });
|
|
10435
10490
|
job.sdkCtx = sdkCtx;
|
|
10436
10491
|
return sdkCtx;
|
|
10437
10492
|
}
|
|
10438
|
-
/** Start the handler for a job. */
|
|
10439
10493
|
runHandler(job, sdkCtx) {
|
|
10440
10494
|
const cap = this.getCapability(job.capability);
|
|
10441
10495
|
const onError = (err) => {
|
|
10442
10496
|
if (isTerminal(job.status)) return;
|
|
10443
10497
|
job.replayProviderSkip = 0;
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
void job.terminalWork;
|
|
10451
|
-
}
|
|
10498
|
+
job.status = "failed";
|
|
10499
|
+
providerMessage(job, "cancel", {
|
|
10500
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
10501
|
+
});
|
|
10502
|
+
job.terminalWork = this.finalizeTerminal(job);
|
|
10503
|
+
void job.terminalWork;
|
|
10452
10504
|
};
|
|
10453
10505
|
if (!cap) {
|
|
10454
10506
|
if (!isTerminal(job.status)) {
|
|
@@ -10476,16 +10528,46 @@ var JobManager = class {
|
|
|
10476
10528
|
onError(err);
|
|
10477
10529
|
}
|
|
10478
10530
|
}
|
|
10479
|
-
/** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
|
|
10480
10531
|
async dispatchMessage(job, type, content) {
|
|
10532
|
+
if (type !== "response" && type !== "payment") {
|
|
10533
|
+
return this.dispatchMessageNow(job, type, content);
|
|
10534
|
+
}
|
|
10535
|
+
const pending = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
|
|
10536
|
+
await this.queueInputResume(job, async () => {
|
|
10537
|
+
const current = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
|
|
10538
|
+
if (pending && current !== pending) return;
|
|
10539
|
+
return this.dispatchMessageNow(job, type, content);
|
|
10540
|
+
});
|
|
10541
|
+
}
|
|
10542
|
+
async queueInputResume(job, resume) {
|
|
10543
|
+
const previous = this.inputResumeTails.get(job) ?? Promise.resolve();
|
|
10544
|
+
const next = previous.catch(() => void 0).then(resume);
|
|
10545
|
+
this.inputResumeTails.set(job, next);
|
|
10546
|
+
try {
|
|
10547
|
+
return await next;
|
|
10548
|
+
} finally {
|
|
10549
|
+
if (this.inputResumeTails.get(job) === next) this.inputResumeTails.delete(job);
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
10552
|
+
async claimLocalInput(job) {
|
|
10553
|
+
await job.messageAppenderTail;
|
|
10554
|
+
return isStreamableJobStore(this.jobStore) ? this.jobStore.claimForProcessing(job.id) : true;
|
|
10555
|
+
}
|
|
10556
|
+
async dispatchMessageNow(job, type, content) {
|
|
10481
10557
|
const ctx = job.sdkCtx;
|
|
10482
10558
|
const cap = this.getCapability(job.capability);
|
|
10483
10559
|
if (type === "response") {
|
|
10484
10560
|
const rc = content;
|
|
10485
10561
|
const pending = job.pendingPrompts.get(rc.prompt_id);
|
|
10486
10562
|
if (pending) {
|
|
10563
|
+
if (!await this.claimLocalInput(job)) {
|
|
10564
|
+
await this.adoptDurableTerminal(job);
|
|
10565
|
+
return;
|
|
10566
|
+
}
|
|
10567
|
+
if (job.pendingPrompts.get(rc.prompt_id) !== pending || isTerminal(job.status)) return;
|
|
10487
10568
|
job.pendingPrompts.delete(rc.prompt_id);
|
|
10488
|
-
job.status = "processing";
|
|
10569
|
+
job.status = job.pendingPrompts.size > 0 || job.pendingPayment ? "awaiting-input" : "processing";
|
|
10570
|
+
if (job.status === "awaiting-input") await this.persistJob(job);
|
|
10489
10571
|
pending.resolve(rc);
|
|
10490
10572
|
} else if (cap?.onResponse && ctx) {
|
|
10491
10573
|
await cap.onResponse(ctx, rc);
|
|
@@ -10494,9 +10576,15 @@ var JobManager = class {
|
|
|
10494
10576
|
const pc = content;
|
|
10495
10577
|
if (job.pendingPayment) {
|
|
10496
10578
|
const pp = job.pendingPayment;
|
|
10579
|
+
if (!await this.claimLocalInput(job)) {
|
|
10580
|
+
await this.adoptDurableTerminal(job);
|
|
10581
|
+
return;
|
|
10582
|
+
}
|
|
10583
|
+
if (job.pendingPayment !== pp || isTerminal(job.status)) return;
|
|
10497
10584
|
job.pendingPayment = null;
|
|
10498
|
-
job.status = "processing";
|
|
10499
|
-
|
|
10585
|
+
job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
|
|
10586
|
+
if (job.status === "awaiting-input") await this.persistJob(job);
|
|
10587
|
+
await pp.resolve(pc);
|
|
10500
10588
|
} else if (cap?.onPayment && ctx) {
|
|
10501
10589
|
await cap.onPayment(ctx, pc);
|
|
10502
10590
|
}
|
|
@@ -10505,6 +10593,7 @@ var JobManager = class {
|
|
|
10505
10593
|
} else if (type === "cancel") {
|
|
10506
10594
|
const reason = content?.reason ?? "Job cancelled";
|
|
10507
10595
|
abortJob(job, reason);
|
|
10596
|
+
job.endedBy = "caller";
|
|
10508
10597
|
job.status = "cancelled";
|
|
10509
10598
|
if (cap?.onCancel && ctx) {
|
|
10510
10599
|
await cap.onCancel(ctx, content);
|
|
@@ -10523,17 +10612,16 @@ var JobManager = class {
|
|
|
10523
10612
|
});
|
|
10524
10613
|
}
|
|
10525
10614
|
}
|
|
10526
|
-
// ── Persistence ──────────────────────────────────────────────────────
|
|
10527
10615
|
/**
|
|
10528
10616
|
* Persist a job to the backing store.
|
|
10529
10617
|
*
|
|
10530
|
-
* Awaits the per-job appender tail
|
|
10531
|
-
*
|
|
10532
|
-
*
|
|
10533
|
-
*
|
|
10534
|
-
*
|
|
10535
|
-
*
|
|
10536
|
-
*
|
|
10618
|
+
* Awaits the per-job appender tail so that:
|
|
10619
|
+
* 1. The snapshot's `messages` is consistent with the `job_messages` table
|
|
10620
|
+
* no row is still in flight at snapshot time.
|
|
10621
|
+
* 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
|
|
10622
|
+
* can't race a still-pending `appendOutgoing` for the final yield
|
|
10623
|
+
* message (which would otherwise wipe the row before an in-flight SSE
|
|
10624
|
+
* subscriber's NOTIFY-driven fetch can see it).
|
|
10537
10625
|
*/
|
|
10538
10626
|
async persistJob(job) {
|
|
10539
10627
|
if (job.messageAppenderTail) {
|
|
@@ -10554,9 +10642,17 @@ var JobManager = class {
|
|
|
10554
10642
|
} else {
|
|
10555
10643
|
await this.jobStore.save(record);
|
|
10556
10644
|
}
|
|
10645
|
+
const saved = await this.jobStore.get(job.id);
|
|
10646
|
+
if (saved) {
|
|
10647
|
+
job.timingKnown = saved.timingKnown;
|
|
10648
|
+
job.endedBy = saved.endedBy;
|
|
10649
|
+
job.terminalAtMs = saved.terminalAtMs;
|
|
10650
|
+
job.callerWaitMs = saved.callerWaitMs;
|
|
10651
|
+
job.awaitingInputAtMs = saved.awaitingInputAtMs;
|
|
10652
|
+
}
|
|
10557
10653
|
}
|
|
10558
10654
|
/**
|
|
10559
|
-
* Cross-machine terminal guard
|
|
10655
|
+
* Cross-machine terminal guard. `buildContext`'s guard reads the
|
|
10560
10656
|
* in-memory `job.status`, so on its own it only protects the machine running
|
|
10561
10657
|
* the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
|
|
10562
10658
|
* on a machine that isn't running the job — `app.ts` cancels it in the store
|
|
@@ -10568,7 +10664,7 @@ var JobManager = class {
|
|
|
10568
10664
|
* fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
|
|
10569
10665
|
* the durable state won and the caller must skip its save.
|
|
10570
10666
|
*
|
|
10571
|
-
* Two triggers: `watchDurableStatus`'s NOTIFY subscription
|
|
10667
|
+
* Two triggers: `watchDurableStatus`'s NOTIFY subscription fires
|
|
10572
10668
|
* this within a round-trip of the remote cancel committing, which is what
|
|
10573
10669
|
* actually stops the provider spend; `persistJob` calls it again on every save
|
|
10574
10670
|
* as the backstop for the window where a cancel commits between a handler's
|
|
@@ -10576,8 +10672,10 @@ var JobManager = class {
|
|
|
10576
10672
|
* stores' terminal-sticky `save`, which keeps the row itself correct).
|
|
10577
10673
|
*/
|
|
10578
10674
|
async adoptDurableTerminal(job) {
|
|
10579
|
-
const
|
|
10580
|
-
if (!
|
|
10675
|
+
const counters = isStreamableJobStore(this.jobStore) ? await this.jobStore.getCounters(job.id).catch(() => void 0) : await this.jobStore.get(job.id).catch(() => void 0);
|
|
10676
|
+
if (!counters || !isTerminal(counters.status) || counters.status === job.status) return false;
|
|
10677
|
+
const durable = await this.jobStore.get(job.id);
|
|
10678
|
+
if (!durable) return false;
|
|
10581
10679
|
console.warn(
|
|
10582
10680
|
JSON.stringify({
|
|
10583
10681
|
level: "job_terminal_elsewhere",
|
|
@@ -10588,6 +10686,11 @@ var JobManager = class {
|
|
|
10588
10686
|
);
|
|
10589
10687
|
job.status = durable.status;
|
|
10590
10688
|
job.summary = durable.summary;
|
|
10689
|
+
job.timingKnown = durable.timingKnown;
|
|
10690
|
+
job.endedBy = durable.endedBy;
|
|
10691
|
+
job.terminalAtMs = durable.terminalAtMs;
|
|
10692
|
+
job.callerWaitMs = durable.callerWaitMs;
|
|
10693
|
+
job.awaitingInputAtMs = durable.awaitingInputAtMs;
|
|
10591
10694
|
const reason = durable.status !== "completed" && durable.summary ? durable.summary : `Job ${durable.status}`;
|
|
10592
10695
|
for (const [, p] of job.pendingPrompts) p.reject(new JobCancelledError(reason));
|
|
10593
10696
|
job.pendingPayment?.reject(new JobCancelledError(reason));
|
|
@@ -10607,7 +10710,7 @@ var JobManager = class {
|
|
|
10607
10710
|
});
|
|
10608
10711
|
}
|
|
10609
10712
|
this.clearIdleTimer(job.id);
|
|
10610
|
-
void this.issueReceiptForStoredJob(job.id);
|
|
10713
|
+
void this.issueReceiptForStoredJob(job.id).then(() => this.reportTerminalJob(job.id));
|
|
10611
10714
|
this.scheduleCleanup(job);
|
|
10612
10715
|
if (durable.status !== "completed" && terminalCostPersisted) {
|
|
10613
10716
|
await this.reportJobCost(job);
|
|
@@ -10653,6 +10756,7 @@ var JobManager = class {
|
|
|
10653
10756
|
async retryTerminalCostHandoff(job, attempt) {
|
|
10654
10757
|
if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
|
|
10655
10758
|
await this.reportJobCost(job);
|
|
10759
|
+
await this.reportTerminalJob(job.id);
|
|
10656
10760
|
}
|
|
10657
10761
|
/** Durably merge a terminal handler owner's latest cost before reporting it. */
|
|
10658
10762
|
async persistAdoptedTerminalCost(job) {
|
|
@@ -10682,7 +10786,7 @@ var JobManager = class {
|
|
|
10682
10786
|
);
|
|
10683
10787
|
}
|
|
10684
10788
|
/**
|
|
10685
|
-
* Watch the durable status of a locally-active job
|
|
10789
|
+
* Watch the durable status of a locally-active job.
|
|
10686
10790
|
*
|
|
10687
10791
|
* `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
|
|
10688
10792
|
* holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
|
|
@@ -10723,7 +10827,7 @@ var JobManager = class {
|
|
|
10723
10827
|
});
|
|
10724
10828
|
}
|
|
10725
10829
|
/**
|
|
10726
|
-
* Tear down a job's durable-status subscription
|
|
10830
|
+
* Tear down a job's durable-status subscription. Must be called
|
|
10727
10831
|
* wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
|
|
10728
10832
|
* the store's shared LISTEN connection. Idempotent.
|
|
10729
10833
|
*/
|
|
@@ -10735,12 +10839,12 @@ var JobManager = class {
|
|
|
10735
10839
|
unsubscribe();
|
|
10736
10840
|
}
|
|
10737
10841
|
/**
|
|
10738
|
-
* NOTIFY-driven durable-status re-read, coalesced per job
|
|
10842
|
+
* NOTIFY-driven durable-status re-read, coalesced per job.
|
|
10739
10843
|
*
|
|
10740
10844
|
* A notify that arrives while a re-read is in flight is queued rather than
|
|
10741
10845
|
* dropped: the in-flight read may have observed the row a moment *before* the
|
|
10742
|
-
* cancel committed, and dropping its notify would
|
|
10743
|
-
*
|
|
10846
|
+
* cancel committed, and dropping its notify would leave the local handler
|
|
10847
|
+
* running until its next store write instead of aborting promptly.
|
|
10744
10848
|
*/
|
|
10745
10849
|
async checkDurableTerminal(job) {
|
|
10746
10850
|
if (this.statusChecksInFlight.has(job.id)) {
|
|
@@ -10770,13 +10874,13 @@ var JobManager = class {
|
|
|
10770
10874
|
/**
|
|
10771
10875
|
* Wire the Tx A appender when the store supports streaming.
|
|
10772
10876
|
*
|
|
10773
|
-
* Tx A
|
|
10877
|
+
* Tx A: outgoing message + `pending_payment_msats` bump land in
|
|
10774
10878
|
* the same transaction. The DB allocates the row's seq via
|
|
10775
10879
|
* `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
|
|
10776
10880
|
* traffic on different machines can never collide on the `(job_id, seq)` PK.
|
|
10777
10881
|
* In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
|
|
10778
10882
|
*
|
|
10779
|
-
* Per-job serialisation
|
|
10883
|
+
* Per-job serialisation: chained through `job.messageAppenderTail`
|
|
10780
10884
|
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
10781
10885
|
* `complete`) commit their `appendOutgoing` transactions in call order.
|
|
10782
10886
|
* Without this, the two transactions race for the `jobs` row lock and the
|
|
@@ -10810,7 +10914,7 @@ var JobManager = class {
|
|
|
10810
10914
|
// In-memory counter, NOT the authoritative DB-allocated seq:
|
|
10811
10915
|
// `appendOutgoing` allocates the real seq via `UPDATE ...
|
|
10812
10916
|
// RETURNING` inside the transaction that just failed, so no DB
|
|
10813
|
-
// seq was assigned for this message
|
|
10917
|
+
// seq was assigned for this message.
|
|
10814
10918
|
localSeq: msg.seq,
|
|
10815
10919
|
note: "localSeq is the in-process counter; the DB-allocated seq was never assigned (append rolled back)",
|
|
10816
10920
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -10821,7 +10925,6 @@ var JobManager = class {
|
|
|
10821
10925
|
job.messageAppenderTail = next;
|
|
10822
10926
|
};
|
|
10823
10927
|
}
|
|
10824
|
-
// ── Idle timeout ─────────────────────────────────────────────────────
|
|
10825
10928
|
/** Start (or restart) the idle timer for a job. */
|
|
10826
10929
|
startIdleTimer(job) {
|
|
10827
10930
|
this.clearIdleTimer(job.id);
|
|
@@ -10839,7 +10942,7 @@ var JobManager = class {
|
|
|
10839
10942
|
this.idleTimers.delete(id);
|
|
10840
10943
|
}
|
|
10841
10944
|
}
|
|
10842
|
-
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard
|
|
10945
|
+
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard. */
|
|
10843
10946
|
async cancelJobIdle(job) {
|
|
10844
10947
|
if (isTerminal(job.status)) return;
|
|
10845
10948
|
const current = await this.jobStore.get(job.id).catch(() => void 0);
|
|
@@ -10858,6 +10961,7 @@ var JobManager = class {
|
|
|
10858
10961
|
if (job.pendingPayment) job.pendingPayment.reject(new Error("Job timed out (idle)"));
|
|
10859
10962
|
job.pendingPrompts.clear();
|
|
10860
10963
|
job.pendingPayment = null;
|
|
10964
|
+
job.endedBy = (current?.status ?? job.status) === "awaiting-input" ? "caller" : "provider";
|
|
10861
10965
|
job.status = "cancelled";
|
|
10862
10966
|
abortJob(job, "Job timed out (idle)");
|
|
10863
10967
|
providerMessage(job, "cancel", { reason: "Job timed out (idle)" });
|
|
@@ -10865,14 +10969,13 @@ var JobManager = class {
|
|
|
10865
10969
|
void job.terminalWork;
|
|
10866
10970
|
this.scheduleCleanup(job);
|
|
10867
10971
|
}
|
|
10868
|
-
// ── Stale-job sweeper (internal-review / internal-review) ──────────────────────────
|
|
10869
10972
|
/**
|
|
10870
10973
|
* Force-terminate stale non-terminal jobs. Two status-aware arms:
|
|
10871
|
-
*
|
|
10872
|
-
*
|
|
10873
|
-
*
|
|
10874
|
-
*
|
|
10875
|
-
*
|
|
10974
|
+
* • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
|
|
10975
|
+
* (`stale_no_terminal_status`) — the caller never paid / responded.
|
|
10976
|
+
* • `processing`/`working` past `processingWatchdogMs` → `failed`
|
|
10977
|
+
* (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
|
|
10978
|
+
* live workers fresh, so a stale row here means a dead process.
|
|
10876
10979
|
*
|
|
10877
10980
|
* Fires on a timer (wired in the constructor) and also callable on demand
|
|
10878
10981
|
* (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
|
|
@@ -10881,17 +10984,17 @@ var JobManager = class {
|
|
|
10881
10984
|
* descriptors — the local activeJobs teardown only makes sense on the
|
|
10882
10985
|
* JobManager that actually hosted the zombie handler.
|
|
10883
10986
|
*
|
|
10884
|
-
* **Both cutoffs float off the wall clock
|
|
10987
|
+
* **Both cutoffs float off the wall clock.** Each is one
|
|
10885
10988
|
* `Date.now()` sample compared against `last_activity_at`, stamped from a
|
|
10886
10989
|
* different sample at a different moment, so a backwards step between the
|
|
10887
10990
|
* two makes every row look more recently active than it is: the query
|
|
10888
10991
|
* matches nothing and both arms go silent for the length of the skew. The
|
|
10889
10992
|
* dead worker's job keeps its `processing` status, its credit hold stays
|
|
10890
|
-
* `pending`, and the
|
|
10891
|
-
* never fires. Same defect, same host class, as
|
|
10993
|
+
* `pending`, and the paid-job-death alert — wired to this reaper —
|
|
10994
|
+
* never fires. Same defect, same host class, as orphan sweep.
|
|
10892
10995
|
*
|
|
10893
10996
|
* **The compensation is capped** — see {@link staleSweepCompensationCapMs}
|
|
10894
|
-
* for where each arm's cap comes from — which
|
|
10997
|
+
* for where each arm's cap comes from — which did not need to do. Its eagerness costs at worst an early release
|
|
10895
10998
|
* of an unclaimed hold, refused outright for any live job row; ours
|
|
10896
10999
|
* force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
|
|
10897
11000
|
* `expectedActivityBefore` is the same compensated threshold the query used,
|
|
@@ -11027,7 +11130,7 @@ var JobManager = class {
|
|
|
11027
11130
|
}
|
|
11028
11131
|
/**
|
|
11029
11132
|
* How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
|
|
11030
|
-
* clock to cover a backwards step
|
|
11133
|
+
* clock to cover a backwards step. The compensation is capped,
|
|
11031
11134
|
* not free: a claimed row must still have been silent for
|
|
11032
11135
|
* `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
|
|
11033
11136
|
* force-fails a job that may well be alive.
|
|
@@ -11047,7 +11150,7 @@ var JobManager = class {
|
|
|
11047
11150
|
* clock stepped. That pair is marginal already; the guard must not make it
|
|
11048
11151
|
* deterministic. The cap collapses to zero there, which is today's
|
|
11049
11152
|
* behaviour: late, never wrong. The `awaiting-input` arm takes no such term
|
|
11050
|
-
*
|
|
11153
|
+
* nothing heartbeats it by design, and its refresh is an inbound caller
|
|
11051
11154
|
* message on no cadence at all.
|
|
11052
11155
|
*
|
|
11053
11156
|
* **Why a magnitude and not a predicate.** The tempting sharper rule is to
|
|
@@ -11066,7 +11169,6 @@ var JobManager = class {
|
|
|
11066
11169
|
if (!heartbeated || this.heartbeatIntervalMs <= 0) return halfWindow;
|
|
11067
11170
|
return Math.max(0, Math.min(halfWindow, windowMs - 2 * this.heartbeatIntervalMs));
|
|
11068
11171
|
}
|
|
11069
|
-
// ── Terminal job content retention ───────────────────────────────────
|
|
11070
11172
|
/**
|
|
11071
11173
|
* Redact terminal job content beyond this DVM's configured window.
|
|
11072
11174
|
*
|
|
@@ -11231,12 +11333,11 @@ var JobManager = class {
|
|
|
11231
11333
|
);
|
|
11232
11334
|
}
|
|
11233
11335
|
}
|
|
11234
|
-
// ── Orphan-draw sweeper (internal-review) + terminal reconciler (internal-review) ──
|
|
11235
11336
|
/**
|
|
11236
11337
|
* Resolve `pending` credit draws the terminal funnel can no longer reach.
|
|
11237
11338
|
* Two arms over one scan, distinguished by whether the draw's job row exists.
|
|
11238
11339
|
*
|
|
11239
|
-
* **No job row
|
|
11340
|
+
* **No job row — release.** allocates the job id and
|
|
11240
11341
|
* commits the ledger draw, with its `job_id` linkage, before `recordReplay`
|
|
11241
11342
|
* and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
|
|
11242
11343
|
* fail-closed `replay_detected` 401 in that window leaves a committed hold
|
|
@@ -11246,12 +11347,12 @@ var JobManager = class {
|
|
|
11246
11347
|
* available balance stays reduced forever. Cosmetic for an implicit N=1
|
|
11247
11348
|
* credit, a silent balance shrink for an explicit N>1 one.
|
|
11248
11349
|
*
|
|
11249
|
-
* **Terminal job row
|
|
11350
|
+
* **Terminal job row — reconcile to its outcome.** The funnel
|
|
11250
11351
|
* demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
|
|
11251
11352
|
* `issueReceipt` catches it, logs `credit_resolve_failed` and returns
|
|
11252
11353
|
* nothing, and until now nothing retried. Same stranded hold, reached through
|
|
11253
11354
|
* a different door — and for a `completed` job it strands the platform's
|
|
11254
|
-
* books too, since draws *are* the revenue events
|
|
11355
|
+
* books too, since draws *are* the revenue events: the caller is
|
|
11255
11356
|
* charged nothing, the balance never moves, and outstanding liability
|
|
11256
11357
|
* (`deposits − draw revenue`) overstates forever. So a late settle books, off
|
|
11257
11358
|
* the committed draw row, through the ordinary reporter path.
|
|
@@ -11379,8 +11480,8 @@ var JobManager = class {
|
|
|
11379
11480
|
}
|
|
11380
11481
|
}
|
|
11381
11482
|
/**
|
|
11382
|
-
* The end-of-scan line for a tick that resolved nothing
|
|
11383
|
-
*
|
|
11483
|
+
* The end-of-scan line for a tick that resolved nothing. Two shapes are
|
|
11484
|
+
* worth a warning, and neither is the ordinary
|
|
11384
11485
|
* one — the timer runs every 5 minutes per ledger in production.
|
|
11385
11486
|
*
|
|
11386
11487
|
* `examined > 0`: aged candidates were looked at and none moved. Usually
|
|
@@ -11389,7 +11490,7 @@ var JobManager = class {
|
|
|
11389
11490
|
*
|
|
11390
11491
|
* `examined === 0`: the query returned nothing. Ordinarily that means there
|
|
11391
11492
|
* is nothing to do — but it is equally what a blinded scan looks like, which
|
|
11392
|
-
* is the case the
|
|
11493
|
+
* is the case the gate could not reach. Probe for the oldest
|
|
11393
11494
|
* `pending` hold at any age and speak up when one exists the scan should
|
|
11394
11495
|
* have seen and didn't: already past the age gate outright, stamped ahead of
|
|
11395
11496
|
* our clock (some other process's clock is fast), or hidden while our own
|
|
@@ -11441,7 +11542,7 @@ var JobManager = class {
|
|
|
11441
11542
|
);
|
|
11442
11543
|
}
|
|
11443
11544
|
/**
|
|
11444
|
-
* Resolve one aged `pending` draw whose job row reached terminal
|
|
11545
|
+
* Resolve one aged `pending` draw whose job row reached terminal,
|
|
11445
11546
|
* mirroring what `resolveCreditDraw` would have done in line. Returns true
|
|
11446
11547
|
* when this call is the one that moved the draw — the caller counts it, and
|
|
11447
11548
|
* only it books.
|
|
@@ -11514,7 +11615,7 @@ var JobManager = class {
|
|
|
11514
11615
|
/**
|
|
11515
11616
|
* Refresh `lastActivityAt` for jobs this process is actively running
|
|
11516
11617
|
* (`processing`/`working`) so the processing watchdog only fires once the
|
|
11517
|
-
* worker is genuinely dead
|
|
11618
|
+
* worker is genuinely dead. `awaiting-input` jobs are
|
|
11518
11619
|
* deliberately excluded — their idle timeout must still elapse. Fires on the
|
|
11519
11620
|
* heartbeat timer; also callable on demand for tests.
|
|
11520
11621
|
*/
|
|
@@ -11534,17 +11635,16 @@ var JobManager = class {
|
|
|
11534
11635
|
this.heartbeatInFlight = false;
|
|
11535
11636
|
}
|
|
11536
11637
|
}
|
|
11537
|
-
// ── Reactivation (replay) ────────────────────────────────────────────
|
|
11538
11638
|
/**
|
|
11539
|
-
* Single-execution claim for a reactivating machine
|
|
11639
|
+
* Single-execution claim for a reactivating machine. Before
|
|
11540
11640
|
* replaying an `awaiting-input` job, wait a bounded grace window for a live
|
|
11541
11641
|
* original handler to win the `awaiting-input → processing` CAS via its
|
|
11542
11642
|
* NOTIFY wake. Resolves:
|
|
11543
|
-
*
|
|
11544
|
-
*
|
|
11545
|
-
*
|
|
11546
|
-
*
|
|
11547
|
-
*
|
|
11643
|
+
* • `false` — the row left `awaiting-input` during the window (the original
|
|
11644
|
+
* handler claimed it / drove it terminal). Stand down; do not replay.
|
|
11645
|
+
* • `true` — the window elapsed still `awaiting-input` (the original worker
|
|
11646
|
+
* is gone or has no live resolver) AND this machine won the claim CAS.
|
|
11647
|
+
* Replay for dead-worker recovery.
|
|
11548
11648
|
*
|
|
11549
11649
|
* Biasing the original handler to win kills the double execution (double
|
|
11550
11650
|
* substrate spend, clobbered artifact) without heartbeating `awaiting-input`
|
|
@@ -11567,7 +11667,7 @@ var JobManager = class {
|
|
|
11567
11667
|
}
|
|
11568
11668
|
}
|
|
11569
11669
|
/**
|
|
11570
|
-
* Record an inbound client message durably via Tx B
|
|
11670
|
+
* Record an inbound client message durably via Tx B and append
|
|
11571
11671
|
* it to the in-memory `job.messages` array. Returns the DB-allocated seq
|
|
11572
11672
|
* (`null` when the store isn't streamable — non-Postgres test fallback).
|
|
11573
11673
|
*
|
|
@@ -11611,13 +11711,13 @@ var JobManager = class {
|
|
|
11611
11711
|
* in-memory `ServerJob`, wires the appender, builds the context, starts
|
|
11612
11712
|
* the idle timer. The caller has already recorded the inbound via Tx B
|
|
11613
11713
|
* and run Tx C (verify + credit); reactivation is now decoupled from
|
|
11614
|
-
* credit
|
|
11714
|
+
* credit. The caller decides whether to run the
|
|
11615
11715
|
* handler (Pattern A: status was `awaiting-input`) or to dispatch the
|
|
11616
11716
|
* message to a custom handler (Pattern B).
|
|
11617
11717
|
*
|
|
11618
11718
|
* For DVMs that declare descriptor-level auth, the persisted
|
|
11619
11719
|
* `record.input` carries the original signed envelope. We re-verify the
|
|
11620
|
-
* signature here
|
|
11720
|
+
* signature here so a DB-layer tamper — compromised admin, SQL
|
|
11621
11721
|
* injection, malicious operator with DB access — can't silently feed an
|
|
11622
11722
|
* attacker-supplied pubkey/envelope into the handler. The drift window
|
|
11623
11723
|
* and replay store are deliberately skipped: the persisted timestamp is
|
|
@@ -11631,7 +11731,7 @@ var JobManager = class {
|
|
|
11631
11731
|
* What gets re-verified is the persisted row read back through
|
|
11632
11732
|
* {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
|
|
11633
11733
|
* those are the caller's own signed bytes, the same ones `/v1/quote` and the
|
|
11634
|
-
* submit checked
|
|
11734
|
+
* submit checked. That makes the tamper check strictly stronger
|
|
11635
11735
|
* than the parsed form it replaced: an injected key the capability schema
|
|
11636
11736
|
* doesn't declare used to be stripped before the signature was checked, so
|
|
11637
11737
|
* it re-verified clean.
|
|
@@ -11676,7 +11776,7 @@ var JobManager = class {
|
|
|
11676
11776
|
return { job, sdkCtx };
|
|
11677
11777
|
}
|
|
11678
11778
|
/**
|
|
11679
|
-
* Verify an inbound payment and apply the credit via Tx C
|
|
11779
|
+
* Verify an inbound payment and apply the credit via Tx C.
|
|
11680
11780
|
* The inbound message must have been recorded via Tx B already
|
|
11681
11781
|
* (`recordInboundPending` returned `inboundSeq`). For non-streamable
|
|
11682
11782
|
* stores (`inboundSeq === null`), falls back to in-memory mutation via
|
|
@@ -11686,12 +11786,12 @@ var JobManager = class {
|
|
|
11686
11786
|
* `pendingPaymentMsats` without external verification. Gated on
|
|
11687
11787
|
* `devModeSkipsPaymentVerification` — the same predicate the upfront path
|
|
11688
11788
|
* reads, so a dev server wired to a mint verifies mid-job payments for real
|
|
11689
|
-
*
|
|
11789
|
+
* plus the explicit `dev_auto` opt-out the dev console uses,
|
|
11690
11790
|
* which is the one caller that has no wallet to pay from. `dev_auto` loses to
|
|
11691
11791
|
* any proof riding the same message: money on the wire always takes the rail,
|
|
11692
11792
|
* so the flag can never leave a real token unspent against a credited job.
|
|
11693
11793
|
*
|
|
11694
|
-
* Tx C is not atomic with the rail commit
|
|
11794
|
+
* Tx C is not atomic with the rail commit. `verifyIncomingPayment`
|
|
11695
11795
|
* commits the proofs *and* the ledger leg in one transaction and returns;
|
|
11696
11796
|
* only then does `verifyAndCredit` write the job row, on its own connection.
|
|
11697
11797
|
* Everything after that call therefore runs with the caller's money already
|
|
@@ -11741,7 +11841,7 @@ var JobManager = class {
|
|
|
11741
11841
|
lockPubkeys: extra?.lockPubkeys,
|
|
11742
11842
|
mintHealthTracker: extra?.mintHealthTracker,
|
|
11743
11843
|
fxFetcher: this.opts.fxFetcher,
|
|
11744
|
-
//
|
|
11844
|
+
// the ledger leg — a mid-job payment is a top-up that funds
|
|
11745
11845
|
// the job's credit and grows its draw, atomically with the rail commit.
|
|
11746
11846
|
creditLedger: this.opts.creditLedger,
|
|
11747
11847
|
processedPayments: this.opts.processedPayments,
|
|
@@ -11876,7 +11976,7 @@ var JobManager = class {
|
|
|
11876
11976
|
return { ...unappliedCredit && { unappliedCredit } };
|
|
11877
11977
|
}
|
|
11878
11978
|
/**
|
|
11879
|
-
* Tx C with a bounded retry
|
|
11979
|
+
* Tx C with a bounded retry.
|
|
11880
11980
|
*
|
|
11881
11981
|
* By the time this runs on the verified path, the rail commit and the ledger
|
|
11882
11982
|
* leg have already committed in a transaction this call is not part of — so a
|
|
@@ -11963,7 +12063,7 @@ var JobManager = class {
|
|
|
11963
12063
|
}
|
|
11964
12064
|
/**
|
|
11965
12065
|
* Answer a payment whose rail and ledger legs committed but whose job row
|
|
11966
|
-
* could not be written
|
|
12066
|
+
* could not be written — every retry spent, the money real.
|
|
11967
12067
|
*
|
|
11968
12068
|
* The refusal names the credit the money landed on so the caller can act on
|
|
11969
12069
|
* it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
|
|
@@ -11972,7 +12072,7 @@ var JobManager = class {
|
|
|
11972
12072
|
* fault an operator should see in their 5xx rate. `retryable` is true because
|
|
11973
12073
|
* the ask is genuinely still outstanding.
|
|
11974
12074
|
*
|
|
11975
|
-
* Nothing is released here. The hold is left to the
|
|
12075
|
+
* Nothing is released here. The hold is left to the reconciler,
|
|
11976
12076
|
* which makes the same decision — unbound draw ⇒ release, never settle — but
|
|
11977
12077
|
* under the credit row's `FOR UPDATE`, after the job is terminal, where it
|
|
11978
12078
|
* cannot race an in-flight COMMIT. Releasing from here would also be
|
|
@@ -12039,7 +12139,7 @@ var JobManager = class {
|
|
|
12039
12139
|
}
|
|
12040
12140
|
/**
|
|
12041
12141
|
* Describe a deposit the job row never took, in the shape both the 201 and
|
|
12042
|
-
* the `payment_unapplied` 500 carry
|
|
12142
|
+
* the `payment_unapplied` 500 carry. `undefined` when the payment
|
|
12043
12143
|
* ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
|
|
12044
12144
|
* there is no credit to name, so the 201 carries no `credit` block and the
|
|
12045
12145
|
* 500 takes its words from `unappliedCopy` directly.
|
|
@@ -12053,7 +12153,7 @@ var JobManager = class {
|
|
|
12053
12153
|
...unappliedCopy(reason, creditId)
|
|
12054
12154
|
};
|
|
12055
12155
|
}
|
|
12056
|
-
/** Book a mid-job top-up's funding as a deposit (
|
|
12156
|
+
/** Book a mid-job top-up's funding as a deposit (the deposit-accounting rule). */
|
|
12057
12157
|
reportCreditDeposit(funding) {
|
|
12058
12158
|
const onCreditFunded = this.opts.onCreditFunded;
|
|
12059
12159
|
if (!funding || !onCreditFunded || !this.opts.dvmId) return;
|
|
@@ -12066,7 +12166,7 @@ var JobManager = class {
|
|
|
12066
12166
|
/**
|
|
12067
12167
|
* Internal: build the BuildContextOpts shared between `createJob` and
|
|
12068
12168
|
* `reactivateJob`. Centralised so persistence/terminal callbacks and the
|
|
12069
|
-
*
|
|
12169
|
+
* NOTIFY-driven cross-machine wake stay in one place.
|
|
12070
12170
|
*/
|
|
12071
12171
|
buildContextOpts(job) {
|
|
12072
12172
|
const jobStore = this.jobStore;
|
|
@@ -12085,7 +12185,16 @@ var JobManager = class {
|
|
|
12085
12185
|
getCounters: isStreamableJobStore(jobStore) ? () => jobStore.getCounters(job.id) : void 0,
|
|
12086
12186
|
drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
|
|
12087
12187
|
pricingCurrency: this.pricingCurrency,
|
|
12088
|
-
claimForProcessing: isStreamableJobStore(jobStore) ? () =>
|
|
12188
|
+
claimForProcessing: isStreamableJobStore(jobStore) ? (resume) => {
|
|
12189
|
+
const pending = job.pendingPayment;
|
|
12190
|
+
return this.queueInputResume(job, async () => {
|
|
12191
|
+
if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
12192
|
+
if (!await this.claimLocalInput(job)) return false;
|
|
12193
|
+
if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
12194
|
+
await resume?.();
|
|
12195
|
+
return true;
|
|
12196
|
+
});
|
|
12197
|
+
} : void 0,
|
|
12089
12198
|
onSuperseded: () => {
|
|
12090
12199
|
this.clearIdleTimer(job.id);
|
|
12091
12200
|
this.unwatchDurableStatus(job.id);
|
|
@@ -12096,7 +12205,7 @@ var JobManager = class {
|
|
|
12096
12205
|
}
|
|
12097
12206
|
/**
|
|
12098
12207
|
* Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
|
|
12099
|
-
* job's remaining pool in
|
|
12208
|
+
* job's remaining pool in.
|
|
12100
12209
|
*
|
|
12101
12210
|
* The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
|
|
12102
12211
|
* `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
|
|
@@ -12114,8 +12223,7 @@ var JobManager = class {
|
|
|
12114
12223
|
* drew and whose pool this can't read or denominate answers `unpriced`, which
|
|
12115
12224
|
* the gate declines to auto-credit from: the msat figure it would otherwise
|
|
12116
12225
|
* fall back on is a slice of the credit's rail value at a ratio pinned
|
|
12117
|
-
* whenever that credit was funded,
|
|
12118
|
-
* to stop spending against.
|
|
12226
|
+
* whenever that credit was funded, so it cannot safely fund a new ask.
|
|
12119
12227
|
*/
|
|
12120
12228
|
async resolveDrawPool(job) {
|
|
12121
12229
|
const ledger = this.opts.creditLedger;
|
|
@@ -12164,7 +12272,7 @@ var JobManager = class {
|
|
|
12164
12272
|
}
|
|
12165
12273
|
/**
|
|
12166
12274
|
* Issue the signed receipt for a job that has reached a terminal status
|
|
12167
|
-
*
|
|
12275
|
+
* Idempotent and safe to call from every terminal path — the
|
|
12168
12276
|
* store owns both the sequence allocation and the write-once persist, so
|
|
12169
12277
|
* two machines racing on the same job converge on identical bytes and burn
|
|
12170
12278
|
* exactly one sequence number.
|
|
@@ -12249,10 +12357,10 @@ var JobManager = class {
|
|
|
12249
12357
|
};
|
|
12250
12358
|
}
|
|
12251
12359
|
/**
|
|
12252
|
-
* Settle or release a terminal job's credit draw
|
|
12360
|
+
* Settle or release a terminal job's credit draw: `completed`
|
|
12253
12361
|
* settles (the hold becomes a real debit); `failed`/`cancelled` releases
|
|
12254
12362
|
* (the hold evaporates — "no debit on job failure", mechanically
|
|
12255
|
-
* superseding the
|
|
12363
|
+
* superseding the 974 no-op refund for credit-paid jobs, including
|
|
12256
12364
|
* the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
|
|
12257
12365
|
* resolution returns the recorded state. Returns the `ReceiptCredit` block
|
|
12258
12366
|
* for the receipt: `balance_after` is the recorded draw trajectory for a
|
|
@@ -12298,7 +12406,7 @@ var JobManager = class {
|
|
|
12298
12406
|
}
|
|
12299
12407
|
}
|
|
12300
12408
|
/**
|
|
12301
|
-
* The one place a terminal job's status becomes a ledger verb
|
|
12409
|
+
* The one place a terminal job's status becomes a ledger verb:
|
|
12302
12410
|
* `completed` settles the hold into a real debit, `failed`/`cancelled`
|
|
12303
12411
|
* release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
|
|
12304
12412
|
* late reconciler (`reconcileTerminalDraw`) — two paths that must never
|
|
@@ -12313,45 +12421,47 @@ var JobManager = class {
|
|
|
12313
12421
|
* cleanup window hand back the same bytes as a cross-machine re-read.
|
|
12314
12422
|
*/
|
|
12315
12423
|
async attachReceipt(job) {
|
|
12316
|
-
const
|
|
12424
|
+
const record = await this.jobStore.get(job.id);
|
|
12425
|
+
if (!record || !isTerminal(record.status)) return;
|
|
12426
|
+
const receipt = await this.issueReceipt(record);
|
|
12317
12427
|
if (receipt) job.receipt = receipt;
|
|
12318
12428
|
}
|
|
12319
12429
|
/**
|
|
12320
|
-
* Read-path repair for a terminal job carrying no receipt
|
|
12430
|
+
* Read-path repair for a terminal job carrying no receipt, for
|
|
12321
12431
|
* either an in-memory job or a store record. Free in the steady state — it
|
|
12322
12432
|
* returns on the `receipt` check without touching the store — so it costs
|
|
12323
12433
|
* only on the cases it exists for:
|
|
12324
12434
|
*
|
|
12325
12435
|
* - **A crash between the two store calls.** `claimReceiptSeq` commits the
|
|
12326
|
-
*
|
|
12327
|
-
*
|
|
12328
|
-
*
|
|
12329
|
-
*
|
|
12330
|
-
*
|
|
12331
|
-
*
|
|
12436
|
+
* sequence number to the job row before `saveReceipt` writes the bytes; a
|
|
12437
|
+
* process death in that window would otherwise strand that number
|
|
12438
|
+
* forever, and a permanent gap is indistinguishable from the deliberate
|
|
12439
|
+
* suppression `seq` exists to expose. Re-issuing here reuses the already
|
|
12440
|
+
* committed number (the claim is idempotent per job) rather than
|
|
12441
|
+
* allocating a second one.
|
|
12332
12442
|
* - **A transient store failure** at the terminal: `issueReceipt` logs
|
|
12333
|
-
*
|
|
12334
|
-
*
|
|
12443
|
+
* `receipt_issue_failed` and returns nothing rather than failing the job,
|
|
12444
|
+
* so the next read retries.
|
|
12335
12445
|
* - **Jobs that terminated before the DVM had a receipt key.** They pick one
|
|
12336
|
-
*
|
|
12446
|
+
* up on first read, with `issued_at` reflecting when it was signed.
|
|
12337
12447
|
*/
|
|
12338
12448
|
async ensureReceipt(target) {
|
|
12339
|
-
if (target.receipt)
|
|
12340
|
-
|
|
12449
|
+
if (target.receipt) {
|
|
12450
|
+
if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
|
|
12451
|
+
return;
|
|
12452
|
+
}
|
|
12453
|
+
const record = "listeners" in target ? await this.jobStore.get(target.id) : target;
|
|
12454
|
+
if (!record || !isTerminal(record.status)) return;
|
|
12341
12455
|
const receipt = await this.issueReceipt(record);
|
|
12342
12456
|
if (receipt) target.receipt = receipt;
|
|
12457
|
+
if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
|
|
12343
12458
|
}
|
|
12344
|
-
/**
|
|
12345
|
-
* Close out a terminal job whose caller hasn't already persisted it: sign
|
|
12346
|
-
* the receipt, then save the snapshot. The snapshot save never writes the
|
|
12347
|
-
* receipt column, so ordering only affects how soon a reader sees the
|
|
12348
|
-
* receipt — this way a caller polling immediately after the terminal
|
|
12349
|
-
* already finds it.
|
|
12350
|
-
*/
|
|
12459
|
+
/** Commit the terminal snapshot before resolving money and signing its durable evidence. */
|
|
12351
12460
|
async finalizeTerminal(job) {
|
|
12461
|
+
await this.persistJob(job);
|
|
12352
12462
|
await this.attachReceipt(job);
|
|
12353
|
-
await this.persistJob(job).catch(() => void 0);
|
|
12354
12463
|
await this.reportJobCost(job);
|
|
12464
|
+
await this.reportTerminalJob(job.id);
|
|
12355
12465
|
}
|
|
12356
12466
|
/**
|
|
12357
12467
|
* Issue a receipt for a job this process doesn't hold in `activeJobs` — a
|
|
@@ -12370,20 +12480,69 @@ var JobManager = class {
|
|
|
12370
12480
|
}
|
|
12371
12481
|
/**
|
|
12372
12482
|
* Finish accounting for a terminal written directly to the store — the
|
|
12373
|
-
* stale reaper and a cancel handled on a different machine
|
|
12483
|
+
* stale reaper and a cancel handled on a different machine.
|
|
12374
12484
|
*/
|
|
12375
12485
|
async finalizeStoredTerminal(jobId) {
|
|
12376
12486
|
const receipt = await this.issueReceiptForStoredJob(jobId);
|
|
12377
12487
|
const record = await this.jobStore.get(jobId).catch(() => void 0);
|
|
12378
|
-
if (record && isTerminal(record.status))
|
|
12488
|
+
if (record && isTerminal(record.status)) {
|
|
12489
|
+
await this.reportJobCost(record);
|
|
12490
|
+
await this.reportTerminalJob(jobId);
|
|
12491
|
+
}
|
|
12379
12492
|
return receipt;
|
|
12380
12493
|
}
|
|
12494
|
+
async reportTerminalJob(jobId) {
|
|
12495
|
+
try {
|
|
12496
|
+
const callback = this.opts.onJobTerminal;
|
|
12497
|
+
const dvmId = this.opts.dvmId;
|
|
12498
|
+
if (!callback || !dvmId) return;
|
|
12499
|
+
const record = await this.jobStore.get(jobId);
|
|
12500
|
+
if (!record || !isReceiptableStatus(record.status) || record.timingKnown === false || record.terminalAtMs === void 0 || record.status !== "completed" && !record.endedBy) return;
|
|
12501
|
+
const receipt = await this.issueReceipt(record);
|
|
12502
|
+
if (this.opts.receiptIssuer && !receipt) return;
|
|
12503
|
+
if (record.creditId && record.drawId && this.opts.creditLedger) {
|
|
12504
|
+
const draw = await this.opts.creditLedger.getDraw({ creditId: record.creditId, drawId: record.drawId });
|
|
12505
|
+
if (!draw || draw.status === "pending") return;
|
|
12506
|
+
}
|
|
12507
|
+
await callback({
|
|
12508
|
+
dvmId,
|
|
12509
|
+
jobId: record.id,
|
|
12510
|
+
capability: record.capability,
|
|
12511
|
+
outcome: record.status,
|
|
12512
|
+
endedBy: record.status === "completed" ? null : record.endedBy ?? "provider",
|
|
12513
|
+
paid: record.paidMsats > 0 || (record.nativeAmount ?? 0) > 0 || !!record.drawId,
|
|
12514
|
+
acceptedAtMs: record.createdAt,
|
|
12515
|
+
terminalAtMs: record.terminalAtMs,
|
|
12516
|
+
callerWaitMs: record.callerWaitMs ?? 0,
|
|
12517
|
+
receiptSeq: receipt?.seq ?? null
|
|
12518
|
+
});
|
|
12519
|
+
if ("markTerminalReported" in this.jobStore) {
|
|
12520
|
+
await this.jobStore.markTerminalReported(jobId);
|
|
12521
|
+
}
|
|
12522
|
+
} catch (error) {
|
|
12523
|
+
console.error(`[${jobId}] Terminal reporting failed:`, error);
|
|
12524
|
+
}
|
|
12525
|
+
}
|
|
12526
|
+
terminalReportRecoveryCursor;
|
|
12527
|
+
async recoverTerminalReports() {
|
|
12528
|
+
const store = this.jobStore;
|
|
12529
|
+
const dvmId = this.opts.dvmId;
|
|
12530
|
+
if (!this.opts.onJobTerminal || !dvmId || !("listUnreportedTerminalReports" in store)) return;
|
|
12531
|
+
const recovery = store;
|
|
12532
|
+
const records = await recovery.listUnreportedTerminalReports(dvmId, this.capabilityNames(), this.terminalReportRecoveryCursor);
|
|
12533
|
+
for (const record of records) await this.reportTerminalJob(record.id);
|
|
12534
|
+
this.terminalReportRecoveryCursor = records.length === 100 ? records[records.length - 1].id : void 0;
|
|
12535
|
+
}
|
|
12381
12536
|
/** Handle job reaching terminal state (completed, failed, cancelled). */
|
|
12382
12537
|
async handleJobTerminal(job) {
|
|
12383
12538
|
this.clearIdleTimer(job.id);
|
|
12539
|
+
if (await this.adoptDurableTerminal(job)) return;
|
|
12540
|
+
const durable = await this.jobStore.get(job.id);
|
|
12541
|
+
if (!durable || !isTerminal(durable.status)) await this.persistJob(job);
|
|
12384
12542
|
await this.attachReceipt(job);
|
|
12385
12543
|
const booked = await this.bookRevenue(job);
|
|
12386
12544
|
if (!booked) await this.reportJobCost(job);
|
|
12545
|
+
await this.reportTerminalJob(job.id);
|
|
12387
12546
|
this.scheduleCleanup(job);
|
|
12388
12547
|
}
|
|
12389
12548
|
/**
|
|
@@ -12396,6 +12555,7 @@ var JobManager = class {
|
|
|
12396
12555
|
async recoverTerminalCosts() {
|
|
12397
12556
|
const store = this.jobStore;
|
|
12398
12557
|
const dvmId = this.opts.dvmId;
|
|
12558
|
+
await this.recoverTerminalReports();
|
|
12399
12559
|
if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
|
|
12400
12560
|
return { examined: 0, queued: 0, complete: true };
|
|
12401
12561
|
}
|
|
@@ -12480,11 +12640,11 @@ var JobManager = class {
|
|
|
12480
12640
|
return true;
|
|
12481
12641
|
}
|
|
12482
12642
|
/**
|
|
12483
|
-
* Report a completed paid job's revenue
|
|
12643
|
+
* Report a completed paid job's revenue under its durable job identity.
|
|
12484
12644
|
* Fire-and-forget — the `RevenueReporter` owns persistence and retry.
|
|
12485
12645
|
*
|
|
12486
12646
|
* For a **credit-backed** job the revenue event is the *settled draw* (spec
|
|
12487
|
-
* §10), and since
|
|
12647
|
+
* §10), and since the behavior was introduced the sats figure corrects the job's mirror of that
|
|
12488
12648
|
* draw against the draw itself: `withDrawBasis` stamps the job when the
|
|
12489
12649
|
* payment lands, but a Bitcoin credit's settle re-prices the draw against
|
|
12490
12650
|
* the funding lots it consumed. The correction is a delta, because a job's
|
|
@@ -12499,7 +12659,7 @@ var JobManager = class {
|
|
|
12499
12659
|
* whose price couldn't be fiat-denominated) nothing changes: the pre-credits
|
|
12500
12660
|
* payload is reported verbatim.
|
|
12501
12661
|
*
|
|
12502
|
-
* Takes the fields rather than a `ServerJob` so the
|
|
12662
|
+
* Takes the fields rather than a `ServerJob` so the reconciler — a
|
|
12503
12663
|
* background sweep that holds no in-process job — books through this exact
|
|
12504
12664
|
* path off the row it read. `paymentTxHash` is passed **verbatim**, never
|
|
12505
12665
|
* recomputed as a `drawSettlementRef`: it is the same column the in-line
|
|
@@ -12516,7 +12676,7 @@ var JobManager = class {
|
|
|
12516
12676
|
* DVM with no reporter). Every drop that costs a booking logs first.
|
|
12517
12677
|
*
|
|
12518
12678
|
* The rail check sits **below** the draw re-read, not in the entry guard
|
|
12519
|
-
*
|
|
12679
|
+
* a settled draw the terminal can't report under is a real debit
|
|
12520
12680
|
* with no revenue event, permanently overstating outstanding liability
|
|
12521
12681
|
* (`deposits − draw revenue`), and it used to return here in silence. Placed
|
|
12522
12682
|
* after the re-read, `revenue_skipped_no_rail` can name the draw and its
|
|
@@ -12579,9 +12739,9 @@ var JobManager = class {
|
|
|
12579
12739
|
paidMsats: job.paidMsats,
|
|
12580
12740
|
creditRail: creditRail ?? null,
|
|
12581
12741
|
// Four distinct causes needing four distinct fixes, so none of them
|
|
12582
|
-
// is the else-branch: the credit predates
|
|
12742
|
+
// is the else-branch: the credit predates and has no rail
|
|
12583
12743
|
// (`credit_rail_null`); it carries one this build doesn't list, the
|
|
12584
|
-
//
|
|
12744
|
+
// sibling class (`credit_rail_unrecognized`); the credit's
|
|
12585
12745
|
// rail is perfectly good and it is the *job row* that lost its own
|
|
12586
12746
|
// (`job_rail_missing`), which no change to the ledger would fix; or
|
|
12587
12747
|
// this manager was built with no `creditLedger` to read the draw
|
|
@@ -12629,7 +12789,7 @@ var JobManager = class {
|
|
|
12629
12789
|
cashuFlow: job.cashuFlow,
|
|
12630
12790
|
// Absent unless the handler declared one, and absent is a fact: the
|
|
12631
12791
|
// platform shows "no costs reported" rather than a zero cost and a
|
|
12632
|
-
// 100% margin
|
|
12792
|
+
// 100% margin. Both halves or neither — a bare amount
|
|
12633
12793
|
// in an unnamed currency is not a cost.
|
|
12634
12794
|
...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
|
|
12635
12795
|
...credit
|
|
@@ -12812,8 +12972,8 @@ var MintHealthTracker = class {
|
|
|
12812
12972
|
firstProbeComplete = false;
|
|
12813
12973
|
constructor(opts) {
|
|
12814
12974
|
this.mints = [...opts.mints];
|
|
12815
|
-
this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs();
|
|
12816
|
-
this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick();
|
|
12975
|
+
this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs(opts.env);
|
|
12976
|
+
this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick(opts.env);
|
|
12817
12977
|
this.fetchImpl = opts.fetchImpl;
|
|
12818
12978
|
this.probeOpts = { ...opts.probe, fetchImpl: opts.fetchImpl };
|
|
12819
12979
|
this.nowFn = opts.now ?? Date.now;
|
|
@@ -12864,7 +13024,7 @@ var MintHealthTracker = class {
|
|
|
12864
13024
|
/**
|
|
12865
13025
|
* True once the first full probe tick has landed. Before this, paid calls
|
|
12866
13026
|
* are held at `503 mint_health_pending` rather than accepted against
|
|
12867
|
-
* unvalidated mints
|
|
13027
|
+
* unvalidated mints.
|
|
12868
13028
|
*/
|
|
12869
13029
|
initialProbeComplete() {
|
|
12870
13030
|
return this.firstProbeComplete;
|
|
@@ -12889,7 +13049,7 @@ var MintHealthTracker = class {
|
|
|
12889
13049
|
* gate — a deterministic, permanent config error, unlike a transient outage.
|
|
12890
13050
|
* Unknown mints return false. `advertisedMints()` uses this to keep a
|
|
12891
13051
|
* permanently-unsettleable mint off `/v1/info` even in the all-sick fallback,
|
|
12892
|
-
* where a merely-flapping mint is still published
|
|
13052
|
+
* where a merely-flapping mint is still published.
|
|
12893
13053
|
*/
|
|
12894
13054
|
isNutIncompatible(mintUrl) {
|
|
12895
13055
|
return this.state.get(normaliseMintUrl(mintUrl))?.nutIncompatible ?? false;
|
|
@@ -12956,7 +13116,7 @@ var MintHealthTracker = class {
|
|
|
12956
13116
|
* One-shot structured summary emitted after the first probe tick. Replaces
|
|
12957
13117
|
* the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
|
|
12958
13118
|
* all-mints-unhealthy warning — the operator alert that the Fly restart-loop
|
|
12959
|
-
* used to be before boot stopped refusing to start
|
|
13119
|
+
* used to be before boot stopped refusing to start.
|
|
12960
13120
|
*/
|
|
12961
13121
|
logBootHealth() {
|
|
12962
13122
|
const mints = Object.entries(this.snapshot()).map(([mintUrl, s]) => ({
|
|
@@ -12981,15 +13141,15 @@ var MintHealthTracker = class {
|
|
|
12981
13141
|
);
|
|
12982
13142
|
}
|
|
12983
13143
|
};
|
|
12984
|
-
function resolveEnvTickMs() {
|
|
12985
|
-
const raw =
|
|
13144
|
+
function resolveEnvTickMs(env = process.env) {
|
|
13145
|
+
const raw = env.DVMKIT_MINT_HEALTH_TICK_MS;
|
|
12986
13146
|
if (!raw) return DEFAULT_TICK_INTERVAL_MS2;
|
|
12987
13147
|
const parsed = Number(raw);
|
|
12988
13148
|
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_TICK_INTERVAL_MS2;
|
|
12989
13149
|
return Math.floor(parsed);
|
|
12990
13150
|
}
|
|
12991
|
-
function resolveEnvFailuresToSick() {
|
|
12992
|
-
const raw =
|
|
13151
|
+
function resolveEnvFailuresToSick(env = process.env) {
|
|
13152
|
+
const raw = env.DVMKIT_MINT_FAILURES_TO_SICK;
|
|
12993
13153
|
if (!raw) return DEFAULT_FAILURES_TO_SICK;
|
|
12994
13154
|
const parsed = Number(raw);
|
|
12995
13155
|
if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_FAILURES_TO_SICK;
|
|
@@ -14040,9 +14200,9 @@ async function handlePendingDrains(c, opts) {
|
|
|
14040
14200
|
sats_basis: amountSats === null ? null : d.owedSats === null ? "rate" : "in_kind",
|
|
14041
14201
|
// The figure a `manual` payout is actually sized in: `amount_micro` is
|
|
14042
14202
|
// fiat, and the operator sends the rail's own token. It is the same debit
|
|
14043
|
-
// `completeX402ChannelDrain` treats as its settlement floor
|
|
14203
|
+
// `completeX402ChannelDrain` treats as its settlement floor,
|
|
14044
14204
|
// so publishing it is what lets an operator settle from the row instead
|
|
14045
|
-
// of assuming their stablecoin happens to be six-decimal
|
|
14205
|
+
// of assuming their stablecoin happens to be six-decimal.
|
|
14046
14206
|
amount_native: d.drainedNative,
|
|
14047
14207
|
settlement,
|
|
14048
14208
|
// Why this row is classified as it is, and the channel an operator would
|
|
@@ -14565,7 +14725,7 @@ async function handleReconcileInvoice(c, opts) {
|
|
|
14565
14725
|
fundedAt: settledAtMs ?? invoice.settledAt ?? nowMs,
|
|
14566
14726
|
expiryMs,
|
|
14567
14727
|
// The settlements table types this deposit `repair` off the
|
|
14568
|
-
// flag
|
|
14728
|
+
// flag: the sats landed at payment time and the
|
|
14569
14729
|
// deposit is the one row they get.
|
|
14570
14730
|
repaired: true
|
|
14571
14731
|
}
|
|
@@ -16560,7 +16720,7 @@ function autoApprovePayment(content) {
|
|
|
16560
16720
|
headers: jobHeaders(true),
|
|
16561
16721
|
// amount_msats rides along so an onPayment handler sees the same shape a
|
|
16562
16722
|
// real rail would hand it. dev_auto is admitted on dev-mode servers only
|
|
16563
|
-
//
|
|
16723
|
+
// hence the status check below rather than a silent catch.
|
|
16564
16724
|
body: JSON.stringify({ type: "payment", content: { dev_auto: true, amount_msats: content.amount_msats || 0 } }),
|
|
16565
16725
|
}).then(function(r) {
|
|
16566
16726
|
if (!r.ok) addMsg("Auto-approve rejected (" + r.status + ") \u2014 dev mode only.", "msg-system");
|
|
@@ -16707,7 +16867,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16707
16867
|
}
|
|
16708
16868
|
})();
|
|
16709
16869
|
try {
|
|
16710
|
-
if (
|
|
16870
|
+
if (opts.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
|
|
16711
16871
|
console.warn(
|
|
16712
16872
|
JSON.stringify({
|
|
16713
16873
|
level: "cashu_opts_env_ignored",
|
|
@@ -16720,12 +16880,12 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16720
16880
|
const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
|
|
16721
16881
|
const dvmId = opts.dvmId;
|
|
16722
16882
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
16723
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
16883
|
+
const { RevenueReporter } = await import("./revenue-reporter-NNCNRY4C.js");
|
|
16724
16884
|
const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
|
|
16725
16885
|
revenueReporter = reporter;
|
|
16726
16886
|
await reporter.init();
|
|
16727
16887
|
reporter.startRetryLoop();
|
|
16728
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
16888
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-5PIRYFVQ.js");
|
|
16729
16889
|
payoutReporter = new PayoutReporter({
|
|
16730
16890
|
dvmId,
|
|
16731
16891
|
db: opts.db,
|
|
@@ -16868,6 +17028,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16868
17028
|
processedPayments,
|
|
16869
17029
|
onJobCompleted: effectiveOnJobCompleted,
|
|
16870
17030
|
onJobCost: effectiveOnJobCost,
|
|
17031
|
+
onJobTerminal: activeRevenueReporter?.reportTerminal.bind(activeRevenueReporter),
|
|
16871
17032
|
onPaidJobDeath: effectiveOnPaidJobDeath,
|
|
16872
17033
|
onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
|
|
16873
17034
|
onCreditFunded: opts.onCreditFunded,
|
|
@@ -16911,7 +17072,7 @@ var DVMServer = class {
|
|
|
16911
17072
|
fxFetcher;
|
|
16912
17073
|
/**
|
|
16913
17074
|
* The currency this DVM prices in and denominates its credit ledger in,
|
|
16914
|
-
* resolved once at construction
|
|
17075
|
+
* resolved once at construction.
|
|
16915
17076
|
*
|
|
16916
17077
|
* `configureDVM` and the CLI loader both resolve this field. The server is
|
|
16917
17078
|
* also exported for direct JavaScript callers, so defaulting at the one
|
|
@@ -16920,29 +17081,24 @@ var DVMServer = class {
|
|
|
16920
17081
|
*/
|
|
16921
17082
|
pricingCurrency;
|
|
16922
17083
|
/**
|
|
16923
|
-
* Last-good lock-pubkey state observed from Postgres
|
|
17084
|
+
* Last-good lock-pubkey state observed from Postgres. Used as the
|
|
16924
17085
|
* fallback when `loadLockPubkeyState` throws — degrades more gracefully than
|
|
16925
17086
|
* falling back to the env-derived seed, which post-rotation is the *retired*
|
|
16926
17087
|
* pubkey. Null until the first successful read.
|
|
16927
17088
|
*/
|
|
16928
17089
|
cachedLockPubkeyState = null;
|
|
16929
|
-
/** Guards the opportunistic Tempo settlement cadence (internal-review). */
|
|
16930
17090
|
tempoSweepInFlight = false;
|
|
16931
17091
|
tempoSweepLastAttemptMs = 0;
|
|
16932
|
-
/** Receipt signer for this DVM (internal-review). Undefined when no key is wired. */
|
|
16933
17092
|
receiptIssuer;
|
|
16934
|
-
/** Credit ledger the payment gate funds/draws through (internal-review). */
|
|
16935
17093
|
creditLedger;
|
|
16936
|
-
/** Durable x402/mpp funding markers (internal-review). */
|
|
16937
17094
|
processedPayments;
|
|
16938
17095
|
authAudience;
|
|
16939
17096
|
/** Lazily constructed when a refreshed facilitator probe gains batch support. */
|
|
16940
17097
|
x402BatchSettlement;
|
|
16941
17098
|
/** Coalesces concurrent requests that first observe batch support. */
|
|
16942
17099
|
x402BatchSettlementInit;
|
|
16943
|
-
/** Durable exact-settlement coordinator shared by every exact payment surface. */
|
|
16944
17100
|
x402ExactSettlement;
|
|
16945
|
-
/** Withdraws this mount's Tempo session capability from the poll
|
|
17101
|
+
/** Withdraws this mount's Tempo session capability from the poll. */
|
|
16946
17102
|
undeclareSessionCapability;
|
|
16947
17103
|
constructor(descriptor, opts) {
|
|
16948
17104
|
this.descriptor = descriptor;
|
|
@@ -16966,7 +17122,7 @@ var DVMServer = class {
|
|
|
16966
17122
|
this.mintHealthTracker = opts.mintHealthTracker;
|
|
16967
17123
|
this.ownsMintHealthTracker = false;
|
|
16968
17124
|
} else if (opts.mints && opts.mints.length > 0) {
|
|
16969
|
-
this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints });
|
|
17125
|
+
this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints, env: opts.env });
|
|
16970
17126
|
this.ownsMintHealthTracker = true;
|
|
16971
17127
|
} else {
|
|
16972
17128
|
this.ownsMintHealthTracker = false;
|
|
@@ -16987,7 +17143,7 @@ var DVMServer = class {
|
|
|
16987
17143
|
mints: opts.mints,
|
|
16988
17144
|
jobStore: opts.jobStore,
|
|
16989
17145
|
devMode: opts.devMode,
|
|
16990
|
-
// Derived, not raw
|
|
17146
|
+
// Derived, not raw. `ctx.requestPayment` gates its x402 and
|
|
16991
17147
|
// Tempo challenge issuance on this list, while `/v1/info`, `/v1/quote`
|
|
16992
17148
|
// and the upfront 402 all read `resolvePaymentMethods()` — so passing the
|
|
16993
17149
|
// raw option meant a DVM that never set `paymentMethods` (no first-party
|
|
@@ -17012,21 +17168,20 @@ var DVMServer = class {
|
|
|
17012
17168
|
dvmId: opts.dvmId,
|
|
17013
17169
|
authAudience: this.authAudience,
|
|
17014
17170
|
fxFetcher: this.fxFetcher,
|
|
17015
|
-
//
|
|
17171
|
+
// the mid-job top-up leg needs the same rail wiring the upfront
|
|
17016
17172
|
// path gets — a divergence here would be a DVM whose upfront payment funds
|
|
17017
17173
|
// a credit and whose mid-job payment silently doesn't.
|
|
17018
17174
|
processedPayments: this.processedPayments,
|
|
17019
17175
|
enqueueCreditDeposit: opts.enqueueCreditDeposit,
|
|
17020
17176
|
enqueueCreditDrawRelease: opts.enqueueCreditDrawRelease,
|
|
17021
17177
|
creditTtlMs: descriptor.credit ? descriptor.credit.ttlSeconds * 1e3 : void 0,
|
|
17022
|
-
// internal-review: the same read `creditCurrency()` serves everywhere else, so a
|
|
17023
|
-
// mid-job ask pins in the denomination this DVM's credits actually use.
|
|
17024
17178
|
pricingCurrency: this.pricingCurrency,
|
|
17025
17179
|
onCreditFunded: opts.onCreditFunded,
|
|
17026
17180
|
onJobCompleted: opts.onJobCompleted,
|
|
17027
17181
|
onPaidJobCompleted: opts.onPaidJobCompleted,
|
|
17028
17182
|
paymentMode: opts.paymentMode,
|
|
17029
17183
|
onJobCost: opts.onJobCost,
|
|
17184
|
+
onJobTerminal: opts.onJobTerminal,
|
|
17030
17185
|
onPaidJobDeath: opts.onPaidJobDeath,
|
|
17031
17186
|
onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
|
|
17032
17187
|
onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
|
|
@@ -17064,7 +17219,7 @@ var DVMServer = class {
|
|
|
17064
17219
|
* Tear down this mount. Every leg is synchronous except the batch-settlement
|
|
17065
17220
|
* server's own `stop()`; the returned promise settles once that has finished,
|
|
17066
17221
|
* so a caller holding a resource behind it — the settlement pool the spend
|
|
17067
|
-
* gate's store owns
|
|
17222
|
+
* gate's store owns — can sequence its close instead of pulling it
|
|
17068
17223
|
* out from under an in-flight stop. A rejection remains observable so the app
|
|
17069
17224
|
* and host teardown layers can finish later cleanup before surfacing it.
|
|
17070
17225
|
*/
|
|
@@ -17165,12 +17320,12 @@ var DVMServer = class {
|
|
|
17165
17320
|
(c) => c.json({
|
|
17166
17321
|
ok: true,
|
|
17167
17322
|
// shortSha, not .slice(0, 7): a dirty-tree deploy bakes a `-dirty`
|
|
17168
|
-
// marker
|
|
17323
|
+
// marker that a raw truncation would cut off the end,
|
|
17169
17324
|
// leaving this endpoint claiming a clean commit while /v1/info's
|
|
17170
17325
|
// untruncated sdk_sha says otherwise — same image, two answers.
|
|
17171
|
-
gitSha: shortSha(
|
|
17172
|
-
deployedAt:
|
|
17173
|
-
deployId:
|
|
17326
|
+
gitSha: shortSha(this.opts.env.GIT_SHA ?? "unknown"),
|
|
17327
|
+
deployedAt: this.opts.env.BUILD_TIME ?? "unknown",
|
|
17328
|
+
deployId: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
|
|
17174
17329
|
})
|
|
17175
17330
|
);
|
|
17176
17331
|
if (this.opts.devMode) {
|
|
@@ -17203,8 +17358,6 @@ var DVMServer = class {
|
|
|
17203
17358
|
getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
|
|
17204
17359
|
graceSeconds: this.graceSeconds(),
|
|
17205
17360
|
nonceStore: this.adminCashuNonceStore,
|
|
17206
|
-
// internal-review: a completed melt is a landed payout, queued in the
|
|
17207
|
-
// transaction that marks it.
|
|
17208
17361
|
...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
|
|
17209
17362
|
});
|
|
17210
17363
|
}
|
|
@@ -17222,13 +17375,13 @@ var DVMServer = class {
|
|
|
17222
17375
|
enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
|
|
17223
17376
|
enqueueCreditDrain: this.opts.enqueueCreditDrain,
|
|
17224
17377
|
lightningReceive: this.opts.lightningReceive,
|
|
17225
|
-
//
|
|
17378
|
+
// x402 repair queue rides the same surface, and resolves
|
|
17226
17379
|
// the rail the same lazy way the verify path does — constructing the
|
|
17227
17380
|
// settlement server probes the facilitator, which must not happen just
|
|
17228
17381
|
// because the admin routes were mounted.
|
|
17229
17382
|
x402BatchSettlement: () => this.resolveX402BatchSettlement(),
|
|
17230
17383
|
x402ExactSettlement: this.x402ExactSettlement,
|
|
17231
|
-
//
|
|
17384
|
+
// Tempo repair queue, same surface again. The mppx handle is
|
|
17232
17385
|
// already built by the time routes mount, so the thunk is uniformity
|
|
17233
17386
|
// rather than laziness — and it is the *handle*, not the store, so a
|
|
17234
17387
|
// DVM wired with an in-memory session store resolves to a rail with no
|
|
@@ -17236,10 +17389,8 @@ var DVMServer = class {
|
|
|
17236
17389
|
tempoDrainRail: () => Promise.resolve(this.opts.mpp),
|
|
17237
17390
|
// Health-filtered, and read per request rather than captured: the
|
|
17238
17391
|
// servicer picking a mint to buy a refund at wants the list that
|
|
17239
|
-
// excludes a mint currently failing its probe
|
|
17392
|
+
// excludes a mint currently failing its probe.
|
|
17240
17393
|
mints: () => this.advertisedMints(),
|
|
17241
|
-
// internal-review: a repaired Tempo close is a landed payout the agent
|
|
17242
|
-
// finished, queued in the transaction that books the drain.
|
|
17243
17394
|
...payoutReporter ? { onTempoDrainReconciled: payoutReporter.tempoRepairHook() } : {}
|
|
17244
17395
|
});
|
|
17245
17396
|
}
|
|
@@ -17250,7 +17401,7 @@ var DVMServer = class {
|
|
|
17250
17401
|
/**
|
|
17251
17402
|
* Load the live lock-pubkey state for this DVM. Reads from
|
|
17252
17403
|
* `dvm_lock_pubkeys` on every call so rotations propagate without needing
|
|
17253
|
-
* a process restart
|
|
17404
|
+
* a process restart. On success the result is cached so a later
|
|
17254
17405
|
* DB outage can degrade to the last-observed state instead of the
|
|
17255
17406
|
* env-derived seed (which post-rotation is the *retired* pubkey).
|
|
17256
17407
|
*/
|
|
@@ -17261,7 +17412,7 @@ var DVMServer = class {
|
|
|
17261
17412
|
}
|
|
17262
17413
|
/**
|
|
17263
17414
|
* Resolve the active lock-pubkey set for the mid-job cashu accept path
|
|
17264
|
-
*
|
|
17415
|
+
* Returns the same `[current, ...retired_in_grace]` shape the
|
|
17265
17416
|
* upfront flow uses at lines 709-718 so accept-time rotation tolerance is
|
|
17266
17417
|
* consistent across upfront and mid-job. Falls back to `undefined` when
|
|
17267
17418
|
* accumulator mode isn't wired (the receive path then rejects with
|
|
@@ -17298,13 +17449,13 @@ var DVMServer = class {
|
|
|
17298
17449
|
}
|
|
17299
17450
|
/**
|
|
17300
17451
|
* Run the routine Tempo settlement cadence opportunistically, on ordinary
|
|
17301
|
-
* traffic
|
|
17452
|
+
* traffic.
|
|
17302
17453
|
*
|
|
17303
17454
|
* Routine settlement used to ride the per-DVM watcher machine's four-minute
|
|
17304
17455
|
* poll, which is exactly what kept an idle DVM awake and billing. It cannot
|
|
17305
17456
|
* move to a timer for the same reason — a timer on a machine that ought to
|
|
17306
17457
|
* be suspended either does not fire or defeats the suspension. So it rides
|
|
17307
|
-
* requests instead, the way Lightning funding is credited
|
|
17458
|
+
* requests instead, the way Lightning funding is credited: a DVM
|
|
17308
17459
|
* with no traffic has nothing to settle on a 24-hour cadence anyway, and the
|
|
17309
17460
|
* forced-close guarantee does not depend on this path at all — that is the
|
|
17310
17461
|
* chain-event wake, which reaches a suspended DVM through Fly Proxy.
|
|
@@ -17344,7 +17495,7 @@ var DVMServer = class {
|
|
|
17344
17495
|
});
|
|
17345
17496
|
}
|
|
17346
17497
|
/**
|
|
17347
|
-
* Whether one advertised MPP method may still be offered
|
|
17498
|
+
* Whether one advertised MPP method may still be offered.
|
|
17348
17499
|
*
|
|
17349
17500
|
* Only `tempo/session` is gated, and only when this DVM is platform-hosted:
|
|
17350
17501
|
* a session channel is safe to accept exactly while something is watching
|
|
@@ -17360,7 +17511,6 @@ var DVMServer = class {
|
|
|
17360
17511
|
tempoSessionOffered(method) {
|
|
17361
17512
|
return this.tempoSessionWithheldReason(method) === void 0;
|
|
17362
17513
|
}
|
|
17363
|
-
/** Stable caller-facing reason for withholding one newly opened session. */
|
|
17364
17514
|
tempoSessionWithheldReason(method) {
|
|
17365
17515
|
if (method.method !== "tempo" || method.intent !== "session") return void 0;
|
|
17366
17516
|
if (this.opts.tempoSettlementReadiness && !this.opts.tempoSettlementReadiness.available()) {
|
|
@@ -17371,7 +17521,6 @@ var DVMServer = class {
|
|
|
17371
17521
|
}
|
|
17372
17522
|
return void 0;
|
|
17373
17523
|
}
|
|
17374
|
-
/** Await only the cold-start reads each fail-closed session gate needs. */
|
|
17375
17524
|
async ensureTempoSessionHealth() {
|
|
17376
17525
|
await Promise.all([
|
|
17377
17526
|
this.opts.tempoObserverHealth?.ensureFresh?.(),
|
|
@@ -17382,7 +17531,6 @@ var DVMServer = class {
|
|
|
17382
17531
|
* Whether this DVM is **configured** to take TIP-1034 session channels — the
|
|
17383
17532
|
* value the observer-health poll carries back to the platform, where it keeps
|
|
17384
17533
|
* `dvms.tempo_session_advertised_at` current between container deploys
|
|
17385
|
-
* (internal-review).
|
|
17386
17534
|
*
|
|
17387
17535
|
* Deliberately the same question the platform's post-deploy `/v1/info` probe
|
|
17388
17536
|
* answers (`infoAdvertisesTempoSession`), from the same two facts: the credit
|
|
@@ -17394,13 +17542,12 @@ var DVMServer = class {
|
|
|
17394
17542
|
* It reads `advertisedMethods` rather than {@link creditMppSplit}'s `offered`,
|
|
17395
17543
|
* and never {@link tempoSessionOffered}: what the column records is
|
|
17396
17544
|
* capability, not availability, so an observer blip must leave it alone. That
|
|
17397
|
-
* distinction is the whole of
|
|
17545
|
+
* distinction is the whole of and answering it here from the gate
|
|
17398
17546
|
* would reintroduce it on a one-minute cadence.
|
|
17399
17547
|
*/
|
|
17400
17548
|
tempoSessionCapability() {
|
|
17401
17549
|
if (!this.opts.mpp) return false;
|
|
17402
17550
|
if (!creditMenuConfigured({
|
|
17403
|
-
// The SDK server only ever runs the container runtime — see `creditMenu`.
|
|
17404
17551
|
runtime: "container",
|
|
17405
17552
|
config: this.descriptor.credit,
|
|
17406
17553
|
hasAuth: !!this.descriptor.auth,
|
|
@@ -17415,7 +17562,7 @@ var DVMServer = class {
|
|
|
17415
17562
|
}
|
|
17416
17563
|
/**
|
|
17417
17564
|
* `POST /_internal/tempo-close-event` — the forced-close wake path
|
|
17418
|
-
*
|
|
17565
|
+
* replacing the per-DVM `tempo-session-watch` Fly Machine.
|
|
17419
17566
|
*
|
|
17420
17567
|
* Deliberately **unauthenticated**, and that is the design rather than an
|
|
17421
17568
|
* omission: the body is an event locator, so the DVM re-reads the event from
|
|
@@ -17494,7 +17641,7 @@ var DVMServer = class {
|
|
|
17494
17641
|
}
|
|
17495
17642
|
/**
|
|
17496
17643
|
* Compose the `/v1/info#builder` block from `opts.builder` (inline SDK
|
|
17497
|
-
* config) with a fallback to deploy-injected env vars
|
|
17644
|
+
* config) with a fallback to deploy-injected env vars. Each
|
|
17498
17645
|
* field is independently sourced (opts wins per-field, env fills gaps);
|
|
17499
17646
|
* all three of pubkey / attestation / signature must resolve to emit the
|
|
17500
17647
|
* block — partial state is treated as absent rather than half-emitted.
|
|
@@ -17507,8 +17654,8 @@ var DVMServer = class {
|
|
|
17507
17654
|
builderAttestationBlock() {
|
|
17508
17655
|
const builder = this.opts.builder;
|
|
17509
17656
|
const env = this.opts.env;
|
|
17510
|
-
const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY
|
|
17511
|
-
const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE
|
|
17657
|
+
const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY;
|
|
17658
|
+
const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE;
|
|
17512
17659
|
const attestation = builder?.attestation ? { ...builder.attestation } : this.parseAttestationFromEnv(env);
|
|
17513
17660
|
if (!pubkey || !signature || !attestation) return null;
|
|
17514
17661
|
return { pubkey, attestation, signature };
|
|
@@ -17540,7 +17687,7 @@ var DVMServer = class {
|
|
|
17540
17687
|
throw new AuthAudienceError();
|
|
17541
17688
|
}
|
|
17542
17689
|
/**
|
|
17543
|
-
* The slug every receipt is stamped with
|
|
17690
|
+
* The slug every receipt is stamped with. Read off the signed
|
|
17544
17691
|
* attestation payload, which is the only place the *attested* slug lives —
|
|
17545
17692
|
* a receipt claiming a different name than the attestation binds would be
|
|
17546
17693
|
* unverifiable. Falls back to the descriptor name for dev/self-host boots
|
|
@@ -17551,14 +17698,13 @@ var DVMServer = class {
|
|
|
17551
17698
|
const slug = attestation?.slug;
|
|
17552
17699
|
return typeof slug === "string" && slug.length > 0 ? slug : this.descriptor.name;
|
|
17553
17700
|
}
|
|
17554
|
-
/** Immutable identity written into every signed receipt. */
|
|
17555
17701
|
receiptDvmId() {
|
|
17556
17702
|
const attestation = this.builderAttestationBlock()?.attestation;
|
|
17557
17703
|
const dvmId = attestation?.dvm_id;
|
|
17558
17704
|
return typeof dvmId === "string" && dvmId.length > 0 ? dvmId : this.opts.dvmId ?? this.descriptor.name;
|
|
17559
17705
|
}
|
|
17560
17706
|
parseAttestationFromEnv(env) {
|
|
17561
|
-
const raw = env.DVMKIT_BUILDER_ATTESTATION
|
|
17707
|
+
const raw = env.DVMKIT_BUILDER_ATTESTATION;
|
|
17562
17708
|
if (!raw) return null;
|
|
17563
17709
|
try {
|
|
17564
17710
|
const parsed = JSON.parse(raw);
|
|
@@ -17569,7 +17715,7 @@ var DVMServer = class {
|
|
|
17569
17715
|
}
|
|
17570
17716
|
}
|
|
17571
17717
|
/**
|
|
17572
|
-
* Owner display block for `/v1/info#owner
|
|
17718
|
+
* Owner display block for `/v1/info#owner`. Resolved from
|
|
17573
17719
|
* `opts.owner` or env vars injected by the platform deploy pipeline.
|
|
17574
17720
|
* Returns null when no owner identity is configured — pre-unification
|
|
17575
17721
|
* DVMs that haven't been redeployed omit the block.
|
|
@@ -17577,11 +17723,11 @@ var DVMServer = class {
|
|
|
17577
17723
|
ownerDisplayBlock() {
|
|
17578
17724
|
const owner = this.opts.owner;
|
|
17579
17725
|
const env = this.opts.env;
|
|
17580
|
-
const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE
|
|
17726
|
+
const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE;
|
|
17581
17727
|
if (!handle) return null;
|
|
17582
|
-
const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ??
|
|
17583
|
-
const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ??
|
|
17584
|
-
const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ??
|
|
17728
|
+
const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? "builder";
|
|
17729
|
+
const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
|
|
17730
|
+
const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? null;
|
|
17585
17731
|
return { handle, display_name: displayName, avatar_url: avatarUrl, type };
|
|
17586
17732
|
}
|
|
17587
17733
|
defaultPaymentMethods() {
|
|
@@ -17600,14 +17746,13 @@ var DVMServer = class {
|
|
|
17600
17746
|
if (!this.opts.x402.batchSettlement?.selfRelay) return support;
|
|
17601
17747
|
return { ...support, batchSettlement: true };
|
|
17602
17748
|
}
|
|
17603
|
-
/** Whether this DVM can state its price exactly in x402's USDC unit. */
|
|
17604
17749
|
x402CurrencySupported() {
|
|
17605
17750
|
return this.pricingCurrency === "usd";
|
|
17606
17751
|
}
|
|
17607
17752
|
/**
|
|
17608
17753
|
* Settleable rails for this DVM, honouring an explicit `opts.paymentMethods`
|
|
17609
17754
|
* override and otherwise deriving from wired rails. Single source of truth so
|
|
17610
|
-
* the `/v1/info` and `/v1/quote` rail enumerations can't drift
|
|
17755
|
+
* the `/v1/info` and `/v1/quote` rail enumerations can't drift.
|
|
17611
17756
|
*/
|
|
17612
17757
|
resolvePaymentMethods() {
|
|
17613
17758
|
const configured = this.opts.paymentMethods ?? this.defaultPaymentMethods();
|
|
@@ -17620,13 +17765,13 @@ var DVMServer = class {
|
|
|
17620
17765
|
}
|
|
17621
17766
|
/**
|
|
17622
17767
|
* Rails a **credit top-up** may arrive on — the funding menu's list
|
|
17623
|
-
*
|
|
17768
|
+
* A superset of {@link resolvePaymentMethods}, and deliberately
|
|
17624
17769
|
* a separate read: `lightning` funds a credit but is not an attached-proof
|
|
17625
17770
|
* wire method, so it must never reach `/v1/info` or `/v1/quote`'s
|
|
17626
|
-
* `payment.methods
|
|
17771
|
+
* `payment.methods`.
|
|
17627
17772
|
*
|
|
17628
|
-
* The rail drops out when the receive wallet is unreachable
|
|
17629
|
-
*
|
|
17773
|
+
* The rail drops out of the funding menu when the receive wallet is unreachable;
|
|
17774
|
+
* advertising it then would hand the caller an option that
|
|
17630
17775
|
* fails on use, while the other rails would have closed the sale.
|
|
17631
17776
|
*/
|
|
17632
17777
|
resolveFundingMethods() {
|
|
@@ -17645,7 +17790,7 @@ var DVMServer = class {
|
|
|
17645
17790
|
}
|
|
17646
17791
|
/**
|
|
17647
17792
|
* The credit menu's MPP instruments, split into what is on offer right now
|
|
17648
|
-
* and what this DVM is configured for but withholding
|
|
17793
|
+
* and what this DVM is configured for but withholding.
|
|
17649
17794
|
*
|
|
17650
17795
|
* The split has to survive into the response rather than being filtered
|
|
17651
17796
|
* away, because "not offered right now" and "not configured" are different
|
|
@@ -17673,7 +17818,7 @@ var DVMServer = class {
|
|
|
17673
17818
|
}
|
|
17674
17819
|
/**
|
|
17675
17820
|
* Which x402 flavours a credit top-up may arrive on — the funding menu's
|
|
17676
|
-
* `x402` sub-block
|
|
17821
|
+
* `x402` sub-block. Hosted health buys `exact`; a genuinely wired
|
|
17677
17822
|
* batch server buys `batch-settlement` independently, including while the
|
|
17678
17823
|
* hosted facilitator is unavailable or stale.
|
|
17679
17824
|
*
|
|
@@ -17690,13 +17835,12 @@ var DVMServer = class {
|
|
|
17690
17835
|
...this.x402BatchAvailable() ? [{ scheme: X402_BATCH_SETTLEMENT_SCHEME, network: x402.network }] : []
|
|
17691
17836
|
];
|
|
17692
17837
|
}
|
|
17693
|
-
/** Whether a channel server is both supported and constructible for credit funding. */
|
|
17694
17838
|
x402BatchAvailable() {
|
|
17695
17839
|
return !!this.x402Support()?.batchSettlement && !!(this.opts.x402BatchSettlement ?? this.opts.x402BatchSettlementFactory);
|
|
17696
17840
|
}
|
|
17697
17841
|
/**
|
|
17698
17842
|
* Apply any Lightning funding invoice this caller has paid since we last
|
|
17699
|
-
* looked — the pull half of pull-based crediting
|
|
17843
|
+
* looked — the pull half of pull-based crediting.
|
|
17700
17844
|
*
|
|
17701
17845
|
* Called from every authenticated surface where the balance is about to
|
|
17702
17846
|
* matter: the `/v1/credit` ops and the `/v1/job` submit. Deliberately **not**
|
|
@@ -17709,7 +17853,7 @@ var DVMServer = class {
|
|
|
17709
17853
|
* outstanding, and never blocks the request when it fails.
|
|
17710
17854
|
*
|
|
17711
17855
|
* The SDK-owned reporter queues each Lightning deposit inside
|
|
17712
|
-
* `settleInvoice`'s transaction
|
|
17856
|
+
* `settleInvoice`'s transaction. The loop below only preserves
|
|
17713
17857
|
* the explicit `onCreditFunded` override, whose caller owns its durability.
|
|
17714
17858
|
*/
|
|
17715
17859
|
async settleLightningFunding(callerPubkey, creditId) {
|
|
@@ -17740,7 +17884,6 @@ var DVMServer = class {
|
|
|
17740
17884
|
});
|
|
17741
17885
|
}
|
|
17742
17886
|
}
|
|
17743
|
-
/** Credit lifetime this DVM mints, in ms — the descriptor's `credit.ttl` or the platform default. */
|
|
17744
17887
|
creditTtlMs() {
|
|
17745
17888
|
return this.descriptor.credit ? this.descriptor.credit.ttlSeconds * 1e3 : IMPLICIT_CREDIT_TTL_MS;
|
|
17746
17889
|
}
|
|
@@ -17761,7 +17904,7 @@ var DVMServer = class {
|
|
|
17761
17904
|
* The per-DVM + per-request rail wiring every payment verification needs —
|
|
17762
17905
|
* everything except what the *operation* contributes (its price, its job or
|
|
17763
17906
|
* fund id, the caller's credit fields). Shared by `/v1/job`'s upfront gate
|
|
17764
|
-
* and `/v1/credit`'s top-up
|
|
17907
|
+
* and `/v1/credit`'s top-up so a rail wired into one is wired
|
|
17765
17908
|
* into both by construction; a divergence here would be a rail that funds
|
|
17766
17909
|
* jobs but silently can't fund credits.
|
|
17767
17910
|
*/
|
|
@@ -17794,7 +17937,7 @@ var DVMServer = class {
|
|
|
17794
17937
|
mppCredential,
|
|
17795
17938
|
mpp: this.opts.mpp,
|
|
17796
17939
|
resourcePath: c.req.path,
|
|
17797
|
-
//
|
|
17940
|
+
// the gate covers issuance and new-channel acceptance,
|
|
17798
17941
|
// not just the two advertisement surfaces. `ensureFresh` above makes this
|
|
17799
17942
|
// an observation rather than a cold-start default on the one surface that
|
|
17800
17943
|
// can act on it.
|
|
@@ -17812,8 +17955,6 @@ var DVMServer = class {
|
|
|
17812
17955
|
requestId: c.req.header("X-Cashu-Request-Id"),
|
|
17813
17956
|
mintHealthTracker: this.mintHealthTracker,
|
|
17814
17957
|
fxFetcher: this.fxFetcher,
|
|
17815
|
-
// internal-review: the ledger leg — every rail payment becomes fund_credit +
|
|
17816
|
-
// draw, atomic with the rail commit.
|
|
17817
17958
|
creditLedger: this.creditLedger,
|
|
17818
17959
|
processedPayments: this.processedPayments,
|
|
17819
17960
|
enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
|
|
@@ -17823,7 +17964,7 @@ var DVMServer = class {
|
|
|
17823
17964
|
}
|
|
17824
17965
|
/**
|
|
17825
17966
|
* Assemble this DVM's funding menu for a response denominated in
|
|
17826
|
-
* `currency
|
|
17967
|
+
* `currency`. All the refusal logic — including the credits-spec
|
|
17827
17968
|
* §3 isolate gate — lives in {@link buildCreditMenu}; this just supplies the
|
|
17828
17969
|
* server's state. `callerPubkey` must be a *verified* envelope pubkey: the
|
|
17829
17970
|
* balance echo is per-caller money and there is no safe fallback identity.
|
|
@@ -17832,8 +17973,6 @@ var DVMServer = class {
|
|
|
17832
17973
|
await this.ensureTempoSessionHealth();
|
|
17833
17974
|
const mppSplit = this.creditMppSplit();
|
|
17834
17975
|
return buildCreditMenu({
|
|
17835
|
-
// The SDK server only ever runs the container runtime; the isolate route
|
|
17836
|
-
// calls the same builder with `"isolate"` and is refused there.
|
|
17837
17976
|
runtime: "container",
|
|
17838
17977
|
config: this.descriptor.credit,
|
|
17839
17978
|
currency,
|
|
@@ -17842,7 +17981,7 @@ var DVMServer = class {
|
|
|
17842
17981
|
tempoWithheld: mppSplit.withheld,
|
|
17843
17982
|
x402Schemes: this.creditX402Schemes(),
|
|
17844
17983
|
lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
|
|
17845
|
-
// Configured, not advertised
|
|
17984
|
+
// Configured, not advertised: `advertisedMints()` is health
|
|
17846
17985
|
// filtered, and a rail that comes and goes with a mint's uptime is worse
|
|
17847
17986
|
// product than one that is honestly absent.
|
|
17848
17987
|
hasAcceptedMint: (this.opts.mints?.length ?? 0) > 0,
|
|
@@ -17879,7 +18018,7 @@ var DVMServer = class {
|
|
|
17879
18018
|
}
|
|
17880
18019
|
/**
|
|
17881
18020
|
* Currency this DVM's credit ledger is denominated in — the builder's
|
|
17882
|
-
* declared pricing currency, `"usd"` unless they said otherwise
|
|
18021
|
+
* declared pricing currency, `"usd"` unless they said otherwise.
|
|
17883
18022
|
*
|
|
17884
18023
|
* Every denomination surface reads this one answer: the funding menu on
|
|
17885
18024
|
* `/v1/quote` and the 402, `POST /v1/credit`, and the quote balance echo.
|
|
@@ -17888,20 +18027,16 @@ var DVMServer = class {
|
|
|
17888
18027
|
* static (USD-literal) price on a non-USD DVM, and a quote denominated
|
|
17889
18028
|
* elsewhere is refused as `quote_currency_mismatch` — so what a job draws in
|
|
17890
18029
|
* is what a top-up funded in, on every path.
|
|
17891
|
-
*
|
|
17892
|
-
* It used to return the literal `"usd"`, which is how a EUR-pricing DVM
|
|
17893
|
-
* came to fund `/v1/credit` top-ups in a denomination none of its jobs
|
|
17894
|
-
* would ever draw from.
|
|
17895
18030
|
*/
|
|
17896
18031
|
creditCurrency() {
|
|
17897
18032
|
return this.pricingCurrency;
|
|
17898
18033
|
}
|
|
17899
18034
|
/**
|
|
17900
|
-
* Quote-response `payment` block
|
|
18035
|
+
* Quote-response `payment` block. Carries the same `methods` array
|
|
17901
18036
|
* `/v1/info` advertises plus a per-rail `refundable` map keyed by method, so a
|
|
17902
18037
|
* caller can see which rails return funds on failure before committing. The
|
|
17903
18038
|
* `methods` shape stays identical to `/v1/info` and the platform isolate route
|
|
17904
|
-
* (
|
|
18039
|
+
* (current nested `payment.methods`); refundability rides alongside.
|
|
17905
18040
|
*/
|
|
17906
18041
|
quotePaymentBlock() {
|
|
17907
18042
|
const methods = this.resolvePaymentMethods();
|
|
@@ -17910,8 +18045,6 @@ var DVMServer = class {
|
|
|
17910
18045
|
refundable: Object.fromEntries(methods.map((m) => [m, RAIL_REFUNDABLE[m]]))
|
|
17911
18046
|
};
|
|
17912
18047
|
}
|
|
17913
|
-
// ── Route handlers ──────────────────────────────────────────────────
|
|
17914
|
-
/** Dev-mode proxy: forwards requests to a remote DVM specified by cookie. */
|
|
17915
18048
|
async handleProxy(c) {
|
|
17916
18049
|
const raw = getCookie(c, "dvmkit_target");
|
|
17917
18050
|
const target = raw ? decodeURIComponent(raw) : null;
|
|
@@ -17947,8 +18080,8 @@ var DVMServer = class {
|
|
|
17947
18080
|
auth: this.descriptor.auth ? [this.descriptor.auth.id] : ["none"],
|
|
17948
18081
|
...this.authAudience ? { auth_audience: this.authAudience } : {},
|
|
17949
18082
|
protocol_version: 1,
|
|
17950
|
-
sdk_sha:
|
|
17951
|
-
deploy_id:
|
|
18083
|
+
sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
|
|
18084
|
+
deploy_id: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
|
|
17952
18085
|
};
|
|
17953
18086
|
const ownerBlock = this.ownerDisplayBlock();
|
|
17954
18087
|
if (ownerBlock) info.owner = ownerBlock;
|
|
@@ -17969,8 +18102,7 @@ var DVMServer = class {
|
|
|
17969
18102
|
network: this.opts.x402.network,
|
|
17970
18103
|
asset: this.opts.x402.asset ?? "USDC",
|
|
17971
18104
|
...this.opts.x402.facilitator && { facilitator: this.opts.x402.facilitator },
|
|
17972
|
-
//
|
|
17973
|
-
// carry their PaymentRequirements before submitting (internal-review).
|
|
18105
|
+
// Advertises whether 402 challenges include x402 PaymentRequirements.
|
|
17974
18106
|
dual_serve: this.opts.x402.dualServe ?? true
|
|
17975
18107
|
};
|
|
17976
18108
|
}
|
|
@@ -17981,11 +18113,11 @@ var DVMServer = class {
|
|
|
17981
18113
|
return reason ? [{ ...method, reason }] : [];
|
|
17982
18114
|
});
|
|
17983
18115
|
info.tempo = {
|
|
17984
|
-
//
|
|
18116
|
+
// withheld sessions are withheld here too. `/v1/info` is
|
|
17985
18117
|
// where a caller decides what to attempt, so advertising a session the
|
|
17986
18118
|
// credit menu will refuse just sends them down a dead end.
|
|
17987
18119
|
methods: configured.filter((method) => this.tempoSessionOffered(method)),
|
|
17988
|
-
// …but say so rather than letting it vanish
|
|
18120
|
+
// …but say so rather than letting it vanish: an agent that
|
|
17989
18121
|
// sees only the shortened list cannot tell a DVM that never did
|
|
17990
18122
|
// sessions from one whose observer is down for a minute.
|
|
17991
18123
|
...withheld.length && { withheld },
|
|
@@ -18017,7 +18149,7 @@ var DVMServer = class {
|
|
|
18017
18149
|
return c.json(info);
|
|
18018
18150
|
}
|
|
18019
18151
|
/**
|
|
18020
|
-
* Render the `/v1/info.capabilities` block
|
|
18152
|
+
* Render the `/v1/info.capabilities` block. Each capability
|
|
18021
18153
|
* advertises its `description`, optional `input_schema`, and optional
|
|
18022
18154
|
* `pricing` block — `pricing.quote.schema` for dynamic-priced capabilities,
|
|
18023
18155
|
* `pricing.max` for static-priced ones. Capabilities without an `input`
|
|
@@ -18227,7 +18359,7 @@ var DVMServer = class {
|
|
|
18227
18359
|
* the two callers — `/v1/quote` and `computeDynamicPrice` on the submit
|
|
18228
18360
|
* path — cannot drift: a field added for one is present in the other by
|
|
18229
18361
|
* construction, and both paths price against the same view of the caller's
|
|
18230
|
-
* credit
|
|
18362
|
+
* credit.
|
|
18231
18363
|
*/
|
|
18232
18364
|
async buildQuoteContext(args) {
|
|
18233
18365
|
const ctx = {
|
|
@@ -18250,15 +18382,15 @@ var DVMServer = class {
|
|
|
18250
18382
|
}
|
|
18251
18383
|
/**
|
|
18252
18384
|
* Validate the `capability` field on a `/v1/job` or `/v1/quote` body
|
|
18253
|
-
*
|
|
18385
|
+
* Returns the canonical name when set + known; otherwise a
|
|
18254
18386
|
* structured 400 envelope agents can relay verbatim to a human caller.
|
|
18255
18387
|
*
|
|
18256
18388
|
* - **Missing** — `body.capability` is absent or empty. Surface as
|
|
18257
|
-
*
|
|
18258
|
-
*
|
|
18389
|
+
* `capability_required` with a hint pointing at `/v1/info` so the agent
|
|
18390
|
+
* can re-discover the capability set.
|
|
18259
18391
|
* - **Unknown** — `body.capability` is set but doesn't match any
|
|
18260
|
-
*
|
|
18261
|
-
*
|
|
18392
|
+
* capability on this descriptor. Surface as `unknown_capability` listing
|
|
18393
|
+
* the known names so the agent can correct without a round-trip.
|
|
18262
18394
|
*/
|
|
18263
18395
|
requireCapabilityFromBody(c, capability) {
|
|
18264
18396
|
const known = this.jobManager.capabilityNames();
|
|
@@ -18317,25 +18449,9 @@ var DVMServer = class {
|
|
|
18317
18449
|
}
|
|
18318
18450
|
}
|
|
18319
18451
|
/**
|
|
18320
|
-
*
|
|
18321
|
-
*
|
|
18322
|
-
*
|
|
18323
|
-
* The two endpoints used to answer the same caller mistake with two unrelated
|
|
18324
|
-
* shapes — quote gated on auth first, so a wrong field name came back 401
|
|
18325
|
-
* `auth_error` + `sub_reason: "schema_invalid"`, while submit parsed first and
|
|
18326
|
-
* came back 400 `invalid_input`. Both named the offending field (internal-review), so
|
|
18327
|
-
* neither was wrong; an agent just had to recognise two codes and two statuses
|
|
18328
|
-
* for one class of mistake, and the CLI's `parseStructuredError` mapped them to
|
|
18329
|
-
* two different `DvmError`s — which is why its structured-input remediation
|
|
18330
|
-
* (`invalid_input` only) never fired for the live fleet failure internal-review was
|
|
18331
|
-
* written about.
|
|
18332
|
-
*
|
|
18333
|
-
* The 400 wins because it is the only shape both endpoints can produce: a DVM
|
|
18334
|
-
* that declares no `auth` has no 401 to give, and quote didn't validate
|
|
18335
|
-
* `cap.input` at all on that path, so it answered 200 to a body submit would
|
|
18336
|
-
* reject. `display` is carried here as well as `message` because this is now
|
|
18337
|
-
* the shape agents branch on, and it reuses the 401's sentence so the words
|
|
18338
|
-
* relayed to a human don't change with the status.
|
|
18452
|
+
* Shared schema-violation response for quote and submit, including auth-less
|
|
18453
|
+
* DVMs. A 400 classifies caller input without requiring an auth gate; `display`
|
|
18454
|
+
* and `message` carry the same relayable explanation.
|
|
18339
18455
|
*/
|
|
18340
18456
|
invalidInputResponse(c, err) {
|
|
18341
18457
|
const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
@@ -18379,13 +18495,13 @@ var DVMServer = class {
|
|
|
18379
18495
|
}
|
|
18380
18496
|
/**
|
|
18381
18497
|
* True when a failed capability-input parse is the auth gate's answer rather
|
|
18382
|
-
* than {@link invalidInputResponse}'s
|
|
18498
|
+
* than {@link invalidInputResponse}'s.
|
|
18383
18499
|
*
|
|
18384
18500
|
* A capability schema may declare the signing-envelope fields required
|
|
18385
18501
|
* (cast's `addEpisodeSchema`) or optional (scribe's `transcribeSchema`). On
|
|
18386
18502
|
* the required shape an *unsigned* request fails the input parse naming
|
|
18387
18503
|
* `pubkey` / `signature` — a signing failure, and answering it with "your
|
|
18388
|
-
* input is invalid: pubkey required" is the
|
|
18504
|
+
* input is invalid: pubkey required" is the mistake in reverse. So a
|
|
18389
18505
|
* parse whose issues are **only** envelope-side falls through to the auth
|
|
18390
18506
|
* gate, which answers 401 `signature_invalid` with the remedy that works.
|
|
18391
18507
|
*
|
|
@@ -18402,13 +18518,13 @@ var DVMServer = class {
|
|
|
18402
18518
|
}
|
|
18403
18519
|
/**
|
|
18404
18520
|
* Mints to advertise to clients on `/v1/info` and `/v1/quote`. Filters the
|
|
18405
|
-
* configured list through the runtime health tracker
|
|
18521
|
+
* configured list through the runtime health tracker. When every
|
|
18406
18522
|
* mint is currently sick it falls back to the still-configured list rather
|
|
18407
18523
|
* than publish an empty rail — a *transient* outage shouldn't read as "no
|
|
18408
18524
|
* Cashu" and push well-behaved agents to bypass the rail. But NUT-incompatible
|
|
18409
|
-
* mints are excluded even from that fallback
|
|
18525
|
+
* mints are excluded even from that fallback: NUT-incompatibility
|
|
18410
18526
|
* is a permanent, deterministic config error the DVM can never settle
|
|
18411
|
-
* against, so
|
|
18527
|
+
* against, so such a mint is never advertised. If *every*
|
|
18412
18528
|
* configured mint is NUT-incompatible the rail correctly goes empty.
|
|
18413
18529
|
*/
|
|
18414
18530
|
advertisedMints() {
|
|
@@ -18837,9 +18953,9 @@ var DVMServer = class {
|
|
|
18837
18953
|
priceMsats,
|
|
18838
18954
|
capabilityName,
|
|
18839
18955
|
requesterTokenHash,
|
|
18840
|
-
//
|
|
18956
|
+
// everything the idempotent-replay path needs to recognise a
|
|
18841
18957
|
// retry of THIS call and re-issue the token its lost response carried.
|
|
18842
|
-
//
|
|
18958
|
+
// `jobId` is the pre-allocated id the payment's ledger draw
|
|
18843
18959
|
// already committed under.
|
|
18844
18960
|
{
|
|
18845
18961
|
requesterToken: jobToken,
|
|
@@ -18878,7 +18994,7 @@ var DVMServer = class {
|
|
|
18878
18994
|
}
|
|
18879
18995
|
}
|
|
18880
18996
|
/**
|
|
18881
|
-
* Book a committed funding as a deposit (
|
|
18997
|
+
* Book a committed funding as a deposit (the deposit-accounting rule). Fire-and-forget
|
|
18882
18998
|
* onto the reporter's durable queue: a funding is a liability the platform
|
|
18883
18999
|
* must show, but a reporting hiccup must never fail the paid request that
|
|
18884
19000
|
* just succeeded.
|
|
@@ -18893,7 +19009,7 @@ var DVMServer = class {
|
|
|
18893
19009
|
);
|
|
18894
19010
|
}
|
|
18895
19011
|
/**
|
|
18896
|
-
* Book a kept short payment as tagged revenue
|
|
19012
|
+
* Book a kept short payment as tagged revenue.
|
|
18897
19013
|
* The proofs are committed and the draw was settled on the spot, so the money
|
|
18898
19014
|
* is earned — but no job exists to carry it, hence the synthetic job id. It
|
|
18899
19015
|
* is deliberately **not** a deposit: nothing is owed back, so a liability
|
|
@@ -18923,7 +19039,7 @@ var DVMServer = class {
|
|
|
18923
19039
|
});
|
|
18924
19040
|
}
|
|
18925
19041
|
/**
|
|
18926
|
-
* The idempotent half of `POST /v1/job
|
|
19042
|
+
* The idempotent half of `POST /v1/job`. A paid submit whose
|
|
18927
19043
|
* response the caller lost is safe to retry — the accumulator's
|
|
18928
19044
|
* `UNIQUE(dvm_id, request_id, proof_secret)` makes the duplicate a no-op —
|
|
18929
19045
|
* but until now it was also useless: the caller got a 409 and the `job_id` /
|
|
@@ -18933,7 +19049,7 @@ var DVMServer = class {
|
|
|
18933
19049
|
* original caller. Proof is reproducing the request, **not** possessing the
|
|
18934
19050
|
* spent Cashu token: the token is a bearer secret, so gating on it alone
|
|
18935
19051
|
* would hand the job (and its result) to anyone who lifted it. The legitimate
|
|
18936
|
-
* retry always has the body — the
|
|
19052
|
+
* retry always has the body — the resume sweep re-POSTs it — while a
|
|
18937
19053
|
* token thief does not. On a descriptor-auth DVM the caller pubkey must match
|
|
18938
19054
|
* too. Anything short of that falls through to the 409 the caller has always
|
|
18939
19055
|
* gotten.
|
|
@@ -18960,8 +19076,8 @@ var DVMServer = class {
|
|
|
18960
19076
|
return buildSubmitResponse(c, prior, prior.requesterToken);
|
|
18961
19077
|
}
|
|
18962
19078
|
/**
|
|
18963
|
-
* The draw-keyed half of idempotent recovery
|
|
18964
|
-
*
|
|
19079
|
+
* The draw-keyed half of idempotent recovery. The signed body's
|
|
19080
|
+
* `(credit_id, draw_id)` matched a recorded draw;
|
|
18965
19081
|
* resolve the job it paid for (the draw carries its pre-allocated job id)
|
|
18966
19082
|
* and re-issue the original response under the SAME proof-of-origin gates
|
|
18967
19083
|
* as the header regime: reproduce the fingerprint, sign as the same caller
|
|
@@ -19050,7 +19166,6 @@ var DVMServer = class {
|
|
|
19050
19166
|
}
|
|
19051
19167
|
return void 0;
|
|
19052
19168
|
}
|
|
19053
|
-
/** Recover one still-pending draw recorded for the stable public job id. */
|
|
19054
19169
|
async recoverDrawAcceptance(opts) {
|
|
19055
19170
|
const dvmId = this.opts.dvmId;
|
|
19056
19171
|
if (!dvmId) return void 0;
|
|
@@ -19082,7 +19197,7 @@ var DVMServer = class {
|
|
|
19082
19197
|
/**
|
|
19083
19198
|
* Recompute the upfront price for a dynamic-priced capability by
|
|
19084
19199
|
* re-invoking its `onQuote.handler` with the parsed job input as `data`
|
|
19085
|
-
*
|
|
19200
|
+
* Returns a 400-style error envelope when the input doesn't
|
|
19086
19201
|
* satisfy `onQuote.schema` or the handler throws — failing closed is the
|
|
19087
19202
|
* right default, since the alternative (running the job with
|
|
19088
19203
|
* `requiredMsats=undefined`) lets unpaid jobs through.
|
|
@@ -19291,13 +19406,6 @@ var DVMServer = class {
|
|
|
19291
19406
|
}
|
|
19292
19407
|
return c.json({ messages: job.messages.filter((m) => m.seq > after) });
|
|
19293
19408
|
}
|
|
19294
|
-
/**
|
|
19295
|
-
* Structured 409 for a write (message POST / cancel) against a job that has
|
|
19296
|
-
* already reached a terminal status (internal-review). The two-audiences convention
|
|
19297
|
-
* wants a `code` an intermediary agent can branch on plus a `hint` it can
|
|
19298
|
-
* relay verbatim — a bare `{ error }` string gave the openclaw nothing to act
|
|
19299
|
-
* on and it re-paid a completed job into this 409 (internal-review step 7).
|
|
19300
|
-
*/
|
|
19301
19409
|
jobNotActiveResponse(c) {
|
|
19302
19410
|
return c.json(
|
|
19303
19411
|
{
|
|
@@ -19470,8 +19578,8 @@ var DVMServer = class {
|
|
|
19470
19578
|
}
|
|
19471
19579
|
/**
|
|
19472
19580
|
* Tear down this machine's in-memory reactivation scaffold when it stands
|
|
19473
|
-
* down
|
|
19474
|
-
* handler that won the single-execution claim
|
|
19581
|
+
* down after losing the reactivation advisory lock or deferring to a live original
|
|
19582
|
+
* handler that won the single-execution claim. Leaving
|
|
19475
19583
|
* the job in `activeJobs` with a live idle timer would, one idle window
|
|
19476
19584
|
* later, fire `cancelJobIdle` → Tx A `cancel` and clobber the winner. We also
|
|
19477
19585
|
* must not persist our stale snapshot; clearing the appender prevents that.
|
|
@@ -19507,6 +19615,7 @@ var DVMServer = class {
|
|
|
19507
19615
|
await jm.finalizeStoredTerminal(id);
|
|
19508
19616
|
return c.json({ status: "cancelled" });
|
|
19509
19617
|
}
|
|
19618
|
+
record.endedBy = "caller";
|
|
19510
19619
|
record.status = "cancelled";
|
|
19511
19620
|
record.lastActivityAt = Date.now();
|
|
19512
19621
|
await jm.store.save(record);
|
|
@@ -19852,7 +19961,7 @@ var PlatformTempoObserverHealth = class {
|
|
|
19852
19961
|
}
|
|
19853
19962
|
}
|
|
19854
19963
|
/**
|
|
19855
|
-
* Register one mount's session capability
|
|
19964
|
+
* Register one mount's session capability.
|
|
19856
19965
|
*
|
|
19857
19966
|
* A host can serve several DVMs behind one `DVMKIT_PLATFORM_TOKEN`, and the
|
|
19858
19967
|
* platform row that token identifies is the deployment as a whole — so the
|
|
@@ -19875,7 +19984,7 @@ var PlatformTempoObserverHealth = class {
|
|
|
19875
19984
|
* mount exists, so at boot there is routinely an open probe carrying **no**
|
|
19876
19985
|
* `tempo_session` at all — handing it back to the mount that just declared
|
|
19877
19986
|
* one would leave a scale-to-zero DVM unstamped until traffic arrived, which
|
|
19878
|
-
* is exactly the container-restart case
|
|
19987
|
+
* is exactly the container-restart case is about. Same shape with
|
|
19879
19988
|
* two mounts: the second would join a probe whose URL already read the first
|
|
19880
19989
|
* one's `false`.
|
|
19881
19990
|
*/
|
|
@@ -19923,7 +20032,7 @@ var PlatformTempoObserverHealth = class {
|
|
|
19923
20032
|
}
|
|
19924
20033
|
/**
|
|
19925
20034
|
* The probe URL, carrying this deployment's session capability when a mount
|
|
19926
|
-
* has declared one
|
|
20035
|
+
* has declared one.
|
|
19927
20036
|
*
|
|
19928
20037
|
* Absent means "no signal", not "no sessions": a container running an older
|
|
19929
20038
|
* SDK build sends nothing, and the platform must leave its stamp exactly
|