@dvmkit/sdk 0.2.0-rc.9 → 0.3.0-rc.10
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 +2 -0
- package/dist/{chunk-V7EVFLAK.js → chunk-2D75MNNG.js} +154 -523
- package/dist/{chunk-BIFLRKMO.js → chunk-2KGG5ORW.js} +1 -1
- package/dist/{chunk-PCUQZDZA.js → chunk-4VHTKNOR.js} +635 -321
- package/dist/{chunk-5URG56JJ.js → chunk-A4CM2BD3.js} +18 -2
- package/dist/{chunk-FT6HTUM4.js → chunk-AJNFXR7J.js} +1 -1
- package/dist/{chunk-S3XAHZQY.js → chunk-COYKJRMM.js} +24 -3
- package/dist/{chunk-UP2F5RRT.js → chunk-D7YQYLLM.js} +1 -4
- package/dist/{chunk-OMIQMMME.js → chunk-FPOLJRBF.js} +3 -3
- package/dist/{chunk-QP53RWAD.js → chunk-HHTL3ZFT.js} +9 -6
- package/dist/{chunk-JDT5LCJC.js → chunk-MD3H3BF7.js} +1 -1
- package/dist/{chunk-KSQEFVFJ.js → chunk-MMCE7XSR.js} +1 -1
- package/dist/{chunk-MRAGS5VP.js → chunk-N5B66UOS.js} +4 -16
- package/dist/{chunk-2ATUAUAO.js → chunk-O6D72KYA.js} +2 -2
- package/dist/{chunk-KXCKFIJI.js → chunk-QX52F6IB.js} +1 -1
- package/dist/{chunk-PHHAYRQV.js → chunk-U5VNEALI.js} +23 -10
- package/dist/{chunk-JLXYOV4Y.js → chunk-V5ZRATJH.js} +1 -1
- package/dist/{chunk-6GRIKOFB.js → chunk-VA7GT6C6.js} +5 -16
- package/dist/{chunk-O2X2CCKH.js → chunk-VIVXB4CD.js} +1 -1
- package/dist/{chunk-L67WTZX2.js → chunk-XMDQOXHO.js} +10 -33
- package/dist/{chunk-QT4ONTST.js → chunk-XRLKGCAW.js} +1 -1
- package/dist/{chunk-KMZXTBLA.js → chunk-XZ7LW4S5.js} +0 -1
- package/dist/{chunk-XQXJKJ3P.js → chunk-YO56VCTQ.js} +0 -4
- package/dist/{credit-ledger-5ZEJRI46.js → credit-ledger-MHA652TV.js} +2 -2
- package/dist/{credit-menu-D4Gcdgc4.d.ts → credit-menu-0-UtJ3hh.d.ts} +179 -533
- package/dist/{fx-B0SLBe5x.d.ts → fx-DhyT1Cjh.d.ts} +1 -1
- package/dist/index.d.ts +5 -17
- package/dist/internal/caller.d.ts +109 -698
- package/dist/internal/caller.js +10 -12
- package/dist/internal/server.d.ts +17 -42
- package/dist/internal/server.js +16 -15
- package/dist/{job-store-B2uZvga4.d.ts → job-store-D3CcZp_g.d.ts} +6 -10
- package/dist/{lightning-backend-CQBnQgsT.d.ts → lightning-backend-BhsyrBLa.d.ts} +1 -15
- package/dist/{memory-credit-ledger-ZOH6C3N4.js → memory-credit-ledger-BYBATRN3.js} +3 -3
- package/dist/{mpp-secret-state-WNAQQ6K4.js → mpp-secret-state-B5GCILVH.js} +1 -1
- package/dist/{mpp-setup-IOJBF7DB.js → mpp-setup-MPP3ST5X.js} +1 -1
- package/dist/{payout-reporter-5PIRYFVQ.js → payout-reporter-KEMUXEUO.js} +2 -2
- package/dist/{postgres-consumed-credential-store-ISRHBMOU.js → postgres-consumed-credential-store-6N2V5MJN.js} +1 -1
- package/dist/postgres-job-store-VE2JRUEC.js +7 -0
- package/dist/postgres-kv-store-DXGTKXLU.js +7 -0
- package/dist/{postgres-replay-store-IZFLTTAC.js → postgres-replay-store-ZF4JNZOW.js} +2 -2
- package/dist/{processed-payment-store-FIDI3RNH.js → processed-payment-store-GKMAWOM3.js} +2 -2
- package/dist/revenue-reporter-O75BGYFK.js +7 -0
- package/dist/server/index.d.ts +37 -140
- package/dist/server/index.js +64 -127
- package/dist/{step-cache-CXg7ziML.d.ts → step-cache-v2yhyhSZ.d.ts} +359 -1112
- package/dist/{tempo-charge-store-76TDAF34.js → tempo-charge-store-SDTUNTKA.js} +2 -2
- package/dist/{tempo-lifecycle-DXM7QXJQ.js → tempo-lifecycle-G5SB4HFC.js} +4 -4
- package/dist/{tempo-session-store-2JNOKJGX.js → tempo-session-store-WF3VZXNF.js} +2 -2
- package/dist/{tempo-wallet-O67H5M4N.js → tempo-wallet-ZFNUDFVO.js} +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/{usd-BNDg1715.d.ts → usd-Cu9G6xv2.d.ts} +1 -1
- package/dist/{x402-7S2EFINY.js → x402-CI6PSHNO.js} +4 -4
- package/package.json +2 -2
- package/dist/postgres-job-store-OGQ6IT4U.js +0 -7
- package/dist/postgres-kv-store-D5E2EZ24.js +0 -7
- package/dist/revenue-reporter-NNCNRY4C.js +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MemoryProcessedPaymentStore,
|
|
3
3
|
ProcessedPaymentReplayError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XRLKGCAW.js";
|
|
5
5
|
import {
|
|
6
6
|
createX402FacilitatorAuthHeaders,
|
|
7
7
|
createX402FacilitatorClient,
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
} from "./chunk-BQ2NMWKE.js";
|
|
11
11
|
import {
|
|
12
12
|
MemoryCreditLedger
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-U5VNEALI.js";
|
|
14
|
+
import {
|
|
15
|
+
PostgresJobStore
|
|
16
|
+
} from "./chunk-HHTL3ZFT.js";
|
|
14
17
|
import {
|
|
15
18
|
SIGNED_ENVELOPE_FIELDS,
|
|
16
19
|
SignedRequestError,
|
|
@@ -35,7 +38,7 @@ import {
|
|
|
35
38
|
localMessage,
|
|
36
39
|
providerMessage,
|
|
37
40
|
toJobRecord
|
|
38
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-MD3H3BF7.js";
|
|
39
42
|
import {
|
|
40
43
|
ADMIN_AUTH_CLOCK_SKEW_SECONDS,
|
|
41
44
|
ADMIN_AUTH_NONCE_TTL_SECONDS,
|
|
@@ -60,7 +63,7 @@ import {
|
|
|
60
63
|
toLockPubkey,
|
|
61
64
|
verifyCashuToken,
|
|
62
65
|
verifyChallenge
|
|
63
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-QX52F6IB.js";
|
|
64
67
|
import {
|
|
65
68
|
X402_BATCH_SETTLEMENT_SCHEME,
|
|
66
69
|
X402_DEFAULT_FACILITATOR,
|
|
@@ -83,7 +86,7 @@ import {
|
|
|
83
86
|
msatsToUsd,
|
|
84
87
|
msatsToUsdc,
|
|
85
88
|
usdcToMsats
|
|
86
|
-
} from "./chunk-
|
|
89
|
+
} from "./chunk-A4CM2BD3.js";
|
|
87
90
|
import {
|
|
88
91
|
resolveRpcOverride
|
|
89
92
|
} from "./chunk-MKI6OVW4.js";
|
|
@@ -100,7 +103,7 @@ import {
|
|
|
100
103
|
pinDimension,
|
|
101
104
|
withSpan,
|
|
102
105
|
withTraceContext
|
|
103
|
-
} from "./chunk-
|
|
106
|
+
} from "./chunk-YO56VCTQ.js";
|
|
104
107
|
import {
|
|
105
108
|
FxRateUnavailableError,
|
|
106
109
|
InvalidCurrencyError,
|
|
@@ -130,10 +133,10 @@ import {
|
|
|
130
133
|
isNonChannelBitcoinRail,
|
|
131
134
|
netOwedSats,
|
|
132
135
|
x402SettlementPending
|
|
133
|
-
} from "./chunk-
|
|
136
|
+
} from "./chunk-2D75MNNG.js";
|
|
134
137
|
import {
|
|
135
138
|
withSdkInitLock
|
|
136
|
-
} from "./chunk-
|
|
139
|
+
} from "./chunk-COYKJRMM.js";
|
|
137
140
|
import {
|
|
138
141
|
withPostgresRecoveryContext
|
|
139
142
|
} from "./chunk-C3MTFLC6.js";
|
|
@@ -141,7 +144,7 @@ import {
|
|
|
141
144
|
advertisedMethods,
|
|
142
145
|
challengeMeta,
|
|
143
146
|
isTempoInsufficientFundsError
|
|
144
|
-
} from "./chunk-
|
|
147
|
+
} from "./chunk-XZ7LW4S5.js";
|
|
145
148
|
|
|
146
149
|
// src/sdk/server/lock-pubkey-store.ts
|
|
147
150
|
async function initLockPubkeyTable(db) {
|
|
@@ -881,16 +884,13 @@ async function buildCreditMenu(args) {
|
|
|
881
884
|
ttl_ms: args.config.ttlSeconds * 1e3,
|
|
882
885
|
currency,
|
|
883
886
|
funding,
|
|
884
|
-
//
|
|
885
|
-
// sessions only would otherwise lose the whole block the moment the gate
|
|
886
|
-
// closed, taking its capability signal with it.
|
|
887
|
+
// Keep withheld Tempo capability discoverable even when no method is currently offered.
|
|
887
888
|
...(tempoMethods.length > 0 || tempoWithheld.length > 0) && {
|
|
888
889
|
tempo: {
|
|
889
890
|
methods: tempoMethods,
|
|
890
891
|
...tempoWithheld.length > 0 && { withheld: tempoWithheld }
|
|
891
892
|
}
|
|
892
893
|
},
|
|
893
|
-
// Tied to the rail's own presence, the way `lightning_min_micro` is below:
|
|
894
894
|
// a builder who overrides `paymentMethods` can drop x402 from `funding`
|
|
895
895
|
// while the rail stays wired, and terms for a rail the menu doesn't offer
|
|
896
896
|
// are the same misadvertisement in the other direction.
|
|
@@ -3651,7 +3651,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3651
3651
|
x402Requirements
|
|
3652
3652
|
});
|
|
3653
3653
|
}
|
|
3654
|
-
const { verifyX402Payment } = await import("./x402-
|
|
3654
|
+
const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
|
|
3655
3655
|
const receipt = await verifyX402Payment(
|
|
3656
3656
|
x402Payment,
|
|
3657
3657
|
x402Config,
|
|
@@ -4585,7 +4585,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
|
|
|
4585
4585
|
snapshot
|
|
4586
4586
|
});
|
|
4587
4587
|
}
|
|
4588
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4588
|
+
const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
|
|
4589
4589
|
const receipt = await verifyX402Payment(
|
|
4590
4590
|
body.content.x402_payment,
|
|
4591
4591
|
opts.x402Config,
|
|
@@ -5002,7 +5002,7 @@ async function processIncomingPayment(job, body, opts) {
|
|
|
5002
5002
|
try {
|
|
5003
5003
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
5004
5004
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
5005
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5005
|
+
const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
|
|
5006
5006
|
const receipt = await verifyX402Payment(
|
|
5007
5007
|
body.content.x402_payment,
|
|
5008
5008
|
opts.x402Config,
|
|
@@ -6691,14 +6691,8 @@ var CREDIT_REQUEST_SCHEMA = z.object({
|
|
|
6691
6691
|
/** Client-generated idempotency key — the same id polls/collects one drain. */
|
|
6692
6692
|
drain_id: z.string().min(1),
|
|
6693
6693
|
/**
|
|
6694
|
-
*
|
|
6695
|
-
*
|
|
6696
|
-
* Deliberately a bare string rather than the `DrainMethod` enum, so a
|
|
6697
|
-
* rail this build does not carry is answered by `validateDrainTarget`
|
|
6698
|
-
* with a typed `drain_method_unsupported` naming the rails that do
|
|
6699
|
-
* work — not by a Zod parse failure that says only "invalid enum". The
|
|
6700
|
-
* caller of a removed rail (`lightning`) and the caller of a
|
|
6701
|
-
* typo deserve the same actionable refusal.
|
|
6694
|
+
* Required on a fresh drain, optional on poll. Keep this a string so an unsupported
|
|
6695
|
+
* rail receives its specific typed refusal before payout-shape validation.
|
|
6702
6696
|
*/
|
|
6703
6697
|
method: z.string().min(1).optional(),
|
|
6704
6698
|
/**
|
|
@@ -6765,6 +6759,18 @@ async function handleCreditRequest(c, deps) {
|
|
|
6765
6759
|
await deps.settleLightning(callerPubkey, body.credit_id);
|
|
6766
6760
|
return handleDrain(c, deps, { callerPubkey, body, envelope });
|
|
6767
6761
|
case "fund":
|
|
6762
|
+
if (deps.withFundingAdmission && body.fund) {
|
|
6763
|
+
const creditId = body.credit_id ?? derivedFundCreditId(callerPubkey, body.fund.fund_id);
|
|
6764
|
+
const [prior, invoice] = await Promise.all([
|
|
6765
|
+
deps.ledger.getFunding({ creditId, fundId: body.fund.fund_id }),
|
|
6766
|
+
body.fund.method === "lightning" ? deps.ledger.getInvoice({ creditId, fundId: body.fund.fund_id }) : Promise.resolve(void 0)
|
|
6767
|
+
]);
|
|
6768
|
+
const priorCredit = prior ? await deps.ledger.get(creditId) : void 0;
|
|
6769
|
+
if (priorCredit?.callerPubkey === callerPubkey || invoice?.callerPubkey === callerPubkey) {
|
|
6770
|
+
return handleFund(c, deps, { callerPubkey, body, envelope, menu });
|
|
6771
|
+
}
|
|
6772
|
+
return deps.withFundingAdmission(c, body, () => handleFund(c, deps, { callerPubkey, body, envelope, menu }));
|
|
6773
|
+
}
|
|
6768
6774
|
return handleFund(c, deps, { callerPubkey, body, envelope, menu });
|
|
6769
6775
|
}
|
|
6770
6776
|
}
|
|
@@ -6824,17 +6830,12 @@ async function handleBalance(c, deps, args) {
|
|
|
6824
6830
|
const primary = pickPrimary(all, args.menu.currency);
|
|
6825
6831
|
return c.json({
|
|
6826
6832
|
op: "balance",
|
|
6827
|
-
//
|
|
6828
|
-
//
|
|
6833
|
+
// Keep expired credit records visible. Nominal expiry alone does not release ownership;
|
|
6834
|
+
// an expiry sweep can later zero the undrawn balance.
|
|
6829
6835
|
credits: all.map(
|
|
6830
6836
|
(credit) => creditBody(credit, pendingChannels.has(credit.tempoChannelId ?? ""))
|
|
6831
6837
|
),
|
|
6832
6838
|
...primary ? { credit: creditBody(primary, pendingChannels.has(primary.tempoChannelId ?? "")) } : {},
|
|
6833
|
-
// the funding menu the quote and 402 surfaces already advertise,
|
|
6834
|
-
// echoed here so one read answers "what do I hold, and what may I add".
|
|
6835
|
-
// Under the key `menu`, not `credit` — that one is the credit projection on
|
|
6836
|
-
// this route. Carried without the caller echo fields: `credits[]` above says
|
|
6837
|
-
// the same thing in more detail, so `deps.menu()` is called without a pubkey.
|
|
6838
6839
|
menu: args.menu
|
|
6839
6840
|
});
|
|
6840
6841
|
}
|
|
@@ -7113,11 +7114,8 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7113
7114
|
resourcePath: c.req.path,
|
|
7114
7115
|
action: "close",
|
|
7115
7116
|
expectedChannelId,
|
|
7116
|
-
//
|
|
7117
|
-
//
|
|
7118
|
-
// broadcasts. Run above the call instead and a credential
|
|
7119
|
-
// signed for another channel touches the reservation on its way to a
|
|
7120
|
-
// refusal that was never going to close anything.
|
|
7117
|
+
// Reassert the intent only after the verifier binds the credential to this channel, and
|
|
7118
|
+
// before broadcast. A wrong-channel signature cannot create or cancel its close intent.
|
|
7121
7119
|
onBound: async () => {
|
|
7122
7120
|
await beginClose(expectedChannelId, record.drainId);
|
|
7123
7121
|
recoveryVersion = await deps.tempoSettlementReadiness?.captureRecoveryVersion();
|
|
@@ -7419,9 +7417,6 @@ async function handleX402ChannelDrain(c, deps, args) {
|
|
|
7419
7417
|
amount: "0",
|
|
7420
7418
|
operation: "refund",
|
|
7421
7419
|
effectId: `${args.credit.creditId}:${args.drainId}`,
|
|
7422
|
-
// The voucher the caller presents is what actually gets revoked and paid,
|
|
7423
|
-
// so the settlement layer refuses one signed for any other channel before
|
|
7424
|
-
// it touches anything.
|
|
7425
7420
|
expectedChannelId: channelId,
|
|
7426
7421
|
refundBoundNative,
|
|
7427
7422
|
commit: async (_funding, tx) => {
|
|
@@ -7640,12 +7635,9 @@ function drainBody(record) {
|
|
|
7640
7635
|
status: record.status,
|
|
7641
7636
|
amount_micro: record.amountMicro,
|
|
7642
7637
|
currency: record.currency,
|
|
7643
|
-
//
|
|
7644
|
-
//
|
|
7645
|
-
//
|
|
7646
|
-
// does not move with the market. Published beside the fiat one rather than
|
|
7647
|
-
// instead of it — the fiat figure is what the caller's balance was
|
|
7648
|
-
// denominated in, and both are things an agent relays.
|
|
7638
|
+
// Fixed in-kind Bitcoin refund obligation after the delivery reserve. It reflects the
|
|
7639
|
+
// funding lots remaining at request time; null means no whole-balance in-kind price was
|
|
7640
|
+
// available.
|
|
7649
7641
|
...record.owedSats !== null ? { owed_sats: record.owedSats } : {},
|
|
7650
7642
|
ledger_seq: record.ledgerSeq,
|
|
7651
7643
|
created_at: record.createdAt,
|
|
@@ -7659,17 +7651,7 @@ function drainCopy(record) {
|
|
|
7659
7651
|
case "pending":
|
|
7660
7652
|
if (record.method === "cashu") {
|
|
7661
7653
|
return {
|
|
7662
|
-
display: record.owedSats === null ? "The refund is approved and being prepared. The balance is already set aside for you." :
|
|
7663
|
-
// Named because it is the obligation, and because it is
|
|
7664
|
-
// fixed: a Bitcoin refund returns the share of the Bitcoin you
|
|
7665
|
-
// deposited that you never spent, so it does not move if the
|
|
7666
|
-
// price does before you collect it. The mint's own
|
|
7667
|
-
// redemption charge is named rather than glossed — it is a sat
|
|
7668
|
-
// or two at a fee-charging mint and nothing at a free one, and
|
|
7669
|
-
// a figure an agent relays to a human should not be a promise
|
|
7670
|
-
// that lands short.
|
|
7671
|
-
`The refund is approved and being prepared: ${record.owedSats} sats, the share of the Bitcoin you deposited that you haven't spent, less whatever the mint charges to hand it over. That figure won't move while you wait, whatever the price does.`
|
|
7672
|
-
),
|
|
7654
|
+
display: record.owedSats === null ? "The refund is approved and being prepared. The balance is already set aside for you." : `The refund is approved and being prepared: ${record.owedSats} sats, the share of the Bitcoin you deposited that you haven't spent, less whatever the mint charges to hand it over. That figure won't move while you wait, whatever the price does.`,
|
|
7673
7655
|
hint: "Poll this op with the same drain_id until a token appears. Preparation can be delayed (e.g. the mint is temporarily down) \u2014 the refund stays owed to you either way."
|
|
7674
7656
|
};
|
|
7675
7657
|
}
|
|
@@ -7795,11 +7777,6 @@ async function handleFund(c, deps, args) {
|
|
|
7795
7777
|
fundId: fund.fund_id,
|
|
7796
7778
|
amountMicro: fund.amount_micro,
|
|
7797
7779
|
menu,
|
|
7798
|
-
// Lightning and MPP name their rail in the signed body. Header-attached
|
|
7799
|
-
// Cashu and x402 are discriminated by `verifyUpfrontPayment`, which still
|
|
7800
|
-
// refuses their mismatches before money moves. Lightning must be checked
|
|
7801
|
-
// here because past this point the bolt11 is minted and payable; MPP is
|
|
7802
|
-
// checked here so discovery never advertises the wrong Tempo instrument.
|
|
7803
7780
|
rail: fund.method,
|
|
7804
7781
|
tempoIntent: fund.method === "tempo" ? fund.intent : void 0
|
|
7805
7782
|
});
|
|
@@ -8090,9 +8067,8 @@ function lightningInvoiceBlock(invoice) {
|
|
|
8090
8067
|
invoice: invoice.bolt11,
|
|
8091
8068
|
payment_hash: invoice.paymentHash,
|
|
8092
8069
|
amount_msats: invoice.amountMsats,
|
|
8093
|
-
//
|
|
8094
|
-
//
|
|
8095
|
-
// in seconds, and a bare `expires_at` invites a reader to assume that.
|
|
8070
|
+
// Wire timestamp is milliseconds; bolt11 lifetimes are conventionally expressed in
|
|
8071
|
+
// seconds.
|
|
8096
8072
|
expires_at_ms: invoice.expiresAt
|
|
8097
8073
|
}
|
|
8098
8074
|
};
|
|
@@ -8323,26 +8299,13 @@ var DRAIN_CONFLICT_COPY = {
|
|
|
8323
8299
|
retryable: true,
|
|
8324
8300
|
drainRegistered: false
|
|
8325
8301
|
},
|
|
8326
|
-
// Deliberately rail-neutral: `refundFloorEvidence` is one floor over both
|
|
8327
|
-
// channel rails, and two doors with two answers about one shortfall is the
|
|
8328
|
-
// drift consolidating it was meant to prevent. Tempo answers its
|
|
8329
|
-
// own through `tempoFloorRefusal`, which reaches the same conclusion a step
|
|
8330
|
-
// earlier and adds the one rail-specific fact — its channel is already closed.
|
|
8331
8302
|
refund_floor: {
|
|
8332
8303
|
display: "The refund reached the chain, but it paid less than the balance this service had recorded, so nothing was booked here. Your on-chain refund arrived; the difference has to be reconciled by the service operator.",
|
|
8333
8304
|
hint: ({ creditId, drainId }) => `Report drain ${drainId} on credit ${creditId} to the service operator \u2014 only they can reconcile the difference. Retrying this drain, or requesting a new drain_id, lands here again.`,
|
|
8334
|
-
// Terminal for the caller, unlike the two above: no amount of waiting or
|
|
8335
|
-
// re-signing changes what the chain already paid.
|
|
8336
8305
|
retryable: false,
|
|
8337
|
-
//
|
|
8338
|
-
//
|
|
8339
|
-
//
|
|
8340
|
-
// `effect_id`, which is the id `X402SettlementRepair.reconcile` books the
|
|
8341
|
-
// drain under — so the caller has to keep it. Dropping it mints a fresh one
|
|
8342
|
-
// that the wedged-channel gate refuses as `settlement_pending` anyway, and
|
|
8343
|
-
// re-presenting *this* id is that gate's own documented exemption. Tempo
|
|
8344
|
-
// reaches the same `true` one artifact earlier, off a `credit_drains` row
|
|
8345
|
-
// its close-intent reservation already committed.
|
|
8306
|
+
// The settlement evidence survives outside the rolled-back ledger transaction. Keep
|
|
8307
|
+
// drain_registered:true so the caller retains this effect ID for repair even when no
|
|
8308
|
+
// drain row committed.
|
|
8346
8309
|
drainRegistered: true
|
|
8347
8310
|
}
|
|
8348
8311
|
};
|
|
@@ -8388,8 +8351,6 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
|
|
|
8388
8351
|
message: error.message,
|
|
8389
8352
|
display: copy?.display,
|
|
8390
8353
|
hint: copy?.hint({ creditId, drainId }),
|
|
8391
|
-
// An unclassified conflict keeps the duplicate-payout copy's own
|
|
8392
|
-
// `retryable`, and the id it was never told anything about.
|
|
8393
8354
|
retryable: copy?.retryable,
|
|
8394
8355
|
extra: { drain_registered: copy?.drainRegistered ?? false }
|
|
8395
8356
|
});
|
|
@@ -8440,8 +8401,8 @@ function creditBody(credit, reconciliationPending = false) {
|
|
|
8440
8401
|
remaining_micro: credit.availableMicro,
|
|
8441
8402
|
expiry_ms: credit.expiryMs,
|
|
8442
8403
|
expired: credit.expired,
|
|
8443
|
-
//
|
|
8444
|
-
//
|
|
8404
|
+
// Nominal expiry does not itself reclaim ownership; a committed sweep can remove undrawn
|
|
8405
|
+
// value.
|
|
8445
8406
|
drainable: credit.status === "active" && credit.availableMicro > 0,
|
|
8446
8407
|
...reconciliationPending && { reconciliation_pending: true },
|
|
8447
8408
|
ledger_seq: credit.lastLedgerSeq
|
|
@@ -8975,11 +8936,9 @@ var X402ExactSettlementServer = class {
|
|
|
8975
8936
|
}
|
|
8976
8937
|
return this.finalize(intent, args.btcUsdRate, args.commit);
|
|
8977
8938
|
}
|
|
8978
|
-
/** Read the operator queue. */
|
|
8979
8939
|
list(args) {
|
|
8980
8940
|
return this.opts.store.list(args);
|
|
8981
8941
|
}
|
|
8982
|
-
/** Read durable attempt evidence for one intent. */
|
|
8983
8942
|
attempts(dvmId, intentId) {
|
|
8984
8943
|
return this.opts.store.attempts(dvmId, intentId);
|
|
8985
8944
|
}
|
|
@@ -9127,6 +9086,371 @@ function safeError(err) {
|
|
|
9127
9086
|
return "settlement_transport_error";
|
|
9128
9087
|
}
|
|
9129
9088
|
|
|
9089
|
+
// src/sdk/server/hosting-control.ts
|
|
9090
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
9091
|
+
function signHostingControl(secret, command) {
|
|
9092
|
+
const signed = command.version === 2 ? [
|
|
9093
|
+
"dvmkit-hosting-v2",
|
|
9094
|
+
command.action,
|
|
9095
|
+
command.scope,
|
|
9096
|
+
command.dvmId,
|
|
9097
|
+
command.generation ?? null,
|
|
9098
|
+
command.leaseExpiresAtMs ?? null,
|
|
9099
|
+
command.deadlineMs ?? null,
|
|
9100
|
+
command.timestampMs,
|
|
9101
|
+
command.nonce
|
|
9102
|
+
] : [
|
|
9103
|
+
"dvmkit-hosting-v1",
|
|
9104
|
+
command.action,
|
|
9105
|
+
command.scope,
|
|
9106
|
+
command.dvmId,
|
|
9107
|
+
command.generation ?? null,
|
|
9108
|
+
command.deadlineMs ?? null,
|
|
9109
|
+
command.timestampMs,
|
|
9110
|
+
command.nonce
|
|
9111
|
+
];
|
|
9112
|
+
return createHmac("sha256", secret).update(JSON.stringify(signed)).digest("hex");
|
|
9113
|
+
}
|
|
9114
|
+
var HostingControl = class {
|
|
9115
|
+
constructor(pool, dvmId, config) {
|
|
9116
|
+
this.pool = pool;
|
|
9117
|
+
this.dvmId = dvmId;
|
|
9118
|
+
this.config = config;
|
|
9119
|
+
if (Buffer.byteLength(config.secret) < 32 || !config.scope) {
|
|
9120
|
+
throw new Error("Hosting control requires a 32-byte secret and a non-empty scope");
|
|
9121
|
+
}
|
|
9122
|
+
}
|
|
9123
|
+
pool;
|
|
9124
|
+
dvmId;
|
|
9125
|
+
config;
|
|
9126
|
+
async init() {
|
|
9127
|
+
await withSdkInitLock(this.pool, async () => {
|
|
9128
|
+
await this.pool.query(`
|
|
9129
|
+
CREATE TABLE IF NOT EXISTS sdk_hosting_control (
|
|
9130
|
+
dvm_id TEXT NOT NULL, scope TEXT NOT NULL, state TEXT NOT NULL,
|
|
9131
|
+
generation BIGINT NOT NULL, deadline_ms BIGINT, lease_expires_at_ms BIGINT,
|
|
9132
|
+
PRIMARY KEY (dvm_id, scope)
|
|
9133
|
+
);
|
|
9134
|
+
CREATE TABLE IF NOT EXISTS sdk_hosting_nonces (
|
|
9135
|
+
dvm_id TEXT NOT NULL, scope TEXT NOT NULL, nonce TEXT NOT NULL,
|
|
9136
|
+
created_at_ms BIGINT NOT NULL,
|
|
9137
|
+
PRIMARY KEY (dvm_id, scope, nonce)
|
|
9138
|
+
);
|
|
9139
|
+
CREATE TABLE IF NOT EXISTS sdk_hosting_admissions (
|
|
9140
|
+
id TEXT PRIMARY KEY, dvm_id TEXT NOT NULL, scope TEXT NOT NULL,
|
|
9141
|
+
job_id TEXT NOT NULL, admitted_at_ms BIGINT NOT NULL,
|
|
9142
|
+
outcome TEXT NOT NULL DEFAULT 'pending', drain_deadline_ms BIGINT,
|
|
9143
|
+
credit_id TEXT, fund_id TEXT, payment_artifact_hash TEXT
|
|
9144
|
+
);
|
|
9145
|
+
ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS drain_deadline_ms BIGINT;
|
|
9146
|
+
ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS credit_id TEXT;
|
|
9147
|
+
ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS fund_id TEXT;
|
|
9148
|
+
ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS payment_artifact_hash TEXT;
|
|
9149
|
+
ALTER TABLE sdk_hosting_control ADD COLUMN IF NOT EXISTS lease_expires_at_ms BIGINT;
|
|
9150
|
+
CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_pending
|
|
9151
|
+
ON sdk_hosting_admissions (dvm_id, scope) WHERE outcome = 'pending';
|
|
9152
|
+
CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_unresolved
|
|
9153
|
+
ON sdk_hosting_admissions (dvm_id, scope) WHERE outcome = 'unresolved';
|
|
9154
|
+
CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_artifact
|
|
9155
|
+
ON sdk_hosting_admissions (dvm_id, scope, payment_artifact_hash)
|
|
9156
|
+
WHERE payment_artifact_hash IS NOT NULL;
|
|
9157
|
+
CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_job
|
|
9158
|
+
ON sdk_hosting_admissions (dvm_id, job_id, scope);
|
|
9159
|
+
CREATE INDEX IF NOT EXISTS sdk_hosting_nonces_age ON sdk_hosting_nonces (created_at_ms);
|
|
9160
|
+
`);
|
|
9161
|
+
await this.pool.query(
|
|
9162
|
+
`INSERT INTO sdk_hosting_control (dvm_id, scope, state, generation)
|
|
9163
|
+
VALUES ($1, $2, 'draining', 0) ON CONFLICT DO NOTHING`,
|
|
9164
|
+
[this.dvmId, this.config.scope]
|
|
9165
|
+
);
|
|
9166
|
+
const client = await this.pool.connect();
|
|
9167
|
+
try {
|
|
9168
|
+
await client.query("BEGIN");
|
|
9169
|
+
const migrated = await client.query(
|
|
9170
|
+
`UPDATE sdk_hosting_control SET state='draining',
|
|
9171
|
+
deadline_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint + 3600000
|
|
9172
|
+
WHERE dvm_id=$1 AND scope=$2 AND state='accepting' AND lease_expires_at_ms IS NULL
|
|
9173
|
+
RETURNING deadline_ms`,
|
|
9174
|
+
[this.dvmId, this.config.scope]
|
|
9175
|
+
);
|
|
9176
|
+
if (migrated.rows[0]) await this.pinAdmissionDeadlines(client, Number(migrated.rows[0].deadline_ms));
|
|
9177
|
+
await client.query("COMMIT");
|
|
9178
|
+
} catch (error) {
|
|
9179
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
9180
|
+
throw error;
|
|
9181
|
+
} finally {
|
|
9182
|
+
client.release();
|
|
9183
|
+
}
|
|
9184
|
+
});
|
|
9185
|
+
}
|
|
9186
|
+
async authorize(command, signature) {
|
|
9187
|
+
if (command.version !== void 0 && command.version !== 2 || command.version === 2 && command.action !== "resume" || command.version === void 0 && command.leaseExpiresAtMs !== void 0 || command.scope !== this.config.scope || command.dvmId !== this.dvmId || !Number.isSafeInteger(command.timestampMs) || Math.abs(Date.now() - command.timestampMs) > 3e5 || typeof command.nonce !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(command.nonce) || !signature || !/^[0-9a-f]{64}$/i.test(signature)) return false;
|
|
9188
|
+
const actual = Buffer.from(signature, "hex");
|
|
9189
|
+
const expected = Buffer.from(signHostingControl(this.config.secret, command), "hex");
|
|
9190
|
+
if (!timingSafeEqual(actual, expected)) return false;
|
|
9191
|
+
const result = await this.pool.query(
|
|
9192
|
+
`INSERT INTO sdk_hosting_nonces (dvm_id, scope, nonce, created_at_ms)
|
|
9193
|
+
VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`,
|
|
9194
|
+
[this.dvmId, this.config.scope, command.nonce, Date.now()]
|
|
9195
|
+
);
|
|
9196
|
+
await this.pool.query("DELETE FROM sdk_hosting_nonces WHERE created_at_ms < $1", [Date.now() - 6e5]);
|
|
9197
|
+
return result.rowCount === 1;
|
|
9198
|
+
}
|
|
9199
|
+
async admit(id, jobId, details) {
|
|
9200
|
+
const client = await this.pool.connect();
|
|
9201
|
+
try {
|
|
9202
|
+
await client.query("BEGIN");
|
|
9203
|
+
const { rows } = await client.query(
|
|
9204
|
+
`SELECT state, lease_expires_at_ms, deadline_ms FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
|
|
9205
|
+
[this.dvmId, this.config.scope]
|
|
9206
|
+
);
|
|
9207
|
+
const now = await this.databaseNow(client);
|
|
9208
|
+
const current = rows[0];
|
|
9209
|
+
if (current) await this.expireIfNeeded(client, current, now);
|
|
9210
|
+
if (current?.state !== "accepting" || current.lease_expires_at_ms === null || Number(current.lease_expires_at_ms) <= now) {
|
|
9211
|
+
await client.query("COMMIT");
|
|
9212
|
+
return false;
|
|
9213
|
+
}
|
|
9214
|
+
await client.query(
|
|
9215
|
+
`INSERT INTO sdk_hosting_admissions
|
|
9216
|
+
(id, dvm_id, scope, job_id, admitted_at_ms, credit_id, fund_id, payment_artifact_hash)
|
|
9217
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
9218
|
+
[
|
|
9219
|
+
id,
|
|
9220
|
+
this.dvmId,
|
|
9221
|
+
this.config.scope,
|
|
9222
|
+
jobId,
|
|
9223
|
+
now,
|
|
9224
|
+
details?.creditId ?? null,
|
|
9225
|
+
details?.fundId ?? null,
|
|
9226
|
+
details?.paymentArtifactHash ?? null
|
|
9227
|
+
]
|
|
9228
|
+
);
|
|
9229
|
+
await client.query("COMMIT");
|
|
9230
|
+
return true;
|
|
9231
|
+
} catch (error) {
|
|
9232
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
9233
|
+
throw error;
|
|
9234
|
+
} finally {
|
|
9235
|
+
client.release();
|
|
9236
|
+
}
|
|
9237
|
+
}
|
|
9238
|
+
async acceptedJobByPaymentArtifactHash(hash2) {
|
|
9239
|
+
const { rows } = await this.pool.query(
|
|
9240
|
+
`SELECT a.job_id FROM sdk_hosting_admissions a
|
|
9241
|
+
JOIN jobs j ON j.id=a.job_id
|
|
9242
|
+
WHERE a.dvm_id=$1 AND a.scope=$2 AND a.payment_artifact_hash=$3
|
|
9243
|
+
AND a.outcome IN ('accepted', 'unresolved', 'pending')
|
|
9244
|
+
ORDER BY a.admitted_at_ms DESC LIMIT 1`,
|
|
9245
|
+
[this.dvmId, this.config.scope, hash2]
|
|
9246
|
+
);
|
|
9247
|
+
return rows[0]?.job_id;
|
|
9248
|
+
}
|
|
9249
|
+
async rebindAdmissionJob(id, jobId) {
|
|
9250
|
+
await this.pool.query(
|
|
9251
|
+
`UPDATE sdk_hosting_admissions SET job_id=$2 WHERE id=$1 AND outcome='pending'`,
|
|
9252
|
+
[id, jobId]
|
|
9253
|
+
);
|
|
9254
|
+
}
|
|
9255
|
+
async finishAdmission(id, outcome, jobId) {
|
|
9256
|
+
await this.pool.query(
|
|
9257
|
+
`UPDATE sdk_hosting_admissions SET outcome=$2, job_id=COALESCE($3, job_id)
|
|
9258
|
+
WHERE id=$1 AND outcome='pending'`,
|
|
9259
|
+
[id, outcome, jobId ?? null]
|
|
9260
|
+
);
|
|
9261
|
+
}
|
|
9262
|
+
async jobDeadline(jobId) {
|
|
9263
|
+
const { rows } = await this.pool.query(
|
|
9264
|
+
`SELECT MIN(drain_deadline_ms)::text AS drain_deadline_ms
|
|
9265
|
+
FROM sdk_hosting_admissions WHERE dvm_id=$1 AND scope=$2 AND job_id=$3`,
|
|
9266
|
+
[this.dvmId, this.config.scope, jobId]
|
|
9267
|
+
);
|
|
9268
|
+
return rows[0]?.drain_deadline_ms === null || !rows[0] ? null : Number(rows[0].drain_deadline_ms);
|
|
9269
|
+
}
|
|
9270
|
+
async dispatchDeadlineReached(jobId) {
|
|
9271
|
+
const { rows } = await this.pool.query(
|
|
9272
|
+
`SELECT EXISTS (
|
|
9273
|
+
SELECT 1 FROM sdk_hosting_admissions
|
|
9274
|
+
WHERE dvm_id=$1 AND scope=$2 AND job_id=$3
|
|
9275
|
+
AND drain_deadline_ms <= FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
9276
|
+
) OR EXISTS (
|
|
9277
|
+
SELECT 1 FROM sdk_hosting_control
|
|
9278
|
+
WHERE dvm_id=$1 AND scope=$2 AND state='draining'
|
|
9279
|
+
AND deadline_ms <= FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
|
|
9280
|
+
) AS reached`,
|
|
9281
|
+
[this.dvmId, this.config.scope, jobId]
|
|
9282
|
+
);
|
|
9283
|
+
return rows[0]?.reached ?? false;
|
|
9284
|
+
}
|
|
9285
|
+
async databaseNow(client) {
|
|
9286
|
+
const { rows } = await client.query(
|
|
9287
|
+
`SELECT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint AS now_ms`
|
|
9288
|
+
);
|
|
9289
|
+
return Number(rows[0].now_ms);
|
|
9290
|
+
}
|
|
9291
|
+
async pinAdmissionDeadlines(client, deadlineMs) {
|
|
9292
|
+
await client.query(
|
|
9293
|
+
`UPDATE sdk_hosting_admissions SET drain_deadline_ms=
|
|
9294
|
+
CASE WHEN drain_deadline_ms IS NULL THEN $3 ELSE LEAST(drain_deadline_ms, $3) END
|
|
9295
|
+
WHERE dvm_id=$1 AND scope=$2 AND outcome <> 'failed'`,
|
|
9296
|
+
[this.dvmId, this.config.scope, deadlineMs]
|
|
9297
|
+
);
|
|
9298
|
+
}
|
|
9299
|
+
async expireIfNeeded(client, row, now) {
|
|
9300
|
+
if (row.state !== "accepting" || row.lease_expires_at_ms !== null && Number(row.lease_expires_at_ms) > now) return;
|
|
9301
|
+
const deadline = row.deadline_ms === null ? now + 36e5 : Number(row.deadline_ms);
|
|
9302
|
+
await client.query(
|
|
9303
|
+
`UPDATE sdk_hosting_control SET state='draining', deadline_ms=$3
|
|
9304
|
+
WHERE dvm_id=$1 AND scope=$2`,
|
|
9305
|
+
[this.dvmId, this.config.scope, deadline]
|
|
9306
|
+
);
|
|
9307
|
+
row.state = "draining";
|
|
9308
|
+
row.deadline_ms = String(deadline);
|
|
9309
|
+
await this.pinAdmissionDeadlines(client, deadline);
|
|
9310
|
+
}
|
|
9311
|
+
async transition(command) {
|
|
9312
|
+
if (command.action !== "drain" && command.action !== "resume" || !Number.isSafeInteger(command.generation) || command.generation < 1 || command.action === "drain" && (command.version !== void 0 || !Number.isSafeInteger(command.deadlineMs) || command.leaseExpiresAtMs !== void 0) || command.action === "resume" && (command.version !== 2 || !Number.isSafeInteger(command.leaseExpiresAtMs) || !Number.isSafeInteger(command.deadlineMs))) return "conflict";
|
|
9313
|
+
const client = await this.pool.connect();
|
|
9314
|
+
try {
|
|
9315
|
+
await client.query("BEGIN");
|
|
9316
|
+
const { rows } = await client.query(
|
|
9317
|
+
`SELECT state, generation, deadline_ms, lease_expires_at_ms FROM sdk_hosting_control
|
|
9318
|
+
WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
|
|
9319
|
+
[this.dvmId, this.config.scope]
|
|
9320
|
+
);
|
|
9321
|
+
const prior = rows[0];
|
|
9322
|
+
if (!prior) throw new Error("Hosting control was not initialized");
|
|
9323
|
+
const now = await this.databaseNow(client);
|
|
9324
|
+
await this.expireIfNeeded(client, prior, now);
|
|
9325
|
+
const generation = Number(prior.generation);
|
|
9326
|
+
const nextState = command.action === "drain" ? "draining" : "accepting";
|
|
9327
|
+
if (command.generation === generation && prior.state === nextState && Number(prior.deadline_ms ?? 0) === (command.deadlineMs ?? 0) && (command.action !== "resume" || Number(prior.lease_expires_at_ms ?? 0) === command.leaseExpiresAtMs)) {
|
|
9328
|
+
await client.query("COMMIT");
|
|
9329
|
+
return "ok";
|
|
9330
|
+
}
|
|
9331
|
+
if (command.generation <= generation) {
|
|
9332
|
+
await client.query("ROLLBACK");
|
|
9333
|
+
return "conflict";
|
|
9334
|
+
}
|
|
9335
|
+
if (command.action === "drain" && (command.deadlineMs <= now || command.deadlineMs > now + 864e5)) {
|
|
9336
|
+
await client.query("ROLLBACK");
|
|
9337
|
+
return "conflict";
|
|
9338
|
+
}
|
|
9339
|
+
if (command.action === "resume" && (command.leaseExpiresAtMs <= now || command.leaseExpiresAtMs > now + 36e5 || command.deadlineMs <= command.leaseExpiresAtMs || command.deadlineMs > command.leaseExpiresAtMs + 36e5)) {
|
|
9340
|
+
await client.query("ROLLBACK");
|
|
9341
|
+
return "conflict";
|
|
9342
|
+
}
|
|
9343
|
+
await client.query(
|
|
9344
|
+
`UPDATE sdk_hosting_control SET state=$3, generation=$4, deadline_ms=$5,
|
|
9345
|
+
lease_expires_at_ms=$6
|
|
9346
|
+
WHERE dvm_id=$1 AND scope=$2`,
|
|
9347
|
+
[
|
|
9348
|
+
this.dvmId,
|
|
9349
|
+
this.config.scope,
|
|
9350
|
+
nextState,
|
|
9351
|
+
command.generation,
|
|
9352
|
+
command.deadlineMs ?? null,
|
|
9353
|
+
command.action === "resume" ? command.leaseExpiresAtMs : null
|
|
9354
|
+
]
|
|
9355
|
+
);
|
|
9356
|
+
if (command.action === "drain") {
|
|
9357
|
+
await this.pinAdmissionDeadlines(client, command.deadlineMs);
|
|
9358
|
+
}
|
|
9359
|
+
await client.query("COMMIT");
|
|
9360
|
+
return "ok";
|
|
9361
|
+
} catch (error) {
|
|
9362
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
9363
|
+
throw error;
|
|
9364
|
+
} finally {
|
|
9365
|
+
client.release();
|
|
9366
|
+
}
|
|
9367
|
+
}
|
|
9368
|
+
async status() {
|
|
9369
|
+
const client = await this.pool.connect();
|
|
9370
|
+
try {
|
|
9371
|
+
await client.query("BEGIN");
|
|
9372
|
+
const locked = await client.query(
|
|
9373
|
+
`SELECT state, deadline_ms, lease_expires_at_ms FROM sdk_hosting_control
|
|
9374
|
+
WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
|
|
9375
|
+
[this.dvmId, this.config.scope]
|
|
9376
|
+
);
|
|
9377
|
+
if (locked.rows[0]) await this.expireIfNeeded(client, locked.rows[0], await this.databaseNow(client));
|
|
9378
|
+
await client.query("COMMIT");
|
|
9379
|
+
} catch (error) {
|
|
9380
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
9381
|
+
throw error;
|
|
9382
|
+
} finally {
|
|
9383
|
+
client.release();
|
|
9384
|
+
}
|
|
9385
|
+
await this.pool.query(
|
|
9386
|
+
`UPDATE sdk_hosting_admissions a SET outcome='accepted'
|
|
9387
|
+
WHERE a.dvm_id=$1 AND a.scope=$2 AND a.outcome='pending'
|
|
9388
|
+
AND EXISTS (SELECT 1 FROM jobs j WHERE j.id=a.job_id)`,
|
|
9389
|
+
[this.dvmId, this.config.scope]
|
|
9390
|
+
);
|
|
9391
|
+
const creditTable = await this.pool.query(
|
|
9392
|
+
"SELECT to_regclass('credit_fundings') IS NOT NULL AS present"
|
|
9393
|
+
);
|
|
9394
|
+
await this.pool.query(
|
|
9395
|
+
`UPDATE sdk_hosting_admissions a SET outcome='accepted'
|
|
9396
|
+
WHERE a.dvm_id=$1 AND a.scope=$2 AND a.outcome='unresolved'
|
|
9397
|
+
AND (EXISTS (SELECT 1 FROM jobs j WHERE j.id=a.job_id)
|
|
9398
|
+
${creditTable.rows[0].present ? `OR (a.credit_id IS NOT NULL AND EXISTS (
|
|
9399
|
+
SELECT 1 FROM credit_fundings f WHERE f.credit_id=a.credit_id AND f.fund_id=a.fund_id
|
|
9400
|
+
))` : ""})`,
|
|
9401
|
+
[this.dvmId, this.config.scope]
|
|
9402
|
+
);
|
|
9403
|
+
const { rows } = await this.pool.query(
|
|
9404
|
+
`SELECT state, generation, deadline_ms, lease_expires_at_ms FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2`,
|
|
9405
|
+
[this.dvmId, this.config.scope]
|
|
9406
|
+
);
|
|
9407
|
+
const active = await this.pool.query(
|
|
9408
|
+
`SELECT j.id FROM jobs j WHERE j.dvm_id=$1
|
|
9409
|
+
AND j.status IN ('processing', 'working', 'awaiting-input')
|
|
9410
|
+
AND EXISTS (SELECT 1 FROM sdk_hosting_admissions a
|
|
9411
|
+
WHERE a.dvm_id=j.dvm_id AND a.job_id=j.id AND a.scope=$2)
|
|
9412
|
+
ORDER BY j.id`,
|
|
9413
|
+
[this.dvmId, this.config.scope]
|
|
9414
|
+
);
|
|
9415
|
+
const unowned = await this.pool.query(
|
|
9416
|
+
`SELECT j.id FROM jobs j WHERE j.dvm_id=$1
|
|
9417
|
+
AND j.status IN ('processing', 'working', 'awaiting-input')
|
|
9418
|
+
AND NOT EXISTS (SELECT 1 FROM sdk_hosting_admissions a
|
|
9419
|
+
WHERE a.dvm_id=j.dvm_id AND a.job_id=j.id)
|
|
9420
|
+
ORDER BY j.id`,
|
|
9421
|
+
[this.dvmId]
|
|
9422
|
+
);
|
|
9423
|
+
const pending = await this.pool.query(
|
|
9424
|
+
`SELECT COUNT(*) FILTER (WHERE outcome='pending')::text AS pending,
|
|
9425
|
+
COUNT(*) FILTER (WHERE outcome='unresolved')::text AS unresolved
|
|
9426
|
+
FROM sdk_hosting_admissions WHERE dvm_id=$1 AND scope=$2`,
|
|
9427
|
+
[this.dvmId, this.config.scope]
|
|
9428
|
+
);
|
|
9429
|
+
const unresolved = await this.pool.query(
|
|
9430
|
+
`SELECT id, job_id FROM sdk_hosting_admissions
|
|
9431
|
+
WHERE dvm_id=$1 AND scope=$2 AND outcome='unresolved' ORDER BY admitted_at_ms, id`,
|
|
9432
|
+
[this.dvmId, this.config.scope]
|
|
9433
|
+
);
|
|
9434
|
+
const inFlight = await this.pool.query(
|
|
9435
|
+
`SELECT id, job_id FROM sdk_hosting_admissions
|
|
9436
|
+
WHERE dvm_id=$1 AND scope=$2 AND outcome='pending' ORDER BY admitted_at_ms, id`,
|
|
9437
|
+
[this.dvmId, this.config.scope]
|
|
9438
|
+
);
|
|
9439
|
+
return {
|
|
9440
|
+
state: rows[0].state,
|
|
9441
|
+
generation: Number(rows[0].generation),
|
|
9442
|
+
deadlineMs: rows[0].deadline_ms === null ? null : Number(rows[0].deadline_ms),
|
|
9443
|
+
leaseExpiresAtMs: rows[0].lease_expires_at_ms === null ? null : Number(rows[0].lease_expires_at_ms),
|
|
9444
|
+
activeJobIds: active.rows.map((row) => row.id),
|
|
9445
|
+
unownedActiveJobIds: unowned.rows.map((row) => row.id),
|
|
9446
|
+
pendingAdmissionCount: Number(pending.rows[0].pending),
|
|
9447
|
+
pendingAdmissions: inFlight.rows.map((row) => ({ admissionId: row.id, workId: row.job_id })),
|
|
9448
|
+
unresolvedAdmissionCount: Number(pending.rows[0].unresolved),
|
|
9449
|
+
unresolvedAdmissions: unresolved.rows.map((row) => ({ admissionId: row.id, jobId: row.job_id }))
|
|
9450
|
+
};
|
|
9451
|
+
}
|
|
9452
|
+
};
|
|
9453
|
+
|
|
9130
9454
|
// src/sdk/server/job-credential.ts
|
|
9131
9455
|
function jobCredentialGate(opts) {
|
|
9132
9456
|
const alternative = opts.acceptsBearer ? ", or the Authorization: Bearer token the job was submitted with" : "";
|
|
@@ -9756,20 +10080,10 @@ var ReceiptIssuer = class {
|
|
|
9756
10080
|
this.pubkey = receiptPubkeyFromSecret(opts.secret);
|
|
9757
10081
|
}
|
|
9758
10082
|
/**
|
|
9759
|
-
*
|
|
9760
|
-
*
|
|
9761
|
-
*
|
|
9762
|
-
*
|
|
9763
|
-
* sequence number and still attest an outcome. Whether feedback is
|
|
9764
|
-
* paid-only is the feedback layer's call, not this one's.
|
|
9765
|
-
*
|
|
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
|
|
9771
|
-
* whose payment funded/drew the ledger, absent otherwise. Signature-
|
|
9772
|
-
* compatible either way: `canonicalize` omits absent keys.
|
|
10083
|
+
* seq is store-allocated; issuedAt is epoch seconds. Free jobs also consume a
|
|
10084
|
+
* sequence and attest their outcome. Timing/attribution comes from the committed
|
|
10085
|
+
* terminal row; legacy rows omit unknown historical fields. credit is present
|
|
10086
|
+
* only for a resolved ledger draw.
|
|
9773
10087
|
*/
|
|
9774
10088
|
issue(record, seq, issuedAt, credit) {
|
|
9775
10089
|
const unsigned = {
|
|
@@ -9880,7 +10194,6 @@ function resultHashFor(record) {
|
|
|
9880
10194
|
}
|
|
9881
10195
|
|
|
9882
10196
|
// src/lib/cashu/mint-info.ts
|
|
9883
|
-
var MINT_INFO_TTL_MS = 5 * 6e4;
|
|
9884
10197
|
async function getKeysetFinalExpiry(mintUrl, keysetId) {
|
|
9885
10198
|
try {
|
|
9886
10199
|
const wallet = await getWallet(mintUrl);
|
|
@@ -10324,11 +10637,9 @@ var JobManager = class {
|
|
|
10324
10637
|
get receiptsEnabled() {
|
|
10325
10638
|
return this.opts.receiptIssuer !== void 0 && isReceiptIssuingStore(this.jobStore);
|
|
10326
10639
|
}
|
|
10327
|
-
/** True when the named capability exists on this descriptor. */
|
|
10328
10640
|
hasCapability(name) {
|
|
10329
10641
|
return Object.prototype.hasOwnProperty.call(this.descriptor.capabilities, name);
|
|
10330
10642
|
}
|
|
10331
|
-
/** Names of every capability this descriptor exposes — used for error envelopes. */
|
|
10332
10643
|
capabilityNames() {
|
|
10333
10644
|
return Object.keys(this.descriptor.capabilities);
|
|
10334
10645
|
}
|
|
@@ -10925,7 +11236,6 @@ var JobManager = class {
|
|
|
10925
11236
|
job.messageAppenderTail = next;
|
|
10926
11237
|
};
|
|
10927
11238
|
}
|
|
10928
|
-
/** Start (or restart) the idle timer for a job. */
|
|
10929
11239
|
startIdleTimer(job) {
|
|
10930
11240
|
this.clearIdleTimer(job.id);
|
|
10931
11241
|
const timeoutMs = this.descriptor.idleTimeout * 1e3;
|
|
@@ -10934,7 +11244,6 @@ var JobManager = class {
|
|
|
10934
11244
|
}, timeoutMs);
|
|
10935
11245
|
this.idleTimers.set(job.id, timer);
|
|
10936
11246
|
}
|
|
10937
|
-
/** Clear the idle timer for a job. */
|
|
10938
11247
|
clearIdleTimer(id) {
|
|
10939
11248
|
const timer = this.idleTimers.get(id);
|
|
10940
11249
|
if (timer) {
|
|
@@ -12533,7 +12842,6 @@ var JobManager = class {
|
|
|
12533
12842
|
for (const record of records) await this.reportTerminalJob(record.id);
|
|
12534
12843
|
this.terminalReportRecoveryCursor = records.length === 100 ? records[records.length - 1].id : void 0;
|
|
12535
12844
|
}
|
|
12536
|
-
/** Handle job reaching terminal state (completed, failed, cancelled). */
|
|
12537
12845
|
async handleJobTerminal(job) {
|
|
12538
12846
|
this.clearIdleTimer(job.id);
|
|
12539
12847
|
if (await this.adoptDurableTerminal(job)) return;
|
|
@@ -12809,7 +13117,6 @@ var JobManager = class {
|
|
|
12809
13117
|
}, 6e4);
|
|
12810
13118
|
this.cleanupTimers.set(job.id, timer);
|
|
12811
13119
|
}
|
|
12812
|
-
/** Clear all timers. Called on server shutdown to allow clean exit. */
|
|
12813
13120
|
shutdown() {
|
|
12814
13121
|
for (const [id] of this.idleTimers) {
|
|
12815
13122
|
this.clearIdleTimer(id);
|
|
@@ -13054,7 +13361,6 @@ var MintHealthTracker = class {
|
|
|
13054
13361
|
isNutIncompatible(mintUrl) {
|
|
13055
13362
|
return this.state.get(normaliseMintUrl(mintUrl))?.nutIncompatible ?? false;
|
|
13056
13363
|
}
|
|
13057
|
-
/** Plain-object snapshot keyed by mint URL — for tests + observability. */
|
|
13058
13364
|
snapshot() {
|
|
13059
13365
|
return Object.fromEntries(Array.from(this.state.entries()).map(([k, v]) => [k, { ...v }]));
|
|
13060
13366
|
}
|
|
@@ -13112,12 +13418,7 @@ var MintHealthTracker = class {
|
|
|
13112
13418
|
);
|
|
13113
13419
|
}
|
|
13114
13420
|
}
|
|
13115
|
-
/**
|
|
13116
|
-
* One-shot structured summary emitted after the first probe tick. Replaces
|
|
13117
|
-
* the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
|
|
13118
|
-
* all-mints-unhealthy warning — the operator alert that the Fly restart-loop
|
|
13119
|
-
* used to be before boot stopped refusing to start.
|
|
13120
|
-
*/
|
|
13421
|
+
/** One boot summary, including an all-unhealthy warning. */
|
|
13121
13422
|
logBootHealth() {
|
|
13122
13423
|
const mints = Object.entries(this.snapshot()).map(([mintUrl, s]) => ({
|
|
13123
13424
|
mintUrl,
|
|
@@ -13248,7 +13549,6 @@ var PostgresX402ExactSettlementStore = class {
|
|
|
13248
13549
|
this.pool = pool;
|
|
13249
13550
|
}
|
|
13250
13551
|
pool;
|
|
13251
|
-
/** Create the exact settlement tables. */
|
|
13252
13552
|
async init() {
|
|
13253
13553
|
await withSdkInitLock(this.pool, async () => {
|
|
13254
13554
|
await this.pool.query(`
|
|
@@ -14194,20 +14494,11 @@ async function handlePendingDrains(c, opts) {
|
|
|
14194
14494
|
amount_micro: d.amountMicro,
|
|
14195
14495
|
amount_sats: amountSats,
|
|
14196
14496
|
amount_msats: amountSats === null ? null : amountSats * 1e3,
|
|
14197
|
-
// Whether this row's sats figure is the in-kind obligation or a live-rate
|
|
14198
|
-
// conversion. An operator settling a refund should be able to see which,
|
|
14199
|
-
// and the servicer's own logs carry it for the same reason.
|
|
14200
14497
|
sats_basis: amountSats === null ? null : d.owedSats === null ? "rate" : "in_kind",
|
|
14201
|
-
//
|
|
14202
|
-
//
|
|
14203
|
-
// `completeX402ChannelDrain` treats as its settlement floor,
|
|
14204
|
-
// so publishing it is what lets an operator settle from the row instead
|
|
14205
|
-
// of assuming their stablecoin happens to be six-decimal.
|
|
14498
|
+
// Rail-native debit, distinct from fiat amount_micro. This is the channel refund floor
|
|
14499
|
+
// and the size a manual payout must cover.
|
|
14206
14500
|
amount_native: d.drainedNative,
|
|
14207
14501
|
settlement,
|
|
14208
|
-
// Why this row is classified as it is, and the channel an operator would
|
|
14209
|
-
// look up to see the close land. `payout` cannot answer it: a
|
|
14210
|
-
// channel-backed mpp drain carries `{}`.
|
|
14211
14502
|
channel_id: d.channelId,
|
|
14212
14503
|
created_at: d.createdAt
|
|
14213
14504
|
};
|
|
@@ -14218,13 +14509,7 @@ async function handlePendingDrains(c, opts) {
|
|
|
14218
14509
|
acc[method] = pending.filter((d) => d.method === method).reduce((sum, d) => sum + d.amountMicro, 0);
|
|
14219
14510
|
return acc;
|
|
14220
14511
|
}, {}),
|
|
14221
|
-
// What no job will ever pay down: the operator's own worklist, in the one
|
|
14222
|
-
// denomination these rows have.
|
|
14223
14512
|
manual_micro: drains.filter((d) => d.settlement === "manual").reduce((sum, d) => sum + d.amount_micro, 0),
|
|
14224
|
-
// The rest of the stablecoin liability — owed, but by a channel close in
|
|
14225
|
-
// flight rather than by a person. Published beside `manual_micro` so the
|
|
14226
|
-
// difference between the two is visible rather than inferred from a
|
|
14227
|
-
// `by_method_micro` figure neither of them matches.
|
|
14228
14513
|
channel_micro: drains.filter((d) => d.settlement === "channel").reduce((sum, d) => sum + d.amount_micro, 0)
|
|
14229
14514
|
};
|
|
14230
14515
|
return c.json({ drains, liability, mints: opts.mints?.() ?? [] });
|
|
@@ -14253,17 +14538,21 @@ async function handleBitcoinLiability(c, opts) {
|
|
|
14253
14538
|
drain_liability_sats: drainSats,
|
|
14254
14539
|
drain_count: promised.length,
|
|
14255
14540
|
total_owed_sats: deposit.depositSats + drainSats,
|
|
14256
|
-
//
|
|
14257
|
-
//
|
|
14258
|
-
//
|
|
14259
|
-
// prices the whole balance off a live rate, so both are reported whole and
|
|
14260
|
-
// neither contributes to `deposit_liability_sats` — a sweep computed
|
|
14261
|
-
// against `total_owed_sats` alone is short by whatever they turn out to be
|
|
14262
|
-
// worth.
|
|
14541
|
+
// Uncovered or basisless balances require whole-balance live-rate pricing and do not
|
|
14542
|
+
// contribute to deposit_liability_sats. total_owed_sats alone cannot reserve their
|
|
14543
|
+
// eventual refunds.
|
|
14263
14544
|
uncovered_credits: deposit.uncoveredCredits,
|
|
14264
|
-
|
|
14545
|
+
uncovered_by_currency: deposit.uncoveredByCurrency.map((currency) => ({
|
|
14546
|
+
currency: currency.currency,
|
|
14547
|
+
micro: currency.micro,
|
|
14548
|
+
credits: currency.credits
|
|
14549
|
+
})),
|
|
14265
14550
|
unbacked_credits: deposit.unbackedCredits,
|
|
14266
|
-
|
|
14551
|
+
unbacked_by_currency: deposit.unbackedByCurrency.map((currency) => ({
|
|
14552
|
+
currency: currency.currency,
|
|
14553
|
+
micro: currency.micro,
|
|
14554
|
+
credits: currency.credits
|
|
14555
|
+
})),
|
|
14267
14556
|
unpriced_drains: drainsUnpriced
|
|
14268
14557
|
});
|
|
14269
14558
|
}
|
|
@@ -14701,9 +14990,6 @@ async function handleReconcileInvoice(c, opts) {
|
|
|
14701
14990
|
const result = await opts.ledger.reconcileBlockedInvoice({
|
|
14702
14991
|
paymentHash,
|
|
14703
14992
|
targetCreditId,
|
|
14704
|
-
// `fund`'s upsert OVERWRITES `expiry_ms`, so a bare `now + ttl` would
|
|
14705
|
-
// shorten a target credit with more life left on it — destroying
|
|
14706
|
-
// spendable value while purporting to add some.
|
|
14707
14993
|
expiryMs,
|
|
14708
14994
|
basis,
|
|
14709
14995
|
...settledAtMs === void 0 ? {} : { settledAtMs },
|
|
@@ -14724,9 +15010,6 @@ async function handleReconcileInvoice(c, opts) {
|
|
|
14724
15010
|
creditCurrency: invoice.currency,
|
|
14725
15011
|
fundedAt: settledAtMs ?? invoice.settledAt ?? nowMs,
|
|
14726
15012
|
expiryMs,
|
|
14727
|
-
// The settlements table types this deposit `repair` off the
|
|
14728
|
-
// flag: the sats landed at payment time and the
|
|
14729
|
-
// deposit is the one row they get.
|
|
14730
15013
|
repaired: true
|
|
14731
15014
|
}
|
|
14732
15015
|
}
|
|
@@ -14809,8 +15092,6 @@ async function handleWriteOffInvoice(c, opts) {
|
|
|
14809
15092
|
amount_micro: written.amountMicro,
|
|
14810
15093
|
currency: written.currency,
|
|
14811
15094
|
note: written.writeOffNote,
|
|
14812
|
-
// Mirrors the reconcile's `replayed`: the operator asked for a decision and
|
|
14813
|
-
// finds one already recorded, possibly with someone else's note on it.
|
|
14814
15095
|
replayed: !outcome.written
|
|
14815
15096
|
});
|
|
14816
15097
|
}
|
|
@@ -16829,6 +17110,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16829
17110
|
let x402ChannelStorage;
|
|
16830
17111
|
let x402BatchSettlement;
|
|
16831
17112
|
let server;
|
|
17113
|
+
let hostingControl;
|
|
16832
17114
|
let shutdownPromise;
|
|
16833
17115
|
const shutdown = () => shutdownPromise ??= (async () => {
|
|
16834
17116
|
const errors = [];
|
|
@@ -16880,12 +17162,12 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16880
17162
|
const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
|
|
16881
17163
|
const dvmId = opts.dvmId;
|
|
16882
17164
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
16883
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
17165
|
+
const { RevenueReporter } = await import("./revenue-reporter-O75BGYFK.js");
|
|
16884
17166
|
const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
|
|
16885
17167
|
revenueReporter = reporter;
|
|
16886
17168
|
await reporter.init();
|
|
16887
17169
|
reporter.startRetryLoop();
|
|
16888
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
17170
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-KEMUXEUO.js");
|
|
16889
17171
|
payoutReporter = new PayoutReporter({
|
|
16890
17172
|
dvmId,
|
|
16891
17173
|
db: opts.db,
|
|
@@ -17022,8 +17304,19 @@ async function createDVMServer(descriptor, opts) {
|
|
|
17022
17304
|
});
|
|
17023
17305
|
}
|
|
17024
17306
|
}) : void 0;
|
|
17307
|
+
if (opts.hostingControl) {
|
|
17308
|
+
if (!opts.db || !opts.dvmId || !(opts.jobStore instanceof PostgresJobStore)) {
|
|
17309
|
+
throw new Error("Hosting control requires a Postgres pool, dvmId, and PostgresJobStore");
|
|
17310
|
+
}
|
|
17311
|
+
if (descriptor.credit && !opts.creditLedger?.durable) {
|
|
17312
|
+
throw new Error("Hosting control with credit requires a durable credit ledger");
|
|
17313
|
+
}
|
|
17314
|
+
hostingControl = new HostingControl(opts.db, opts.dvmId, opts.hostingControl);
|
|
17315
|
+
await hostingControl.init();
|
|
17316
|
+
}
|
|
17025
17317
|
server = new DVMServer(descriptor, {
|
|
17026
17318
|
...opts,
|
|
17319
|
+
hostingControlInstance: hostingControl,
|
|
17027
17320
|
creditLedger,
|
|
17028
17321
|
processedPayments,
|
|
17029
17322
|
onJobCompleted: effectiveOnJobCompleted,
|
|
@@ -17100,6 +17393,7 @@ var DVMServer = class {
|
|
|
17100
17393
|
x402ExactSettlement;
|
|
17101
17394
|
/** Withdraws this mount's Tempo session capability from the poll. */
|
|
17102
17395
|
undeclareSessionCapability;
|
|
17396
|
+
hostingSweepTimer;
|
|
17103
17397
|
constructor(descriptor, opts) {
|
|
17104
17398
|
this.descriptor = descriptor;
|
|
17105
17399
|
this.pricingCurrency = descriptor.currency ?? "usd";
|
|
@@ -17143,17 +17437,9 @@ var DVMServer = class {
|
|
|
17143
17437
|
mints: opts.mints,
|
|
17144
17438
|
jobStore: opts.jobStore,
|
|
17145
17439
|
devMode: opts.devMode,
|
|
17146
|
-
//
|
|
17147
|
-
//
|
|
17148
|
-
//
|
|
17149
|
-
// raw option meant a DVM that never set `paymentMethods` (no first-party
|
|
17150
|
-
// one does; the descriptor documents it as "derived from configured
|
|
17151
|
-
// rails") advertised both rails, took the upfront payment on either, and
|
|
17152
|
-
// then emitted a mid-job payment-request carrying no rail at all. The
|
|
17153
|
-
// caller's responder had nothing to sign and the job stalled at
|
|
17154
|
-
// needs-approval — reachable only from a rung that raises a real mid-job
|
|
17155
|
-
// ask, which nothing above L1 did. Facilitator availability is still
|
|
17156
|
-
// re-checked per ask in `context.ts`, so a static list is safe here.
|
|
17440
|
+
// Use the same derived rail list as discovery/upfront payment; otherwise
|
|
17441
|
+
// default-configured rails vanish from mid-job asks. Availability is rechecked
|
|
17442
|
+
// per ask.
|
|
17157
17443
|
paymentMethods: (opts.paymentMethods ?? this.defaultPaymentMethods()).filter(
|
|
17158
17444
|
(method) => method !== "x402" || this.x402CurrencySupported()
|
|
17159
17445
|
),
|
|
@@ -17191,6 +17477,17 @@ var DVMServer = class {
|
|
|
17191
17477
|
this.app = new Hono();
|
|
17192
17478
|
try {
|
|
17193
17479
|
this.registerRoutes();
|
|
17480
|
+
if (opts.hostingControlInstance) {
|
|
17481
|
+
this.hostingSweepTimer = setInterval(() => {
|
|
17482
|
+
void this.sweepHostingDeadline().catch((error) => {
|
|
17483
|
+
console.error("Hosting deadline sweep failed:", error);
|
|
17484
|
+
});
|
|
17485
|
+
}, 1e3);
|
|
17486
|
+
this.hostingSweepTimer.unref();
|
|
17487
|
+
void this.sweepHostingDeadline().catch((error) => {
|
|
17488
|
+
console.error("Hosting deadline sweep failed:", error);
|
|
17489
|
+
});
|
|
17490
|
+
}
|
|
17194
17491
|
if (this.ownsMintHealthTracker) this.mintHealthTracker?.start();
|
|
17195
17492
|
if (this.opts.tempoObserverHealth?.declareSessionCapability) {
|
|
17196
17493
|
this.undeclareSessionCapability = this.opts.tempoObserverHealth.declareSessionCapability(
|
|
@@ -17224,6 +17521,7 @@ var DVMServer = class {
|
|
|
17224
17521
|
* and host teardown layers can finish later cleanup before surfacing it.
|
|
17225
17522
|
*/
|
|
17226
17523
|
shutdown() {
|
|
17524
|
+
if (this.hostingSweepTimer) clearInterval(this.hostingSweepTimer);
|
|
17227
17525
|
this.jobManager.shutdown();
|
|
17228
17526
|
const settlement = this.x402BatchSettlementInit ?? Promise.resolve(this.x402BatchSettlement);
|
|
17229
17527
|
const stopped = settlement.then((s) => s?.stop());
|
|
@@ -17232,6 +17530,40 @@ var DVMServer = class {
|
|
|
17232
17530
|
this.undeclareSessionCapability = void 0;
|
|
17233
17531
|
return stopped;
|
|
17234
17532
|
}
|
|
17533
|
+
async sweepHostingDeadline() {
|
|
17534
|
+
const control = this.opts.hostingControlInstance;
|
|
17535
|
+
if (!control) return;
|
|
17536
|
+
const status = await control.status();
|
|
17537
|
+
const store = this.jobManager.store;
|
|
17538
|
+
if (!(store instanceof PostgresJobStore)) return;
|
|
17539
|
+
for (const jobId of status.activeJobIds) {
|
|
17540
|
+
if (await control.dispatchDeadlineReached(jobId) && await store.cancelJobForHosting(jobId, "Hosting drain deadline reached")) {
|
|
17541
|
+
await this.jobManager.finalizeStoredTerminal(jobId);
|
|
17542
|
+
}
|
|
17543
|
+
}
|
|
17544
|
+
}
|
|
17545
|
+
async handleHostingCommand(c, statusOnly) {
|
|
17546
|
+
const control = this.opts.hostingControlInstance;
|
|
17547
|
+
if (!control) return c.json({ error: "not_found" }, 404);
|
|
17548
|
+
let command;
|
|
17549
|
+
try {
|
|
17550
|
+
command = await c.req.json();
|
|
17551
|
+
} catch {
|
|
17552
|
+
return c.json({ error: "invalid_hosting_control" }, 400);
|
|
17553
|
+
}
|
|
17554
|
+
if (!command || (statusOnly ? command.action !== "status" : command.action !== "drain" && command.action !== "resume")) {
|
|
17555
|
+
return c.json({ error: "invalid_hosting_control" }, 400);
|
|
17556
|
+
}
|
|
17557
|
+
if (!await control.authorize(command, c.req.header("X-DVM-Hosting-Signature"))) {
|
|
17558
|
+
return c.json({ error: "hosting_control_unauthorized" }, 401);
|
|
17559
|
+
}
|
|
17560
|
+
if (!statusOnly && command.action === "resume") await this.sweepHostingDeadline();
|
|
17561
|
+
if (!statusOnly && await control.transition(command) === "conflict") {
|
|
17562
|
+
return c.json({ error: "hosting_generation_conflict" }, 409);
|
|
17563
|
+
}
|
|
17564
|
+
if (!statusOnly) await this.sweepHostingDeadline();
|
|
17565
|
+
return c.json(await control.status());
|
|
17566
|
+
}
|
|
17235
17567
|
registerRoutes() {
|
|
17236
17568
|
const dvmSlug = this.descriptor.name;
|
|
17237
17569
|
const builderPubkey = this.opts.builder?.pubkey;
|
|
@@ -17339,6 +17671,10 @@ var DVMServer = class {
|
|
|
17339
17671
|
this.app.get("/v1/info", (c) => this.handleInfo(c));
|
|
17340
17672
|
this.app.post("/v1/quote", (c) => this.handleQuote(c));
|
|
17341
17673
|
this.app.post("/v1/job", (c) => this.handleCreateJob(c));
|
|
17674
|
+
if (this.opts.hostingControlInstance) {
|
|
17675
|
+
this.app.post("/_hosting/control", (c) => this.handleHostingCommand(c, false));
|
|
17676
|
+
this.app.post("/_hosting/status", (c) => this.handleHostingCommand(c, true));
|
|
17677
|
+
}
|
|
17342
17678
|
this.app.post("/v1/request-resume/:requestId", (c) => this.handleCreateJob(c, true));
|
|
17343
17679
|
this.app.post("/v1/request-result/:requestId", (c) => this.handleRequestResult(c));
|
|
17344
17680
|
this.app.get("/v1/job/:id", (c) => this.handleGetJob(c));
|
|
@@ -17411,12 +17747,8 @@ var DVMServer = class {
|
|
|
17411
17747
|
return state;
|
|
17412
17748
|
}
|
|
17413
17749
|
/**
|
|
17414
|
-
*
|
|
17415
|
-
*
|
|
17416
|
-
* upfront flow uses at lines 709-718 so accept-time rotation tolerance is
|
|
17417
|
-
* consistent across upfront and mid-job. Falls back to `undefined` when
|
|
17418
|
-
* accumulator mode isn't wired (the receive path then rejects with
|
|
17419
|
-
* `cashu_not_p2pk_locked`).
|
|
17750
|
+
* Mid-job and upfront receive paths use current plus grace-period retired keys.
|
|
17751
|
+
* Without accumulator mode this is undefined and the receive path refuses it.
|
|
17420
17752
|
*/
|
|
17421
17753
|
async resolveLockPubkeysForReceive() {
|
|
17422
17754
|
if (this.opts.disposableCashu && this.opts.lockPubkey) return [toLockPubkey(this.opts.lockPubkey)];
|
|
@@ -17431,12 +17763,8 @@ var DVMServer = class {
|
|
|
17431
17763
|
}
|
|
17432
17764
|
}
|
|
17433
17765
|
/**
|
|
17434
|
-
*
|
|
17435
|
-
*
|
|
17436
|
-
* cached state, and only as a final resort to the env-derived seed in a
|
|
17437
|
-
* single-entry array. The cache makes the degradation safe across a
|
|
17438
|
-
* rotation — without it the env seed advertises and accepts the retired
|
|
17439
|
-
* pubkey while the DB is unreachable.
|
|
17766
|
+
* Read Postgres first, then fall back to last-observed state, then the env seed.
|
|
17767
|
+
* Caching avoids reverting an observed rotation to the old seed during an outage.
|
|
17440
17768
|
*/
|
|
17441
17769
|
async lockPubkeyStateForRead(db, dvmId) {
|
|
17442
17770
|
try {
|
|
@@ -17448,22 +17776,9 @@ var DVMServer = class {
|
|
|
17448
17776
|
}
|
|
17449
17777
|
}
|
|
17450
17778
|
/**
|
|
17451
|
-
*
|
|
17452
|
-
*
|
|
17453
|
-
*
|
|
17454
|
-
* Routine settlement used to ride the per-DVM watcher machine's four-minute
|
|
17455
|
-
* poll, which is exactly what kept an idle DVM awake and billing. It cannot
|
|
17456
|
-
* move to a timer for the same reason — a timer on a machine that ought to
|
|
17457
|
-
* be suspended either does not fire or defeats the suspension. So it rides
|
|
17458
|
-
* requests instead, the way Lightning funding is credited: a DVM
|
|
17459
|
-
* with no traffic has nothing to settle on a 24-hour cadence anyway, and the
|
|
17460
|
-
* forced-close guarantee does not depend on this path at all — that is the
|
|
17461
|
-
* chain-event wake, which reaches a suspended DVM through Fly Proxy.
|
|
17462
|
-
*
|
|
17463
|
-
* Fire-and-forget and self-throttled: the request never waits on it, only
|
|
17464
|
-
* one runs at a time, and a failure is logged rather than surfaced. The
|
|
17465
|
-
* sweep re-checks each channel's own schedule, so the throttle here only
|
|
17466
|
-
* bounds how often we pay for the store scan.
|
|
17779
|
+
* Opportunistic traffic avoids a timer that keeps an idle machine awake. Forced
|
|
17780
|
+
* close servicing is independent. Fire-and-forget, one sweep at a time; failures
|
|
17781
|
+
* are logged. The throttle bounds scans, not each channel's settlement schedule.
|
|
17467
17782
|
*/
|
|
17468
17783
|
maybeSweepTempoSessions() {
|
|
17469
17784
|
const sweep = this.opts.mpp?.sweepTempoSessions;
|
|
@@ -17528,22 +17843,9 @@ var DVMServer = class {
|
|
|
17528
17843
|
]);
|
|
17529
17844
|
}
|
|
17530
17845
|
/**
|
|
17531
|
-
*
|
|
17532
|
-
*
|
|
17533
|
-
*
|
|
17534
|
-
*
|
|
17535
|
-
* Deliberately the same question the platform's post-deploy `/v1/info` probe
|
|
17536
|
-
* answers (`infoAdvertisesTempoSession`), from the same two facts: the credit
|
|
17537
|
-
* menu exists at all, and the configured MPP methods include `tempo/session`.
|
|
17538
|
-
* Credit-scoped because sessions are confined to `POST /v1/credit` — a
|
|
17539
|
-
* top-level session method with no credit menu is not a channel anyone can
|
|
17540
|
-
* open, so it is not a candidate the payee fallback should reach for.
|
|
17541
|
-
*
|
|
17542
|
-
* It reads `advertisedMethods` rather than {@link creditMppSplit}'s `offered`,
|
|
17543
|
-
* and never {@link tempoSessionOffered}: what the column records is
|
|
17544
|
-
* capability, not availability, so an observer blip must leave it alone. That
|
|
17545
|
-
* distinction is the whole of and answering it here from the gate
|
|
17546
|
-
* would reintroduce it on a one-minute cadence.
|
|
17846
|
+
* Report configured session capability independently of current availability.
|
|
17847
|
+
* A temporary observer outage must not remove the platform's watch signal.
|
|
17848
|
+
* Credit menu, auth and session method configuration still gate that capability.
|
|
17547
17849
|
*/
|
|
17548
17850
|
tempoSessionCapability() {
|
|
17549
17851
|
if (!this.opts.mpp) return false;
|
|
@@ -17561,25 +17863,11 @@ var DVMServer = class {
|
|
|
17561
17863
|
);
|
|
17562
17864
|
}
|
|
17563
17865
|
/**
|
|
17564
|
-
*
|
|
17565
|
-
*
|
|
17566
|
-
*
|
|
17567
|
-
*
|
|
17568
|
-
*
|
|
17569
|
-
* its own Tempo RPC and verifies chain, escrow, topic and local channel
|
|
17570
|
-
* ownership before touching money (see `handleTempoCloseEvent`). An
|
|
17571
|
-
* unverifiable locator can therefore do nothing, which is what lets the
|
|
17572
|
-
* platform wake a DVM without holding a signing key or a recoverable copy of
|
|
17573
|
-
* the DVM's bearer token.
|
|
17574
|
-
*
|
|
17575
|
-
* What an unauthenticated caller *can* do is spend the DVM's resources: each
|
|
17576
|
-
* POST is an unthrottled outbound `eth_getTransactionReceipt` against the
|
|
17577
|
-
* builder's own Tempo provider, billed to them, before anything about the
|
|
17578
|
-
* locator has been checked — the verification needs the receipt to happen at
|
|
17579
|
-
* all. So the cost of an open endpoint is metered RPC calls, not money: no
|
|
17580
|
-
* locator can move value except one naming a real close of a channel this
|
|
17581
|
-
* DVM owns, and settling that is what the routine cadence would do anyway.
|
|
17582
|
-
* Watch the provider's quota rather than the ledger.
|
|
17866
|
+
* Unauthenticated wake carrying only an event locator. After parsing, verify
|
|
17867
|
+
* the receipt from the DVM's own RPC, including chain, escrow, topic and local
|
|
17868
|
+
* channel ownership before settlement. The observer needs no spending secret.
|
|
17869
|
+
* Syntactically valid requests can still consume unthrottled provider RPC quota;
|
|
17870
|
+
* malformed locators return 400 before that read.
|
|
17583
17871
|
*/
|
|
17584
17872
|
registerTempoCloseEventRoute() {
|
|
17585
17873
|
const mpp = this.opts.mpp;
|
|
@@ -17640,16 +17928,8 @@ var DVMServer = class {
|
|
|
17640
17928
|
});
|
|
17641
17929
|
}
|
|
17642
17930
|
/**
|
|
17643
|
-
*
|
|
17644
|
-
*
|
|
17645
|
-
* field is independently sourced (opts wins per-field, env fills gaps);
|
|
17646
|
-
* all three of pubkey / attestation / signature must resolve to emit the
|
|
17647
|
-
* block — partial state is treated as absent rather than half-emitted.
|
|
17648
|
-
*
|
|
17649
|
-
* Env-var form: `DVMKIT_BUILDER_ATTESTATION` is a JSON string (Fly secrets
|
|
17650
|
-
* can't carry structured data), so it's parsed here. A malformed JSON
|
|
17651
|
-
* string drops the block rather than throwing, so a corrupted env var
|
|
17652
|
-
* can't take the DVM down.
|
|
17931
|
+
* Options override environment per field. Emit only when pubkey, attestation
|
|
17932
|
+
* and signature all exist; malformed attestation JSON omits the block.
|
|
17653
17933
|
*/
|
|
17654
17934
|
builderAttestationBlock() {
|
|
17655
17935
|
const builder = this.opts.builder;
|
|
@@ -18013,6 +18293,32 @@ var DVMServer = class {
|
|
|
18013
18293
|
receiptIssuer: this.receiptIssuer,
|
|
18014
18294
|
lightning: this.opts.lightningReceive ? { receive: this.opts.lightningReceive } : void 0,
|
|
18015
18295
|
settleLightning: (callerPubkey, creditId) => this.settleLightningFunding(callerPubkey, creditId),
|
|
18296
|
+
withFundingAdmission: this.opts.hostingControlInstance ? async (ctx, body, run) => {
|
|
18297
|
+
const control = this.opts.hostingControlInstance;
|
|
18298
|
+
const admissionId = randomUUID4();
|
|
18299
|
+
const fundId = body.fund?.fund_id ?? admissionId;
|
|
18300
|
+
const creditId = body.credit_id ?? derivedFundCreditId(body.pubkey, fundId);
|
|
18301
|
+
const workId = `credit-fund:${creditId}:${fundId}`;
|
|
18302
|
+
if (!await control.admit(admissionId, workId, { creditId, fundId })) {
|
|
18303
|
+
return ctx.json({
|
|
18304
|
+
error: "hosting_draining",
|
|
18305
|
+
message: "This service is draining and is not accepting new credit funding.",
|
|
18306
|
+
hint: "Balance, reclaim, and already-issued funding recovery remain available."
|
|
18307
|
+
}, 503);
|
|
18308
|
+
}
|
|
18309
|
+
const authorization = ctx.req.header("Authorization");
|
|
18310
|
+
const paymentArtifact = Boolean(
|
|
18311
|
+
ctx.req.header("X-Cashu") || ctx.req.header("PAYMENT-SIGNATURE") || ctx.req.header("X-PAYMENT") || authorization && /^Payment\s/i.test(authorization)
|
|
18312
|
+
);
|
|
18313
|
+
let outcome = paymentArtifact ? "unresolved" : "failed";
|
|
18314
|
+
try {
|
|
18315
|
+
const response = await run();
|
|
18316
|
+
if (response.ok || body.fund?.method === "lightning" && await this.creditLedger.getInvoice({ creditId, fundId })) outcome = "accepted";
|
|
18317
|
+
return response;
|
|
18318
|
+
} finally {
|
|
18319
|
+
await control.finishAdmission(admissionId, outcome);
|
|
18320
|
+
}
|
|
18321
|
+
} : void 0,
|
|
18016
18322
|
tempoSettlementReadiness: this.opts.tempoSettlementReadiness
|
|
18017
18323
|
});
|
|
18018
18324
|
}
|
|
@@ -18032,11 +18338,8 @@ var DVMServer = class {
|
|
|
18032
18338
|
return this.pricingCurrency;
|
|
18033
18339
|
}
|
|
18034
18340
|
/**
|
|
18035
|
-
*
|
|
18036
|
-
*
|
|
18037
|
-
* caller can see which rails return funds on failure before committing. The
|
|
18038
|
-
* `methods` shape stays identical to `/v1/info` and the platform isolate route
|
|
18039
|
-
* (current nested `payment.methods`); refundability rides alongside.
|
|
18341
|
+
* Advertise the same method list as info. Refundability describes the rail
|
|
18342
|
+
* itself; failed jobs release their pending credit draw separately.
|
|
18040
18343
|
*/
|
|
18041
18344
|
quotePaymentBlock() {
|
|
18042
18345
|
const methods = this.resolvePaymentMethods();
|
|
@@ -18149,12 +18452,8 @@ var DVMServer = class {
|
|
|
18149
18452
|
return c.json(info);
|
|
18150
18453
|
}
|
|
18151
18454
|
/**
|
|
18152
|
-
*
|
|
18153
|
-
*
|
|
18154
|
-
* `pricing` block — `pricing.quote.schema` for dynamic-priced capabilities,
|
|
18155
|
-
* `pricing.max` for static-priced ones. Capabilities without an `input`
|
|
18156
|
-
* schema or any pricing surface omit those fields entirely; free
|
|
18157
|
-
* capabilities (no `price`) omit `pricing` entirely.
|
|
18455
|
+
* Input-mode JSON Schema keeps defaulted fields optional on the advertised
|
|
18456
|
+
* wire contract. Capability names are the descriptor's keys.
|
|
18158
18457
|
*/
|
|
18159
18458
|
renderCapabilities() {
|
|
18160
18459
|
const out = {};
|
|
@@ -18355,11 +18654,8 @@ var DVMServer = class {
|
|
|
18355
18654
|
return c.json(res);
|
|
18356
18655
|
}
|
|
18357
18656
|
/**
|
|
18358
|
-
*
|
|
18359
|
-
*
|
|
18360
|
-
* path — cannot drift: a field added for one is present in the other by
|
|
18361
|
-
* construction, and both paths price against the same view of the caller's
|
|
18362
|
-
* credit.
|
|
18657
|
+
* Callers supply quote-schema-parsed data when a quote schema exists, alongside
|
|
18658
|
+
* raw input text and params. Quote and job pricing share these runtime helpers.
|
|
18363
18659
|
*/
|
|
18364
18660
|
async buildQuoteContext(args) {
|
|
18365
18661
|
const ctx = {
|
|
@@ -18381,16 +18677,8 @@ var DVMServer = class {
|
|
|
18381
18677
|
return ctx;
|
|
18382
18678
|
}
|
|
18383
18679
|
/**
|
|
18384
|
-
*
|
|
18385
|
-
*
|
|
18386
|
-
* structured 400 envelope agents can relay verbatim to a human caller.
|
|
18387
|
-
*
|
|
18388
|
-
* - **Missing** — `body.capability` is absent or empty. Surface as
|
|
18389
|
-
* `capability_required` with a hint pointing at `/v1/info` so the agent
|
|
18390
|
-
* can re-discover the capability set.
|
|
18391
|
-
* - **Unknown** — `body.capability` is set but doesn't match any
|
|
18392
|
-
* capability on this descriptor. Surface as `unknown_capability` listing
|
|
18393
|
-
* the known names so the agent can correct without a round-trip.
|
|
18680
|
+
* Missing or unknown capabilities return structured errors with the current
|
|
18681
|
+
* info-schema URL.
|
|
18394
18682
|
*/
|
|
18395
18683
|
requireCapabilityFromBody(c, capability) {
|
|
18396
18684
|
const known = this.jobManager.capabilityNames();
|
|
@@ -18494,22 +18782,9 @@ var DVMServer = class {
|
|
|
18494
18782
|
);
|
|
18495
18783
|
}
|
|
18496
18784
|
/**
|
|
18497
|
-
*
|
|
18498
|
-
*
|
|
18499
|
-
*
|
|
18500
|
-
* A capability schema may declare the signing-envelope fields required
|
|
18501
|
-
* (cast's `addEpisodeSchema`) or optional (scribe's `transcribeSchema`). On
|
|
18502
|
-
* the required shape an *unsigned* request fails the input parse naming
|
|
18503
|
-
* `pubkey` / `signature` — a signing failure, and answering it with "your
|
|
18504
|
-
* input is invalid: pubkey required" is the mistake in reverse. So a
|
|
18505
|
-
* parse whose issues are **only** envelope-side falls through to the auth
|
|
18506
|
-
* gate, which answers 401 `signature_invalid` with the remedy that works.
|
|
18507
|
-
*
|
|
18508
|
-
* Deferring is safe because the gate below re-parses the same schema over the
|
|
18509
|
-
* same bytes, so a caller that reaches it with an envelope-side failure is
|
|
18510
|
-
* rejected there for certain; both routes still carry a backstop for the
|
|
18511
|
-
* impossible case. It applies only to a DVM that declares `auth` — with no
|
|
18512
|
-
* gate below, the 400 is the only answer available.
|
|
18785
|
+
* On authenticated DVMs, envelope-only parse failures defer to auth
|
|
18786
|
+
* (401 signature_invalid). Builder input issues remain 400 invalid_input.
|
|
18787
|
+
* Auth reparses the same schema/raw input; a backstop refuses disagreement.
|
|
18513
18788
|
*/
|
|
18514
18789
|
deferToAuthGate(err) {
|
|
18515
18790
|
if (!this.descriptor.auth) return false;
|
|
@@ -18708,8 +18983,40 @@ var DVMServer = class {
|
|
|
18708
18983
|
{ capability: capabilityName, input: body.input, params: body.params, data: body.data },
|
|
18709
18984
|
{ stripAuthEnvelope: false }
|
|
18710
18985
|
) : void 0;
|
|
18986
|
+
const authorization = c.req.header("Authorization");
|
|
18987
|
+
const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? (authorization && /^Payment\s/i.test(authorization) ? authorization : void 0);
|
|
18988
|
+
const hostingPaymentArtifactHash = attachedPayment ? createHash7("sha256").update(attachedPayment).digest("hex") : void 0;
|
|
18989
|
+
if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
|
|
18990
|
+
const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
|
|
18991
|
+
hostingPaymentArtifactHash
|
|
18992
|
+
);
|
|
18993
|
+
const prior = priorId ? await this.jobManager.store.get(priorId) : void 0;
|
|
18994
|
+
if (prior && prior.requestFingerprint === fingerprint && prior.requesterId === c.get("requesterId") && (!this.descriptor.auth || prior.requesterPubkey === authPubkey)) {
|
|
18995
|
+
await this.jobManager.ensureReceipt(prior);
|
|
18996
|
+
return buildSubmitResponse(c, prior, prior.requesterToken);
|
|
18997
|
+
}
|
|
18998
|
+
}
|
|
18999
|
+
if (!resumeOnly && this.opts.hostingControlInstance && creditEnvelope && authPubkey) {
|
|
19000
|
+
const draw = await this.creditLedger.getDraw({
|
|
19001
|
+
creditId: creditEnvelope.creditId,
|
|
19002
|
+
drawId: creditEnvelope.drawId
|
|
19003
|
+
});
|
|
19004
|
+
if (draw?.jobId && await this.jobManager.store.get(draw.jobId)) {
|
|
19005
|
+
const recovered = await this.recoverDrawnJob(c, {
|
|
19006
|
+
creditId: creditEnvelope.creditId,
|
|
19007
|
+
drawId: creditEnvelope.drawId,
|
|
19008
|
+
jobId: draw.jobId,
|
|
19009
|
+
fingerprint,
|
|
19010
|
+
callerPubkey: authPubkey
|
|
19011
|
+
});
|
|
19012
|
+
if (recovered) return recovered;
|
|
19013
|
+
}
|
|
19014
|
+
}
|
|
18711
19015
|
let publicRequestClaim;
|
|
18712
19016
|
let publicRequestClaimResult;
|
|
19017
|
+
let hostingAdmissionId;
|
|
19018
|
+
let hostingAdmissionAccepted = false;
|
|
19019
|
+
let hostingPaymentMayHaveLanded = false;
|
|
18713
19020
|
if (publicRequestId && authPubkey && recoveryStore) {
|
|
18714
19021
|
publicRequestClaim = {
|
|
18715
19022
|
requestId: publicRequestId,
|
|
@@ -18732,6 +19039,18 @@ var DVMServer = class {
|
|
|
18732
19039
|
}
|
|
18733
19040
|
}
|
|
18734
19041
|
let jobId = publicRequestClaimResult?.jobId ?? this.jobManager.allocateJobId();
|
|
19042
|
+
if (!resumeOnly && this.opts.hostingControlInstance) {
|
|
19043
|
+
hostingAdmissionId = randomUUID4();
|
|
19044
|
+
if (!await this.opts.hostingControlInstance.admit(hostingAdmissionId, jobId, {
|
|
19045
|
+
paymentArtifactHash: hostingPaymentArtifactHash
|
|
19046
|
+
})) {
|
|
19047
|
+
return c.json({
|
|
19048
|
+
error: "hosting_draining",
|
|
19049
|
+
message: "This service is draining and is not accepting new jobs or payments.",
|
|
19050
|
+
hint: "Keep any existing job token to fetch its result or reclaim unused credit. Retry a new job after service resumes."
|
|
19051
|
+
}, 503);
|
|
19052
|
+
}
|
|
19053
|
+
}
|
|
18735
19054
|
let payment = { paidMsats: 0, receivedProofs: [] };
|
|
18736
19055
|
let resumedAcceptedPayment = false;
|
|
18737
19056
|
if (resumeOnly) {
|
|
@@ -18788,6 +19107,9 @@ var DVMServer = class {
|
|
|
18788
19107
|
const skipPayment = devModeSkipsPaymentVerification(this.opts);
|
|
18789
19108
|
if (!skipPayment && !resumedAcceptedPayment) {
|
|
18790
19109
|
const railOpts = await this.railVerifyOpts(c);
|
|
19110
|
+
hostingPaymentMayHaveLanded = Boolean(
|
|
19111
|
+
creditEnvelope || railOpts.cashuToken || railOpts.x402Payment || railOpts.mppCredential
|
|
19112
|
+
);
|
|
18791
19113
|
try {
|
|
18792
19114
|
payment = await loggers.isolate.span(
|
|
18793
19115
|
"dvm.payment.validate",
|
|
@@ -18901,8 +19223,12 @@ var DVMServer = class {
|
|
|
18901
19223
|
);
|
|
18902
19224
|
}
|
|
18903
19225
|
const exactAdmission = payment.x402ExactAdmission;
|
|
19226
|
+
hostingPaymentMayHaveLanded ||= resumedAcceptedPayment || payment.paidMsats > 0 || Boolean(payment.funding || exactAdmission);
|
|
18904
19227
|
if (exactAdmission) {
|
|
18905
19228
|
jobId = exactAdmission.jobId;
|
|
19229
|
+
if (hostingAdmissionId) {
|
|
19230
|
+
await this.opts.hostingControlInstance.rebindAdmissionJob(hostingAdmissionId, jobId);
|
|
19231
|
+
}
|
|
18906
19232
|
const prior = await this.jobManager.store.get(jobId);
|
|
18907
19233
|
if (prior) {
|
|
18908
19234
|
if (prior.requestFingerprint !== fingerprint || prior.requesterPubkey !== authPubkey || prior.requesterId !== c.get("requesterId")) {
|
|
@@ -18953,10 +19279,6 @@ var DVMServer = class {
|
|
|
18953
19279
|
priceMsats,
|
|
18954
19280
|
capabilityName,
|
|
18955
19281
|
requesterTokenHash,
|
|
18956
|
-
// everything the idempotent-replay path needs to recognise a
|
|
18957
|
-
// retry of THIS call and re-issue the token its lost response carried.
|
|
18958
|
-
// `jobId` is the pre-allocated id the payment's ledger draw
|
|
18959
|
-
// already committed under.
|
|
18960
19282
|
{
|
|
18961
19283
|
requesterToken: jobToken,
|
|
18962
19284
|
requestFingerprint: fingerprint,
|
|
@@ -18967,6 +19289,19 @@ var DVMServer = class {
|
|
|
18967
19289
|
}
|
|
18968
19290
|
);
|
|
18969
19291
|
await jm.persistJob(job);
|
|
19292
|
+
if (hostingAdmissionId) {
|
|
19293
|
+
await this.opts.hostingControlInstance.finishAdmission(hostingAdmissionId, "accepted", jobId);
|
|
19294
|
+
hostingAdmissionAccepted = true;
|
|
19295
|
+
}
|
|
19296
|
+
await this.opts.hostingControlInstance?.status();
|
|
19297
|
+
if (this.opts.hostingControlInstance && await this.opts.hostingControlInstance.dispatchDeadlineReached(jobId)) {
|
|
19298
|
+
const store = jm.store;
|
|
19299
|
+
if (store instanceof PostgresJobStore && await store.cancelJobForHosting(jobId, "Hosting drain deadline reached")) {
|
|
19300
|
+
await jm.finalizeStoredTerminal(jobId);
|
|
19301
|
+
}
|
|
19302
|
+
const terminal = await store.get(jobId);
|
|
19303
|
+
if (terminal) return buildSubmitResponse(c, terminal, jobToken);
|
|
19304
|
+
}
|
|
18970
19305
|
if (exactAdmission) {
|
|
18971
19306
|
await this.x402ExactSettlement?.completeAdmission(
|
|
18972
19307
|
this.opts.dvmId ?? "",
|
|
@@ -18988,6 +19323,12 @@ var DVMServer = class {
|
|
|
18988
19323
|
}
|
|
18989
19324
|
return response;
|
|
18990
19325
|
} finally {
|
|
19326
|
+
if (hostingAdmissionId && !hostingAdmissionAccepted) {
|
|
19327
|
+
await this.opts.hostingControlInstance.finishAdmission(
|
|
19328
|
+
hostingAdmissionId,
|
|
19329
|
+
hostingPaymentMayHaveLanded ? "unresolved" : "failed"
|
|
19330
|
+
);
|
|
19331
|
+
}
|
|
18991
19332
|
if (publicRequestClaim && recoveryStore) {
|
|
18992
19333
|
await recoveryStore.releaseRequestIdClaim(publicRequestClaim);
|
|
18993
19334
|
}
|
|
@@ -19039,20 +19380,9 @@ var DVMServer = class {
|
|
|
19039
19380
|
});
|
|
19040
19381
|
}
|
|
19041
19382
|
/**
|
|
19042
|
-
*
|
|
19043
|
-
*
|
|
19044
|
-
*
|
|
19045
|
-
* but until now it was also useless: the caller got a 409 and the `job_id` /
|
|
19046
|
-
* `job_token` it paid for stayed unreachable forever.
|
|
19047
|
-
*
|
|
19048
|
-
* Returns the original job's submit response iff the retry proves it is the
|
|
19049
|
-
* original caller. Proof is reproducing the request, **not** possessing the
|
|
19050
|
-
* spent Cashu token: the token is a bearer secret, so gating on it alone
|
|
19051
|
-
* would hand the job (and its result) to anyone who lifted it. The legitimate
|
|
19052
|
-
* retry always has the body — the resume sweep re-POSTs it — while a
|
|
19053
|
-
* token thief does not. On a descriptor-auth DVM the caller pubkey must match
|
|
19054
|
-
* too. Anything short of that falls through to the 409 the caller has always
|
|
19055
|
-
* gotten.
|
|
19383
|
+
* Recover a lost paid response only for matching request fingerprint,
|
|
19384
|
+
* authenticated pubkey and requester identity. A spent bearer token alone
|
|
19385
|
+
* is not proof of request ownership.
|
|
19056
19386
|
*/
|
|
19057
19387
|
async recoverReplayedJob(c, opts) {
|
|
19058
19388
|
if (!opts.requestId) return void 0;
|
|
@@ -19076,16 +19406,9 @@ var DVMServer = class {
|
|
|
19076
19406
|
return buildSubmitResponse(c, prior, prior.requesterToken);
|
|
19077
19407
|
}
|
|
19078
19408
|
/**
|
|
19079
|
-
*
|
|
19080
|
-
*
|
|
19081
|
-
*
|
|
19082
|
-
* and re-issue the original response under the SAME proof-of-origin gates
|
|
19083
|
-
* as the header regime: reproduce the fingerprint, sign as the same caller
|
|
19084
|
-
* (explicit draws only exist on descriptor-auth DVMs), match requesterId.
|
|
19085
|
-
*
|
|
19086
|
-
* A recorded draw whose job row never landed (crash between the ledger
|
|
19087
|
-
* commit and the job persist) has nothing to hand back — refuse with the
|
|
19088
|
-
* 409 and log loudly; the pending hold is surfaced by `listPendingDraws`.
|
|
19409
|
+
* Resolve the recorded draw's job under the same fingerprint/pubkey/requester
|
|
19410
|
+
* checks as header recovery. If no job exists, the caller may still recover a
|
|
19411
|
+
* pending durable acceptance under its bound public request-id claim.
|
|
19089
19412
|
*/
|
|
19090
19413
|
async recoverDrawnJob(c, opts) {
|
|
19091
19414
|
let jobId = opts.jobId;
|
|
@@ -19195,12 +19518,9 @@ var DVMServer = class {
|
|
|
19195
19518
|
return payment;
|
|
19196
19519
|
}
|
|
19197
19520
|
/**
|
|
19198
|
-
* Recompute
|
|
19199
|
-
*
|
|
19200
|
-
*
|
|
19201
|
-
* satisfy `onQuote.schema` or the handler throws — failing closed is the
|
|
19202
|
-
* right default, since the alternative (running the job with
|
|
19203
|
-
* `requiredMsats=undefined`) lets unpaid jobs through.
|
|
19521
|
+
* Recompute dynamic upfront pricing from parsed job input, applying the quote
|
|
19522
|
+
* schema separately. Refuse invalid input/results, currency mismatch or missing
|
|
19523
|
+
* FX rather than running without a price (400/500/503 as classified below).
|
|
19204
19524
|
*/
|
|
19205
19525
|
async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey) {
|
|
19206
19526
|
let data = parsedInput;
|
|
@@ -19977,16 +20297,9 @@ var PlatformTempoObserverHealth = class {
|
|
|
19977
20297
|
};
|
|
19978
20298
|
}
|
|
19979
20299
|
/**
|
|
19980
|
-
*
|
|
19981
|
-
*
|
|
19982
|
-
*
|
|
19983
|
-
* the current set of declarations. `host.ts` primes this client before any
|
|
19984
|
-
* mount exists, so at boot there is routinely an open probe carrying **no**
|
|
19985
|
-
* `tempo_session` at all — handing it back to the mount that just declared
|
|
19986
|
-
* one would leave a scale-to-zero DVM unstamped until traffic arrived, which
|
|
19987
|
-
* is exactly the container-restart case is about. Same shape with
|
|
19988
|
-
* two mounts: the second would join a probe whose URL already read the first
|
|
19989
|
-
* one's `false`.
|
|
20300
|
+
* De-duplicate probes only while their declaration epoch is current. A mount
|
|
20301
|
+
* declared during boot or another mount's probe must await a fresh probe so
|
|
20302
|
+
* the platform receives the updated capability, even before traffic arrives.
|
|
19990
20303
|
*/
|
|
19991
20304
|
async refresh() {
|
|
19992
20305
|
const open = this.inFlight;
|
|
@@ -20147,6 +20460,7 @@ export {
|
|
|
20147
20460
|
createAdminCashuNonceStore,
|
|
20148
20461
|
X402ExactSettlementServer,
|
|
20149
20462
|
X402ExactSettlementEvidenceMissingError,
|
|
20463
|
+
signHostingControl,
|
|
20150
20464
|
jobCredentialGate,
|
|
20151
20465
|
REQUEST_ID_FIELD,
|
|
20152
20466
|
protocolEnvelopeIgnoreFields,
|