@dvmkit/sdk 0.1.2-rc.7 → 0.1.3-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/chunk-2K7E3N2D.js +1709 -0
- package/dist/{chunk-N4VTG3KH.js → chunk-3ZHMQCYP.js} +645 -3027
- package/dist/{chunk-TKA6ZP4M.js → chunk-4B56DEEV.js} +50 -426
- package/dist/chunk-BIFLRKMO.js +87 -0
- package/dist/chunk-BQ2NMWKE.js +160 -0
- package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
- package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
- package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
- package/dist/chunk-EPNDZ5DH.js +1010 -0
- package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
- package/dist/chunk-KXZUCCEY.js +142 -0
- package/dist/{chunk-6JZIX5WW.js → chunk-LLXV32HA.js} +82 -5
- package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
- package/dist/{chunk-2ABMGUDS.js → chunk-NFRM5QYP.js} +83 -10
- package/dist/chunk-RW5LP57K.js +44 -0
- package/dist/chunk-YDIYXGYL.js +384 -0
- package/dist/{chunk-JGGI65I3.js → chunk-Z4BNLUZF.js} +1 -150
- package/dist/{credit-ledger-ED6JXKVD.js → credit-ledger-2DFQHNLB.js} +2 -2
- package/dist/{credit-menu-BM4qCD5U.d.ts → credit-menu-C7zAJElJ.d.ts} +1632 -1946
- package/dist/{fx-C-liI3oY.d.ts → fx-BF_SG2i0.d.ts} +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +8 -4
- package/dist/internal/caller.d.ts +10429 -0
- package/dist/internal/caller.js +9957 -0
- package/dist/internal/index.d.ts +2 -10816
- package/dist/internal/index.js +2 -10130
- package/dist/internal/server.d.ts +404 -0
- package/dist/internal/server.js +202 -0
- package/dist/job-store-Bn23V3QU.d.ts +576 -0
- package/dist/lightning-backend-C04nH94l.d.ts +367 -0
- package/dist/{memory-credit-ledger-XJ5VQEVP.js → memory-credit-ledger-OP24Z2KO.js} +3 -3
- package/dist/{postgres-job-store-J5F4GUWU.js → postgres-job-store-TAONYLIF.js} +1 -1
- package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-XXSU5KVB.js} +1 -1
- package/dist/server/index.d.ts +27 -11
- package/dist/server/index.js +93 -69
- package/dist/{job-store-C53VQ5uu.d.ts → step-cache-3cT4Shk0.d.ts} +31 -585
- package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
- package/dist/testing/index.d.ts +16 -4
- package/dist/testing/index.js +7 -3
- package/dist/{usd-gLcJB1ps.d.ts → usd-BnuXoFl5.d.ts} +1 -1
- package/dist/wallet-CJC8lwxx.d.ts +29 -0
- package/dist/{x402-FTG2GRAQ.js → x402-T2C5MX3T.js} +6 -3
- package/package.json +6 -2
- package/dist/{chunk-RU7SXHLO.js → chunk-UP2F5RRT.js} +3 -3
|
@@ -1,35 +1,15 @@
|
|
|
1
1
|
import { Hono, Context } from 'hono';
|
|
2
2
|
import { Pool } from 'pg';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { a0 as Message, ag as FundingMethod, W as FundingReceipt, bN as CreditSnapshot, X as JobReceipt, S as SDKJobContext, av as StepCache, u as ResponseContent, P as PaymentContent, a1 as MessageType, bi as X402Receipt, aD as X402Config, bh as X402ExactVersionSupport, cQ as X402SettlementIntent, ap as PaymentRequirementsV2, c0 as CreditLedgerQuerier, cR as X402SettlementCursor, bP as X402SettlementStatus, cS as X402SettlementWriteOff, bM as X402RefundSettlementGate, cT as X402FacilitatorAuth, cU as X402BatchSettlementConfig, cw as PostgresX402ChannelStorage, cV as X402PayoutObserver, bq as MppxServer, aa as CashuMode, bL as CreditLedgerLike, cW as CreditDepositEnqueue, cX as X402SettlementReconciliationReason, an as PaymentRequirements, $ as MppxCredential, o as PaymentMethod, bl as CreditDepositPayload, bO as DrawResult, cm as CreditLedgerError, a5 as ReceiptCredit, ad as DrainReceiptEvent, ac as DrainReceipt, K as KVStore, y as SignedRequestAudience, cY as CreditDrawReleaseEnqueue, cy as RevenueSkippedNoRailPayload, Z as ZodLike, a as DVMDescriptor, bU as CreditInvoiceRecord, bV as InvoiceSettlement, cg as ClientCompatibilityGate, ce as ClientCompatibility, bt as PayoutReporter, R as ResolvedCreditConfig, g as CreditView } from './step-cache-3cT4Shk0.js';
|
|
4
|
+
import { b as FxRateSnapshot, F as FxFetcher } from './fx-BF_SG2i0.js';
|
|
5
|
+
import { g as LockPubkey, f as CheckMintHealthOptions, b as LightningBackend, A as AttestationPayload } from './lightning-backend-C04nH94l.js';
|
|
6
|
+
import { T as TopUpCapUnenforcedReason, A as AppendOutgoingOptions, J as JobRecord, b as JobStore } from './job-store-Bn23V3QU.js';
|
|
5
7
|
import { ProofLike, SerializedDLEQ } from '@cashu/cashu-ts';
|
|
6
8
|
import { Challenge } from 'mppx';
|
|
7
9
|
import { SettleResponse, SupportedResponse } from '@x402/core/types';
|
|
8
10
|
import { Channel, AutoSettlementConfig } from '@x402/evm/batch-settlement/server';
|
|
9
11
|
import { FacilitatorClient } from '@x402/core/server';
|
|
10
12
|
|
|
11
|
-
declare const lockPubkeyBrand: unique symbol;
|
|
12
|
-
/**
|
|
13
|
-
* NUT-11 P2PK lock pubkey, lowercase-by-construction. Hex-encoded compressed
|
|
14
|
-
* secp256k1 pubkey (66 chars starting with `02` / `03`), normalised via
|
|
15
|
-
* `toLockPubkey`. The brand exists so any in-memory site that compares
|
|
16
|
-
* lock-pubkeys against each other — receive matching, monitor orphan/retired
|
|
17
|
-
* lookups, melt-pending keypair maps — gets a compile-time guarantee that
|
|
18
|
-
* both sides have been normalised. Raw strings off the wire or out of the DB
|
|
19
|
-
* must pass through `toLockPubkey` before they're treated as one.
|
|
20
|
-
*/
|
|
21
|
-
type LockPubkey = string & {
|
|
22
|
-
readonly [lockPubkeyBrand]: never;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Brand a raw lock-pubkey string. Trims surrounding whitespace and lowercases
|
|
26
|
-
* the hex so equality is structural. Idempotent on values that are already
|
|
27
|
-
* lowercase. Does not validate the curve point — assertions about hex shape
|
|
28
|
-
* live at the wire-validation layer (e.g. the `/admin/cashu/rotate-lock-pubkey`
|
|
29
|
-
* regex in `admin-cashu.ts`).
|
|
30
|
-
*/
|
|
31
|
-
declare function toLockPubkey(raw: string): LockPubkey;
|
|
32
|
-
|
|
33
13
|
/** A row in the per-DVM `wallet_accumulator` table (internal-review). */
|
|
34
14
|
interface WalletAccumulatorRow {
|
|
35
15
|
id: string;
|
|
@@ -50,11 +30,10 @@ interface WalletAccumulatorRow {
|
|
|
50
30
|
meltedAt: number | null;
|
|
51
31
|
meltStatus: "melted" | "failed" | "drained" | null;
|
|
52
32
|
/**
|
|
53
|
-
* Mint-issued melt-quote id
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* persisted — those fall through to the manual-`UPDATE` hint path.
|
|
33
|
+
* Mint-issued melt-quote id recorded by the admin `mark-melted` endpoint
|
|
34
|
+
* alongside the payment preimage after an external payout completes. Null
|
|
35
|
+
* until payout audit data is recorded, including on pending rows and legacy
|
|
36
|
+
* rows created before the column existed.
|
|
58
37
|
*/
|
|
59
38
|
meltQuoteId: string | null;
|
|
60
39
|
/**
|
|
@@ -66,8 +45,8 @@ interface WalletAccumulatorRow {
|
|
|
66
45
|
}
|
|
67
46
|
/**
|
|
68
47
|
* Subset of `pg.Pool` the store uses for queries. `Pool` and `PoolClient`
|
|
69
|
-
* both satisfy this — important so the
|
|
70
|
-
* client that holds the advisory lock.
|
|
48
|
+
* both satisfy this — important so the accumulator monitor can run reads on a
|
|
49
|
+
* single client that holds the advisory lock.
|
|
71
50
|
*/
|
|
72
51
|
interface AccumulatorQuerier {
|
|
73
52
|
query: Pool["query"];
|
|
@@ -131,348 +110,6 @@ declare function clearAccumulatorForDvm(db: AccumulatorQuerier, dvmId: string):
|
|
|
131
110
|
*/
|
|
132
111
|
declare function hashLockKey(input: string): bigint;
|
|
133
112
|
|
|
134
|
-
/**
|
|
135
|
-
* Canonical deploy-time attestation payload (internal-review). One signature serves
|
|
136
|
-
* both auth-on-deploy and the `/v1/info#builder` attestation — the bytes the
|
|
137
|
-
* builder signs are the canonical JSON of this object. Making a field required
|
|
138
|
-
* also requires the coordinated fleet-before-caller release note in
|
|
139
|
-
* `public compatibility guide`.
|
|
140
|
-
*/
|
|
141
|
-
interface AttestationPayload {
|
|
142
|
-
/** Immutable platform or self-hosted DVM identifier this attestation applies to. */
|
|
143
|
-
dvm_id: string;
|
|
144
|
-
/** DVM slug the attestation applies to (must match the deploy request's slug). */
|
|
145
|
-
slug: string;
|
|
146
|
-
/** sha256(`canonicaliseForSigning(capabilities)`) hex. Binds the attestation
|
|
147
|
-
* to the capability shape declared at deploy time. */
|
|
148
|
-
capabilities_hash: string;
|
|
149
|
-
/** Builder identity x-only secp256k1 pubkey, hex (32 bytes / 64 chars). */
|
|
150
|
-
builder_pubkey: string;
|
|
151
|
-
/**
|
|
152
|
-
* Per-DVM receipt-signing x-only pubkey, hex (internal-review). Binds the key the
|
|
153
|
-
* deployed DVM signs job receipts with to the cold builder identity: the
|
|
154
|
-
* builder signs this payload, so a receipt verifying under
|
|
155
|
-
* `receipt_pubkey` inherits the builder's authority.
|
|
156
|
-
*
|
|
157
|
-
* Optional: attestations signed before receipts shipped omit it and still
|
|
158
|
-
* verify byte-for-byte (canonical JSON drops absent keys), and deploy paths
|
|
159
|
-
* that can't provision the matching secret leave it unset rather than
|
|
160
|
-
* attesting a key the running process doesn't hold.
|
|
161
|
-
*/
|
|
162
|
-
receipt_pubkey?: string;
|
|
163
|
-
/** Unix seconds at which the attestation was signed. */
|
|
164
|
-
deployed_at: number;
|
|
165
|
-
}
|
|
166
|
-
/** Result of {@link generateIdentity}: hex pubkey + hex secret. */
|
|
167
|
-
interface BuilderIdentityKeypair {
|
|
168
|
-
/** secp256k1 BIP-340 x-only pubkey, 32 bytes hex (64 chars). */
|
|
169
|
-
pubkey: string;
|
|
170
|
-
/** secp256k1 secret, 32 bytes hex (64 chars). */
|
|
171
|
-
secret: string;
|
|
172
|
-
}
|
|
173
|
-
/** Generate a fresh BIP-340 Schnorr keypair for builder identity. */
|
|
174
|
-
declare function generateIdentity(): BuilderIdentityKeypair;
|
|
175
|
-
/**
|
|
176
|
-
* Read the builder identity secret from `~/.dvmkit/builder.key`. Returns
|
|
177
|
-
* `null` when absent so callers can surface an `identity_required` error.
|
|
178
|
-
*
|
|
179
|
-
* `writeIdentity` persists the key `0o600`, but a destination mode survives
|
|
180
|
-
* `cp`/`scp`/git-checkout intact only by luck — those can land it `0o644`,
|
|
181
|
-
* leaving the signing key world-readable on a multi-user box. On POSIX we
|
|
182
|
-
* detect loose group/other bits, warn, and best-effort re-tighten to `0o600`
|
|
183
|
-
* (same self-heal posture as `writeIdentity`) rather than throw — a hard
|
|
184
|
-
* failure would brick a CLI whose key got widened in transit.
|
|
185
|
-
*/
|
|
186
|
-
declare function loadIdentitySecret(): string | null;
|
|
187
|
-
/**
|
|
188
|
-
* Persist a builder identity keypair: pubkey into `~/.dvmkit/builder.json`
|
|
189
|
-
* (top-level), secret into `~/.dvmkit/builder.key` (mode 0600 on POSIX).
|
|
190
|
-
* Atomic tmp+rename on the secret file so a crash mid-write never leaves a
|
|
191
|
-
* truncated key on disk — a torn write here is unrecoverable; the operator
|
|
192
|
-
* loses the ability to re-sign attestations for every DVM under this key.
|
|
193
|
-
*
|
|
194
|
-
* Refuses to overwrite when an identity already exists unless `force` is
|
|
195
|
-
* passed; the force path warns that any DVMs deployed under the old key
|
|
196
|
-
* need to be re-deployed to refresh their attestations.
|
|
197
|
-
*/
|
|
198
|
-
declare function writeIdentity(keypair: BuilderIdentityKeypair, opts?: {
|
|
199
|
-
force?: boolean;
|
|
200
|
-
}): void;
|
|
201
|
-
/**
|
|
202
|
-
* Compute the canonical `capabilities_hash` (sha256-hex of canonical JSON)
|
|
203
|
-
* for the supplied capabilities block. Shared between the CLI signer and any
|
|
204
|
-
* future verifier so both compute identical bytes.
|
|
205
|
-
*/
|
|
206
|
-
declare function hashCapabilities(capabilities: JsonValue): string;
|
|
207
|
-
/**
|
|
208
|
-
* Assemble the deploy-time attestation payload (internal-review). `deployedAt`
|
|
209
|
-
* defaults to the current Unix second; tests can pin it for reproducibility.
|
|
210
|
-
*/
|
|
211
|
-
declare function buildAttestation(args: {
|
|
212
|
-
dvmId: string;
|
|
213
|
-
slug: string;
|
|
214
|
-
capabilities: JsonValue;
|
|
215
|
-
builderPubkey: string;
|
|
216
|
-
/** Receipt-signing pubkey to attest (internal-review). Omitted when the deploy path
|
|
217
|
-
* can't provision the matching `DVMKIT_RECEIPT_KEY`. */
|
|
218
|
-
receiptPubkey?: string;
|
|
219
|
-
deployedAt?: number;
|
|
220
|
-
}): AttestationPayload;
|
|
221
|
-
/**
|
|
222
|
-
* Sign an attestation payload with the builder identity secret. Returns the
|
|
223
|
-
* 64-byte Schnorr signature as hex. The canonical bytes are produced via the
|
|
224
|
-
* same `canonicaliseForSigning` the verifier uses — both sides hash identical
|
|
225
|
-
* input regardless of object key order.
|
|
226
|
-
*/
|
|
227
|
-
declare function signAttestation(payload: AttestationPayload, secretHex: string): string;
|
|
228
|
-
/**
|
|
229
|
-
* Verify a deploy-time attestation. Returns `true` iff the signature is a
|
|
230
|
-
* valid BIP-340 Schnorr signature of `canonicaliseForSigning(payload)` under
|
|
231
|
-
* `pubkeyHex`. Independent of the platform binding — callers (the deploy
|
|
232
|
-
* route, `/v1/info` clients) cross-check the pubkey against their trust root.
|
|
233
|
-
*/
|
|
234
|
-
declare function verifyAttestation(payload: AttestationPayload, signatureHex: string, pubkeyHex: string): boolean;
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Per-method amount limits a mint advertises on its NUT-4 / NUT-5 bolt11/sat
|
|
238
|
-
* entry (internal-review). Both values are in the method's own unit — `sat` here,
|
|
239
|
-
* never msats.
|
|
240
|
-
*
|
|
241
|
-
* `null` means the mint advertised nothing in that direction, which reads as
|
|
242
|
-
* unbounded. A mint saying nothing is not a mint saying zero, so silence never
|
|
243
|
-
* narrows what we'll attempt — same defensive posture as the absent-`disabled`
|
|
244
|
-
* rule in {@link canMintBolt11Sat}.
|
|
245
|
-
*/
|
|
246
|
-
interface MintAmountBounds {
|
|
247
|
-
/** Smallest amount the mint will quote, or `null` when unadvertised. */
|
|
248
|
-
minSats: number | null;
|
|
249
|
-
/** Largest amount the mint will quote, or `null` when unadvertised. */
|
|
250
|
-
maxSats: number | null;
|
|
251
|
-
}
|
|
252
|
-
/** Error categories returned when a mint health probe fails. */
|
|
253
|
-
type HealthErrorCode = "timeout" | "http_error" | "network" | "parse_error" | "missing_fields";
|
|
254
|
-
/** Outcome of a single health probe against one mint. */
|
|
255
|
-
interface MintHealthCheckResult {
|
|
256
|
-
/** Mint URL exactly as passed in (not normalised). */
|
|
257
|
-
mintUrl: string;
|
|
258
|
-
/** True iff `/v1/info` returned 200, parsed as JSON, and had `pubkey` + `version`. */
|
|
259
|
-
ok: boolean;
|
|
260
|
-
/** Wall-clock duration of the successful (or final failed) attempt in ms. */
|
|
261
|
-
latencyMs: number;
|
|
262
|
-
/** NUT-06 mint pubkey when present. */
|
|
263
|
-
pubkey?: string;
|
|
264
|
-
/** NUT-06 mint version string (e.g. "Nutshell/0.20.0") when present. */
|
|
265
|
-
version?: string;
|
|
266
|
-
/** Sorted list of NUT keys the mint declares support for. */
|
|
267
|
-
supportedNuts?: string[];
|
|
268
|
-
/** Subset of `DEFAULT_REQUIRED_NUTS` the mint does not declare. Empty array when fully supported. */
|
|
269
|
-
missingRequiredNuts?: string[];
|
|
270
|
-
/** True when the mint advertises NUT-04 with method `bolt11` and unit `sat`. */
|
|
271
|
-
bolt11SatMintSupported?: boolean;
|
|
272
|
-
/** True when the mint advertises NUT-05 with method `bolt11` and unit `sat`. */
|
|
273
|
-
bolt11SatMeltSupported?: boolean;
|
|
274
|
-
/**
|
|
275
|
-
* True when the mint can currently *issue* — NUT-04 advertises bolt11/sat and
|
|
276
|
-
* is not switched off (`disabled: true`). Distinct from
|
|
277
|
-
* {@link bolt11SatMintSupported}, which only asks whether the method exists:
|
|
278
|
-
* a mint in melt-only recovery keeps advertising the method and turns the
|
|
279
|
-
* flag on. Consulted when picking a funding default (internal-review).
|
|
280
|
-
*/
|
|
281
|
-
mintingEnabled?: boolean;
|
|
282
|
-
/** Same question for NUT-05 (melt) — whether the mint can currently pay out. */
|
|
283
|
-
meltingEnabled?: boolean;
|
|
284
|
-
/**
|
|
285
|
-
* True when the mint can currently serve `/v1/swap` — see {@link canSwap} for
|
|
286
|
-
* why an unadvertised NUT-3 reads as `true`. This is what a per-call spend
|
|
287
|
-
* needs, so a `false` here is the reason a spend routes around this mint
|
|
288
|
-
* (internal-review).
|
|
289
|
-
*/
|
|
290
|
-
swapEnabled?: boolean;
|
|
291
|
-
/**
|
|
292
|
-
* Amount limits the mint advertises for issuing (NUT-04 bolt11/sat), in sats
|
|
293
|
-
* (internal-review). Consulted when picking a funding default and when refusing an
|
|
294
|
-
* amount locally, so a fund outside the range never opens a Lightning quote.
|
|
295
|
-
*/
|
|
296
|
-
mintAmountBounds?: MintAmountBounds;
|
|
297
|
-
/** The NUT-05 twin — limits on a single payout, which `dvm wallet cash-out` sizes against. */
|
|
298
|
-
meltAmountBounds?: MintAmountBounds;
|
|
299
|
-
/**
|
|
300
|
-
* True when the mint passes the full {@link assertNutSupport} gate — every
|
|
301
|
-
* required NUT present (not `supported: false`) and bolt11/sat on NUT-4 +
|
|
302
|
-
* NUT-5. The runtime `MintHealthTracker` treats a mint as healthy only when
|
|
303
|
-
* this is true, reproducing the boot-time NUT gate post-`listen` (internal-review).
|
|
304
|
-
*/
|
|
305
|
-
nutCompliant?: boolean;
|
|
306
|
-
/** Raw `nuts` object from the `/v1/info` response. Used by `assertNutSupport`. */
|
|
307
|
-
nutsRaw?: Record<string, unknown>;
|
|
308
|
-
/** Populated when `ok` is false. */
|
|
309
|
-
error?: {
|
|
310
|
-
code: HealthErrorCode;
|
|
311
|
-
message: string;
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
/** Tunables for `checkMintHealth`. */
|
|
315
|
-
interface CheckMintHealthOptions {
|
|
316
|
-
/** Per-attempt timeout in ms. Default: 5000. */
|
|
317
|
-
timeoutMs?: number;
|
|
318
|
-
/** Total attempts including the first. Default: 3. */
|
|
319
|
-
retries?: number;
|
|
320
|
-
/** Inter-attempt backoff schedule in ms. Default: [200, 500, 1000]. */
|
|
321
|
-
backoffMs?: number[];
|
|
322
|
-
/** Injected `fetch` for tests. Default: global fetch. */
|
|
323
|
-
fetchImpl?: typeof fetch;
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Probe a Cashu mint's `/v1/info` endpoint with timeout + retries, and
|
|
327
|
-
* report whether it serves the NUT-04 + NUT-05 capabilities first-party DVMs
|
|
328
|
-
* depend on. Never throws — failures are returned as `{ ok: false, error }`.
|
|
329
|
-
*/
|
|
330
|
-
declare function checkMintHealth(mintUrl: string, opts?: CheckMintHealthOptions): Promise<MintHealthCheckResult>;
|
|
331
|
-
/**
|
|
332
|
-
* Whether the mint can currently serve `/v1/swap` — the operation every
|
|
333
|
-
* per-call Cashu payment depends on (internal-review).
|
|
334
|
-
*
|
|
335
|
-
* **An absent NUT-3 means yes.** Swap is a baseline mint operation and healthy
|
|
336
|
-
* mints do not enumerate it — neither coinos nor lnvoltz advertises a `"3"` key
|
|
337
|
-
* (see `public design rationale`).
|
|
338
|
-
* Reading silence as "incapable" would rule out every working mint.
|
|
339
|
-
*
|
|
340
|
-
* A mint that has *switched swaps off* does say so: a mint in melt-only
|
|
341
|
-
* recovery publishes a `"3"` entry flagged `disabled` / `supported: false`
|
|
342
|
-
* alongside its live NUT-5. Both spellings are honoured because this is the
|
|
343
|
-
* only signal distinguishing "can't swap" from "swap failed for some other
|
|
344
|
-
* reason", and it costs nothing to accept either.
|
|
345
|
-
*
|
|
346
|
-
* Never a boot-time gate — like {@link canMintBolt11Sat}, this informs mint
|
|
347
|
-
* *selection* only, so a mint that can still melt stays loadable for cash-out.
|
|
348
|
-
*/
|
|
349
|
-
declare function canSwap(nuts: Record<string, unknown>): boolean;
|
|
350
|
-
/** Whether `sats` falls inside a mint's advertised limits, and which end it broke. */
|
|
351
|
-
type AmountBoundsVerdict = "ok" | "below_min" | "above_max";
|
|
352
|
-
/**
|
|
353
|
-
* Judge one amount against one direction's limits. The single predicate both
|
|
354
|
-
* `dvm wallet fund` (mint selection, pre-quote refusal) and
|
|
355
|
-
* `dvm wallet cash-out` (per-leg sizing) ask, so the two commands can't drift
|
|
356
|
-
* on what "out of range" means.
|
|
357
|
-
*/
|
|
358
|
-
declare function amountBoundsVerdict(sats: number, bounds?: MintAmountBounds): AmountBoundsVerdict;
|
|
359
|
-
/**
|
|
360
|
-
* Assert that a mint's NUT-06 `nuts` object advertises all required capabilities.
|
|
361
|
-
* Two-phase check:
|
|
362
|
-
* 1. Dict-level — each NUT in `requiredNuts` must be present and not have
|
|
363
|
-
* `supported: false`. Throws naming the missing NUT numbers and mint URL.
|
|
364
|
-
* 2. Method-level — NUT-4 and NUT-5 must each advertise a `bolt11`/`sat`
|
|
365
|
-
* entry in their `methods` array (covers NUT-23). Throws naming the
|
|
366
|
-
* missing method.
|
|
367
|
-
*/
|
|
368
|
-
declare function assertNutSupport(mintUrl: string, nuts: Record<string, unknown>, requiredNuts?: readonly number[]): void;
|
|
369
|
-
|
|
370
|
-
/** A Lightning invoice created by the backend. */
|
|
371
|
-
interface CreatedInvoice {
|
|
372
|
-
/** BOLT11 payment request string. */
|
|
373
|
-
bolt11: string;
|
|
374
|
-
/** Payment hash (hex) — primary key for looking up settlement. */
|
|
375
|
-
paymentHash: string;
|
|
376
|
-
/** Amount in millisatoshis. */
|
|
377
|
-
amountMsats: number;
|
|
378
|
-
/** Unix milliseconds when the invoice expires. */
|
|
379
|
-
expiresAt: number;
|
|
380
|
-
}
|
|
381
|
-
/** Result of looking up an invoice's settlement status. */
|
|
382
|
-
interface InvoiceStatus {
|
|
383
|
-
/** True once the invoice has been paid and preimage is known. */
|
|
384
|
-
settled: boolean;
|
|
385
|
-
/** Preimage (hex) — present only after settlement. */
|
|
386
|
-
preimage?: string;
|
|
387
|
-
/** Unix milliseconds when the invoice settled (if settled). */
|
|
388
|
-
settledAt?: number;
|
|
389
|
-
/** Unix milliseconds when the invoice expires. */
|
|
390
|
-
expiresAt?: number;
|
|
391
|
-
}
|
|
392
|
-
/** How a payment reached a settled state — see `payInvoiceReconciling`. */
|
|
393
|
-
type LightningPayOutcome = "paid" | "reconciled" | "republished";
|
|
394
|
-
/** Settled payment plus the audit trail of how it got there. */
|
|
395
|
-
interface LightningPayment {
|
|
396
|
-
/** Payment preimage (hex) — proof the invoice was paid. */
|
|
397
|
-
preimage: string;
|
|
398
|
-
/** Routing fee reported by the wallet, in millisatoshis. Absent means unknown. */
|
|
399
|
-
feesPaidMsats?: number;
|
|
400
|
-
/** Whether the wallet answered directly, or the outcome had to be reconciled. */
|
|
401
|
-
outcome: LightningPayOutcome;
|
|
402
|
-
}
|
|
403
|
-
/** Bounds for a read-only wallet transaction snapshot. */
|
|
404
|
-
interface LightningTransactionListOptions {
|
|
405
|
-
/** Include transactions created at or after this Unix timestamp (seconds). */
|
|
406
|
-
from?: number;
|
|
407
|
-
/** Maximum number of transactions the wallet may return. */
|
|
408
|
-
limit?: number;
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Credential-safe transaction evidence used to account for a wallet debit.
|
|
412
|
-
*
|
|
413
|
-
* Invoice, payment hash and preimage are deliberately replaced by digests or
|
|
414
|
-
* omitted. `fingerprint` identifies the complete reduced row; `invoiceHash`
|
|
415
|
-
* lets a caller bind an outgoing row to the invoice it just paid.
|
|
416
|
-
*/
|
|
417
|
-
interface LightningTransactionSnapshot {
|
|
418
|
-
fingerprint: string;
|
|
419
|
-
invoiceHash?: string;
|
|
420
|
-
type: "incoming" | "outgoing";
|
|
421
|
-
amountMsats: number;
|
|
422
|
-
/** Wallet-reported routing fee. Absent when the wallet omitted it. */
|
|
423
|
-
feesPaidMsats?: number;
|
|
424
|
-
createdAt: number;
|
|
425
|
-
settledAt?: number;
|
|
426
|
-
}
|
|
427
|
-
/** What a backend can tell us about the wallet behind it. */
|
|
428
|
-
interface LightningWalletInfo {
|
|
429
|
-
/** Human-readable wallet name (e.g. "Alby Hub"). */
|
|
430
|
-
alias?: string;
|
|
431
|
-
/** Operations the wallet supports (e.g. `pay_invoice`, `lookup_invoice`). */
|
|
432
|
-
methods: string[];
|
|
433
|
-
/**
|
|
434
|
-
* Budget fields the wallet volunteered, if any.
|
|
435
|
-
*
|
|
436
|
-
* No rail behind this interface is required to expose a connection's
|
|
437
|
-
* spending cap, and NWC — today's only implementation — has no method that
|
|
438
|
-
* returns one. Present it when a wallet offers it, never infer it.
|
|
439
|
-
*/
|
|
440
|
-
walletReportedBudget?: Record<string, unknown>;
|
|
441
|
-
}
|
|
442
|
-
/** Abstraction over Lightning wallet backends (NWC, future). */
|
|
443
|
-
interface LightningBackend {
|
|
444
|
-
/**
|
|
445
|
-
* Pay a bolt11 invoice exactly once. Returns the preimage on success.
|
|
446
|
-
*
|
|
447
|
-
* Implementations must never re-send a payment on an unknown outcome without
|
|
448
|
-
* first reconciling against the wallet — the rails behind this interface have
|
|
449
|
-
* no idempotency key, so a blind retry is a second payment.
|
|
450
|
-
*
|
|
451
|
-
* `amountMsats` is passed when known (callers always have it from the
|
|
452
|
-
* provider's payment-request). Backends that don't need it may ignore the
|
|
453
|
-
* param.
|
|
454
|
-
*/
|
|
455
|
-
payInvoice(bolt11: string, amountMsats?: number): Promise<LightningPayment>;
|
|
456
|
-
/** Get wallet balance in millisatoshis. */
|
|
457
|
-
getBalance(): Promise<number>;
|
|
458
|
-
/** Get wallet info (name, supported methods, any volunteered budget fields). */
|
|
459
|
-
getInfo(): Promise<LightningWalletInfo>;
|
|
460
|
-
/** Create a new incoming invoice for the given amount and description. */
|
|
461
|
-
createInvoice(params: {
|
|
462
|
-
amountMsats: number;
|
|
463
|
-
description?: string;
|
|
464
|
-
expirySeconds?: number;
|
|
465
|
-
}): Promise<CreatedInvoice>;
|
|
466
|
-
/** Look up an invoice's settlement status by payment hash. */
|
|
467
|
-
lookupInvoice(paymentHash: string): Promise<InvoiceStatus>;
|
|
468
|
-
/**
|
|
469
|
-
* Return a bounded, credential-safe transaction snapshot when supported.
|
|
470
|
-
* Optional so existing backends remain source-compatible; without it an
|
|
471
|
-
* exact total wallet debit cannot be attributed.
|
|
472
|
-
*/
|
|
473
|
-
listTransactions?(options: LightningTransactionListOptions): Promise<LightningTransactionSnapshot[]>;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
113
|
/**
|
|
477
114
|
* Tracks MPP challenge ids that have already been consumed by an upfront-flow
|
|
478
115
|
* request, scoped per-DVM realm. Entries auto-expire after a TTL and the store
|
|
@@ -525,230 +162,229 @@ interface CreditExpirySweepOpts {
|
|
|
525
162
|
limit?: number;
|
|
526
163
|
}
|
|
527
164
|
|
|
528
|
-
/**
|
|
529
|
-
interface
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
/** Prime the cached signal at boot when the source supports it. */
|
|
533
|
-
refresh?(): Promise<void>;
|
|
165
|
+
/** Promise resolver for a pending client prompt response. */
|
|
166
|
+
interface PromptResolver {
|
|
167
|
+
resolve: (value: ResponseContent) => void;
|
|
168
|
+
reject: (reason: Error) => void;
|
|
534
169
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
170
|
+
/** Promise resolver for a pending client payment. */
|
|
171
|
+
interface PaymentResolver {
|
|
172
|
+
resolve: (value: PaymentContent) => void;
|
|
173
|
+
reject: (reason: Error) => void;
|
|
174
|
+
}
|
|
175
|
+
/** In-memory representation of a running job. */
|
|
176
|
+
interface ServerJob {
|
|
177
|
+
id: string;
|
|
178
|
+
tags: string[];
|
|
538
179
|
/**
|
|
539
|
-
*
|
|
540
|
-
*
|
|
180
|
+
* Capability name this job belongs to (internal-review). Set at submission from
|
|
181
|
+
* `POST /v1/job` body's `capability` field; routes the SDK runtime to the
|
|
182
|
+
* right `onJob` / `onResponse` / `onPayment` handler and the right input
|
|
183
|
+
* schema. Required — the wire layer rejects requests that omit it.
|
|
541
184
|
*/
|
|
542
|
-
|
|
185
|
+
capability: string;
|
|
186
|
+
input: string;
|
|
187
|
+
params: Record<string, string>;
|
|
188
|
+
requesterId: string;
|
|
543
189
|
/**
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
190
|
+
* SHA-256 hex of the per-job opaque `job_token` minted at creation for
|
|
191
|
+
* anonymous callers (internal-review). Mirror of `JobRecord.requesterTokenHash`;
|
|
192
|
+
* see that field for the wire-level gating semantics.
|
|
547
193
|
*/
|
|
548
|
-
|
|
549
|
-
/** Invoice lifetime in seconds. Defaults to {@link DEFAULT_INVOICE_TTL_SECONDS}. */
|
|
550
|
-
invoiceTtlSeconds?: number;
|
|
194
|
+
requesterTokenHash?: string;
|
|
551
195
|
/**
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
* {@link DEFAULT_LIGHTNING_FUNDING_MIN_SATS}.
|
|
196
|
+
* Submission provenance (internal-review) — mirrors `JobRecord.requesterToken` /
|
|
197
|
+
* `requestFingerprint` / `requesterPubkey`. Carried on the job purely so
|
|
198
|
+
* `toJobRecord` persists it; nothing in the runtime reads it. It's the
|
|
199
|
+
* replay gate: a retried paid submit that reproduces the fingerprint (and,
|
|
200
|
+
* on a signed DVM, the pubkey) is handed this token back.
|
|
558
201
|
*/
|
|
559
|
-
|
|
202
|
+
requesterToken?: string;
|
|
203
|
+
requestFingerprint?: string;
|
|
204
|
+
requesterPubkey?: string;
|
|
205
|
+
requestId?: string;
|
|
206
|
+
/** HTTP path independently recorded when the caller proof was accepted. */
|
|
207
|
+
authRequestPath?: string;
|
|
208
|
+
status: "processing" | "completed" | "failed" | "awaiting-input" | "cancelled" | "working";
|
|
209
|
+
summary?: string;
|
|
210
|
+
messages: Message[];
|
|
211
|
+
seq: number;
|
|
212
|
+
paidMsats: number;
|
|
213
|
+
paymentMint?: string;
|
|
214
|
+
/** Rail of the most recent successful credit. Set on each credit branch in payment processing. */
|
|
215
|
+
paymentRail?: FundingMethod;
|
|
560
216
|
/**
|
|
561
|
-
*
|
|
562
|
-
*
|
|
217
|
+
* Settlement reference for the most-recent successful credit. Threaded
|
|
218
|
+
* into `onJobCompleted` so the platform revenue ledger can populate
|
|
219
|
+
* `revenue_events.tx_hash`. mpp/x402 carry the credential's `challenge.id`
|
|
220
|
+
* / EVM tx hash from the facilitator (internal-review); cashu accumulator carries
|
|
221
|
+
* the `X-Cashu-Request-Id` UUID (internal-review). Updated on every credit
|
|
222
|
+
* (upfront or mid-job via `processIncomingPayment`) — internal-review.
|
|
223
|
+
* Single-row-per-job accounting keeps the most recent credit's hash,
|
|
224
|
+
* mirroring `paymentRail`.
|
|
563
225
|
*/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
declare const DEFAULT_INVOICE_TTL_SECONDS = 900;
|
|
568
|
-
/**
|
|
569
|
-
* Floor on the invoice lifetime: twice the signed-request drift window, so the
|
|
570
|
-
* bolt11 always outlives the funding negotiation that produced it (the issue's
|
|
571
|
-
* "invoice expiry ≥ the funding-negotiation window"). A shorter one would
|
|
572
|
-
* expire inside the caller's own retry budget and strand them mid-top-up.
|
|
573
|
-
*/
|
|
574
|
-
declare const MIN_INVOICE_TTL_SECONDS = 600;
|
|
575
|
-
/**
|
|
576
|
-
* The builder-side Lightning receive leg (internal-review, credits spec §4).
|
|
577
|
-
*
|
|
578
|
-
* Issues a bolt11 over a **receive-only** NWC connection while the DVM is
|
|
579
|
-
* awake serving the 402, and credits the ledger when a later request observes
|
|
580
|
-
* settlement. Two properties are load-bearing:
|
|
581
|
-
*
|
|
582
|
-
* - **It never holds a send credential.** `pay_invoice` on this connection is
|
|
583
|
-
* refused at construction, so a compromised DVM can mint invoices and
|
|
584
|
-
* nothing else. The drain/refund sender (internal-review) is a separate, budgeted
|
|
585
|
-
* connection by rule.
|
|
586
|
-
* - **Crediting is pull-based — there is no settlement watcher.** The caller's
|
|
587
|
-
* next request drives `lookup_invoice`, which is what makes this work on a
|
|
588
|
-
* suspend-to-zero fleet: a machine that is asleep has nothing to miss.
|
|
589
|
-
* Wallet downtime at that moment delays crediting; it never loses money,
|
|
590
|
-
* because the invoice→credit binding is a durable row.
|
|
591
|
-
*
|
|
592
|
-
* Reachability is **cached**, never probed per request: the funding menu is
|
|
593
|
-
* assembled on every quote and every 402, so a live NIP-47 round trip there
|
|
594
|
-
* would put a nostr relay in the latency path of every priced call.
|
|
595
|
-
*/
|
|
596
|
-
declare class LightningReceive {
|
|
597
|
-
private readonly source;
|
|
598
|
-
readonly invoiceTtlSeconds: number;
|
|
599
|
-
readonly fundingMinSats: number;
|
|
600
|
-
private health;
|
|
601
|
-
private checkedAtMs;
|
|
602
|
-
private refreshing;
|
|
603
|
-
private permissionRefusal;
|
|
604
|
-
private constructor();
|
|
605
|
-
private readonly railHealth;
|
|
226
|
+
paymentTxHash?: string;
|
|
227
|
+
/** Chain transaction returned to callers recovering x402 or Tempo acceptance. */
|
|
228
|
+
paymentTransactionHash?: string;
|
|
606
229
|
/**
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* succeeds. A connection that can spend, or one that can't state what it can
|
|
614
|
-
* do, is a different thing entirely: it is a standing money risk that no
|
|
615
|
-
* amount of retrying fixes, so it fails the boot loudly with the fix in the
|
|
616
|
-
* message.
|
|
230
|
+
* Rail-native amount accumulated across all successful credits on this
|
|
231
|
+
* job (sats for mpp, USDC microunits for x402). The single
|
|
232
|
+
* `revenue_events` row written at completion uses this as `gross_native`,
|
|
233
|
+
* so multi-credit jobs sum the per-credit native amounts (internal-review).
|
|
234
|
+
* internal-review wired the upfront credit; mid-job credits accumulate same-rail
|
|
235
|
+
* and reset on a cross-rail switch (see `processIncomingPayment`).
|
|
617
236
|
*/
|
|
618
|
-
|
|
237
|
+
nativeAmount?: number;
|
|
238
|
+
/** Native asset tag — paired with `nativeAmount`. internal-review. */
|
|
239
|
+
nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
|
|
240
|
+
/** Cashu flow discriminator written into `revenue_events.metadata.cashu_flow` (internal-review). */
|
|
241
|
+
cashuFlow?: "p2pk_accumulator";
|
|
242
|
+
/** Credit the upfront payment funded/drew (internal-review). See `JobRecord.creditId`. */
|
|
243
|
+
creditId?: string;
|
|
244
|
+
/** The draw placed for this job on `creditId` (internal-review). */
|
|
245
|
+
drawId?: string;
|
|
246
|
+
/** Original funding artifact returned by an explicit fund-and-draw. */
|
|
247
|
+
fundingReceipt?: FundingReceipt;
|
|
248
|
+
fundingCredit?: CreditSnapshot;
|
|
249
|
+
receivedProofs: ProofLike[];
|
|
250
|
+
pendingPaymentMsats?: number;
|
|
619
251
|
/**
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
252
|
+
* Per-job binding for MPP credentials. Contains the `challenge.id` of every
|
|
253
|
+
* mppx challenge issued in the most-recent `requestPayment` yield. mppx HMAC
|
|
254
|
+
* binds challenges to realm/method/amount/expiry, not to the dvmkit job-id;
|
|
255
|
+
* verifying the incoming credential's `challenge.id` is in this set blocks
|
|
256
|
+
* a credential lifted from a sibling job that requested the same fields.
|
|
257
|
+
* Cleared on successful credit (internal-review).
|
|
626
258
|
*/
|
|
627
|
-
|
|
259
|
+
pendingMppChallengeIds?: string[];
|
|
628
260
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
*/
|
|
638
|
-
issue(ledger: CreditLedgerLike, args: {
|
|
639
|
-
creditId: string;
|
|
640
|
-
fundId: string;
|
|
641
|
-
callerPubkey: string;
|
|
642
|
-
currency: string;
|
|
643
|
-
amountMicro: number;
|
|
644
|
-
amountMsats: number;
|
|
645
|
-
description?: string;
|
|
646
|
-
}): Promise<CreditInvoiceRecord>;
|
|
261
|
+
* Per-job binding for x402 mid-job credentials (internal-review). 32-byte 0x-hex
|
|
262
|
+
* nonce issued by `requestPayment` and stamped onto the outbound x402
|
|
263
|
+
* envelope; the caller signs `TransferWithAuthorization` against this exact
|
|
264
|
+
* `bytes32`. Verification rejects an incoming `x402_payment` whose decoded
|
|
265
|
+
* authorization nonce differs, blocking a cross-job replay before the
|
|
266
|
+
* facilitator settles. Cleared on successful credit.
|
|
267
|
+
*/
|
|
268
|
+
pendingX402Nonce?: string;
|
|
647
269
|
/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* The operator's reconcile verb runs this before it credits anything. A
|
|
651
|
-
* `blocked` row implies payment — {@link applyOne} returns above the block
|
|
652
|
-
* classification when the lookup says unpaid — but that is *this fleet's
|
|
653
|
-
* belief, recorded possibly weeks ago*, and the verb it gates mints balance
|
|
654
|
-
* against it. One round trip turns the belief into a fact and recovers the
|
|
655
|
-
* true settlement instant for `settled_at`, which a blocked row never got to
|
|
656
|
-
* write.
|
|
657
|
-
*
|
|
658
|
-
* Unlike {@link settlePending} this **throws**: there is no request whose
|
|
659
|
-
* latency it protects, and crediting on an unverifiable wallet is exactly
|
|
660
|
-
* what it exists to prevent. `NOT_FOUND` is the one exception, and it is not
|
|
661
|
-
* an outage — it is the wallet saying it has never seen this hash, the same
|
|
662
|
-
* reading {@link applyOne} and the caller-side reconcile in `src/lib/nwc.ts`
|
|
663
|
-
* take. It comes back as `known: false` because the operator's repair for it
|
|
664
|
-
* ("you are pointed at a different wallet than the one that minted this")
|
|
665
|
-
* is not the repair for an unpaid invoice, and certainly not for a retry.
|
|
270
|
+
* Exact USDC microunit amount paired with `pendingX402Nonce`. Kept as a
|
|
271
|
+
* decimal string so the EIP-3009 uint256 value remains lossless.
|
|
666
272
|
*/
|
|
667
|
-
|
|
668
|
-
settled: boolean;
|
|
669
|
-
settledAt?: number;
|
|
670
|
-
known: boolean;
|
|
671
|
-
}>;
|
|
273
|
+
pendingX402AmountUsdcMicro?: string;
|
|
672
274
|
/**
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* The pending rows are read locally first, so the common case (nothing
|
|
677
|
-
* outstanding) costs one indexed SELECT and no network at all. Bounded to
|
|
678
|
-
* {@link MAX_SETTLE_CHECKS} invoices on a short deadline, and **never throws
|
|
679
|
-
* into the request**: a wallet failure here means the caller's balance is
|
|
680
|
-
* merely not updated yet, which the ordinary `insufficient_credit` path
|
|
681
|
-
* already states honestly.
|
|
682
|
-
*
|
|
683
|
-
* **Each invoice gets its own failure boundary.** The sweep window is a few
|
|
684
|
-
* rows wide, so a row that fails on its own terms — a `fund_id` the caller
|
|
685
|
-
* reused on another rail, a credit someone else opened first — must not take
|
|
686
|
-
* the rest of the pass down with it: the next invoice in the window may be
|
|
687
|
-
* the paid one, and it would never be looked up. Only a transport failure
|
|
688
|
-
* ends the pass early, because there the wallet itself is gone and the
|
|
689
|
-
* remaining lookups would just spend the caller's latency confirming it.
|
|
275
|
+
* Fiat micro-units the outstanding `requestPayment` ask is worth, pinned when
|
|
276
|
+
* the payment-request was emitted (internal-review). See `JobRecord`.
|
|
690
277
|
*/
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
creditTtlMs: number;
|
|
695
|
-
dvmId?: string;
|
|
696
|
-
enqueueCreditDeposit?: CreditDepositEnqueue;
|
|
697
|
-
nowMs?: number;
|
|
698
|
-
}): Promise<InvoiceSettlement[]>;
|
|
278
|
+
pendingPaymentFiatMicro?: number;
|
|
279
|
+
/** Currency of {@link pendingPaymentFiatMicro} (lowercase ISO-4217). */
|
|
280
|
+
pendingPaymentFiatCurrency?: string;
|
|
699
281
|
/**
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
* Throws whatever the wallet or the ledger threw — classifying that is
|
|
704
|
-
* {@link retireOrLog}'s job, so this stays one invoice's happy path.
|
|
282
|
+
* Cumulative fiat micro this job has asked for mid-job, the ceiling on its
|
|
283
|
+
* draw growth (internal-review). See `JobRecord.askedTopUpMicro` for the sentinel.
|
|
705
284
|
*/
|
|
706
|
-
|
|
285
|
+
askedTopUpMicro?: number;
|
|
707
286
|
/**
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
* The split that matters is permanent-versus-transient, because it decides
|
|
712
|
-
* whether the row keeps a slot in the sweep window. A transient failure — the
|
|
713
|
-
* database blinked, the wallet answered oddly — leaves it `pending` and the
|
|
714
|
-
* caller's next request retries it. A **permanent** one means the ledger will
|
|
715
|
-
* refuse this invoice identically forever, so leaving it pending costs a
|
|
716
|
-
* `lookup_invoice` on every subsequent request and, at
|
|
717
|
-
* {@link MAX_SETTLE_CHECKS} of them, fills the window so a genuinely payable
|
|
718
|
-
* invoice behind them is never even looked up. Those get `blocked`, which
|
|
719
|
-
* drops them out of the sweep and hands the operator the payment hash.
|
|
287
|
+
* Currency of {@link askedTopUpMicro} — and this job's sticky ask
|
|
288
|
+
* denomination (internal-review). See `JobRecord.askedTopUpCurrency`.
|
|
720
289
|
*/
|
|
721
|
-
|
|
290
|
+
askedTopUpCurrency?: string;
|
|
722
291
|
/**
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
*
|
|
292
|
+
* Why this job's draw growth is not capped, when it isn't (internal-review). Mirror
|
|
293
|
+
* of `JobRecord.topUpCapUnenforcedReason`.
|
|
294
|
+
*/
|
|
295
|
+
topUpCapUnenforcedReason?: TopUpCapUnenforcedReason;
|
|
296
|
+
/** Price the job was charged at (msats). Used to detect overpayment for change/melt gating. */
|
|
297
|
+
requiredMsats?: number;
|
|
298
|
+
/**
|
|
299
|
+
* The signed receipt for this job's terminal outcome (internal-review). Mirror of
|
|
300
|
+
* `JobRecord.receipt`, set once the terminal funnel has signed and persisted
|
|
301
|
+
* it so the local read paths (which serve from `activeJobs` during the
|
|
302
|
+
* cleanup window) hand back the same bytes as a cross-machine re-read.
|
|
303
|
+
*/
|
|
304
|
+
receipt?: JobReceipt;
|
|
305
|
+
/**
|
|
306
|
+
* The in-flight terminal chain — persist, then sign + store the receipt
|
|
307
|
+
* (internal-review). Assigned synchronously by `ctx.complete()` / `ctx.fail()` at
|
|
308
|
+
* the instant the job goes terminal, so the `POST /v1/job` fast path can
|
|
309
|
+
* await it (bounded) and put the receipt on the synchronous 200 rather than
|
|
310
|
+
* making the caller poll for it. Never rejects — the chain swallows its own
|
|
311
|
+
* failures.
|
|
312
|
+
*/
|
|
313
|
+
terminalWork?: Promise<void>;
|
|
314
|
+
listeners: Set<(msg: Message) => void>;
|
|
315
|
+
/**
|
|
316
|
+
* Cancellation signal for the running handler (internal-review). Aborted whenever
|
|
317
|
+
* the job is cancelled on this process — caller cancel, idle timeout,
|
|
318
|
+
* stale-sweep teardown, supersession. Surfaced to builders as `ctx.signal`
|
|
319
|
+
* and auto-threaded into `ctx.fetch`, so an in-flight provider call
|
|
320
|
+
* (ElevenLabs, Scrapfly, …) is torn down instead of billing the caller for
|
|
321
|
+
* work they just cancelled. Per-process runtime state like `listeners` —
|
|
322
|
+
* never persisted, and re-created fresh on reactivation.
|
|
323
|
+
*/
|
|
324
|
+
abort: AbortController;
|
|
325
|
+
/**
|
|
326
|
+
* Hook for cross-machine message persistence (Tx A, internal-review). Set by
|
|
327
|
+
* `JobManager` when the store is streamable. The appender runs the
|
|
328
|
+
* outgoing-message tx atomically with any payment-request counter bump so
|
|
329
|
+
* `(message, pending_payment_msats)` land together — never separately.
|
|
731
330
|
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
* is **caller-chosen**: reuse it on cashu or x402 after this bolt11 was
|
|
736
|
-
* minted and the row that collided is a different payment entirely, so the
|
|
737
|
-
* sats this invoice received have nowhere to land.
|
|
331
|
+
* Per-job serialised (internal-review): each call chains onto `messageAppenderTail`
|
|
332
|
+
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
333
|
+
* `complete`) can't race for the DB-side seq allocation.
|
|
738
334
|
*/
|
|
739
|
-
|
|
740
|
-
private withBackend;
|
|
741
|
-
/** Record activity without letting it promote an unvalidated connection. */
|
|
742
|
-
private noteOperationOk;
|
|
743
|
-
private notePermissionsOk;
|
|
744
|
-
private notePermissionRefused;
|
|
335
|
+
messageAppender?: (msg: Message, opts?: AppendOutgoingOptions) => void;
|
|
745
336
|
/**
|
|
746
|
-
*
|
|
747
|
-
*
|
|
337
|
+
* Tail of the per-job appender chain (internal-review). `wireMessageAppender` updates
|
|
338
|
+
* this on every call so `persistJob` can await it before the terminal snapshot
|
|
339
|
+
* save runs `DELETE FROM job_messages`.
|
|
748
340
|
*/
|
|
749
|
-
|
|
750
|
-
|
|
341
|
+
messageAppenderTail?: Promise<unknown>;
|
|
342
|
+
sdkCtx?: SDKJobContext<unknown, unknown>;
|
|
343
|
+
stepCache: StepCache;
|
|
344
|
+
state: unknown;
|
|
345
|
+
pendingPrompts: Map<string, PromptResolver>;
|
|
346
|
+
pendingPayment: PaymentResolver | null;
|
|
347
|
+
/** When not null, indicates replay mode — prompt/payment scan message history for cached responses. */
|
|
348
|
+
replayHighSeq: number | null;
|
|
349
|
+
/** Number of provider messages to suppress during replay. Cleared on handler error. */
|
|
350
|
+
replayProviderSkip: number;
|
|
351
|
+
/** Index of the next payment to match during replay (for multi-payment handlers). */
|
|
352
|
+
replayPaymentIndex: number;
|
|
353
|
+
/**
|
|
354
|
+
* Set once Tx C applies the current payment ask during this reactivation.
|
|
355
|
+
* Runtime-only: replayed prompts may mutate `status`, but never clear this
|
|
356
|
+
* marker before the matching payment is consumed.
|
|
357
|
+
*/
|
|
358
|
+
reactivationPaymentApplied?: true;
|
|
359
|
+
/** Unix ms timestamp of job creation. */
|
|
360
|
+
createdAt: number;
|
|
361
|
+
/** Unix ms timestamp of last activity (message, yield, terminal). */
|
|
362
|
+
lastActivityAt: number;
|
|
363
|
+
}
|
|
364
|
+
/** Extract the persistent fields from a ServerJob into a JobRecord. */
|
|
365
|
+
declare function toJobRecord(job: ServerJob): JobRecord;
|
|
366
|
+
/** Reconstitute a ServerJob from a persisted JobRecord with fresh runtime fields. */
|
|
367
|
+
declare function fromJobRecord(record: JobRecord): ServerJob;
|
|
368
|
+
/**
|
|
369
|
+
* Signal cancellation to a running handler (internal-review). Idempotent — aborting an
|
|
370
|
+
* already-aborted controller is a no-op, so the overlapping teardown paths
|
|
371
|
+
* (caller cancel then stale sweep, say) can each call it unconditionally.
|
|
372
|
+
*/
|
|
373
|
+
declare function abortJob(job: ServerJob, reason: string): void;
|
|
374
|
+
/**
|
|
375
|
+
* Reason carried on `ctx.signal.reason` and thrown by post-terminal `ctx.prompt`
|
|
376
|
+
* / `ctx.requestPayment` calls. Builders can `instanceof`-check it to tell a
|
|
377
|
+
* cancellation apart from a provider-side abort.
|
|
378
|
+
*/
|
|
379
|
+
declare class JobCancelledError extends Error {
|
|
380
|
+
constructor(message?: string);
|
|
751
381
|
}
|
|
382
|
+
/** Append a provider-sent message to the job and notify SSE listeners. */
|
|
383
|
+
declare function providerMessage(job: ServerJob, type: MessageType, content: Record<string, unknown> | object): void;
|
|
384
|
+
/** Check if a job status is terminal (completed, failed, cancelled). */
|
|
385
|
+
declare function isTerminal(status: ServerJob["status"]): boolean;
|
|
386
|
+
/** Check if a message is a yield point (ends the provider's turn). */
|
|
387
|
+
declare function isYieldMessage(msg: Message): boolean;
|
|
752
388
|
|
|
753
389
|
/**
|
|
754
390
|
* Per-DVM runtime mint-health tracker (internal-review). Alongside the platform's
|
|
@@ -894,419 +530,38 @@ declare class MintHealthTracker {
|
|
|
894
530
|
}
|
|
895
531
|
|
|
896
532
|
/**
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
533
|
+
* Credit fields riding the signed request body (internal-review, spec §2).
|
|
534
|
+
*
|
|
535
|
+
* An explicit draw references the credit INSIDE the secp256k1/Schnorr-signed
|
|
536
|
+
* `body.data` — no new caller crypto. Wire field names (snake_case, matching
|
|
537
|
+
* the rest of the wire surface):
|
|
538
|
+
*
|
|
539
|
+
* - `credit_id` — the credit to draw against.
|
|
540
|
+
* - `draw_id` — client-generated draw id; the ledger's idempotency key.
|
|
541
|
+
* - `fund` — optional `{ amount_micro, commitment }` funding commitment:
|
|
542
|
+
* present iff a funding artifact (X-Cashu / X-PAYMENT / mpp credential)
|
|
543
|
+
* rides the same request. `commitment` is the SHA-256 hex over the raw
|
|
544
|
+
* artifact bytes, binding the header-borne proof — which sits OUTSIDE the
|
|
545
|
+
* signed body — to the credit its sender intended (spec §2 condition 3).
|
|
546
|
+
*
|
|
547
|
+
* These are envelope-level fields, not capability input: **the SDK** removes
|
|
548
|
+
* them before any builder-owned schema parses ({@link stripCreditEnvelope} in
|
|
549
|
+
* `JobManager.parseInput` and on the quote path; {@link
|
|
550
|
+
* creditEnvelopeIgnoreFields} feeding the auth verifier's own schema check), so
|
|
551
|
+
* handlers never see them and a top-level `.strict()` capability schema is
|
|
552
|
+
* fine (internal-review). Relying on Zod's default strip mode instead was the trap:
|
|
553
|
+
* `.strict()` rejects unknown keys, so an explicit draw 400'd at parse before
|
|
554
|
+
* the envelope was ever extracted.
|
|
555
|
+
*
|
|
556
|
+
* The strip never touches the canonical signing bytes. Every auth call site
|
|
557
|
+
* verifies the raw wire body (`signedRequestInput`, internal-review), so the credit
|
|
558
|
+
* fields are in the verified bytes because they were never taken out of them —
|
|
559
|
+
* the strip is only ever applied to what a schema is about to parse.
|
|
900
560
|
*/
|
|
901
|
-
interface
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
type: "builder" | "org";
|
|
906
|
-
}
|
|
907
|
-
/** Optional builder identity surfaced on `/v1/info` (forward-compatible stub). */
|
|
908
|
-
interface BuilderIdentity {
|
|
909
|
-
id?: string;
|
|
910
|
-
name?: string;
|
|
911
|
-
url?: string;
|
|
912
|
-
/**
|
|
913
|
-
* Builder identity x-only secp256k1 pubkey (internal-review). When populated, the
|
|
914
|
-
* `/v1/info#builder` block also carries `attestation` + `signature` so
|
|
915
|
-
* consumers can verify the deploy was signed by the holder of this key.
|
|
916
|
-
* Populated at deploy time from `DVMKIT_BUILDER_PUBKEY` (Fly secret); the
|
|
917
|
-
* SDK never re-signs at runtime.
|
|
918
|
-
*/
|
|
919
|
-
pubkey?: string;
|
|
920
|
-
/** Canonical deploy-time attestation payload (internal-review). Served verbatim. */
|
|
921
|
-
attestation?: AttestationPayload;
|
|
922
|
-
/** Schnorr signature over `canonicaliseForSigning(attestation)` (internal-review). */
|
|
923
|
-
signature?: string;
|
|
924
|
-
}
|
|
925
|
-
/** Platform reporter overrides — the host falls back to env when omitted. */
|
|
926
|
-
interface PlatformReporterOpts {
|
|
927
|
-
/** Bearer token. Defaults to `DVMKIT_PLATFORM_TOKEN` env. */
|
|
928
|
-
token?: string;
|
|
929
|
-
/** Platform internal URL. Defaults to `DVMKIT_PLATFORM_URL` env. */
|
|
930
|
-
url?: string;
|
|
931
|
-
}
|
|
932
|
-
/** Options for {@link createDVMHost}. */
|
|
933
|
-
interface DVMHostOpts {
|
|
934
|
-
/**
|
|
935
|
-
* Postgres connection string. Defaults to `DATABASE_URL` env.
|
|
936
|
-
* Required at runtime (internal-review). Boot fails when missing unless `jobStore`
|
|
937
|
-
* is explicitly supplied or `devMode` is true (test/dev escape hatches).
|
|
938
|
-
*/
|
|
939
|
-
database?: string;
|
|
940
|
-
/** Listen port. Defaults to `PORT` env or 8080. */
|
|
941
|
-
port?: number;
|
|
942
|
-
/** Environment variables. Defaults to `process.env`. */
|
|
943
|
-
env?: Record<string, string>;
|
|
944
|
-
/** Builder identity for `/v1/info` (forward-compatible). */
|
|
945
|
-
builder?: BuilderIdentity;
|
|
946
|
-
/** Owner display identity for `/v1/info#owner` (internal-review). Falls back to env vars. */
|
|
947
|
-
owner?: OwnerDisplay;
|
|
948
|
-
/** Platform reporter overrides. Defaults to env-derived values. */
|
|
949
|
-
platformReporter?: PlatformReporterOpts;
|
|
950
|
-
/** Override the SDK's fx fetcher. Defaults to env-configured CoinGecko. */
|
|
951
|
-
fx?: FxFetcher;
|
|
952
|
-
/**
|
|
953
|
-
* Custom `/health` handler. When set, the SDK installs this instead of the
|
|
954
|
-
* default `{ status: "ok" }` responder — useful for runtime-specific health
|
|
955
|
-
* (pool depth, draining state, 503 while draining).
|
|
956
|
-
*/
|
|
957
|
-
healthHandler?: (c: Context) => Response | Promise<Response>;
|
|
958
|
-
/** KV store override. Defaults to PostgresKVStore (or MemoryKVStore in dev). */
|
|
959
|
-
store?: KVStore;
|
|
960
|
-
/** JobStore override. Takes precedence over `database`. */
|
|
961
|
-
jobStore?: JobStore;
|
|
962
|
-
/**
|
|
963
|
-
* Existing Postgres pool to reuse instead of opening one from `database`
|
|
964
|
-
* (internal-review test seam). When set, the host builds its JobStore / KVStore /
|
|
965
|
-
* cashu accumulator on this pool and leaves it open at shutdown — the caller
|
|
966
|
-
* owns it. Lets the e2e harness run DVMs in `p2pk-accumulator` mode against
|
|
967
|
-
* its single shared pool rather than spawning a pool per DVM.
|
|
968
|
-
*/
|
|
969
|
-
pool?: Pool;
|
|
970
|
-
/** Cashu mints accepted. Defaults to env `DVMKIT_CASHU_MINTS`. */
|
|
971
|
-
mints?: string[];
|
|
972
|
-
/** Cashu receive mode override. */
|
|
973
|
-
cashuMode?: CashuMode;
|
|
974
|
-
/** MPP handle override. Defaults to env-resolved via `createMppFromOpts`. */
|
|
975
|
-
mpp?: MppxServer;
|
|
976
|
-
/** x402 stablecoin payment configuration. Defaults to env-resolved. */
|
|
977
|
-
x402?: X402Config;
|
|
978
|
-
/** Payment methods override. Defaults to derived from configured rails. */
|
|
979
|
-
paymentMethods?: PaymentMethod[];
|
|
980
|
-
/**
|
|
981
|
-
* Lightning receive leg for credit funding (internal-review). Defaults to the
|
|
982
|
-
* env-resolved `DVMKIT_NWC_RECEIVE_URI` /
|
|
983
|
-
* `DVMKIT_NWC_RECEIVE_INVOICE_TTL_SECONDS`. Pass `backend` to inject a
|
|
984
|
-
* wallet directly — a test seam that skips the connect-time probe, so
|
|
985
|
-
* production must always come through the URI.
|
|
986
|
-
*/
|
|
987
|
-
lightningReceive?: LightningReceiveConfig;
|
|
988
|
-
/** When true, payment is skipped if no mints are configured (dev/test). */
|
|
989
|
-
devMode?: boolean;
|
|
990
|
-
/** Consumed-credential store override (test seam). */
|
|
991
|
-
consumedCredentialStore?: ConsumedCredentialStore;
|
|
992
|
-
/** Mint-health tracker override (test seam). */
|
|
993
|
-
mintHealthTracker?: MintHealthTracker;
|
|
994
|
-
/**
|
|
995
|
-
* Wrap the SDK compatibility gate before it is exposed to descriptor custom
|
|
996
|
-
* routes. Platform hosts use this to enforce their independently released
|
|
997
|
-
* caller rollout policy; self-hosted SDK consumers receive the default gate.
|
|
998
|
-
*/
|
|
999
|
-
wrapRouteClientCompatibility?: (sdkGate: ClientCompatibilityGate) => ClientCompatibilityGate;
|
|
1000
|
-
}
|
|
1001
|
-
/** Per-mount options. Currently only `prefix` is supported. */
|
|
1002
|
-
interface MountOpts {
|
|
1003
|
-
/** Path prefix for this DVM's protocol routes (e.g. `/delete-feed`). */
|
|
1004
|
-
prefix?: string;
|
|
1005
|
-
}
|
|
1006
|
-
/** Live DVM host. Single wiring locus for the SDK (internal-review). */
|
|
1007
|
-
interface DVMHost {
|
|
1008
|
-
/**
|
|
1009
|
-
* Underlying Hono app for custom non-protocol routes that belong to the
|
|
1010
|
-
* host (cross-DVM `/admin`, cron callbacks, etc.). For DVM-scoped routes,
|
|
1011
|
-
* declare `routes(app)` on the descriptor.
|
|
1012
|
-
*/
|
|
1013
|
-
readonly app: Hono;
|
|
1014
|
-
/**
|
|
1015
|
-
* Resolved Postgres pool — undefined before `host.serve()` completes
|
|
1016
|
-
* database resolution, set thereafter. Exposed so DVM-local Postgres
|
|
1017
|
-
* consumers (e.g. scrape's `ScrapeDb` per-fetch event store, internal-review)
|
|
1018
|
-
* can reuse the host's pool rather than constructing their own. Wire
|
|
1019
|
-
* inside descriptor `onBoot` (fires after pool init, before listener opens).
|
|
1020
|
-
*/
|
|
1021
|
-
readonly pool: Pool | undefined;
|
|
1022
|
-
/**
|
|
1023
|
-
* Money-safe credit ledger (internal-review) — undefined before `host.serve()`
|
|
1024
|
-
* resolves stores, set thereafter. Postgres-backed when a pool exists;
|
|
1025
|
-
* pool-less hosts get a shared in-memory ledger so the internal-review fund+draw
|
|
1026
|
-
* semantics hold everywhere. Same wiring window as `pool` (descriptor
|
|
1027
|
-
* `onBoot` fires after init, before the listener opens).
|
|
1028
|
-
*/
|
|
1029
|
-
readonly creditLedger: CreditLedgerLike | undefined;
|
|
1030
|
-
/**
|
|
1031
|
-
* Add a DVM to this host. Multiple mounts compose at distinct prefixes;
|
|
1032
|
-
* a single mount with no prefix attaches at root.
|
|
1033
|
-
*/
|
|
1034
|
-
mount<S, I extends ZodLike | undefined>(descriptor: DVMDescriptor<S, I>, opts?: MountOpts): void;
|
|
1035
|
-
/** Start listening. Returns the live server handle. */
|
|
1036
|
-
serve(opts?: {
|
|
1037
|
-
port?: number;
|
|
1038
|
-
}): Promise<{
|
|
1039
|
-
url: string;
|
|
1040
|
-
close: () => Promise<void>;
|
|
1041
|
-
}>;
|
|
1042
|
-
/** Graceful shutdown. Runs descriptor `onShutdown` hooks in reverse mount order. */
|
|
1043
|
-
shutdown(): Promise<void>;
|
|
1044
|
-
}
|
|
1045
|
-
/**
|
|
1046
|
-
* Single wiring locus for SDK-side construction (internal-review). Replaces both the
|
|
1047
|
-
* `serve()` wrapper and the `createDVMServer` direct path. Reads env once,
|
|
1048
|
-
* resolves payment rails, owns the Postgres pool, and mounts each DVM
|
|
1049
|
-
* descriptor as a Hono sub-app on `host.app`.
|
|
1050
|
-
*/
|
|
1051
|
-
declare function createDVMHost(opts?: DVMHostOpts): DVMHost;
|
|
1052
|
-
|
|
1053
|
-
/** Promise resolver for a pending client prompt response. */
|
|
1054
|
-
interface PromptResolver {
|
|
1055
|
-
resolve: (value: ResponseContent) => void;
|
|
1056
|
-
reject: (reason: Error) => void;
|
|
1057
|
-
}
|
|
1058
|
-
/** Promise resolver for a pending client payment. */
|
|
1059
|
-
interface PaymentResolver {
|
|
1060
|
-
resolve: (value: PaymentContent) => void;
|
|
1061
|
-
reject: (reason: Error) => void;
|
|
1062
|
-
}
|
|
1063
|
-
/** In-memory representation of a running job. */
|
|
1064
|
-
interface ServerJob {
|
|
1065
|
-
id: string;
|
|
1066
|
-
tags: string[];
|
|
1067
|
-
/**
|
|
1068
|
-
* Capability name this job belongs to (internal-review). Set at submission from
|
|
1069
|
-
* `POST /v1/job` body's `capability` field; routes the SDK runtime to the
|
|
1070
|
-
* right `onJob` / `onResponse` / `onPayment` handler and the right input
|
|
1071
|
-
* schema. Required — the wire layer rejects requests that omit it.
|
|
1072
|
-
*/
|
|
1073
|
-
capability: string;
|
|
1074
|
-
input: string;
|
|
1075
|
-
params: Record<string, string>;
|
|
1076
|
-
requesterId: string;
|
|
1077
|
-
/**
|
|
1078
|
-
* SHA-256 hex of the per-job opaque `job_token` minted at creation for
|
|
1079
|
-
* anonymous callers (internal-review). Mirror of `JobRecord.requesterTokenHash`;
|
|
1080
|
-
* see that field for the wire-level gating semantics.
|
|
1081
|
-
*/
|
|
1082
|
-
requesterTokenHash?: string;
|
|
1083
|
-
/**
|
|
1084
|
-
* Submission provenance (internal-review) — mirrors `JobRecord.requesterToken` /
|
|
1085
|
-
* `requestFingerprint` / `requesterPubkey`. Carried on the job purely so
|
|
1086
|
-
* `toJobRecord` persists it; nothing in the runtime reads it. It's the
|
|
1087
|
-
* replay gate: a retried paid submit that reproduces the fingerprint (and,
|
|
1088
|
-
* on a signed DVM, the pubkey) is handed this token back.
|
|
1089
|
-
*/
|
|
1090
|
-
requesterToken?: string;
|
|
1091
|
-
requestFingerprint?: string;
|
|
1092
|
-
requesterPubkey?: string;
|
|
1093
|
-
requestId?: string;
|
|
1094
|
-
/** HTTP path independently recorded when the caller proof was accepted. */
|
|
1095
|
-
authRequestPath?: string;
|
|
1096
|
-
status: "processing" | "completed" | "failed" | "awaiting-input" | "cancelled" | "working";
|
|
1097
|
-
summary?: string;
|
|
1098
|
-
messages: Message[];
|
|
1099
|
-
seq: number;
|
|
1100
|
-
paidMsats: number;
|
|
1101
|
-
paymentMint?: string;
|
|
1102
|
-
/** Rail of the most recent successful credit. Set on each credit branch in payment processing. */
|
|
1103
|
-
paymentRail?: FundingMethod;
|
|
1104
|
-
/**
|
|
1105
|
-
* Settlement reference for the most-recent successful credit. Threaded
|
|
1106
|
-
* into `onJobCompleted` so the platform revenue ledger can populate
|
|
1107
|
-
* `revenue_events.tx_hash`. mpp/x402 carry the credential's `challenge.id`
|
|
1108
|
-
* / EVM tx hash from the facilitator (internal-review); cashu accumulator carries
|
|
1109
|
-
* the `X-Cashu-Request-Id` UUID (internal-review). Updated on every credit
|
|
1110
|
-
* (upfront or mid-job via `processIncomingPayment`) — internal-review.
|
|
1111
|
-
* Single-row-per-job accounting keeps the most recent credit's hash,
|
|
1112
|
-
* mirroring `paymentRail`.
|
|
1113
|
-
*/
|
|
1114
|
-
paymentTxHash?: string;
|
|
1115
|
-
/** Chain transaction returned to callers recovering x402 or Tempo acceptance. */
|
|
1116
|
-
paymentTransactionHash?: string;
|
|
1117
|
-
/**
|
|
1118
|
-
* Rail-native amount accumulated across all successful credits on this
|
|
1119
|
-
* job (sats for mpp, USDC microunits for x402). The single
|
|
1120
|
-
* `revenue_events` row written at completion uses this as `gross_native`,
|
|
1121
|
-
* so multi-credit jobs sum the per-credit native amounts (internal-review).
|
|
1122
|
-
* internal-review wired the upfront credit; mid-job credits accumulate same-rail
|
|
1123
|
-
* and reset on a cross-rail switch (see `processIncomingPayment`).
|
|
1124
|
-
*/
|
|
1125
|
-
nativeAmount?: number;
|
|
1126
|
-
/** Native asset tag — paired with `nativeAmount`. internal-review. */
|
|
1127
|
-
nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
|
|
1128
|
-
/** Cashu flow discriminator written into `revenue_events.metadata.cashu_flow` (internal-review). */
|
|
1129
|
-
cashuFlow?: "p2pk_accumulator";
|
|
1130
|
-
/** Credit the upfront payment funded/drew (internal-review). See `JobRecord.creditId`. */
|
|
1131
|
-
creditId?: string;
|
|
1132
|
-
/** The draw placed for this job on `creditId` (internal-review). */
|
|
1133
|
-
drawId?: string;
|
|
1134
|
-
/** Original funding artifact returned by an explicit fund-and-draw. */
|
|
1135
|
-
fundingReceipt?: FundingReceipt;
|
|
1136
|
-
fundingCredit?: CreditSnapshot;
|
|
1137
|
-
receivedProofs: ProofLike[];
|
|
1138
|
-
pendingPaymentMsats?: number;
|
|
1139
|
-
/**
|
|
1140
|
-
* Per-job binding for MPP credentials. Contains the `challenge.id` of every
|
|
1141
|
-
* mppx challenge issued in the most-recent `requestPayment` yield. mppx HMAC
|
|
1142
|
-
* binds challenges to realm/method/amount/expiry, not to the dvmkit job-id;
|
|
1143
|
-
* verifying the incoming credential's `challenge.id` is in this set blocks
|
|
1144
|
-
* a credential lifted from a sibling job that requested the same fields.
|
|
1145
|
-
* Cleared on successful credit (internal-review).
|
|
1146
|
-
*/
|
|
1147
|
-
pendingMppChallengeIds?: string[];
|
|
1148
|
-
/**
|
|
1149
|
-
* Per-job binding for x402 mid-job credentials (internal-review). 32-byte 0x-hex
|
|
1150
|
-
* nonce issued by `requestPayment` and stamped onto the outbound x402
|
|
1151
|
-
* envelope; the caller signs `TransferWithAuthorization` against this exact
|
|
1152
|
-
* `bytes32`. Verification rejects an incoming `x402_payment` whose decoded
|
|
1153
|
-
* authorization nonce differs, blocking a cross-job replay before the
|
|
1154
|
-
* facilitator settles. Cleared on successful credit.
|
|
1155
|
-
*/
|
|
1156
|
-
pendingX402Nonce?: string;
|
|
1157
|
-
/**
|
|
1158
|
-
* Exact USDC microunit amount paired with `pendingX402Nonce`. Kept as a
|
|
1159
|
-
* decimal string so the EIP-3009 uint256 value remains lossless.
|
|
1160
|
-
*/
|
|
1161
|
-
pendingX402AmountUsdcMicro?: string;
|
|
1162
|
-
/**
|
|
1163
|
-
* Fiat micro-units the outstanding `requestPayment` ask is worth, pinned when
|
|
1164
|
-
* the payment-request was emitted (internal-review). See `JobRecord`.
|
|
1165
|
-
*/
|
|
1166
|
-
pendingPaymentFiatMicro?: number;
|
|
1167
|
-
/** Currency of {@link pendingPaymentFiatMicro} (lowercase ISO-4217). */
|
|
1168
|
-
pendingPaymentFiatCurrency?: string;
|
|
1169
|
-
/**
|
|
1170
|
-
* Cumulative fiat micro this job has asked for mid-job, the ceiling on its
|
|
1171
|
-
* draw growth (internal-review). See `JobRecord.askedTopUpMicro` for the sentinel.
|
|
1172
|
-
*/
|
|
1173
|
-
askedTopUpMicro?: number;
|
|
1174
|
-
/**
|
|
1175
|
-
* Currency of {@link askedTopUpMicro} — and this job's sticky ask
|
|
1176
|
-
* denomination (internal-review). See `JobRecord.askedTopUpCurrency`.
|
|
1177
|
-
*/
|
|
1178
|
-
askedTopUpCurrency?: string;
|
|
1179
|
-
/**
|
|
1180
|
-
* Why this job's draw growth is not capped, when it isn't (internal-review). Mirror
|
|
1181
|
-
* of `JobRecord.topUpCapUnenforcedReason`.
|
|
1182
|
-
*/
|
|
1183
|
-
topUpCapUnenforcedReason?: TopUpCapUnenforcedReason;
|
|
1184
|
-
/** Price the job was charged at (msats). Used to detect overpayment for change/melt gating. */
|
|
1185
|
-
requiredMsats?: number;
|
|
1186
|
-
/**
|
|
1187
|
-
* The signed receipt for this job's terminal outcome (internal-review). Mirror of
|
|
1188
|
-
* `JobRecord.receipt`, set once the terminal funnel has signed and persisted
|
|
1189
|
-
* it so the local read paths (which serve from `activeJobs` during the
|
|
1190
|
-
* cleanup window) hand back the same bytes as a cross-machine re-read.
|
|
1191
|
-
*/
|
|
1192
|
-
receipt?: JobReceipt;
|
|
1193
|
-
/**
|
|
1194
|
-
* The in-flight terminal chain — persist, then sign + store the receipt
|
|
1195
|
-
* (internal-review). Assigned synchronously by `ctx.complete()` / `ctx.fail()` at
|
|
1196
|
-
* the instant the job goes terminal, so the `POST /v1/job` fast path can
|
|
1197
|
-
* await it (bounded) and put the receipt on the synchronous 200 rather than
|
|
1198
|
-
* making the caller poll for it. Never rejects — the chain swallows its own
|
|
1199
|
-
* failures.
|
|
1200
|
-
*/
|
|
1201
|
-
terminalWork?: Promise<void>;
|
|
1202
|
-
listeners: Set<(msg: Message) => void>;
|
|
1203
|
-
/**
|
|
1204
|
-
* Cancellation signal for the running handler (internal-review). Aborted whenever
|
|
1205
|
-
* the job is cancelled on this process — caller cancel, idle timeout,
|
|
1206
|
-
* stale-sweep teardown, supersession. Surfaced to builders as `ctx.signal`
|
|
1207
|
-
* and auto-threaded into `ctx.fetch`, so an in-flight provider call
|
|
1208
|
-
* (ElevenLabs, Scrapfly, …) is torn down instead of billing the caller for
|
|
1209
|
-
* work they just cancelled. Per-process runtime state like `listeners` —
|
|
1210
|
-
* never persisted, and re-created fresh on reactivation.
|
|
1211
|
-
*/
|
|
1212
|
-
abort: AbortController;
|
|
1213
|
-
/**
|
|
1214
|
-
* Hook for cross-machine message persistence (Tx A, internal-review). Set by
|
|
1215
|
-
* `JobManager` when the store is streamable. The appender runs the
|
|
1216
|
-
* outgoing-message tx atomically with any payment-request counter bump so
|
|
1217
|
-
* `(message, pending_payment_msats)` land together — never separately.
|
|
1218
|
-
*
|
|
1219
|
-
* Per-job serialised (internal-review): each call chains onto `messageAppenderTail`
|
|
1220
|
-
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
1221
|
-
* `complete`) can't race for the DB-side seq allocation.
|
|
1222
|
-
*/
|
|
1223
|
-
messageAppender?: (msg: Message, opts?: AppendOutgoingOptions) => void;
|
|
1224
|
-
/**
|
|
1225
|
-
* Tail of the per-job appender chain (internal-review). `wireMessageAppender` updates
|
|
1226
|
-
* this on every call so `persistJob` can await it before the terminal snapshot
|
|
1227
|
-
* save runs `DELETE FROM job_messages`.
|
|
1228
|
-
*/
|
|
1229
|
-
messageAppenderTail?: Promise<unknown>;
|
|
1230
|
-
sdkCtx?: SDKJobContext<unknown, unknown>;
|
|
1231
|
-
stepCache: StepCache;
|
|
1232
|
-
state: unknown;
|
|
1233
|
-
pendingPrompts: Map<string, PromptResolver>;
|
|
1234
|
-
pendingPayment: PaymentResolver | null;
|
|
1235
|
-
/** When not null, indicates replay mode — prompt/payment scan message history for cached responses. */
|
|
1236
|
-
replayHighSeq: number | null;
|
|
1237
|
-
/** Number of provider messages to suppress during replay. Cleared on handler error. */
|
|
1238
|
-
replayProviderSkip: number;
|
|
1239
|
-
/** Index of the next payment to match during replay (for multi-payment handlers). */
|
|
1240
|
-
replayPaymentIndex: number;
|
|
1241
|
-
/**
|
|
1242
|
-
* Set once Tx C applies the current payment ask during this reactivation.
|
|
1243
|
-
* Runtime-only: replayed prompts may mutate `status`, but never clear this
|
|
1244
|
-
* marker before the matching payment is consumed.
|
|
1245
|
-
*/
|
|
1246
|
-
reactivationPaymentApplied?: true;
|
|
1247
|
-
/** Unix ms timestamp of job creation. */
|
|
1248
|
-
createdAt: number;
|
|
1249
|
-
/** Unix ms timestamp of last activity (message, yield, terminal). */
|
|
1250
|
-
lastActivityAt: number;
|
|
1251
|
-
}
|
|
1252
|
-
/** Extract the persistent fields from a ServerJob into a JobRecord. */
|
|
1253
|
-
declare function toJobRecord(job: ServerJob): JobRecord;
|
|
1254
|
-
/** Reconstitute a ServerJob from a persisted JobRecord with fresh runtime fields. */
|
|
1255
|
-
declare function fromJobRecord(record: JobRecord): ServerJob;
|
|
1256
|
-
/**
|
|
1257
|
-
* Signal cancellation to a running handler (internal-review). Idempotent — aborting an
|
|
1258
|
-
* already-aborted controller is a no-op, so the overlapping teardown paths
|
|
1259
|
-
* (caller cancel then stale sweep, say) can each call it unconditionally.
|
|
1260
|
-
*/
|
|
1261
|
-
declare function abortJob(job: ServerJob, reason: string): void;
|
|
1262
|
-
/**
|
|
1263
|
-
* Reason carried on `ctx.signal.reason` and thrown by post-terminal `ctx.prompt`
|
|
1264
|
-
* / `ctx.requestPayment` calls. Builders can `instanceof`-check it to tell a
|
|
1265
|
-
* cancellation apart from a provider-side abort.
|
|
1266
|
-
*/
|
|
1267
|
-
declare class JobCancelledError extends Error {
|
|
1268
|
-
constructor(message?: string);
|
|
1269
|
-
}
|
|
1270
|
-
/** Append a provider-sent message to the job and notify SSE listeners. */
|
|
1271
|
-
declare function providerMessage(job: ServerJob, type: MessageType, content: Record<string, unknown> | object): void;
|
|
1272
|
-
/** Check if a job status is terminal (completed, failed, cancelled). */
|
|
1273
|
-
declare function isTerminal(status: ServerJob["status"]): boolean;
|
|
1274
|
-
/** Check if a message is a yield point (ends the provider's turn). */
|
|
1275
|
-
declare function isYieldMessage(msg: Message): boolean;
|
|
1276
|
-
|
|
1277
|
-
/**
|
|
1278
|
-
* Credit fields riding the signed request body (internal-review, spec §2).
|
|
1279
|
-
*
|
|
1280
|
-
* An explicit draw references the credit INSIDE the secp256k1/Schnorr-signed
|
|
1281
|
-
* `body.data` — no new caller crypto. Wire field names (snake_case, matching
|
|
1282
|
-
* the rest of the wire surface):
|
|
1283
|
-
*
|
|
1284
|
-
* - `credit_id` — the credit to draw against.
|
|
1285
|
-
* - `draw_id` — client-generated draw id; the ledger's idempotency key.
|
|
1286
|
-
* - `fund` — optional `{ amount_micro, commitment }` funding commitment:
|
|
1287
|
-
* present iff a funding artifact (X-Cashu / X-PAYMENT / mpp credential)
|
|
1288
|
-
* rides the same request. `commitment` is the SHA-256 hex over the raw
|
|
1289
|
-
* artifact bytes, binding the header-borne proof — which sits OUTSIDE the
|
|
1290
|
-
* signed body — to the credit its sender intended (spec §2 condition 3).
|
|
1291
|
-
*
|
|
1292
|
-
* These are envelope-level fields, not capability input: **the SDK** removes
|
|
1293
|
-
* them before any builder-owned schema parses ({@link stripCreditEnvelope} in
|
|
1294
|
-
* `JobManager.parseInput` and on the quote path; {@link
|
|
1295
|
-
* creditEnvelopeIgnoreFields} feeding the auth verifier's own schema check), so
|
|
1296
|
-
* handlers never see them and a top-level `.strict()` capability schema is
|
|
1297
|
-
* fine (internal-review). Relying on Zod's default strip mode instead was the trap:
|
|
1298
|
-
* `.strict()` rejects unknown keys, so an explicit draw 400'd at parse before
|
|
1299
|
-
* the envelope was ever extracted.
|
|
1300
|
-
*
|
|
1301
|
-
* The strip never touches the canonical signing bytes. Every auth call site
|
|
1302
|
-
* verifies the raw wire body (`signedRequestInput`, internal-review), so the credit
|
|
1303
|
-
* fields are in the verified bytes because they were never taken out of them —
|
|
1304
|
-
* the strip is only ever applied to what a schema is about to parse.
|
|
1305
|
-
*/
|
|
1306
|
-
interface CreditEnvelope {
|
|
1307
|
-
creditId: string;
|
|
1308
|
-
drawId: string;
|
|
1309
|
-
fund?: CreditFundCommitment;
|
|
561
|
+
interface CreditEnvelope {
|
|
562
|
+
creditId: string;
|
|
563
|
+
drawId: string;
|
|
564
|
+
fund?: CreditFundCommitment;
|
|
1310
565
|
}
|
|
1311
566
|
/** The signed funding commitment for a fund-and-draw request. */
|
|
1312
567
|
interface CreditFundCommitment {
|
|
@@ -1620,16 +875,11 @@ declare function fundedMicroFor(priceFiat: PriceFiat, paidMsats: number, require
|
|
|
1620
875
|
* quote is 11 sats and the old advertisement demanded 10,718 µUSDC — 7% above
|
|
1621
876
|
* the price the caller was quoted and the ledger recorded.
|
|
1622
877
|
*
|
|
1623
|
-
*
|
|
1624
|
-
*
|
|
1625
|
-
*
|
|
1626
|
-
*
|
|
1627
|
-
* The rate-derived path survives for anything this rule can't express: a
|
|
1628
|
-
* non-USD `priceFiat` (the µUSDC figure is a real fx conversion there, and
|
|
1629
|
-
* still drifts — internal-review), and a host that verifies without a `priceFiat` at
|
|
1630
|
-
* all. Both keep today's behaviour rather than guessing a denomination.
|
|
878
|
+
* Every caller gates x402 on a USD-priced DVM before reaching this helper.
|
|
879
|
+
* Keeping the helper USD-only makes a future advertisement call site fail
|
|
880
|
+
* closed instead of quietly restoring the rate-derived non-USD path.
|
|
1631
881
|
*/
|
|
1632
|
-
declare function x402RequiredUsdcMicro(priceFiat: PriceFiat
|
|
882
|
+
declare function x402RequiredUsdcMicro(priceFiat: PriceFiat): number;
|
|
1633
883
|
/**
|
|
1634
884
|
* A settled x402 authorization's share of a quoted figure, taken against the
|
|
1635
885
|
* `maxAmountRequired` the 402 advertised (internal-review) — `quoted` unmodified when
|
|
@@ -1639,7 +889,7 @@ declare function x402RequiredUsdcMicro(priceFiat: PriceFiat | undefined, require
|
|
|
1639
889
|
*
|
|
1640
890
|
* x402 must NOT reach {@link fundedMicroFor} through `usdcToMsats`. The
|
|
1641
891
|
* advertised figure is stated in µUSDC by {@link x402RequiredUsdcMicro} — the
|
|
1642
|
-
* price verbatim for a USD job
|
|
892
|
+
* price verbatim for a USD job — and the
|
|
1643
893
|
* receipt converts back with `usdcToMsats`, a `Math.round`, which is the
|
|
1644
894
|
* inverse of neither. The round trip lands at or above `requiredMsats`, which
|
|
1645
895
|
* took the pro-rata branch and valued a payment of exactly the advertised
|
|
@@ -2524,6 +1774,8 @@ interface PaymentInfo {
|
|
|
2524
1774
|
*/
|
|
2525
1775
|
interface CreditFundingReport {
|
|
2526
1776
|
creditId: string;
|
|
1777
|
+
/** Canonical owner returned by the ledger after applying the funding. */
|
|
1778
|
+
callerPubkey: string;
|
|
2527
1779
|
/** The rail's own settlement reference — the deposit's idempotency key. */
|
|
2528
1780
|
fundingId: string;
|
|
2529
1781
|
rail: FundingMethod;
|
|
@@ -2931,7 +2183,7 @@ declare function verifyTempoSessionManagementCredential(args: {
|
|
|
2931
2183
|
* here: the former is already complete, the latter is a challenge envelope, not
|
|
2932
2184
|
* an error report.
|
|
2933
2185
|
*/
|
|
2934
|
-
type PaymentErrorCode = "payment_invalid" | "x402_settlement_ambiguous" | "cashu_unavailable" | "cashu_not_p2pk_locked" | "mint_health_pending" | "cashu_mint_sick" | "payment_insufficient" | "payment_unapplied" | "insufficient_credit" | "credit_expired" | "credit_unbacked" | "credit_not_found" | "tempo_channel_closing" | "settlement_pending" | "draw_conflict" | "funding_commitment_mismatch" | "credit_not_supported" | "one_shot_credit_not_supported" | "credit_below_min" | "below_rail_minimum" | "credit_over_max" | "credit_id_unavailable" | "nothing_to_drain" | "drain_below_dust" | "drain_not_found" | "drain_conflict" | "drain_method_unsupported" | "tempo_channel_exit_required" | "x402_settlement_submission_failed" | X402SettlementReconciliationReason | "lightning_settlement_pending" | "lightning_invoice_expired" | "lightning_unavailable" | AccumulatorRejectionCode;
|
|
2186
|
+
type PaymentErrorCode = "payment_invalid" | "x402_not_offered" | "x402_settlement_ambiguous" | "cashu_unavailable" | "cashu_not_p2pk_locked" | "mint_health_pending" | "cashu_mint_sick" | "payment_insufficient" | "payment_unapplied" | "insufficient_credit" | "credit_expired" | "credit_unbacked" | "credit_not_found" | "tempo_channel_closing" | "settlement_pending" | "draw_conflict" | "funding_commitment_mismatch" | "credit_not_supported" | "one_shot_credit_not_supported" | "credit_below_min" | "below_rail_minimum" | "credit_over_max" | "credit_id_unavailable" | "nothing_to_drain" | "drain_below_dust" | "drain_not_found" | "drain_conflict" | "drain_method_unsupported" | "tempo_channel_exit_required" | "x402_settlement_submission_failed" | X402SettlementReconciliationReason | "lightning_settlement_pending" | "lightning_invoice_expired" | "lightning_unavailable" | AccumulatorRejectionCode;
|
|
2935
2187
|
/**
|
|
2936
2188
|
* Build a flat payment-error body carrying the four agent-facing fields every
|
|
2937
2189
|
* payment error must have (internal-review): the machine `code`, the operator-facing
|
|
@@ -2984,6 +2236,8 @@ declare function buildPaymentErrorResponse(body: Record<string, unknown>, challe
|
|
|
2984
2236
|
interface IncomingPaymentOpts {
|
|
2985
2237
|
mints?: string[];
|
|
2986
2238
|
x402Config?: X402Config;
|
|
2239
|
+
/** Declared DVM pricing currency. x402 is accepted only when this is USD. */
|
|
2240
|
+
pricingCurrency?: string;
|
|
2987
2241
|
/** Durable exact-settlement coordinator shared with the upfront path. */
|
|
2988
2242
|
x402ExactSettlement?: X402ExactSettlementServer;
|
|
2989
2243
|
/** Exact generations the facilitator currently advertises. Defaults to both. */
|
|
@@ -3312,1094 +2566,1522 @@ interface X402FacilitatorHealthOpts extends X402FacilitatorProbeOpts {
|
|
|
3312
2566
|
refreshTtlMs?: number;
|
|
3313
2567
|
maxStalenessMs?: number;
|
|
3314
2568
|
}
|
|
3315
|
-
/** Live, synchronous capability view consumed while payment responses are rendered. */
|
|
3316
|
-
interface X402FacilitatorHealthLike {
|
|
3317
|
-
/** Prime or refresh the capability observation. */
|
|
3318
|
-
refresh(): Promise<void>;
|
|
3319
|
-
/** Return current support and begin a background refresh when its TTL has elapsed. */
|
|
3320
|
-
support(): X402FacilitatorSupport;
|
|
3321
|
-
/** Last successful raw response, reused by batch initialization without another request. */
|
|
3322
|
-
supportedResponse?(): SupportedResponse | undefined;
|
|
2569
|
+
/** Live, synchronous capability view consumed while payment responses are rendered. */
|
|
2570
|
+
interface X402FacilitatorHealthLike {
|
|
2571
|
+
/** Prime or refresh the capability observation. */
|
|
2572
|
+
refresh(): Promise<void>;
|
|
2573
|
+
/** Return current support and begin a background refresh when its TTL has elapsed. */
|
|
2574
|
+
support(): X402FacilitatorSupport;
|
|
2575
|
+
/** Last successful raw response, reused by batch initialization without another request. */
|
|
2576
|
+
supportedResponse?(): SupportedResponse | undefined;
|
|
2577
|
+
}
|
|
2578
|
+
/**
|
|
2579
|
+
* TTL-cached facilitator capability gate. Failed refreshes retain the last
|
|
2580
|
+
* successful observation until the same three-hour ceiling used by the
|
|
2581
|
+
* Lightning rail; a cold-start failure temporarily assumes exact dual-serve
|
|
2582
|
+
* so a facilitator blip does not suppress a healthy payment rail for a whole
|
|
2583
|
+
* process lifetime. Batch settlement is never assumed without an observation.
|
|
2584
|
+
*/
|
|
2585
|
+
declare class X402FacilitatorHealth implements X402FacilitatorHealthLike {
|
|
2586
|
+
private readonly config;
|
|
2587
|
+
private readonly opts;
|
|
2588
|
+
private readonly now;
|
|
2589
|
+
private readonly refreshTtlMs;
|
|
2590
|
+
private readonly maxStalenessMs;
|
|
2591
|
+
private readonly startedAtMs;
|
|
2592
|
+
private observation;
|
|
2593
|
+
private lastAttemptAtMs;
|
|
2594
|
+
private inFlight;
|
|
2595
|
+
private ceilingLoggedForMs;
|
|
2596
|
+
private lastSupportedResponse;
|
|
2597
|
+
constructor(config: X402Config, opts?: X402FacilitatorHealthOpts);
|
|
2598
|
+
/** Refresh the observation, coalescing concurrent requests. */
|
|
2599
|
+
refresh(): Promise<void>;
|
|
2600
|
+
/** Answer synchronously from cache and start a non-blocking refresh when due. */
|
|
2601
|
+
support(): X402FacilitatorSupport;
|
|
2602
|
+
/** Return the raw response paired with the last successful observation. */
|
|
2603
|
+
supportedResponse(): SupportedResponse | undefined;
|
|
2604
|
+
private get facilitator();
|
|
2605
|
+
private get network();
|
|
2606
|
+
private refreshOnce;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
/** Options for creating a JobManager. */
|
|
2610
|
+
interface JobManagerOpts {
|
|
2611
|
+
/** Environment variables for ctx.env. */
|
|
2612
|
+
env: Record<string, string>;
|
|
2613
|
+
/** KV store for ctx.store. */
|
|
2614
|
+
store: KVStore;
|
|
2615
|
+
/** Cashu mints accepted for payment. */
|
|
2616
|
+
mints?: string[];
|
|
2617
|
+
/** Persistent backing store for job records. Default: in-memory. */
|
|
2618
|
+
jobStore?: JobStore;
|
|
2619
|
+
/** When true, payment messages auto-credit without verification. */
|
|
2620
|
+
devMode?: boolean;
|
|
2621
|
+
/** Payment methods this DVM accepts. Default: derived by the server from configured rails. */
|
|
2622
|
+
paymentMethods?: PaymentMethod[];
|
|
2623
|
+
/** x402 stablecoin payment configuration. */
|
|
2624
|
+
x402?: X402Config;
|
|
2625
|
+
/** Durable exact-settlement coordinator shared with upfront verification. */
|
|
2626
|
+
x402ExactSettlement?: X402ExactSettlementServer;
|
|
2627
|
+
/** Live facilitator capabilities for version-specific mid-job payment handling. */
|
|
2628
|
+
x402FacilitatorHealth?: X402FacilitatorHealthLike;
|
|
2629
|
+
/** MPP multi-rail payment handle (built via `createMppFromOpts`). */
|
|
2630
|
+
mpp?: MppxServer;
|
|
2631
|
+
/** Cashu receive mode (internal-review). */
|
|
2632
|
+
cashuMode?: CashuMode;
|
|
2633
|
+
/** Builder's NUT-11 P2PK lock pubkey (internal-review). Required for accumulator mode. */
|
|
2634
|
+
lockPubkey?: string;
|
|
2635
|
+
/** Postgres pool for wallet accumulator persistence. */
|
|
2636
|
+
db?: Pool;
|
|
2637
|
+
/**
|
|
2638
|
+
* Agent-wallet (internal-review): per-DVM identifier used for the scheduler advisory
|
|
2639
|
+
* lock and the `(dvm_id, request_id)` replay key. Defaults to `DVMKIT_DVM_ID`
|
|
2640
|
+
* env at boot in `createDVMHost()`.
|
|
2641
|
+
*/
|
|
2642
|
+
dvmId?: string;
|
|
2643
|
+
/** Audience expected by persisted v2 caller statements. */
|
|
2644
|
+
authAudience?: SignedRequestAudience;
|
|
2645
|
+
/**
|
|
2646
|
+
* Shared fx fetcher used to convert USD-string descriptor prices and
|
|
2647
|
+
* fiat-form `requestPayment` calls into sats (internal-review). The SDK server owns
|
|
2648
|
+
* one per process so concurrent quotes share the 60s cache window.
|
|
2649
|
+
*/
|
|
2650
|
+
fxFetcher: FxFetcher;
|
|
2651
|
+
/**
|
|
2652
|
+
* Signs a receipt at every terminal transition (internal-review). Absent when the
|
|
2653
|
+
* DVM has no receipt key wired — jobs then terminate exactly as before and
|
|
2654
|
+
* `/v1/info` doesn't advertise `receipts`.
|
|
2655
|
+
*/
|
|
2656
|
+
receiptIssuer?: ReceiptIssuer;
|
|
2657
|
+
/**
|
|
2658
|
+
* Credit ledger for the terminal funnel (internal-review): a job carrying a
|
|
2659
|
+
* `creditId`/`drawId` settles its draw on success and releases it on
|
|
2660
|
+
* failure/cancel — this is how "no debit on job failure" is mechanically
|
|
2661
|
+
* real. The resolution runs before receipt issuance so the receipt's
|
|
2662
|
+
* `credit` block countersigns the post-resolution balance.
|
|
2663
|
+
*/
|
|
2664
|
+
creditLedger?: CreditLedgerLike;
|
|
2665
|
+
/**
|
|
2666
|
+
* Durable processed-payment markers (internal-review), threaded through to the
|
|
2667
|
+
* mid-job top-up path (internal-review) so an x402/mpp payment's marker, `fund`, and
|
|
2668
|
+
* `growDraw` commit together.
|
|
2669
|
+
*/
|
|
2670
|
+
processedPayments?: ProcessedPaymentStore;
|
|
2671
|
+
/**
|
|
2672
|
+
* Reporter-owned transactional deposit enqueue threaded into mid-job rail
|
|
2673
|
+
* commits (internal-review).
|
|
2674
|
+
*/
|
|
2675
|
+
enqueueCreditDeposit?: CreditDepositEnqueue;
|
|
2676
|
+
/** Reporter outbox inserted atomically with a terminal draw release (internal-review). */
|
|
2677
|
+
enqueueCreditDrawRelease?: CreditDrawReleaseEnqueue;
|
|
2678
|
+
/** Credit lifetime a top-up mints with, from the DVM's advertised `credit.ttl`. */
|
|
2679
|
+
creditTtlMs?: number;
|
|
2680
|
+
/**
|
|
2681
|
+
* The DVM's declared pricing currency (internal-review), which is the currency of
|
|
2682
|
+
* every credit it opens. Threaded into the job context so a mid-job ask on a
|
|
2683
|
+
* job that holds no credit yet is still pinned in the denomination its first
|
|
2684
|
+
* payment will open the credit in (internal-review). `DVMServer` passes its resolved
|
|
2685
|
+
* `pricingCurrency`; a host building a `JobManager` directly falls back to the
|
|
2686
|
+
* descriptor, and then to `"usd"`.
|
|
2687
|
+
*/
|
|
2688
|
+
pricingCurrency?: string;
|
|
2689
|
+
/**
|
|
2690
|
+
* Callback fired when a mid-job top-up moves money onto a credit (internal-review).
|
|
2691
|
+
* The platform books it as a **deposit** — a liability until drawn, never
|
|
2692
|
+
* summed into revenue — so a top-up that skipped this would leave the
|
|
2693
|
+
* outstanding-liability view short. Mirrors `DVMServer.reportCreditDeposit`,
|
|
2694
|
+
* which handles the upfront and `/v1/credit` legs.
|
|
2695
|
+
*/
|
|
2696
|
+
onCreditFunded?: (info: CreditDepositPayload) => void | Promise<void>;
|
|
2697
|
+
/**
|
|
2698
|
+
* Callback fired when a paid job completes (internal-review). Container-runtime DVMs
|
|
2699
|
+
* use this to report revenue to the platform via `RevenueReporter`. Mirrors
|
|
2700
|
+
* the isolate path's `IsolateJobManager.opts.onJobCompleted`.
|
|
2701
|
+
*/
|
|
2702
|
+
onJobCompleted?: (info: {
|
|
2703
|
+
dvmId: string;
|
|
2704
|
+
jobId: string;
|
|
2705
|
+
paidMsats: number;
|
|
2706
|
+
paymentMint?: string;
|
|
2707
|
+
rail: string;
|
|
2708
|
+
paymentTxHash?: string;
|
|
2709
|
+
nativeAmount?: number;
|
|
2710
|
+
nativeAsset?: string;
|
|
2711
|
+
cashuFlow?: string;
|
|
2712
|
+
creditId?: string;
|
|
2713
|
+
drawId?: string;
|
|
2714
|
+
drawAmountMicro?: number;
|
|
2715
|
+
creditCurrency?: string;
|
|
2716
|
+
settledAt?: number;
|
|
2717
|
+
fundingRef?: string;
|
|
2718
|
+
kind?: string;
|
|
2719
|
+
}) => void | Promise<void>;
|
|
2720
|
+
/**
|
|
2721
|
+
* Callback fired when the stale-job reaper force-fails a *paid* job — its
|
|
2722
|
+
* pending credit draw is released rather than settled, because only a
|
|
2723
|
+
* completed job settles a draw (internal-review). Container-runtime DVMs wire this
|
|
2724
|
+
* to a paid-job-death report → operator `notice` (internal-review). Never fires for
|
|
2725
|
+
* free jobs (`paidMsats === 0`). Fired fire-and-forget so a throw or a slow
|
|
2726
|
+
* report can't wedge the sweep.
|
|
2727
|
+
*/
|
|
2728
|
+
onPaidJobDeath?: (info: {
|
|
2729
|
+
dvmId: string;
|
|
2730
|
+
jobId: string;
|
|
2731
|
+
capability: string;
|
|
2732
|
+
paidMsats: number;
|
|
2733
|
+
rail: string;
|
|
2734
|
+
reason: "worker_died_mid_job" | "stale_no_terminal_status";
|
|
2735
|
+
nativeAmount?: number;
|
|
2736
|
+
nativeAsset?: string;
|
|
2737
|
+
paymentMint?: string;
|
|
2738
|
+
}) => void | Promise<void>;
|
|
2739
|
+
/**
|
|
2740
|
+
* Callback fired when a settled credit draw cannot book a revenue event
|
|
2741
|
+
* because no usable payment rail is available (internal-review). Container-runtime
|
|
2742
|
+
* DVMs auto-wire this to the platform's deduped operator notice. Fired
|
|
2743
|
+
* fire-and-forget so alert delivery cannot block terminal handling or draw
|
|
2744
|
+
* reconciliation.
|
|
2745
|
+
*/
|
|
2746
|
+
onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
|
|
2747
|
+
/**
|
|
2748
|
+
* Stale-job sweep threshold in ms (internal-review). Jobs in a non-terminal status
|
|
2749
|
+
* (`processing` / `working` / `awaiting-input`) whose `lastActivityAt` is
|
|
2750
|
+
* older than this are force-cancelled with reason `stale_no_terminal_status`,
|
|
2751
|
+
* so callers always reach a terminal status instead of polling forever after
|
|
2752
|
+
* a worker crash, OOM, or silent `messageAppender` failure. Default:
|
|
2753
|
+
* `2 × descriptor.idleTimeout + STALE_JOB_WATCHDOG_HEADROOM_MS`, matching
|
|
2754
|
+
* the internal-review acceptance criterion against scrape's `JOB_TIMEOUT_MS`.
|
|
2755
|
+
* Builders whose per-handler wall-clock bound exceeds 90s must override
|
|
2756
|
+
* this — otherwise the watchdog fires on a legitimate in-flight handler.
|
|
2757
|
+
* Set to `0` to disable (test seam).
|
|
2758
|
+
*/
|
|
2759
|
+
staleJobTimeoutMs?: number;
|
|
2760
|
+
/**
|
|
2761
|
+
* Sweep interval for the stale-job watchdog. Default: `min(60s, max(15s,
|
|
2762
|
+
* tightestActiveTimeout / 4))`. Tests override to fire deterministically.
|
|
2763
|
+
*/
|
|
2764
|
+
staleJobSweepIntervalMs?: number;
|
|
2765
|
+
/**
|
|
2766
|
+
* Worker-liveness watchdog in ms (internal-review). Jobs in `processing`/`working`
|
|
2767
|
+
* whose `lastActivityAt` is older than this are marked `failed` (dead
|
|
2768
|
+
* worker), separately from the longer `awaiting-input` idle timeout above.
|
|
2769
|
+
* Default: `descriptor.processingWatchdog * 1000` or
|
|
2770
|
+
* `DEFAULT_PROCESSING_WATCHDOG_MS`. Set to `0` to disable (test seam).
|
|
2771
|
+
*/
|
|
2772
|
+
processingWatchdogMs?: number;
|
|
2773
|
+
/**
|
|
2774
|
+
* Interval at which locally-active `processing`/`working` jobs have their
|
|
2775
|
+
* `lastActivityAt` bumped so the processing watchdog only fires on a truly
|
|
2776
|
+
* dead worker (internal-review). Default: `DEFAULT_HEARTBEAT_INTERVAL_MS`. Set to
|
|
2777
|
+
* `0` to disable (test seam). Also floors how far the internal-review clock guard
|
|
2778
|
+
* may compensate a backwards step on the processing arm — a cadence close to
|
|
2779
|
+
* `processingWatchdogMs` leaves it no room and it compensates nothing.
|
|
2780
|
+
*/
|
|
2781
|
+
heartbeatIntervalMs?: number;
|
|
2782
|
+
/**
|
|
2783
|
+
* Age in ms at which a `pending` credit draw whose `job_id` matches no row in
|
|
2784
|
+
* the job store is released as orphaned (internal-review). The draw commits before
|
|
2785
|
+
* the job row is persisted, so a crash or a fail-closed refusal in that
|
|
2786
|
+
* window strands a hold no terminal path can ever reach. Must stay
|
|
2787
|
+
* comfortably above the submit path's draw→persist latency. Set to `0` to
|
|
2788
|
+
* disable the watchdog entirely — stranded holds then stay stranded, so
|
|
2789
|
+
* treat it as a money knob, not a tuning one. Default:
|
|
2790
|
+
* {@link DEFAULT_ORPHAN_DRAW_AGE_MS}.
|
|
2791
|
+
*/
|
|
2792
|
+
orphanDrawAgeMs?: number;
|
|
2793
|
+
/**
|
|
2794
|
+
* Age in ms at which a `pending` credit draw whose job row **is** terminal is
|
|
2795
|
+
* reconciled against that row's outcome (internal-review) — settled for a
|
|
2796
|
+
* `completed` job, released for a `failed`/`cancelled` one. Covers the hole
|
|
2797
|
+
* `orphanDrawAgeMs` cannot: `resolveCreditDraw` throwing on a ledger error
|
|
2798
|
+
* leaves a terminal job whose hold no later path retries.
|
|
2799
|
+
*
|
|
2800
|
+
* Rides the same scan as the orphan arm, which has two consequences for the
|
|
2801
|
+
* value you set here. Setting `orphanDrawAgeMs` to `0` disables this arm too.
|
|
2802
|
+
* And the effective gate is `max(orphanDrawAgeMs, terminalDrawReconcileAgeMs)`
|
|
2803
|
+
* — the scan never returns a row younger than its own cutoff, so anything
|
|
2804
|
+
* below `orphanDrawAgeMs` is silently clamped up to it. That floor is
|
|
2805
|
+
* deliberate rather than a wart: honouring a narrower value would mean
|
|
2806
|
+
* widening the scan, which hands the orphan arm rows younger than
|
|
2807
|
+
* `orphanDrawAgeMs` that it must not release. It can only ever delay a
|
|
2808
|
+
* settle, never advance one, which is the safe direction for a debit.
|
|
2809
|
+
*
|
|
2810
|
+
* Set to `0` to disable this arm alone — a money knob, not a tuning one.
|
|
2811
|
+
* Default: {@link DEFAULT_TERMINAL_DRAW_RECONCILE_AGE_MS}.
|
|
2812
|
+
*/
|
|
2813
|
+
terminalDrawReconcileAgeMs?: number;
|
|
2814
|
+
/**
|
|
2815
|
+
* Sweep interval for the orphan-draw watchdog. Default:
|
|
2816
|
+
* {@link DEFAULT_ORPHAN_DRAW_SWEEP_INTERVAL_MS}. Set to `0` to disable — the
|
|
2817
|
+
* test seam, since suites drive `sweepOrphanDraws()` directly.
|
|
2818
|
+
*/
|
|
2819
|
+
orphanDrawSweepIntervalMs?: number;
|
|
2820
|
+
/** Retention sweep cadence. Default: daily; `0` disables the interval. */
|
|
2821
|
+
jobRetentionSweepIntervalMs?: number;
|
|
2822
|
+
/** Delay before the first retention pass. Default: 30s; `0` disables it. */
|
|
2823
|
+
jobRetentionSweepBootDelayMs?: number;
|
|
2824
|
+
/** Operator-alert boundary invoked whenever a retention pass fails. */
|
|
2825
|
+
onJobRetentionSweepFailed?: (info: {
|
|
2826
|
+
dvmId: string;
|
|
2827
|
+
dvmName: string;
|
|
2828
|
+
retentionDays: number;
|
|
2829
|
+
error: string;
|
|
2830
|
+
}) => void | Promise<void>;
|
|
2831
|
+
/** Clear-on-recovery boundary invoked after every complete, clean pass. */
|
|
2832
|
+
onJobRetentionSweepRecovered?: (info: {
|
|
2833
|
+
dvmId: string;
|
|
2834
|
+
dvmName: string;
|
|
2835
|
+
retentionDays: number;
|
|
2836
|
+
examined: number;
|
|
2837
|
+
redacted: number;
|
|
2838
|
+
}) => void | Promise<void>;
|
|
2839
|
+
/**
|
|
2840
|
+
* Grace window in ms a reactivating machine waits for a live original handler
|
|
2841
|
+
* to win the single-execution claim before replaying an `awaiting-input` job
|
|
2842
|
+
* itself (internal-review). Default: `DEFAULT_REACTIVATION_CLAIM_GRACE_MS`. Widen if
|
|
2843
|
+
* cross-machine NOTIFY latency causes spurious double executions; lower in
|
|
2844
|
+
* tests for speed.
|
|
2845
|
+
*/
|
|
2846
|
+
reactivationClaimGraceMs?: number;
|
|
2847
|
+
}
|
|
2848
|
+
type ResolvedInput<I extends ZodLike | undefined> = I extends ZodLike<infer O> ? O : string;
|
|
2849
|
+
/**
|
|
2850
|
+
* A deposit this payment funded that the job row did **not** take (internal-review).
|
|
2851
|
+
*
|
|
2852
|
+
* Reached when two mid-job top-ups race on a free-then-paid job: each opens its
|
|
2853
|
+
* own implicit credit, and `verifyAndCredit`'s set-if-null bind keeps one. The
|
|
2854
|
+
* loser's money is real, owned by the caller, and reclaimable — but nothing
|
|
2855
|
+
* named it, so the caller would have to derive `imp:<rail>:<payment_id>` to
|
|
2856
|
+
* reach their own balance. This rides the 201 so they don't have to.
|
|
2857
|
+
*
|
|
2858
|
+
* The response is the fast surface, not the only one: the credit is opened
|
|
2859
|
+
* under the caller's verified pubkey, so `POST /v1/credit` op `balance` lists
|
|
2860
|
+
* it and op `drain` reclaims it once the hold resolves.
|
|
2861
|
+
*/
|
|
2862
|
+
interface UnappliedCredit {
|
|
2863
|
+
credit_id: string;
|
|
2864
|
+
credited_micro: number;
|
|
2865
|
+
credit_currency: string;
|
|
2866
|
+
display: string;
|
|
2867
|
+
hint: string;
|
|
3323
2868
|
}
|
|
3324
2869
|
/**
|
|
3325
|
-
*
|
|
3326
|
-
*
|
|
3327
|
-
*
|
|
3328
|
-
* so a facilitator blip does not suppress a healthy payment rail for a whole
|
|
3329
|
-
* process lifetime. Batch settlement is never assumed without an observation.
|
|
2870
|
+
* What {@link JobManager.processPayment} tells the route. `error` is a refusal
|
|
2871
|
+
* to serialise verbatim; otherwise the payment was accepted, optionally
|
|
2872
|
+
* carrying an {@link UnappliedCredit} to fold into the 201.
|
|
3330
2873
|
*/
|
|
3331
|
-
|
|
3332
|
-
|
|
2874
|
+
type ProcessPaymentOutcome = {
|
|
2875
|
+
error: {
|
|
2876
|
+
body: Record<string, unknown>;
|
|
2877
|
+
status: number;
|
|
2878
|
+
};
|
|
2879
|
+
unappliedCredit?: undefined;
|
|
2880
|
+
} | {
|
|
2881
|
+
error?: undefined;
|
|
2882
|
+
unappliedCredit?: UnappliedCredit;
|
|
2883
|
+
};
|
|
2884
|
+
/**
|
|
2885
|
+
* Transport-agnostic job lifecycle manager.
|
|
2886
|
+
*
|
|
2887
|
+
* Handles job creation, handler invocation, message dispatch, persistence,
|
|
2888
|
+
* idle timeout, and durable replay. Shared by both the HTTP server and
|
|
2889
|
+
* relay provider.
|
|
2890
|
+
*/
|
|
2891
|
+
declare class JobManager<State, InputSchema extends ZodLike | undefined> {
|
|
2892
|
+
private readonly descriptor;
|
|
3333
2893
|
private readonly opts;
|
|
3334
|
-
private readonly
|
|
3335
|
-
private readonly
|
|
3336
|
-
private readonly
|
|
3337
|
-
private readonly
|
|
3338
|
-
|
|
3339
|
-
private
|
|
3340
|
-
|
|
3341
|
-
private
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
private get facilitator();
|
|
3351
|
-
private get network();
|
|
3352
|
-
private refreshOnce;
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
/** Options for creating a JobManager. */
|
|
3356
|
-
interface JobManagerOpts {
|
|
3357
|
-
/** Environment variables for ctx.env. */
|
|
3358
|
-
env: Record<string, string>;
|
|
3359
|
-
/** KV store for ctx.store. */
|
|
3360
|
-
store: KVStore;
|
|
3361
|
-
/** Cashu mints accepted for payment. */
|
|
3362
|
-
mints?: string[];
|
|
3363
|
-
/** Persistent backing store for job records. Default: in-memory. */
|
|
3364
|
-
jobStore?: JobStore;
|
|
3365
|
-
/** When true, payment messages auto-credit without verification. */
|
|
3366
|
-
devMode?: boolean;
|
|
3367
|
-
/** Payment methods this DVM accepts. Default: derived by the server from configured rails. */
|
|
3368
|
-
paymentMethods?: PaymentMethod[];
|
|
3369
|
-
/** x402 stablecoin payment configuration. */
|
|
3370
|
-
x402?: X402Config;
|
|
3371
|
-
/** Durable exact-settlement coordinator shared with upfront verification. */
|
|
3372
|
-
x402ExactSettlement?: X402ExactSettlementServer;
|
|
3373
|
-
/** Live facilitator capabilities for version-specific mid-job payment handling. */
|
|
3374
|
-
x402FacilitatorHealth?: X402FacilitatorHealthLike;
|
|
3375
|
-
/** MPP multi-rail payment handle (built via `createMppFromOpts`). */
|
|
3376
|
-
mpp?: MppxServer;
|
|
3377
|
-
/** Cashu receive mode (internal-review). */
|
|
3378
|
-
cashuMode?: CashuMode;
|
|
3379
|
-
/** Builder's NUT-11 P2PK lock pubkey (internal-review). Required for accumulator mode. */
|
|
3380
|
-
lockPubkey?: string;
|
|
3381
|
-
/** Postgres pool for wallet accumulator persistence. */
|
|
3382
|
-
db?: Pool;
|
|
2894
|
+
private readonly _activeJobs;
|
|
2895
|
+
private readonly jobStore;
|
|
2896
|
+
private readonly idleTimers;
|
|
2897
|
+
private readonly cleanupTimers;
|
|
2898
|
+
/** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
|
|
2899
|
+
private readonly statusWatchers;
|
|
2900
|
+
/** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
|
|
2901
|
+
private readonly statusChecksInFlight;
|
|
2902
|
+
/** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
|
|
2903
|
+
private readonly statusChecksQueued;
|
|
2904
|
+
private readonly sessionId;
|
|
2905
|
+
private nextJobId;
|
|
2906
|
+
private accumulatorMonitor?;
|
|
2907
|
+
private readonly staleJobTimeoutMs;
|
|
2908
|
+
private readonly processingWatchdogMs;
|
|
2909
|
+
private readonly reactivationClaimGraceMs;
|
|
3383
2910
|
/**
|
|
3384
|
-
*
|
|
3385
|
-
*
|
|
3386
|
-
*
|
|
2911
|
+
* Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
|
|
2912
|
+
* guard's compensation on the processing arm — see
|
|
2913
|
+
* {@link staleSweepCompensationCapMs}.
|
|
3387
2914
|
*/
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
2915
|
+
private readonly heartbeatIntervalMs;
|
|
2916
|
+
private readonly orphanDrawAgeMs;
|
|
2917
|
+
private readonly terminalDrawReconcileAgeMs;
|
|
2918
|
+
private staleSweepTimer?;
|
|
2919
|
+
private heartbeatTimer?;
|
|
2920
|
+
private orphanDrawSweepTimer?;
|
|
2921
|
+
private jobRetentionSweepTimer?;
|
|
2922
|
+
private jobRetentionBootTimer?;
|
|
2923
|
+
/** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
|
|
2924
|
+
private orphanSweepClaimed;
|
|
2925
|
+
/** Where a page-capped tick left off; cleared once a tick reaches the end. */
|
|
2926
|
+
private orphanSweepResumeFrom?;
|
|
2927
|
+
private jobRetentionResumeFrom?;
|
|
3391
2928
|
/**
|
|
3392
|
-
*
|
|
3393
|
-
*
|
|
3394
|
-
*
|
|
2929
|
+
* A `Date.now()` reading that cannot regress within this manager's lifetime
|
|
2930
|
+
* (internal-review). Anchored at construction, so a manager already running when
|
|
2931
|
+
* the clock stepped is covered. The stale-job sweep's two cutoffs, the
|
|
2932
|
+
* orphan-draw age gate and the reactivation claim grace all read it; see
|
|
2933
|
+
* {@link createMonotonicClock} for what it costs and where it is clamped.
|
|
3395
2934
|
*/
|
|
3396
|
-
|
|
2935
|
+
private readonly monotonicNowMs;
|
|
2936
|
+
private sweepInFlight;
|
|
2937
|
+
private orphanSweepInFlight;
|
|
2938
|
+
private jobRetentionSweepInFlight;
|
|
2939
|
+
private heartbeatInFlight;
|
|
3397
2940
|
/**
|
|
3398
|
-
*
|
|
3399
|
-
*
|
|
3400
|
-
*
|
|
2941
|
+
* The denomination of every credit this DVM opens (internal-review). Resolved once,
|
|
2942
|
+
* with the same boundary fallback `DVMServer` applies for direct JavaScript
|
|
2943
|
+
* callers that force an incomplete value past the descriptor contract.
|
|
3401
2944
|
*/
|
|
3402
|
-
|
|
2945
|
+
private readonly pricingCurrency;
|
|
2946
|
+
constructor(descriptor: DVMDescriptor<State, InputSchema>, opts: JobManagerOpts);
|
|
2947
|
+
/** Active in-memory jobs. */
|
|
2948
|
+
get activeJobs(): Map<string, ServerJob>;
|
|
2949
|
+
/** The backing job store. */
|
|
2950
|
+
get store(): JobStore;
|
|
3403
2951
|
/**
|
|
3404
|
-
*
|
|
3405
|
-
*
|
|
3406
|
-
*
|
|
3407
|
-
*
|
|
3408
|
-
* `credit` block countersigns the post-resolution balance.
|
|
2952
|
+
* True when this DVM will actually issue receipts (internal-review) — a key *and*
|
|
2953
|
+
* a store that can allocate sequence numbers and persist bytes write-once.
|
|
2954
|
+
* `/v1/info#receipts` reads this rather than the key alone, so the flag can
|
|
2955
|
+
* never promise something `issueReceipt` silently declines to do.
|
|
3409
2956
|
*/
|
|
3410
|
-
|
|
2957
|
+
get receiptsEnabled(): boolean;
|
|
2958
|
+
/** True when the named capability exists on this descriptor. */
|
|
2959
|
+
hasCapability(name: string): boolean;
|
|
2960
|
+
/** Names of every capability this descriptor exposes — used for error envelopes. */
|
|
2961
|
+
capabilityNames(): string[];
|
|
3411
2962
|
/**
|
|
3412
|
-
*
|
|
3413
|
-
*
|
|
3414
|
-
* `
|
|
2963
|
+
* Resolve a capability's static `price` to msats (internal-review).
|
|
2964
|
+
*
|
|
2965
|
+
* `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
|
|
2966
|
+
* Returns `undefined` for dynamic-priced capabilities (those that declare
|
|
2967
|
+
* `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
|
|
3415
2968
|
*/
|
|
3416
|
-
|
|
2969
|
+
currentPriceMsats(capability: string): Promise<number | undefined>;
|
|
2970
|
+
/**
|
|
2971
|
+
* Translate a capability's static `price` into the fiat envelope used by
|
|
2972
|
+
* the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
|
|
2973
|
+
* Returns `undefined` for dynamic-priced capabilities and for unknown
|
|
2974
|
+
* names (the route handler validates the name before invoking this).
|
|
2975
|
+
*/
|
|
2976
|
+
currentPricingMax(capability: string): {
|
|
2977
|
+
amount: number;
|
|
2978
|
+
currency: string;
|
|
2979
|
+
} | undefined;
|
|
2980
|
+
/**
|
|
2981
|
+
* Parse a request body through a capability's input schema (internal-review).
|
|
2982
|
+
*
|
|
2983
|
+
* Resolution order when a capability declares an `input` schema:
|
|
2984
|
+
* 1. `body.data` is preferred — agents pass structured fields directly
|
|
2985
|
+
* (CLI builds this from `--param k=v`).
|
|
2986
|
+
* 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
|
|
2987
|
+
* JSON-string contract.
|
|
2988
|
+
* 3. Neither usable → throw `MissingStructuredInputError` so the route can
|
|
2989
|
+
* return `invalid_input` with a hint pointing at `/v1/info`.
|
|
2990
|
+
*
|
|
2991
|
+
* When the capability has no `input` schema, returns `body.input` raw
|
|
2992
|
+
* (primitive path).
|
|
2993
|
+
*
|
|
2994
|
+
* The internal-review credit envelope is removed before the schema runs, on both
|
|
2995
|
+
* branches (internal-review) — it rides the signed body but is protocol-level, so a
|
|
2996
|
+
* top-level `.strict()` capability schema would otherwise reject every
|
|
2997
|
+
* explicit draw as an unknown key, before the envelope was even extracted.
|
|
2998
|
+
* The persisted `record.input` the internal-review reactivation path re-reads is the
|
|
2999
|
+
* pre-Zod wire form, so it comes through the second branch carrying them too.
|
|
3000
|
+
*/
|
|
3001
|
+
parseInput(body: {
|
|
3002
|
+
input?: string;
|
|
3003
|
+
data?: unknown;
|
|
3004
|
+
}, capability: string): unknown;
|
|
3417
3005
|
/**
|
|
3418
|
-
*
|
|
3419
|
-
*
|
|
3006
|
+
* Look up the per-capability descriptor by name (internal-review). Throws when the
|
|
3007
|
+
* name doesn't exist — the route layer validates body.capability first, so
|
|
3008
|
+
* reaching this with an unknown name is a programmer error.
|
|
3420
3009
|
*/
|
|
3421
|
-
|
|
3422
|
-
/** Reporter outbox inserted atomically with a terminal draw release (internal-review). */
|
|
3423
|
-
enqueueCreditDrawRelease?: CreditDrawReleaseEnqueue;
|
|
3424
|
-
/** Credit lifetime a top-up mints with, from the DVM's advertised `credit.ttl`. */
|
|
3425
|
-
creditTtlMs?: number;
|
|
3010
|
+
private requireCapability;
|
|
3426
3011
|
/**
|
|
3427
|
-
*
|
|
3428
|
-
*
|
|
3429
|
-
*
|
|
3430
|
-
*
|
|
3431
|
-
*
|
|
3432
|
-
* descriptor, and then to `"usd"`.
|
|
3012
|
+
* Soft capability lookup — `undefined` when the name isn't defined. The
|
|
3013
|
+
* single place the `descriptor.capabilities` index cast lives; callers that
|
|
3014
|
+
* tolerate a missing capability (the route layer pre-validates, or the cap
|
|
3015
|
+
* was deleted from the descriptor after a job was persisted) route through
|
|
3016
|
+
* here instead of re-casting inline.
|
|
3433
3017
|
*/
|
|
3434
|
-
|
|
3018
|
+
private getCapability;
|
|
3435
3019
|
/**
|
|
3436
|
-
*
|
|
3437
|
-
*
|
|
3438
|
-
*
|
|
3439
|
-
*
|
|
3440
|
-
*
|
|
3020
|
+
* Allocate the id the next job will be created under (internal-review). The
|
|
3021
|
+
* submit path calls this BEFORE payment verification so the ledger draw
|
|
3022
|
+
* commits with its job linkage, then passes the id back through
|
|
3023
|
+
* `createJob`'s provenance. Ids allocated for requests whose payment is
|
|
3024
|
+
* refused are simply never used — the sequence has gaps, which nothing
|
|
3025
|
+
* reads meaning into.
|
|
3441
3026
|
*/
|
|
3442
|
-
|
|
3027
|
+
allocateJobId(): string;
|
|
3443
3028
|
/**
|
|
3444
|
-
*
|
|
3445
|
-
*
|
|
3446
|
-
* the
|
|
3029
|
+
* Create a new job from a request body. `provenance` (internal-review) is what the
|
|
3030
|
+
* idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
|
|
3031
|
+
* to re-issue, and the fingerprint + caller pubkey a retry must reproduce
|
|
3032
|
+
* to be given it.
|
|
3447
3033
|
*/
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3034
|
+
createJob(body: {
|
|
3035
|
+
input: string;
|
|
3036
|
+
params?: Record<string, string>;
|
|
3037
|
+
}, requesterId: string, payment: PaymentInfo, requiredMsats: number | undefined, capability: string, requesterTokenHash?: string, provenance?: {
|
|
3038
|
+
requesterToken?: string;
|
|
3039
|
+
requestFingerprint?: string;
|
|
3040
|
+
requesterPubkey?: string;
|
|
3041
|
+
requestId?: string;
|
|
3042
|
+
authRequestPath?: string;
|
|
3043
|
+
/**
|
|
3044
|
+
* Pre-allocated id from {@link allocateJobId} (internal-review) — the submit
|
|
3045
|
+
* path allocates before payment verification so the ledger draw can
|
|
3046
|
+
* record the job it pays for.
|
|
3047
|
+
*/
|
|
3048
|
+
jobId?: string;
|
|
3049
|
+
}): ServerJob;
|
|
3050
|
+
/** Build an SDKJobContext and attach it to the job. */
|
|
3051
|
+
buildAndAttachContext(job: ServerJob, parsedInput: unknown): SDKJobContext<State, ResolvedInput<InputSchema>>;
|
|
3052
|
+
/** Start the handler for a job. */
|
|
3053
|
+
runHandler(job: ServerJob, sdkCtx: SDKJobContext<unknown, unknown>): void;
|
|
3054
|
+
/** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
|
|
3055
|
+
dispatchMessage(job: ServerJob, type: MessageType, content: unknown): Promise<void>;
|
|
3466
3056
|
/**
|
|
3467
|
-
*
|
|
3468
|
-
*
|
|
3469
|
-
*
|
|
3470
|
-
*
|
|
3471
|
-
*
|
|
3472
|
-
*
|
|
3057
|
+
* Persist a job to the backing store.
|
|
3058
|
+
*
|
|
3059
|
+
* Awaits the per-job appender tail (internal-review) so that:
|
|
3060
|
+
* 1. The snapshot's `messages` is consistent with the `job_messages` table
|
|
3061
|
+
* — no row is still in flight at snapshot time.
|
|
3062
|
+
* 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
|
|
3063
|
+
* can't race a still-pending `appendOutgoing` for the final yield
|
|
3064
|
+
* message (which would otherwise wipe the row before an in-flight SSE
|
|
3065
|
+
* subscriber's NOTIFY-driven fetch can see it).
|
|
3473
3066
|
*/
|
|
3474
|
-
|
|
3475
|
-
dvmId: string;
|
|
3476
|
-
jobId: string;
|
|
3477
|
-
capability: string;
|
|
3478
|
-
paidMsats: number;
|
|
3479
|
-
rail: string;
|
|
3480
|
-
reason: "worker_died_mid_job" | "stale_no_terminal_status";
|
|
3481
|
-
nativeAmount?: number;
|
|
3482
|
-
nativeAsset?: string;
|
|
3483
|
-
paymentMint?: string;
|
|
3484
|
-
}) => void | Promise<void>;
|
|
3067
|
+
persistJob(job: ServerJob): Promise<void>;
|
|
3485
3068
|
/**
|
|
3486
|
-
*
|
|
3487
|
-
*
|
|
3488
|
-
*
|
|
3489
|
-
*
|
|
3490
|
-
*
|
|
3069
|
+
* Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
|
|
3070
|
+
* in-memory `job.status`, so on its own it only protects the machine running
|
|
3071
|
+
* the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
|
|
3072
|
+
* on a machine that isn't running the job — `app.ts` cancels it in the store
|
|
3073
|
+
* and this process learns of it only through the durable row.
|
|
3074
|
+
*
|
|
3075
|
+
* Adopting the durable status blocks the rest of the handler's writes via the
|
|
3076
|
+
* local guard, makes `handleJobTerminal` see a non-completed job so it skips
|
|
3077
|
+
* the revenue report, rejects a suspended handler's prompt/payment yields, and
|
|
3078
|
+
* fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
|
|
3079
|
+
* the durable state won and the caller must skip its save.
|
|
3080
|
+
*
|
|
3081
|
+
* Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
|
|
3082
|
+
* this within a round-trip of the remote cancel committing, which is what
|
|
3083
|
+
* actually stops the provider spend; `persistJob` calls it again on every save
|
|
3084
|
+
* as the backstop for the window where a cancel commits between a handler's
|
|
3085
|
+
* read and its write (the check-then-write it can still lose is covered by the
|
|
3086
|
+
* stores' terminal-sticky `save`, which keeps the row itself correct).
|
|
3491
3087
|
*/
|
|
3492
|
-
|
|
3088
|
+
private adoptDurableTerminal;
|
|
3493
3089
|
/**
|
|
3494
|
-
*
|
|
3495
|
-
*
|
|
3496
|
-
*
|
|
3497
|
-
*
|
|
3498
|
-
*
|
|
3499
|
-
*
|
|
3500
|
-
*
|
|
3501
|
-
*
|
|
3502
|
-
*
|
|
3503
|
-
*
|
|
3090
|
+
* Watch the durable status of a locally-active job (internal-review).
|
|
3091
|
+
*
|
|
3092
|
+
* `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
|
|
3093
|
+
* holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
|
|
3094
|
+
* lands somewhere else, and that machine's store-only cancel (`cancelJob`)
|
|
3095
|
+
* commits the terminal status, appends the `cancel` message, and `pg_notify`s
|
|
3096
|
+
* in one transaction. This subscription is how the handler's machine hears it:
|
|
3097
|
+
* on each notify, re-read the durable status and adopt it if it went terminal.
|
|
3098
|
+
* Without it, the handler only notices at its next store write, and the
|
|
3099
|
+
* in-flight provider call — the spend the cancel was meant to stop — runs to
|
|
3100
|
+
* completion.
|
|
3101
|
+
*
|
|
3102
|
+
* Every locally-emitted message notifies this machine too, so the re-read is
|
|
3103
|
+
* coalesced: at most one `getCounters` in flight per job, and none once the
|
|
3104
|
+
* job is locally terminal.
|
|
3504
3105
|
*/
|
|
3505
|
-
|
|
3106
|
+
private watchDurableStatus;
|
|
3506
3107
|
/**
|
|
3507
|
-
*
|
|
3508
|
-
*
|
|
3108
|
+
* Tear down a job's durable-status subscription (internal-review). Must be called
|
|
3109
|
+
* wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
|
|
3110
|
+
* the store's shared LISTEN connection. Idempotent.
|
|
3509
3111
|
*/
|
|
3510
|
-
|
|
3112
|
+
unwatchDurableStatus(jobId: string): void;
|
|
3511
3113
|
/**
|
|
3512
|
-
*
|
|
3513
|
-
*
|
|
3514
|
-
*
|
|
3515
|
-
*
|
|
3516
|
-
*
|
|
3114
|
+
* NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
|
|
3115
|
+
*
|
|
3116
|
+
* A notify that arrives while a re-read is in flight is queued rather than
|
|
3117
|
+
* dropped: the in-flight read may have observed the row a moment *before* the
|
|
3118
|
+
* cancel committed, and dropping its notify would put the abort back where
|
|
3119
|
+
* this issue found it — waiting for the handler's next store write.
|
|
3517
3120
|
*/
|
|
3518
|
-
|
|
3121
|
+
private checkDurableTerminal;
|
|
3519
3122
|
/**
|
|
3520
|
-
*
|
|
3521
|
-
*
|
|
3522
|
-
*
|
|
3523
|
-
*
|
|
3524
|
-
*
|
|
3525
|
-
*
|
|
3123
|
+
* Wire the Tx A appender when the store supports streaming.
|
|
3124
|
+
*
|
|
3125
|
+
* Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
|
|
3126
|
+
* the same transaction. The DB allocates the row's seq via
|
|
3127
|
+
* `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
|
|
3128
|
+
* traffic on different machines can never collide on the `(job_id, seq)` PK.
|
|
3129
|
+
* In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
|
|
3130
|
+
*
|
|
3131
|
+
* Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
|
|
3132
|
+
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
3133
|
+
* `complete`) commit their `appendOutgoing` transactions in call order.
|
|
3134
|
+
* Without this, the two transactions race for the `jobs` row lock and the
|
|
3135
|
+
* DB-side seq can be allocated in the opposite order — which both renumbers
|
|
3136
|
+
* the persisted messages and reorders the NOTIFY events feeding the
|
|
3137
|
+
* cross-machine SSE subscriber. The subscriber closes the stream on the
|
|
3138
|
+
* first yield message it sees, so a NOTIFY for `complete` arriving before
|
|
3139
|
+
* `artifact`'s NOTIFY silently drops the artifact.
|
|
3526
3140
|
*/
|
|
3527
|
-
|
|
3141
|
+
private wireMessageAppender;
|
|
3142
|
+
/** Start (or restart) the idle timer for a job. */
|
|
3143
|
+
startIdleTimer(job: ServerJob): void;
|
|
3144
|
+
/** Clear the idle timer for a job. */
|
|
3145
|
+
clearIdleTimer(id: string): void;
|
|
3146
|
+
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
|
|
3147
|
+
cancelJobIdle(job: ServerJob): Promise<void>;
|
|
3528
3148
|
/**
|
|
3529
|
-
*
|
|
3530
|
-
*
|
|
3531
|
-
* the
|
|
3532
|
-
*
|
|
3533
|
-
*
|
|
3534
|
-
*
|
|
3535
|
-
*
|
|
3536
|
-
*
|
|
3149
|
+
* Force-terminate stale non-terminal jobs. Two status-aware arms:
|
|
3150
|
+
* • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
|
|
3151
|
+
* (`stale_no_terminal_status`) — the caller never paid / responded.
|
|
3152
|
+
* • `processing`/`working` past `processingWatchdogMs` → `failed`
|
|
3153
|
+
* (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
|
|
3154
|
+
* live workers fresh, so a stale row here means a dead process.
|
|
3155
|
+
*
|
|
3156
|
+
* Fires on a timer (wired in the constructor) and also callable on demand
|
|
3157
|
+
* (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
|
|
3158
|
+
* concurrently from multiple DVM machines. Scoped to capabilities this
|
|
3159
|
+
* JobManager owns so multi-mount hosts don't steal stuck rows from sibling
|
|
3160
|
+
* descriptors — the local activeJobs teardown only makes sense on the
|
|
3161
|
+
* JobManager that actually hosted the zombie handler.
|
|
3162
|
+
*
|
|
3163
|
+
* **Both cutoffs float off the wall clock (internal-review).** Each is one
|
|
3164
|
+
* `Date.now()` sample compared against `last_activity_at`, stamped from a
|
|
3165
|
+
* different sample at a different moment, so a backwards step between the
|
|
3166
|
+
* two makes every row look more recently active than it is: the query
|
|
3167
|
+
* matches nothing and both arms go silent for the length of the skew. The
|
|
3168
|
+
* dead worker's job keeps its `processing` status, its credit hold stays
|
|
3169
|
+
* `pending`, and the internal-review paid-job-death alert — wired to this reaper —
|
|
3170
|
+
* never fires. Same defect, same host class, as internal-review's orphan sweep.
|
|
3171
|
+
*
|
|
3172
|
+
* **The compensation is capped** — see {@link staleSweepCompensationCapMs}
|
|
3173
|
+
* for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
|
|
3174
|
+
* of an unclaimed hold, refused outright for any live job row; ours
|
|
3175
|
+
* force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
|
|
3176
|
+
* `expectedActivityBefore` is the same compensated threshold the query used,
|
|
3177
|
+
* and a live worker's post-step heartbeat writes the low, post-step
|
|
3178
|
+
* `Date.now()`, so `last_activity_at <= expectedActivityBefore` still holds.
|
|
3179
|
+
* The CAS guards a job checking in *between* the find and the claim with a
|
|
3180
|
+
* stamp above the cutoff — a race, not a clock.
|
|
3181
|
+
*
|
|
3182
|
+
* **Some blindness is inherent, and no cap choice removes it.** A backwards
|
|
3183
|
+
* step of `S` inverts the stamp ordering: everything stamped after it reads
|
|
3184
|
+
* `S` older than everything stamped before. A live worker heartbeating right
|
|
3185
|
+
* now stamps `wall - S`; a job genuinely idle for `age` stamps `wall - age`.
|
|
3186
|
+
* The stale row is the lower of the two — the one a cutoff reaches first —
|
|
3187
|
+
* only once `age` exceeds `S`. Below that the reaper cannot tell them apart,
|
|
3188
|
+
* so any cutoff that claimed the stale job would force-fail every live
|
|
3189
|
+
* worker on the host with it. The cap picks where on that curve to sit: on
|
|
3190
|
+
* the defaults a row must have been silent for `S + window / 2`, and a live
|
|
3191
|
+
* worker keeps five missed heartbeats of margin. The
|
|
3192
|
+
* alternative to a missed reap is mild and self-healing — the orphan sweep
|
|
3193
|
+
* backstops the hold, and the arm recovers when the wall catches up. The
|
|
3194
|
+
* alternative to a false reap is an irreversible force-fail of paid work.
|
|
3195
|
+
*
|
|
3196
|
+
* The stamps themselves stay wall-clock epoch-ms on purpose — they are
|
|
3197
|
+
* written by whichever machine touched the job and read by every other one,
|
|
3198
|
+
* so only the reading side can float.
|
|
3537
3199
|
*/
|
|
3538
|
-
|
|
3200
|
+
sweepStaleJobs(): Promise<{
|
|
3201
|
+
swept: number;
|
|
3202
|
+
}>;
|
|
3539
3203
|
/**
|
|
3540
|
-
*
|
|
3541
|
-
*
|
|
3542
|
-
*
|
|
3543
|
-
* `
|
|
3544
|
-
*
|
|
3204
|
+
* How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
|
|
3205
|
+
* clock to cover a backwards step (internal-review). The compensation is capped,
|
|
3206
|
+
* not free: a claimed row must still have been silent for
|
|
3207
|
+
* `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
|
|
3208
|
+
* force-fails a job that may well be alive.
|
|
3545
3209
|
*
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
3548
|
-
*
|
|
3549
|
-
*
|
|
3550
|
-
*
|
|
3551
|
-
*
|
|
3552
|
-
*
|
|
3553
|
-
* `orphanDrawAgeMs` that it must not release. It can only ever delay a
|
|
3554
|
-
* settle, never advance one, which is the safe direction for a debit.
|
|
3210
|
+
* **Half the window** is the floor that governs a wide window — five missed
|
|
3211
|
+
* heartbeats on the processing defaults. On the `awaiting-input` arm it also
|
|
3212
|
+
* preserves a real invariant: with the derived
|
|
3213
|
+
* `2 x idleTimeout + headroom`, half is `idleTimeout + 45s`, so the sweeper
|
|
3214
|
+
* still cannot fire before the in-process idle timer would have. (A builder
|
|
3215
|
+
* who overrides `staleJobTimeoutMs` below `2 x idleTimeout` has already
|
|
3216
|
+
* opted out of that, guard or no guard.)
|
|
3555
3217
|
*
|
|
3556
|
-
*
|
|
3557
|
-
*
|
|
3218
|
+
* **Two heartbeat intervals** is the floor that governs a window configured
|
|
3219
|
+
* close to the beat cadence — `processingWatchdog: 40` against the 30s
|
|
3220
|
+
* default beat, where half the window is less than one beat and a worker
|
|
3221
|
+
* heartbeating exactly on schedule would be reaped the moment the host's
|
|
3222
|
+
* clock stepped. That pair is marginal already; the guard must not make it
|
|
3223
|
+
* deterministic. The cap collapses to zero there, which is today's
|
|
3224
|
+
* behaviour: late, never wrong. The `awaiting-input` arm takes no such term
|
|
3225
|
+
* — nothing heartbeats it by design, and its refresh is an inbound caller
|
|
3226
|
+
* message on no cadence at all.
|
|
3227
|
+
*
|
|
3228
|
+
* **Why a magnitude and not a predicate.** The tempting sharper rule is to
|
|
3229
|
+
* compensate per row, fully, for any stamp that reads ahead of our wall —
|
|
3230
|
+
* one our own post-step heartbeat could not have written. It is unsafe: a
|
|
3231
|
+
* future-stamped row is equally what a live job heartbeated by a peer whose
|
|
3232
|
+
* clock runs fast looks like, and from the stamp alone the two are
|
|
3233
|
+
* indistinguishable. The same reasoning bounds the cap from the other side —
|
|
3234
|
+
* while our clock is stepped, the guard force-fails the live jobs of any
|
|
3235
|
+
* peer slower than `windowMs - cap`, which is 2.5 minutes of tolerated
|
|
3236
|
+
* disagreement on the defaults, far outside anything NTP-managed hosts show.
|
|
3558
3237
|
*/
|
|
3559
|
-
|
|
3238
|
+
private staleSweepCompensationCapMs;
|
|
3560
3239
|
/**
|
|
3561
|
-
*
|
|
3562
|
-
*
|
|
3563
|
-
*
|
|
3564
|
-
|
|
3565
|
-
|
|
3240
|
+
* Redact terminal job content beyond this DVM's configured window.
|
|
3241
|
+
*
|
|
3242
|
+
* The scan advances over every candidate, including rows that fail draw
|
|
3243
|
+
* resolution, so one money-path fault cannot starve everything behind it.
|
|
3244
|
+
* Any failed row remains unredacted and the next tick restarts from the head;
|
|
3245
|
+
* a page-capped clean pass instead retains its cursor and resumes at the
|
|
3246
|
+
* tail. Failure and recovery callbacks are awaited but never allowed to
|
|
3247
|
+
* change the sweep result.
|
|
3248
|
+
*/
|
|
3249
|
+
sweepJobRetention(): Promise<{
|
|
3250
|
+
examined: number;
|
|
3251
|
+
redacted: number;
|
|
3252
|
+
complete: boolean;
|
|
3253
|
+
}>;
|
|
3254
|
+
/** Resolve every hold and persist any configured receipt before content disappears. */
|
|
3255
|
+
private prepareJobForRedaction;
|
|
3256
|
+
private notifyJobRetentionFailed;
|
|
3257
|
+
private notifyJobRetentionRecovered;
|
|
3566
3258
|
/**
|
|
3567
|
-
*
|
|
3568
|
-
*
|
|
3569
|
-
*
|
|
3570
|
-
*
|
|
3571
|
-
*
|
|
3259
|
+
* Resolve `pending` credit draws the terminal funnel can no longer reach.
|
|
3260
|
+
* Two arms over one scan, distinguished by whether the draw's job row exists.
|
|
3261
|
+
*
|
|
3262
|
+
* **No job row (internal-review) — release.** internal-review allocates the job id and
|
|
3263
|
+
* commits the ledger draw, with its `job_id` linkage, before `recordReplay`
|
|
3264
|
+
* and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
|
|
3265
|
+
* fail-closed `replay_detected` 401 in that window leaves a committed hold
|
|
3266
|
+
* that **no** terminal path can ever reach: `issueReceipt` →
|
|
3267
|
+
* `resolveCreditDraw` keys off the `creditId`/`drawId` persisted on the job
|
|
3268
|
+
* row, so with no row there is no release path at all and the caller's
|
|
3269
|
+
* available balance stays reduced forever. Cosmetic for an implicit N=1
|
|
3270
|
+
* credit, a silent balance shrink for an explicit N>1 one.
|
|
3271
|
+
*
|
|
3272
|
+
* **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
|
|
3273
|
+
* demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
|
|
3274
|
+
* `issueReceipt` catches it, logs `credit_resolve_failed` and returns
|
|
3275
|
+
* nothing, and until now nothing retried. Same stranded hold, reached through
|
|
3276
|
+
* a different door — and for a `completed` job it strands the platform's
|
|
3277
|
+
* books too, since draws *are* the revenue events (internal-review): the caller is
|
|
3278
|
+
* charged nothing, the balance never moves, and outstanding liability
|
|
3279
|
+
* (`deposits − draw revenue`) overstates forever. So a late settle books, off
|
|
3280
|
+
* the committed draw row, through the ordinary reporter path.
|
|
3281
|
+
*
|
|
3282
|
+
* **A non-terminal job row is still never touched** — the stale-job sweep
|
|
3283
|
+
* above drives those to terminal first, and its `issueReceiptForStoredJob`
|
|
3284
|
+
* resolves the draw on the way. Those skipped holds are why the scan
|
|
3285
|
+
* keyset-paginates rather than re-reading one batch: they stay `pending`
|
|
3286
|
+
* indefinitely (an `awaiting-input` job on a 30-minute idle timeout sits well
|
|
3287
|
+
* past the orphan cutoff), so a fixed first page of them would starve every
|
|
3288
|
+
* real orphan behind it, on every tick, forever. The cursor advances over
|
|
3289
|
+
* every row examined; a tick that hits {@link ORPHAN_DRAW_SWEEP_MAX_PAGES}
|
|
3290
|
+
* logs `orphan_draw_sweep_truncated` and the next one resumes where it
|
|
3291
|
+
* stopped, so progress is bounded per tick but never blocked.
|
|
3292
|
+
*
|
|
3293
|
+
* Fires on a timer (wired in the constructor) and also callable on demand
|
|
3294
|
+
* (tests, ops). Both `settle` and `release` are idempotent under the credit
|
|
3295
|
+
* row's `FOR UPDATE`, so concurrent sweepers on several machines — and a
|
|
3296
|
+
* sweeper racing the in-line funnel — resolve the hold exactly once; the
|
|
3297
|
+
* losers see `replayed: true` and book nothing.
|
|
3572
3298
|
*/
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
* A deposit this payment funded that the job row did **not** take (internal-review).
|
|
3578
|
-
*
|
|
3579
|
-
* Reached when two mid-job top-ups race on a free-then-paid job: each opens its
|
|
3580
|
-
* own implicit credit, and `verifyAndCredit`'s set-if-null bind keeps one. The
|
|
3581
|
-
* loser's money is real, owned by the caller, and reclaimable — but nothing
|
|
3582
|
-
* named it, so the caller would have to derive `imp:<rail>:<payment_id>` to
|
|
3583
|
-
* reach their own balance. This rides the 201 so they don't have to.
|
|
3584
|
-
*
|
|
3585
|
-
* The response is the fast surface, not the only one: the credit is opened
|
|
3586
|
-
* under the caller's verified pubkey, so `POST /v1/credit` op `balance` lists
|
|
3587
|
-
* it and op `drain` reclaims it once the hold resolves.
|
|
3588
|
-
*/
|
|
3589
|
-
interface UnappliedCredit {
|
|
3590
|
-
credit_id: string;
|
|
3591
|
-
credited_micro: number;
|
|
3592
|
-
credit_currency: string;
|
|
3593
|
-
display: string;
|
|
3594
|
-
hint: string;
|
|
3595
|
-
}
|
|
3596
|
-
/**
|
|
3597
|
-
* What {@link JobManager.processPayment} tells the route. `error` is a refusal
|
|
3598
|
-
* to serialise verbatim; otherwise the payment was accepted, optionally
|
|
3599
|
-
* carrying an {@link UnappliedCredit} to fold into the 201.
|
|
3600
|
-
*/
|
|
3601
|
-
type ProcessPaymentOutcome = {
|
|
3602
|
-
error: {
|
|
3603
|
-
body: Record<string, unknown>;
|
|
3604
|
-
status: number;
|
|
3605
|
-
};
|
|
3606
|
-
unappliedCredit?: undefined;
|
|
3607
|
-
} | {
|
|
3608
|
-
error?: undefined;
|
|
3609
|
-
unappliedCredit?: UnappliedCredit;
|
|
3610
|
-
};
|
|
3611
|
-
/**
|
|
3612
|
-
* Transport-agnostic job lifecycle manager.
|
|
3613
|
-
*
|
|
3614
|
-
* Handles job creation, handler invocation, message dispatch, persistence,
|
|
3615
|
-
* idle timeout, and durable replay. Shared by both the HTTP server and
|
|
3616
|
-
* relay provider.
|
|
3617
|
-
*/
|
|
3618
|
-
declare class JobManager<State, InputSchema extends ZodLike | undefined> {
|
|
3619
|
-
private readonly descriptor;
|
|
3620
|
-
private readonly opts;
|
|
3621
|
-
private readonly _activeJobs;
|
|
3622
|
-
private readonly jobStore;
|
|
3623
|
-
private readonly idleTimers;
|
|
3624
|
-
private readonly cleanupTimers;
|
|
3625
|
-
/** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
|
|
3626
|
-
private readonly statusWatchers;
|
|
3627
|
-
/** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
|
|
3628
|
-
private readonly statusChecksInFlight;
|
|
3629
|
-
/** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
|
|
3630
|
-
private readonly statusChecksQueued;
|
|
3631
|
-
private readonly sessionId;
|
|
3632
|
-
private nextJobId;
|
|
3633
|
-
private accumulatorMonitor?;
|
|
3634
|
-
private readonly staleJobTimeoutMs;
|
|
3635
|
-
private readonly processingWatchdogMs;
|
|
3636
|
-
private readonly reactivationClaimGraceMs;
|
|
3299
|
+
sweepOrphanDraws(): Promise<{
|
|
3300
|
+
released: number;
|
|
3301
|
+
reconciled: number;
|
|
3302
|
+
}>;
|
|
3637
3303
|
/**
|
|
3638
|
-
*
|
|
3639
|
-
*
|
|
3640
|
-
*
|
|
3304
|
+
* The end-of-scan line for a tick that resolved nothing (internal-review, extended
|
|
3305
|
+
* by internal-review). Two shapes are worth a warning, and neither is the ordinary
|
|
3306
|
+
* one — the timer runs every 5 minutes per ledger in production.
|
|
3307
|
+
*
|
|
3308
|
+
* `examined > 0`: aged candidates were looked at and none moved. Usually
|
|
3309
|
+
* legitimate (their jobs are still running, or terminal inside the reconcile
|
|
3310
|
+
* grace period), but it is also what a silently skipped hold looks like.
|
|
3311
|
+
*
|
|
3312
|
+
* `examined === 0`: the query returned nothing. Ordinarily that means there
|
|
3313
|
+
* is nothing to do — but it is equally what a blinded scan looks like, which
|
|
3314
|
+
* is the case the internal-review gate could not reach. Probe for the oldest
|
|
3315
|
+
* `pending` hold at any age and speak up when one exists the scan should
|
|
3316
|
+
* have seen and didn't: already past the age gate outright, stamped ahead of
|
|
3317
|
+
* our clock (some other process's clock is fast), or hidden while our own
|
|
3318
|
+
* clock is held forward over a backwards step. A ledger holding nothing but
|
|
3319
|
+
* young draws stays quiet.
|
|
3320
|
+
*
|
|
3321
|
+
* The probe is diagnostics, so it is wrapped: a tick that swept cleanly must
|
|
3322
|
+
* never report failure because the extra read fell over. A probe that throws
|
|
3323
|
+
* says so on the same line rather than replacing the tick's outcome.
|
|
3641
3324
|
*/
|
|
3642
|
-
private
|
|
3643
|
-
private readonly orphanDrawAgeMs;
|
|
3644
|
-
private readonly terminalDrawReconcileAgeMs;
|
|
3645
|
-
private staleSweepTimer?;
|
|
3646
|
-
private heartbeatTimer?;
|
|
3647
|
-
private orphanDrawSweepTimer?;
|
|
3648
|
-
/** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
|
|
3649
|
-
private orphanSweepClaimed;
|
|
3650
|
-
/** Where a page-capped tick left off; cleared once a tick reaches the end. */
|
|
3651
|
-
private orphanSweepResumeFrom?;
|
|
3325
|
+
private reportSweepOutcome;
|
|
3652
3326
|
/**
|
|
3653
|
-
*
|
|
3654
|
-
*
|
|
3655
|
-
*
|
|
3656
|
-
*
|
|
3657
|
-
*
|
|
3327
|
+
* Resolve one aged `pending` draw whose job row reached terminal (internal-review),
|
|
3328
|
+
* mirroring what `resolveCreditDraw` would have done in line. Returns true
|
|
3329
|
+
* when this call is the one that moved the draw — the caller counts it, and
|
|
3330
|
+
* only it books.
|
|
3331
|
+
*
|
|
3332
|
+
* **The settle is gated on the job row naming _this_ draw.** The scan finds
|
|
3333
|
+
* the draw by `credit_draws.job_id`, but the job's own `credit_id`/`draw_id`
|
|
3334
|
+
* are written by a *later* transaction than the one that committed the draw
|
|
3335
|
+
* (`verifyIncomingPayment` commits fund+draw; `verifyAndCredit` binds the
|
|
3336
|
+
* row), and a free-then-paid job binds set-if-null. A store failure or a
|
|
3337
|
+
* second concurrent top-up therefore leaves a hold tagged with the job whose
|
|
3338
|
+
* row points somewhere else, or nowhere. Settling that would debit the caller
|
|
3339
|
+
* a second time for one job — and invisibly, since `revenue_events`'
|
|
3340
|
+
* `UNIQUE (dvm_id, job_id, rail)` would drop the booking. So an unbound hold
|
|
3341
|
+
* is **released**, never settled: the job's payment was the draw it is bound
|
|
3342
|
+
* to, and this one bought nothing.
|
|
3343
|
+
*
|
|
3344
|
+
* Concurrency needs no guard of its own. `settle`/`release` run under the
|
|
3345
|
+
* credit row's `FOR UPDATE`, so exactly one caller — a sibling reconciler,
|
|
3346
|
+
* or the in-line funnel arriving late — sees `replayed: false`, which makes
|
|
3347
|
+
* the booking gate a true mutex rather than a hopeful one. The one race left
|
|
3348
|
+
* open is another machine's `handleJobTerminal` stalled between resolving the
|
|
3349
|
+
* draw and booking it, which would double-report; the platform's
|
|
3350
|
+
* `(dvm_id, credit_id, draw_id)` and `(dvm_id, job_id, rail)` unique indexes
|
|
3351
|
+
* absorb that.
|
|
3352
|
+
*
|
|
3353
|
+
* `invalid_draw_state` — the funnel having already resolved the draw to the
|
|
3354
|
+
* other state — is the expected loss, logged and swallowed like the orphan
|
|
3355
|
+
* arm's, so one contended hold can't strand the rest of the batch.
|
|
3658
3356
|
*/
|
|
3659
|
-
private
|
|
3660
|
-
private sweepInFlight;
|
|
3661
|
-
private orphanSweepInFlight;
|
|
3662
|
-
private heartbeatInFlight;
|
|
3357
|
+
private reconcileTerminalDraw;
|
|
3663
3358
|
/**
|
|
3664
|
-
*
|
|
3665
|
-
*
|
|
3666
|
-
*
|
|
3359
|
+
* Refresh `lastActivityAt` for jobs this process is actively running
|
|
3360
|
+
* (`processing`/`working`) so the processing watchdog only fires once the
|
|
3361
|
+
* worker is genuinely dead (internal-review). `awaiting-input` jobs are
|
|
3362
|
+
* deliberately excluded — their idle timeout must still elapse. Fires on the
|
|
3363
|
+
* heartbeat timer; also callable on demand for tests.
|
|
3667
3364
|
*/
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
get activeJobs(): Map<string, ServerJob>;
|
|
3672
|
-
/** The backing job store. */
|
|
3673
|
-
get store(): JobStore;
|
|
3365
|
+
heartbeatActiveJobs(): Promise<{
|
|
3366
|
+
beat: number;
|
|
3367
|
+
}>;
|
|
3674
3368
|
/**
|
|
3675
|
-
*
|
|
3676
|
-
*
|
|
3677
|
-
*
|
|
3678
|
-
*
|
|
3369
|
+
* Single-execution claim for a reactivating machine (internal-review). Before
|
|
3370
|
+
* replaying an `awaiting-input` job, wait a bounded grace window for a live
|
|
3371
|
+
* original handler to win the `awaiting-input → processing` CAS via its
|
|
3372
|
+
* NOTIFY wake. Resolves:
|
|
3373
|
+
* • `false` — the row left `awaiting-input` during the window (the original
|
|
3374
|
+
* handler claimed it / drove it terminal). Stand down; do not replay.
|
|
3375
|
+
* • `true` — the window elapsed still `awaiting-input` (the original worker
|
|
3376
|
+
* is gone or has no live resolver) AND this machine won the claim CAS.
|
|
3377
|
+
* Replay for dead-worker recovery.
|
|
3378
|
+
*
|
|
3379
|
+
* Biasing the original handler to win kills the double execution (double
|
|
3380
|
+
* substrate spend, clobbered artifact) without heartbeating `awaiting-input`
|
|
3381
|
+
* (which would break the caller-input idle timeout). Non-streamable stores
|
|
3382
|
+
* have no cross-machine race, so the claim is a no-op `true`.
|
|
3679
3383
|
*/
|
|
3680
|
-
|
|
3681
|
-
/** True when the named capability exists on this descriptor. */
|
|
3682
|
-
hasCapability(name: string): boolean;
|
|
3683
|
-
/** Names of every capability this descriptor exposes — used for error envelopes. */
|
|
3684
|
-
capabilityNames(): string[];
|
|
3384
|
+
claimAwaitingInputForReactivation(jobId: string): Promise<boolean>;
|
|
3685
3385
|
/**
|
|
3686
|
-
*
|
|
3386
|
+
* Record an inbound client message durably via Tx B (internal-review) and append
|
|
3387
|
+
* it to the in-memory `job.messages` array. Returns the DB-allocated seq
|
|
3388
|
+
* (`null` when the store isn't streamable — non-Postgres test fallback).
|
|
3687
3389
|
*
|
|
3688
|
-
*
|
|
3689
|
-
*
|
|
3690
|
-
*
|
|
3390
|
+
* Status starts as `pending-verification`; a follow-up Tx C call
|
|
3391
|
+
* (`processPayment` for payments, `markInboundVerified` for everything
|
|
3392
|
+
* else) transitions it to `verified` once the inbound is accepted.
|
|
3691
3393
|
*/
|
|
3692
|
-
|
|
3394
|
+
recordInboundPending(job: ServerJob, msgType: MessageType, msgContent: Record<string, unknown>): Promise<number | null>;
|
|
3693
3395
|
/**
|
|
3694
|
-
*
|
|
3695
|
-
*
|
|
3696
|
-
* Returns `undefined` for dynamic-priced capabilities and for unknown
|
|
3697
|
-
* names (the route handler validates the name before invoking this).
|
|
3396
|
+
* Tx C complement for non-payment inbound messages — flip the row to
|
|
3397
|
+
* `verified` so cross-machine readers see it via `subscribeMessages`.
|
|
3698
3398
|
*/
|
|
3699
|
-
|
|
3700
|
-
amount: number;
|
|
3701
|
-
currency: string;
|
|
3702
|
-
} | undefined;
|
|
3399
|
+
markInboundVerified(jobId: string, inboundSeq: number | null): Promise<void>;
|
|
3703
3400
|
/**
|
|
3704
|
-
*
|
|
3705
|
-
*
|
|
3706
|
-
*
|
|
3707
|
-
*
|
|
3708
|
-
*
|
|
3709
|
-
*
|
|
3710
|
-
*
|
|
3711
|
-
* 3. Neither usable → throw `MissingStructuredInputError` so the route can
|
|
3712
|
-
* return `invalid_input` with a hint pointing at `/v1/info`.
|
|
3401
|
+
* Reactivate a suspended/event-driven job from the store. Builds the
|
|
3402
|
+
* in-memory `ServerJob`, wires the appender, builds the context, starts
|
|
3403
|
+
* the idle timer. The caller has already recorded the inbound via Tx B
|
|
3404
|
+
* and run Tx C (verify + credit); reactivation is now decoupled from
|
|
3405
|
+
* credit (internal-review, internal-review). The caller decides whether to run the
|
|
3406
|
+
* handler (Pattern A: status was `awaiting-input`) or to dispatch the
|
|
3407
|
+
* message to a custom handler (Pattern B).
|
|
3713
3408
|
*
|
|
3714
|
-
*
|
|
3715
|
-
*
|
|
3409
|
+
* For DVMs that declare descriptor-level auth, the persisted
|
|
3410
|
+
* `record.input` carries the original signed envelope. We re-verify the
|
|
3411
|
+
* signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
|
|
3412
|
+
* injection, malicious operator with DB access — can't silently feed an
|
|
3413
|
+
* attacker-supplied pubkey/envelope into the handler. The drift window
|
|
3414
|
+
* and replay store are deliberately skipped: the persisted timestamp is
|
|
3415
|
+
* from the original signing instant (long past the 5-min drift bound for
|
|
3416
|
+
* any long-running job), and the nonce was already committed at submission.
|
|
3417
|
+
* On failure: throws `SignedRequestError` ({@link AuthSchemaError} when
|
|
3418
|
+
* the capability has no input schema to verify against); the caller is
|
|
3419
|
+
* expected to mark the row failed and surface a 401 / 500 to the inbound
|
|
3420
|
+
* caller.
|
|
3716
3421
|
*
|
|
3717
|
-
*
|
|
3718
|
-
*
|
|
3719
|
-
*
|
|
3720
|
-
*
|
|
3721
|
-
*
|
|
3722
|
-
*
|
|
3723
|
-
|
|
3724
|
-
parseInput(body: {
|
|
3725
|
-
input?: string;
|
|
3726
|
-
data?: unknown;
|
|
3727
|
-
}, capability: string): unknown;
|
|
3728
|
-
/**
|
|
3729
|
-
* Look up the per-capability descriptor by name (internal-review). Throws when the
|
|
3730
|
-
* name doesn't exist — the route layer validates body.capability first, so
|
|
3731
|
-
* reaching this with an unknown name is a programmer error.
|
|
3422
|
+
* What gets re-verified is the persisted row read back through
|
|
3423
|
+
* {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
|
|
3424
|
+
* those are the caller's own signed bytes, the same ones `/v1/quote` and the
|
|
3425
|
+
* submit checked (internal-review). That makes the tamper check strictly stronger
|
|
3426
|
+
* than the parsed form it replaced: an injected key the capability schema
|
|
3427
|
+
* doesn't declare used to be stripped before the signature was checked, so
|
|
3428
|
+
* it re-verified clean.
|
|
3732
3429
|
*/
|
|
3733
|
-
|
|
3430
|
+
reactivateJob(record: JobRecord): {
|
|
3431
|
+
job: ServerJob;
|
|
3432
|
+
sdkCtx: SDKJobContext<unknown, unknown>;
|
|
3433
|
+
};
|
|
3734
3434
|
/**
|
|
3735
|
-
*
|
|
3736
|
-
*
|
|
3737
|
-
*
|
|
3738
|
-
*
|
|
3739
|
-
*
|
|
3435
|
+
* Verify an inbound payment and apply the credit via Tx C (internal-review).
|
|
3436
|
+
* The inbound message must have been recorded via Tx B already
|
|
3437
|
+
* (`recordInboundPending` returned `inboundSeq`). For non-streamable
|
|
3438
|
+
* stores (`inboundSeq === null`), falls back to in-memory mutation via
|
|
3439
|
+
* `applyPaymentInfoToJob`.
|
|
3440
|
+
*
|
|
3441
|
+
* Dev-mode short-circuit: auto-credits the outstanding
|
|
3442
|
+
* `pendingPaymentMsats` without external verification. Gated on
|
|
3443
|
+
* `devModeSkipsPaymentVerification` — the same predicate the upfront path
|
|
3444
|
+
* reads, so a dev server wired to a mint verifies mid-job payments for real
|
|
3445
|
+
* (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
|
|
3446
|
+
* which is the one caller that has no wallet to pay from. `dev_auto` loses to
|
|
3447
|
+
* any proof riding the same message: money on the wire always takes the rail,
|
|
3448
|
+
* so the flag can never leave a real token unspent against a credited job.
|
|
3449
|
+
*
|
|
3450
|
+
* Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
|
|
3451
|
+
* commits the proofs *and* the ledger leg in one transaction and returns;
|
|
3452
|
+
* only then does `verifyAndCredit` write the job row, on its own connection.
|
|
3453
|
+
* Everything after that call therefore runs with the caller's money already
|
|
3454
|
+
* moved, which is why the write is retried rather than left to throw, and why
|
|
3455
|
+
* an exhausted retry answers with the credit it landed on instead of a bare
|
|
3456
|
+
* 500. See `creditInbound`.
|
|
3740
3457
|
*/
|
|
3741
|
-
|
|
3458
|
+
processPayment(job: ServerJob, body: {
|
|
3459
|
+
type: MessageType;
|
|
3460
|
+
content: Record<string, unknown>;
|
|
3461
|
+
}, inboundSeq: number | null, extra?: {
|
|
3462
|
+
lockPubkeys?: LockPubkey[];
|
|
3463
|
+
mintHealthTracker?: MintHealthTracker;
|
|
3464
|
+
}): Promise<ProcessPaymentOutcome>;
|
|
3742
3465
|
/**
|
|
3743
|
-
*
|
|
3744
|
-
*
|
|
3745
|
-
*
|
|
3746
|
-
*
|
|
3747
|
-
*
|
|
3748
|
-
*
|
|
3466
|
+
* Tx C with a bounded retry (internal-review).
|
|
3467
|
+
*
|
|
3468
|
+
* By the time this runs on the verified path, the rail commit and the ledger
|
|
3469
|
+
* leg have already committed in a transaction this call is not part of — so a
|
|
3470
|
+
* throw here is money moved against a job row that records none of it, and
|
|
3471
|
+
* letting it propagate was the bug. A transient store error is the ordinary
|
|
3472
|
+
* cause and a second attempt clears it.
|
|
3473
|
+
*
|
|
3474
|
+
* The retry is safe by construction, not by convention: `verifyAndCredit`
|
|
3475
|
+
* CASes on `job_messages.status = 'pending-verification'` inside its own
|
|
3476
|
+
* transaction, so a call whose COMMIT actually landed before the ack was lost
|
|
3477
|
+
* finds the row already `verified` and returns `alreadyVerified: true`
|
|
3478
|
+
* carrying the counters and binding that first attempt committed. Re-applying
|
|
3479
|
+
* the delta is impossible either way.
|
|
3480
|
+
*
|
|
3481
|
+
* That argument covers every attempt but the last, whose error nothing
|
|
3482
|
+
* re-checks — and a connection-level fault is exactly the shape that loses
|
|
3483
|
+
* the ack on *all* of them, idempotent replays included (the no-op path
|
|
3484
|
+
* COMMITs too). So the exhausted path asks the row before reporting a
|
|
3485
|
+
* failure: `getVerifiedInbound` reads, transaction-free, whether a Tx C for
|
|
3486
|
+
* this seq committed. Without it a fully credited, fully bound job answers
|
|
3487
|
+
* `payment_unapplied` and invites a retry, and a caller who takes it pays
|
|
3488
|
+
* twice for one ask — the second payment growing the now-bound draw and
|
|
3489
|
+
* settling as revenue.
|
|
3490
|
+
*
|
|
3491
|
+
* The read runs on the pool that just failed those COMMITs, so it is retried
|
|
3492
|
+
* too, and a read that fails all the way through is reported as *unknown*
|
|
3493
|
+
* rather than as nothing: `CreditInboundExhaustedError.readError` is what
|
|
3494
|
+
* separates "checked the row, nothing landed" from "couldn't check", both on
|
|
3495
|
+
* the operator's log line and in the copy the caller acts on.
|
|
3749
3496
|
*/
|
|
3750
|
-
|
|
3497
|
+
private creditInbound;
|
|
3751
3498
|
/**
|
|
3752
|
-
*
|
|
3753
|
-
*
|
|
3754
|
-
*
|
|
3755
|
-
*
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3499
|
+
* Answer a payment whose rail and ledger legs committed but whose job row
|
|
3500
|
+
* could not be written (internal-review) — every retry spent, the money real.
|
|
3501
|
+
*
|
|
3502
|
+
* The refusal names the credit the money landed on so the caller can act on
|
|
3503
|
+
* it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
|
|
3504
|
+
* 500 rather than a 402 on purpose: the payment was valid and was accepted,
|
|
3505
|
+
* so telling the caller it was insufficient would be a lie, and this is a
|
|
3506
|
+
* fault an operator should see in their 5xx rate. `retryable` is true because
|
|
3507
|
+
* the ask is genuinely still outstanding.
|
|
3508
|
+
*
|
|
3509
|
+
* Nothing is released here. The hold is left to the internal-review reconciler,
|
|
3510
|
+
* which makes the same decision — unbound draw ⇒ release, never settle — but
|
|
3511
|
+
* under the credit row's `FOR UPDATE`, after the job is terminal, where it
|
|
3512
|
+
* cannot race an in-flight COMMIT. Releasing from here would also be
|
|
3513
|
+
* irreversible: a released draw can be neither re-grown nor re-opened, so one
|
|
3514
|
+
* bad call would silently de-ledger every later top-up on the job.
|
|
3515
|
+
*
|
|
3516
|
+
* Whether the row was *read* is load-bearing on both surfaces. When the
|
|
3517
|
+
* post-exhaustion read-back failed as well, `credit_job_row_unwritten` says so
|
|
3518
|
+
* (`row_checked: false` plus the read's own error) instead of implying an
|
|
3519
|
+
* operator can trust the row is clean, and the caller gets the `"unconfirmed"`
|
|
3520
|
+
* copy rather than a flat "pay it again" it could be charged twice for.
|
|
3521
|
+
*
|
|
3522
|
+
* That copy overrides `display` as well as `hint`, on every branch including
|
|
3523
|
+
* the ledger-less one. `display` is the sentence an agent relays to its human
|
|
3524
|
+
* (repo convention: JSON carries the words, not just the facts), so a body
|
|
3525
|
+
* whose `hint` says the outcome is unknown while its `display` still asserts
|
|
3526
|
+
* the ask is outstanding is read as "pay again" by the audience that acts on
|
|
3527
|
+
* it — and a payment that did land is charged on top.
|
|
3528
|
+
*/
|
|
3529
|
+
private reportUnappliedPayment;
|
|
3779
3530
|
/**
|
|
3780
|
-
*
|
|
3781
|
-
*
|
|
3782
|
-
*
|
|
3783
|
-
*
|
|
3784
|
-
*
|
|
3785
|
-
* 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
|
|
3786
|
-
* can't race a still-pending `appendOutgoing` for the final yield
|
|
3787
|
-
* message (which would otherwise wipe the row before an in-flight SSE
|
|
3788
|
-
* subscriber's NOTIFY-driven fetch can see it).
|
|
3531
|
+
* Describe a deposit the job row never took, in the shape both the 201 and
|
|
3532
|
+
* the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
|
|
3533
|
+
* ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
|
|
3534
|
+
* there is no credit to name, so the 201 carries no `credit` block and the
|
|
3535
|
+
* 500 takes its words from `unappliedCopy` directly.
|
|
3789
3536
|
*/
|
|
3790
|
-
|
|
3537
|
+
private unappliedCredit;
|
|
3538
|
+
/** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
|
|
3539
|
+
private reportCreditDeposit;
|
|
3791
3540
|
/**
|
|
3792
|
-
*
|
|
3793
|
-
*
|
|
3794
|
-
*
|
|
3795
|
-
|
|
3796
|
-
|
|
3541
|
+
* Internal: build the BuildContextOpts shared between `createJob` and
|
|
3542
|
+
* `reactivateJob`. Centralised so persistence/terminal callbacks and the
|
|
3543
|
+
* internal-review NOTIFY-driven cross-machine wake stay in one place.
|
|
3544
|
+
*/
|
|
3545
|
+
private buildContextOpts;
|
|
3546
|
+
/**
|
|
3547
|
+
* Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
|
|
3548
|
+
* job's remaining pool in (internal-review).
|
|
3797
3549
|
*
|
|
3798
|
-
*
|
|
3799
|
-
*
|
|
3800
|
-
* the
|
|
3801
|
-
*
|
|
3802
|
-
*
|
|
3550
|
+
* The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
|
|
3551
|
+
* `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
|
|
3552
|
+
* from the same rail payment id — a fact about a row that was written once
|
|
3553
|
+
* and never moves. The tempting alternative, comparing `job.payment_tx_hash`
|
|
3554
|
+
* against `drawSettlementRef` (what the mid-job preflight does to inherit
|
|
3555
|
+
* `drawBasis`), silently rots: Tx C overwrites that column on every mid-job
|
|
3556
|
+
* credit, and a payment whose ledger leg was skipped — a cross-rail top-up,
|
|
3557
|
+
* an fx outage, a draw already resolved — leaves the rail's own reference
|
|
3558
|
+
* there, so an explicit-draw job would read as implicit from then on.
|
|
3803
3559
|
*
|
|
3804
|
-
*
|
|
3805
|
-
*
|
|
3806
|
-
*
|
|
3807
|
-
*
|
|
3808
|
-
*
|
|
3809
|
-
*
|
|
3560
|
+
* Never throws, and never answers `rail` on a guess. `rail` is claimed only
|
|
3561
|
+
* where `withDrawBasis` demonstrably left `PaymentInfo.paidMsats` alone — no
|
|
3562
|
+
* draw at all, or a draw carrying no rail value to overlay it with. A job that
|
|
3563
|
+
* drew and whose pool this can't read or denominate answers `unpriced`, which
|
|
3564
|
+
* the gate declines to auto-credit from: the msat figure it would otherwise
|
|
3565
|
+
* fall back on is a slice of the credit's rail value at a ratio pinned
|
|
3566
|
+
* whenever that credit was funded, which is the exact figure this issue exists
|
|
3567
|
+
* to stop spending against.
|
|
3810
3568
|
*/
|
|
3811
|
-
private
|
|
3569
|
+
private resolveDrawPool;
|
|
3570
|
+
/** Internal: structured warn for the one path that can't price a drawn job's pool. */
|
|
3571
|
+
private warnDrawPool;
|
|
3812
3572
|
/**
|
|
3813
|
-
*
|
|
3814
|
-
*
|
|
3815
|
-
*
|
|
3816
|
-
*
|
|
3817
|
-
*
|
|
3818
|
-
* commits the terminal status, appends the `cancel` message, and `pg_notify`s
|
|
3819
|
-
* in one transaction. This subscription is how the handler's machine hears it:
|
|
3820
|
-
* on each notify, re-read the durable status and adopt it if it went terminal.
|
|
3821
|
-
* Without it, the handler only notices at its next store write, and the
|
|
3822
|
-
* in-flight provider call — the spend the cancel was meant to stop — runs to
|
|
3823
|
-
* completion.
|
|
3573
|
+
* Issue the signed receipt for a job that has reached a terminal status
|
|
3574
|
+
* (internal-review). Idempotent and safe to call from every terminal path — the
|
|
3575
|
+
* store owns both the sequence allocation and the write-once persist, so
|
|
3576
|
+
* two machines racing on the same job converge on identical bytes and burn
|
|
3577
|
+
* exactly one sequence number.
|
|
3824
3578
|
*
|
|
3825
|
-
*
|
|
3826
|
-
*
|
|
3827
|
-
*
|
|
3579
|
+
* Never throws. A receipt is evidence about a job, not part of delivering
|
|
3580
|
+
* it: if signing or persistence fails we log structured and serve the
|
|
3581
|
+
* response without one (same posture as the `cashu_refund_failed` branch in
|
|
3582
|
+
* `context.ts`). The gap is visible to callers — the `seq` series skips a
|
|
3583
|
+
* number — which is precisely the completeness signal receipts exist for.
|
|
3828
3584
|
*/
|
|
3829
|
-
|
|
3585
|
+
issueReceipt(record: JobRecord): Promise<JobReceipt | undefined>;
|
|
3586
|
+
/** Build the common settle/release args, adding the hosted release outbox when wired. */
|
|
3587
|
+
private drawResolutionArgs;
|
|
3830
3588
|
/**
|
|
3831
|
-
*
|
|
3832
|
-
*
|
|
3833
|
-
* the
|
|
3589
|
+
* Settle or release a terminal job's credit draw (internal-review): `completed`
|
|
3590
|
+
* settles (the hold becomes a real debit); `failed`/`cancelled` releases
|
|
3591
|
+
* (the hold evaporates — "no debit on job failure", mechanically
|
|
3592
|
+
* superseding the internal-review/974 no-op refund for credit-paid jobs, including
|
|
3593
|
+
* the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
|
|
3594
|
+
* resolution returns the recorded state. Returns the `ReceiptCredit` block
|
|
3595
|
+
* for the receipt: `balance_after` is the recorded draw trajectory for a
|
|
3596
|
+
* settled draw, and the restored available balance for a released one.
|
|
3834
3597
|
*/
|
|
3835
|
-
|
|
3598
|
+
private resolveCreditDraw;
|
|
3836
3599
|
/**
|
|
3837
|
-
*
|
|
3838
|
-
*
|
|
3839
|
-
*
|
|
3840
|
-
*
|
|
3841
|
-
*
|
|
3842
|
-
* this issue found it — waiting for the handler's next store write.
|
|
3600
|
+
* The one place a terminal job's status becomes a ledger verb (internal-review):
|
|
3601
|
+
* `completed` settles the hold into a real debit, `failed`/`cancelled`
|
|
3602
|
+
* release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
|
|
3603
|
+
* late reconciler (`reconcileTerminalDraw`) — two paths that must never
|
|
3604
|
+
* disagree about what an outcome means for the caller's money.
|
|
3843
3605
|
*/
|
|
3844
|
-
private
|
|
3606
|
+
private settlesDraw;
|
|
3845
3607
|
/**
|
|
3846
|
-
*
|
|
3847
|
-
*
|
|
3848
|
-
*
|
|
3849
|
-
* the same transaction. The DB allocates the row's seq via
|
|
3850
|
-
* `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
|
|
3851
|
-
* traffic on different machines can never collide on the `(job_id, seq)` PK.
|
|
3852
|
-
* In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
|
|
3853
|
-
*
|
|
3854
|
-
* Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
|
|
3855
|
-
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
3856
|
-
* `complete`) commit their `appendOutgoing` transactions in call order.
|
|
3857
|
-
* Without this, the two transactions race for the `jobs` row lock and the
|
|
3858
|
-
* DB-side seq can be allocated in the opposite order — which both renumbers
|
|
3859
|
-
* the persisted messages and reorders the NOTIFY events feeding the
|
|
3860
|
-
* cross-machine SSE subscriber. The subscriber closes the stream on the
|
|
3861
|
-
* first yield message it sees, so a NOTIFY for `complete` arriving before
|
|
3862
|
-
* `artifact`'s NOTIFY silently drops the artifact.
|
|
3608
|
+
* Sign a locally-held terminal job's receipt and mirror it onto the
|
|
3609
|
+
* in-memory job, so the read paths served out of `activeJobs` during the
|
|
3610
|
+
* cleanup window hand back the same bytes as a cross-machine re-read.
|
|
3863
3611
|
*/
|
|
3864
|
-
private
|
|
3865
|
-
/** Start (or restart) the idle timer for a job. */
|
|
3866
|
-
startIdleTimer(job: ServerJob): void;
|
|
3867
|
-
/** Clear the idle timer for a job. */
|
|
3868
|
-
clearIdleTimer(id: string): void;
|
|
3869
|
-
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
|
|
3870
|
-
cancelJobIdle(job: ServerJob): Promise<void>;
|
|
3612
|
+
private attachReceipt;
|
|
3871
3613
|
/**
|
|
3872
|
-
*
|
|
3873
|
-
*
|
|
3874
|
-
*
|
|
3875
|
-
*
|
|
3876
|
-
* (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
|
|
3877
|
-
* live workers fresh, so a stale row here means a dead process.
|
|
3878
|
-
*
|
|
3879
|
-
* Fires on a timer (wired in the constructor) and also callable on demand
|
|
3880
|
-
* (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
|
|
3881
|
-
* concurrently from multiple DVM machines. Scoped to capabilities this
|
|
3882
|
-
* JobManager owns so multi-mount hosts don't steal stuck rows from sibling
|
|
3883
|
-
* descriptors — the local activeJobs teardown only makes sense on the
|
|
3884
|
-
* JobManager that actually hosted the zombie handler.
|
|
3885
|
-
*
|
|
3886
|
-
* **Both cutoffs float off the wall clock (internal-review).** Each is one
|
|
3887
|
-
* `Date.now()` sample compared against `last_activity_at`, stamped from a
|
|
3888
|
-
* different sample at a different moment, so a backwards step between the
|
|
3889
|
-
* two makes every row look more recently active than it is: the query
|
|
3890
|
-
* matches nothing and both arms go silent for the length of the skew. The
|
|
3891
|
-
* dead worker's job keeps its `processing` status, its credit hold stays
|
|
3892
|
-
* `pending`, and the internal-review paid-job-death alert — wired to this reaper —
|
|
3893
|
-
* never fires. Same defect, same host class, as internal-review's orphan sweep.
|
|
3894
|
-
*
|
|
3895
|
-
* **The compensation is capped** — see {@link staleSweepCompensationCapMs}
|
|
3896
|
-
* for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
|
|
3897
|
-
* of an unclaimed hold, refused outright for any live job row; ours
|
|
3898
|
-
* force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
|
|
3899
|
-
* `expectedActivityBefore` is the same compensated threshold the query used,
|
|
3900
|
-
* and a live worker's post-step heartbeat writes the low, post-step
|
|
3901
|
-
* `Date.now()`, so `last_activity_at <= expectedActivityBefore` still holds.
|
|
3902
|
-
* The CAS guards a job checking in *between* the find and the claim with a
|
|
3903
|
-
* stamp above the cutoff — a race, not a clock.
|
|
3904
|
-
*
|
|
3905
|
-
* **Some blindness is inherent, and no cap choice removes it.** A backwards
|
|
3906
|
-
* step of `S` inverts the stamp ordering: everything stamped after it reads
|
|
3907
|
-
* `S` older than everything stamped before. A live worker heartbeating right
|
|
3908
|
-
* now stamps `wall - S`; a job genuinely idle for `age` stamps `wall - age`.
|
|
3909
|
-
* The stale row is the lower of the two — the one a cutoff reaches first —
|
|
3910
|
-
* only once `age` exceeds `S`. Below that the reaper cannot tell them apart,
|
|
3911
|
-
* so any cutoff that claimed the stale job would force-fail every live
|
|
3912
|
-
* worker on the host with it. The cap picks where on that curve to sit: on
|
|
3913
|
-
* the defaults a row must have been silent for `S + window / 2`, and a live
|
|
3914
|
-
* worker keeps five missed heartbeats of margin. The
|
|
3915
|
-
* alternative to a missed reap is mild and self-healing — the orphan sweep
|
|
3916
|
-
* backstops the hold, and the arm recovers when the wall catches up. The
|
|
3917
|
-
* alternative to a false reap is an irreversible force-fail of paid work.
|
|
3614
|
+
* Read-path repair for a terminal job carrying no receipt (internal-review), for
|
|
3615
|
+
* either an in-memory job or a store record. Free in the steady state — it
|
|
3616
|
+
* returns on the `receipt` check without touching the store — so it costs
|
|
3617
|
+
* only on the cases it exists for:
|
|
3918
3618
|
*
|
|
3919
|
-
*
|
|
3920
|
-
*
|
|
3921
|
-
*
|
|
3619
|
+
* - **A crash between the two store calls.** `claimReceiptSeq` commits the
|
|
3620
|
+
* sequence number to the job row before `saveReceipt` writes the bytes; a
|
|
3621
|
+
* process death in that window would otherwise strand that number
|
|
3622
|
+
* forever, and a permanent gap is indistinguishable from the deliberate
|
|
3623
|
+
* suppression `seq` exists to expose. Re-issuing here reuses the already
|
|
3624
|
+
* committed number (the claim is idempotent per job) rather than
|
|
3625
|
+
* allocating a second one.
|
|
3626
|
+
* - **A transient store failure** at the terminal: `issueReceipt` logs
|
|
3627
|
+
* `receipt_issue_failed` and returns nothing rather than failing the job,
|
|
3628
|
+
* so the next read retries.
|
|
3629
|
+
* - **Jobs that terminated before the DVM had a receipt key.** They pick one
|
|
3630
|
+
* up on first read, with `issued_at` reflecting when it was signed.
|
|
3922
3631
|
*/
|
|
3923
|
-
|
|
3924
|
-
swept: number;
|
|
3925
|
-
}>;
|
|
3632
|
+
ensureReceipt(target: ServerJob | JobRecord): Promise<void>;
|
|
3926
3633
|
/**
|
|
3927
|
-
*
|
|
3928
|
-
*
|
|
3929
|
-
*
|
|
3930
|
-
*
|
|
3931
|
-
*
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
*
|
|
3936
|
-
*
|
|
3937
|
-
*
|
|
3938
|
-
*
|
|
3939
|
-
* opted out of that, guard or no guard.)
|
|
3940
|
-
*
|
|
3941
|
-
* **Two heartbeat intervals** is the floor that governs a window configured
|
|
3942
|
-
* close to the beat cadence — `processingWatchdog: 40` against the 30s
|
|
3943
|
-
* default beat, where half the window is less than one beat and a worker
|
|
3944
|
-
* heartbeating exactly on schedule would be reaped the moment the host's
|
|
3945
|
-
* clock stepped. That pair is marginal already; the guard must not make it
|
|
3946
|
-
* deterministic. The cap collapses to zero there, which is today's
|
|
3947
|
-
* behaviour: late, never wrong. The `awaiting-input` arm takes no such term
|
|
3948
|
-
* — nothing heartbeats it by design, and its refresh is an inbound caller
|
|
3949
|
-
* message on no cadence at all.
|
|
3950
|
-
*
|
|
3951
|
-
* **Why a magnitude and not a predicate.** The tempting sharper rule is to
|
|
3952
|
-
* compensate per row, fully, for any stamp that reads ahead of our wall —
|
|
3953
|
-
* one our own post-step heartbeat could not have written. It is unsafe: a
|
|
3954
|
-
* future-stamped row is equally what a live job heartbeated by a peer whose
|
|
3955
|
-
* clock runs fast looks like, and from the stamp alone the two are
|
|
3956
|
-
* indistinguishable. The same reasoning bounds the cap from the other side —
|
|
3957
|
-
* while our clock is stepped, the guard force-fails the live jobs of any
|
|
3958
|
-
* peer slower than `windowMs - cap`, which is 2.5 minutes of tolerated
|
|
3959
|
-
* disagreement on the defaults, far outside anything NTP-managed hosts show.
|
|
3634
|
+
* Close out a terminal job whose caller hasn't already persisted it: sign
|
|
3635
|
+
* the receipt, then save the snapshot. The snapshot save never writes the
|
|
3636
|
+
* receipt column, so ordering only affects how soon a reader sees the
|
|
3637
|
+
* receipt — this way a caller polling immediately after the terminal
|
|
3638
|
+
* already finds it.
|
|
3639
|
+
*/
|
|
3640
|
+
private finalizeTerminal;
|
|
3641
|
+
/**
|
|
3642
|
+
* Issue a receipt for a job this process doesn't hold in `activeJobs` — a
|
|
3643
|
+
* cross-machine cancel, or a row the stale sweeper just reaped. Re-reads the
|
|
3644
|
+
* record so the receipt is built from the committed terminal row rather than
|
|
3645
|
+
* from whatever the caller happened to have in hand.
|
|
3960
3646
|
*/
|
|
3961
|
-
|
|
3647
|
+
issueReceiptForStoredJob(jobId: string): Promise<JobReceipt | undefined>;
|
|
3648
|
+
/** Handle job reaching terminal state (completed, failed, cancelled). */
|
|
3649
|
+
private handleJobTerminal;
|
|
3962
3650
|
/**
|
|
3963
|
-
*
|
|
3964
|
-
*
|
|
3965
|
-
*
|
|
3966
|
-
* **No job row (internal-review) — release.** internal-review allocates the job id and
|
|
3967
|
-
* commits the ledger draw, with its `job_id` linkage, before `recordReplay`
|
|
3968
|
-
* and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
|
|
3969
|
-
* fail-closed `replay_detected` 401 in that window leaves a committed hold
|
|
3970
|
-
* that **no** terminal path can ever reach: `issueReceipt` →
|
|
3971
|
-
* `resolveCreditDraw` keys off the `creditId`/`drawId` persisted on the job
|
|
3972
|
-
* row, so with no row there is no release path at all and the caller's
|
|
3973
|
-
* available balance stays reduced forever. Cosmetic for an implicit N=1
|
|
3974
|
-
* credit, a silent balance shrink for an explicit N>1 one.
|
|
3975
|
-
*
|
|
3976
|
-
* **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
|
|
3977
|
-
* demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
|
|
3978
|
-
* `issueReceipt` catches it, logs `credit_resolve_failed` and returns
|
|
3979
|
-
* nothing, and until now nothing retried. Same stranded hold, reached through
|
|
3980
|
-
* a different door — and for a `completed` job it strands the platform's
|
|
3981
|
-
* books too, since draws *are* the revenue events (internal-review): the caller is
|
|
3982
|
-
* charged nothing, the balance never moves, and outstanding liability
|
|
3983
|
-
* (`deposits − draw revenue`) overstates forever. So a late settle books, off
|
|
3984
|
-
* the committed draw row, through the ordinary reporter path.
|
|
3651
|
+
* Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
|
|
3652
|
+
* Fire-and-forget — the `RevenueReporter` owns persistence and retry.
|
|
3985
3653
|
*
|
|
3986
|
-
* **
|
|
3987
|
-
*
|
|
3988
|
-
*
|
|
3989
|
-
*
|
|
3990
|
-
*
|
|
3991
|
-
*
|
|
3992
|
-
*
|
|
3993
|
-
*
|
|
3994
|
-
*
|
|
3995
|
-
*
|
|
3654
|
+
* For a **credit-backed** job the revenue event is the *settled draw* (spec
|
|
3655
|
+
* §10), and since internal-review the sats figure corrects the job's mirror of that
|
|
3656
|
+
* draw against the draw itself: `withDrawBasis` stamps the job when the
|
|
3657
|
+
* payment lands, but a Bitcoin credit's settle re-prices the draw against
|
|
3658
|
+
* the funding lots it consumed. The correction is a delta, because a job's
|
|
3659
|
+
* counter can carry legs no draw ever absorbed — except where the draw's
|
|
3660
|
+
* forecast was zero and the counter therefore never held a component to
|
|
3661
|
+
* correct. The event is dated at settlement rather than at report time. A
|
|
3662
|
+
* released draw — the failed/cancelled path — is deliberately unreachable
|
|
3663
|
+
* here, which is what makes "no debit on job failure" true in the books as
|
|
3664
|
+
* well as the ledger.
|
|
3996
3665
|
*
|
|
3997
|
-
*
|
|
3998
|
-
*
|
|
3999
|
-
*
|
|
4000
|
-
* sweeper racing the in-line funnel — resolve the hold exactly once; the
|
|
4001
|
-
* losers see `replayed: true` and book nothing.
|
|
4002
|
-
*/
|
|
4003
|
-
sweepOrphanDraws(): Promise<{
|
|
4004
|
-
released: number;
|
|
4005
|
-
reconciled: number;
|
|
4006
|
-
}>;
|
|
4007
|
-
/**
|
|
4008
|
-
* The end-of-scan line for a tick that resolved nothing (internal-review, extended
|
|
4009
|
-
* by internal-review). Two shapes are worth a warning, and neither is the ordinary
|
|
4010
|
-
* one — the timer runs every 5 minutes per ledger in production.
|
|
3666
|
+
* For everything else (free-then-paid jobs, isolate-shaped flows, a DVM
|
|
3667
|
+
* whose price couldn't be fiat-denominated) nothing changes: the pre-credits
|
|
3668
|
+
* payload is reported verbatim.
|
|
4011
3669
|
*
|
|
4012
|
-
*
|
|
4013
|
-
*
|
|
4014
|
-
*
|
|
3670
|
+
* Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
|
|
3671
|
+
* background sweep that holds no in-process job — books through this exact
|
|
3672
|
+
* path off the row it read. `paymentTxHash` is passed **verbatim**, never
|
|
3673
|
+
* recomputed as a `drawSettlementRef`: it is the same column the in-line
|
|
3674
|
+
* funnel reads, so the two bookings are byte-identical by construction and
|
|
3675
|
+
* the platform's `UNIQUE (rail, tx_hash)` dedupes them. Deriving it instead
|
|
3676
|
+
* would diverge on an implicit N=1 job (whose row carries the rail's own
|
|
3677
|
+
* reference) and on any job whose column a mid-job top-up overwrote — the
|
|
3678
|
+
* same rot `resolveDrawPool` documents as the reason not to read regime off
|
|
3679
|
+
* this column.
|
|
4015
3680
|
*
|
|
4016
|
-
*
|
|
4017
|
-
*
|
|
4018
|
-
*
|
|
4019
|
-
*
|
|
4020
|
-
* have seen and didn't: already past the age gate outright, stamped ahead of
|
|
4021
|
-
* our clock (some other process's clock is fast), or hidden while our own
|
|
4022
|
-
* clock is held forward over a backwards step. A ledger holding nothing but
|
|
4023
|
-
* young draws stays quiet.
|
|
3681
|
+
* Returns whether the report was handed to `onJobCompleted`, so a caller
|
|
3682
|
+
* that logs the booking says what actually happened rather than what it
|
|
3683
|
+
* assumed — the guards below still drop legitimate shapes (a free job, a
|
|
3684
|
+
* DVM with no reporter). Every drop that costs a booking logs first.
|
|
4024
3685
|
*
|
|
4025
|
-
* The
|
|
4026
|
-
*
|
|
4027
|
-
*
|
|
3686
|
+
* The rail check sits **below** the draw re-read, not in the entry guard
|
|
3687
|
+
* (internal-review): a settled draw the terminal can't report under is a real debit
|
|
3688
|
+
* with no revenue event, permanently overstating outstanding liability
|
|
3689
|
+
* (`deposits − draw revenue`), and it used to return here in silence. Placed
|
|
3690
|
+
* after the re-read, `revenue_skipped_no_rail` can name the draw and its
|
|
3691
|
+
* amount, and can distinguish the four causes — see its `reason` below.
|
|
4028
3692
|
*/
|
|
4029
|
-
private
|
|
3693
|
+
private bookRevenue;
|
|
3694
|
+
/** Schedule job removal from activeJobs after a delay so clients can still poll final status. */
|
|
3695
|
+
scheduleCleanup(job: ServerJob): void;
|
|
3696
|
+
/** Clear all timers. Called on server shutdown to allow clean exit. */
|
|
3697
|
+
shutdown(): void;
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
/** Cached liveness signal for a platform-hosted DVM's Lightning receive rail. */
|
|
3701
|
+
interface LightningRailHealth {
|
|
3702
|
+
/** Whether the receive rail may be advertised at this instant. */
|
|
3703
|
+
available(): boolean;
|
|
3704
|
+
/** Prime the cached signal at boot when the source supports it. */
|
|
3705
|
+
refresh?(): Promise<void>;
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
/** Builder-facing wiring for the Lightning receive leg (internal-review). */
|
|
3709
|
+
interface LightningReceiveConfig {
|
|
4030
3710
|
/**
|
|
4031
|
-
*
|
|
4032
|
-
*
|
|
4033
|
-
* when this call is the one that moved the draw — the caller counts it, and
|
|
4034
|
-
* only it books.
|
|
4035
|
-
*
|
|
4036
|
-
* **The settle is gated on the job row naming _this_ draw.** The scan finds
|
|
4037
|
-
* the draw by `credit_draws.job_id`, but the job's own `credit_id`/`draw_id`
|
|
4038
|
-
* are written by a *later* transaction than the one that committed the draw
|
|
4039
|
-
* (`verifyIncomingPayment` commits fund+draw; `verifyAndCredit` binds the
|
|
4040
|
-
* row), and a free-then-paid job binds set-if-null. A store failure or a
|
|
4041
|
-
* second concurrent top-up therefore leaves a hold tagged with the job whose
|
|
4042
|
-
* row points somewhere else, or nowhere. Settling that would debit the caller
|
|
4043
|
-
* a second time for one job — and invisibly, since `revenue_events`'
|
|
4044
|
-
* `UNIQUE (dvm_id, job_id, rail)` would drop the booking. So an unbound hold
|
|
4045
|
-
* is **released**, never settled: the job's payment was the draw it is bound
|
|
4046
|
-
* to, and this one bought nothing.
|
|
4047
|
-
*
|
|
4048
|
-
* Concurrency needs no guard of its own. `settle`/`release` run under the
|
|
4049
|
-
* credit row's `FOR UPDATE`, so exactly one caller — a sibling reconciler,
|
|
4050
|
-
* or the in-line funnel arriving late — sees `replayed: false`, which makes
|
|
4051
|
-
* the booking gate a true mutex rather than a hopeful one. The one race left
|
|
4052
|
-
* open is another machine's `handleJobTerminal` stalled between resolving the
|
|
4053
|
-
* draw and booking it, which would double-report; the platform's
|
|
4054
|
-
* `(dvm_id, credit_id, draw_id)` and `(dvm_id, job_id, rail)` unique indexes
|
|
4055
|
-
* absorb that.
|
|
4056
|
-
*
|
|
4057
|
-
* `invalid_draw_state` — the funnel having already resolved the draw to the
|
|
4058
|
-
* other state — is the expected loss, logged and swallowed like the orphan
|
|
4059
|
-
* arm's, so one contended hold can't strand the rest of the batch.
|
|
3711
|
+
* Receive-only NWC connection URI (`DVMKIT_NWC_RECEIVE_URI`). Validated at
|
|
3712
|
+
* boot: a connection that can *spend* is refused outright.
|
|
4060
3713
|
*/
|
|
4061
|
-
|
|
3714
|
+
uri?: string;
|
|
4062
3715
|
/**
|
|
4063
|
-
*
|
|
4064
|
-
*
|
|
4065
|
-
*
|
|
4066
|
-
* deliberately excluded — their idle timeout must still elapse. Fires on the
|
|
4067
|
-
* heartbeat timer; also callable on demand for tests.
|
|
3716
|
+
* Pre-built backend, bypassing URI parsing and the connect-time probe. Test
|
|
3717
|
+
* seam only — production always goes through `uri` so the receive-only
|
|
3718
|
+
* property is actually checked.
|
|
4068
3719
|
*/
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
3720
|
+
backend?: LightningBackend;
|
|
3721
|
+
/** Invoice lifetime in seconds. Defaults to {@link DEFAULT_INVOICE_TTL_SECONDS}. */
|
|
3722
|
+
invoiceTtlSeconds?: number;
|
|
4072
3723
|
/**
|
|
4073
|
-
*
|
|
4074
|
-
*
|
|
4075
|
-
*
|
|
4076
|
-
*
|
|
4077
|
-
*
|
|
4078
|
-
*
|
|
4079
|
-
* • `true` — the window elapsed still `awaiting-input` (the original worker
|
|
4080
|
-
* is gone or has no live resolver) AND this machine won the claim CAS.
|
|
4081
|
-
* Replay for dead-worker recovery.
|
|
4082
|
-
*
|
|
4083
|
-
* Biasing the original handler to win kills the double execution (double
|
|
4084
|
-
* substrate spend, clobbered artifact) without heartbeating `awaiting-input`
|
|
4085
|
-
* (which would break the caller-input idle timeout). Non-streamable stores
|
|
4086
|
-
* have no cross-machine race, so the claim is a no-op `true`.
|
|
3724
|
+
* Smallest funding this deployment can receive over Lightning, in sats
|
|
3725
|
+
* (`DVMKIT_LIGHTNING_FUNDING_MIN_SATS`). The floor is a property of the
|
|
3726
|
+
* receive wallet's channel policy (`htlc_minimum_msat` upstream), so it is
|
|
3727
|
+
* per-deployment and sats-physical — the menu renders it into fiat with a
|
|
3728
|
+
* margin, but everything that *enforces* it compares raw sats. Defaults to
|
|
3729
|
+
* {@link DEFAULT_LIGHTNING_FUNDING_MIN_SATS}.
|
|
4087
3730
|
*/
|
|
4088
|
-
|
|
3731
|
+
fundingMinSats?: number;
|
|
4089
3732
|
/**
|
|
4090
|
-
*
|
|
4091
|
-
* it
|
|
4092
|
-
* (`null` when the store isn't streamable — non-Postgres test fallback).
|
|
4093
|
-
*
|
|
4094
|
-
* Status starts as `pending-verification`; a follow-up Tx C call
|
|
4095
|
-
* (`processPayment` for payments, `markInboundVerified` for everything
|
|
4096
|
-
* else) transitions it to `verified` once the inbound is accepted.
|
|
3733
|
+
* Optional platform-derived channel-liveness gate. Platform-hosted DVMs use
|
|
3734
|
+
* it alongside their NWC reachability probe; self-hosted DVMs omit it.
|
|
4097
3735
|
*/
|
|
4098
|
-
|
|
3736
|
+
railHealth?: LightningRailHealth;
|
|
3737
|
+
}
|
|
3738
|
+
/** Default bolt11 lifetime — long enough to pay by hand, short enough to retire. */
|
|
3739
|
+
declare const DEFAULT_INVOICE_TTL_SECONDS = 900;
|
|
3740
|
+
/**
|
|
3741
|
+
* Floor on the invoice lifetime: twice the signed-request drift window, so the
|
|
3742
|
+
* bolt11 always outlives the funding negotiation that produced it (the issue's
|
|
3743
|
+
* "invoice expiry ≥ the funding-negotiation window"). A shorter one would
|
|
3744
|
+
* expire inside the caller's own retry budget and strand them mid-top-up.
|
|
3745
|
+
*/
|
|
3746
|
+
declare const MIN_INVOICE_TTL_SECONDS = 600;
|
|
3747
|
+
/**
|
|
3748
|
+
* The builder-side Lightning receive leg (internal-review, credits spec §4).
|
|
3749
|
+
*
|
|
3750
|
+
* Issues a bolt11 over a **receive-only** NWC connection while the DVM is
|
|
3751
|
+
* awake serving the 402, and credits the ledger when a later request observes
|
|
3752
|
+
* settlement. Two properties are load-bearing:
|
|
3753
|
+
*
|
|
3754
|
+
* - **It never holds a send credential.** `pay_invoice` on this connection is
|
|
3755
|
+
* refused at construction, so a compromised DVM can mint invoices and
|
|
3756
|
+
* nothing else. The drain/refund sender (internal-review) is a separate, budgeted
|
|
3757
|
+
* connection by rule.
|
|
3758
|
+
* - **Crediting is pull-based — there is no settlement watcher.** The caller's
|
|
3759
|
+
* next request drives `lookup_invoice`, which is what makes this work on a
|
|
3760
|
+
* suspend-to-zero fleet: a machine that is asleep has nothing to miss.
|
|
3761
|
+
* Wallet downtime at that moment delays crediting; it never loses money,
|
|
3762
|
+
* because the invoice→credit binding is a durable row.
|
|
3763
|
+
*
|
|
3764
|
+
* Reachability is **cached**, never probed per request: the funding menu is
|
|
3765
|
+
* assembled on every quote and every 402, so a live NIP-47 round trip there
|
|
3766
|
+
* would put a nostr relay in the latency path of every priced call.
|
|
3767
|
+
*/
|
|
3768
|
+
declare class LightningReceive {
|
|
3769
|
+
private readonly source;
|
|
3770
|
+
readonly invoiceTtlSeconds: number;
|
|
3771
|
+
readonly fundingMinSats: number;
|
|
3772
|
+
private health;
|
|
3773
|
+
private checkedAtMs;
|
|
3774
|
+
private refreshing;
|
|
3775
|
+
private permissionRefusal;
|
|
3776
|
+
private constructor();
|
|
3777
|
+
private readonly railHealth;
|
|
4099
3778
|
/**
|
|
4100
|
-
*
|
|
4101
|
-
* `
|
|
3779
|
+
* Validate the configured connection and build the receive leg, or return
|
|
3780
|
+
* `undefined` when this DVM didn't configure one.
|
|
3781
|
+
*
|
|
3782
|
+
* **Throws on a credential that is wrong, degrades on one that is merely
|
|
3783
|
+
* unreachable.** A wallet outage at boot must not stop a DVM whose other
|
|
3784
|
+
* rails are fine — the menu simply omits `lightning` until a later probe
|
|
3785
|
+
* succeeds. A connection that can spend, or one that can't state what it can
|
|
3786
|
+
* do, is a different thing entirely: it is a standing money risk that no
|
|
3787
|
+
* amount of retrying fixes, so it fails the boot loudly with the fix in the
|
|
3788
|
+
* message.
|
|
4102
3789
|
*/
|
|
4103
|
-
|
|
3790
|
+
static create(config: LightningReceiveConfig): Promise<LightningReceive | undefined>;
|
|
4104
3791
|
/**
|
|
4105
|
-
*
|
|
4106
|
-
* in-memory `ServerJob`, wires the appender, builds the context, starts
|
|
4107
|
-
* the idle timer. The caller has already recorded the inbound via Tx B
|
|
4108
|
-
* and run Tx C (verify + credit); reactivation is now decoupled from
|
|
4109
|
-
* credit (internal-review, internal-review). The caller decides whether to run the
|
|
4110
|
-
* handler (Pattern A: status was `awaiting-input`) or to dispatch the
|
|
4111
|
-
* message to a custom handler (Pattern B).
|
|
4112
|
-
*
|
|
4113
|
-
* For DVMs that declare descriptor-level auth, the persisted
|
|
4114
|
-
* `record.input` carries the original signed envelope. We re-verify the
|
|
4115
|
-
* signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
|
|
4116
|
-
* injection, malicious operator with DB access — can't silently feed an
|
|
4117
|
-
* attacker-supplied pubkey/envelope into the handler. The drift window
|
|
4118
|
-
* and replay store are deliberately skipped: the persisted timestamp is
|
|
4119
|
-
* from the original signing instant (long past the 5-min drift bound for
|
|
4120
|
-
* any long-running job), and the nonce was already committed at submission.
|
|
4121
|
-
* On failure: throws `SignedRequestError` ({@link AuthSchemaError} when
|
|
4122
|
-
* the capability has no input schema to verify against); the caller is
|
|
4123
|
-
* expected to mark the row failed and surface a 401 / 500 to the inbound
|
|
4124
|
-
* caller.
|
|
3792
|
+
* Whether `lightning` belongs on the funding menu right now.
|
|
4125
3793
|
*
|
|
4126
|
-
*
|
|
4127
|
-
*
|
|
4128
|
-
*
|
|
4129
|
-
*
|
|
4130
|
-
* than the parsed form it replaced: an injected key the capability schema
|
|
4131
|
-
* doesn't declare used to be stripped before the signature was checked, so
|
|
4132
|
-
* it re-verified clean.
|
|
3794
|
+
* Reads the cached observation and never blocks; a stale one kicks off a
|
|
3795
|
+
* background refresh and answers with what we last knew. Advertising a rail
|
|
3796
|
+
* whose wallet is down would hand the caller an option that 402s on use —
|
|
3797
|
+
* the §4 posture is to drop it and let the sale survive on the others.
|
|
4133
3798
|
*/
|
|
4134
|
-
|
|
4135
|
-
job: ServerJob;
|
|
4136
|
-
sdkCtx: SDKJobContext<unknown, unknown>;
|
|
4137
|
-
};
|
|
3799
|
+
available(): boolean;
|
|
4138
3800
|
/**
|
|
4139
|
-
*
|
|
4140
|
-
* The inbound message must have been recorded via Tx B already
|
|
4141
|
-
* (`recordInboundPending` returned `inboundSeq`). For non-streamable
|
|
4142
|
-
* stores (`inboundSeq === null`), falls back to in-memory mutation via
|
|
4143
|
-
* `applyPaymentInfoToJob`.
|
|
4144
|
-
*
|
|
4145
|
-
* Dev-mode short-circuit: auto-credits the outstanding
|
|
4146
|
-
* `pendingPaymentMsats` without external verification. Gated on
|
|
4147
|
-
* `devModeSkipsPaymentVerification` — the same predicate the upfront path
|
|
4148
|
-
* reads, so a dev server wired to a mint verifies mid-job payments for real
|
|
4149
|
-
* (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
|
|
4150
|
-
* which is the one caller that has no wallet to pay from. `dev_auto` loses to
|
|
4151
|
-
* any proof riding the same message: money on the wire always takes the rail,
|
|
4152
|
-
* so the flag can never leave a real token unspent against a credited job.
|
|
3801
|
+
* Issue (or re-issue) the bolt11 funding `(creditId, fundId)`.
|
|
4153
3802
|
*
|
|
4154
|
-
*
|
|
4155
|
-
*
|
|
4156
|
-
*
|
|
4157
|
-
*
|
|
4158
|
-
*
|
|
4159
|
-
*
|
|
4160
|
-
* 500. See `creditInbound`.
|
|
3803
|
+
* The invoice is minted **for** the credit and amount named in the caller's
|
|
3804
|
+
* signed body — that binding is the internal-review condition-3 commitment on this
|
|
3805
|
+
* rail, and it is why no artifact hash rides the request: there is no
|
|
3806
|
+
* caller-supplied artifact to hash. A re-poll returns the stored row rather
|
|
3807
|
+
* than minting again; a second bolt11 for one `fund_id` would leave two
|
|
3808
|
+
* payable invoices against a funding that can only be credited once.
|
|
4161
3809
|
*/
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
3810
|
+
issue(ledger: CreditLedgerLike, args: {
|
|
3811
|
+
creditId: string;
|
|
3812
|
+
fundId: string;
|
|
3813
|
+
callerPubkey: string;
|
|
3814
|
+
currency: string;
|
|
3815
|
+
amountMicro: number;
|
|
3816
|
+
amountMsats: number;
|
|
3817
|
+
description?: string;
|
|
3818
|
+
}): Promise<CreditInvoiceRecord>;
|
|
4169
3819
|
/**
|
|
4170
|
-
*
|
|
3820
|
+
* Ask the wallet whether one payment hash is paid, and when (internal-review).
|
|
4171
3821
|
*
|
|
4172
|
-
*
|
|
4173
|
-
*
|
|
4174
|
-
*
|
|
4175
|
-
*
|
|
4176
|
-
*
|
|
3822
|
+
* The operator's reconcile verb runs this before it credits anything. A
|
|
3823
|
+
* `blocked` row implies payment — {@link applyOne} returns above the block
|
|
3824
|
+
* classification when the lookup says unpaid — but that is *this fleet's
|
|
3825
|
+
* belief, recorded possibly weeks ago*, and the verb it gates mints balance
|
|
3826
|
+
* against it. One round trip turns the belief into a fact and recovers the
|
|
3827
|
+
* true settlement instant for `settled_at`, which a blocked row never got to
|
|
3828
|
+
* write.
|
|
4177
3829
|
*
|
|
4178
|
-
*
|
|
4179
|
-
*
|
|
4180
|
-
*
|
|
4181
|
-
*
|
|
4182
|
-
*
|
|
4183
|
-
* the
|
|
3830
|
+
* Unlike {@link settlePending} this **throws**: there is no request whose
|
|
3831
|
+
* latency it protects, and crediting on an unverifiable wallet is exactly
|
|
3832
|
+
* what it exists to prevent. `NOT_FOUND` is the one exception, and it is not
|
|
3833
|
+
* an outage — it is the wallet saying it has never seen this hash, the same
|
|
3834
|
+
* reading {@link applyOne} and the caller-side reconcile in `src/lib/nwc.ts`
|
|
3835
|
+
* take. It comes back as `known: false` because the operator's repair for it
|
|
3836
|
+
* ("you are pointed at a different wallet than the one that minted this")
|
|
3837
|
+
* is not the repair for an unpaid invoice, and certainly not for a retry.
|
|
3838
|
+
*/
|
|
3839
|
+
lookupSettlement(paymentHash: string): Promise<{
|
|
3840
|
+
settled: boolean;
|
|
3841
|
+
settledAt?: number;
|
|
3842
|
+
known: boolean;
|
|
3843
|
+
}>;
|
|
3844
|
+
/**
|
|
3845
|
+
* Consult the wallet about this caller's outstanding invoices and credit the
|
|
3846
|
+
* ones that settled — the pull half of pull-based crediting.
|
|
4184
3847
|
*
|
|
4185
|
-
*
|
|
4186
|
-
*
|
|
4187
|
-
*
|
|
4188
|
-
*
|
|
4189
|
-
*
|
|
4190
|
-
*
|
|
4191
|
-
* `payment_unapplied` and invites a retry, and a caller who takes it pays
|
|
4192
|
-
* twice for one ask — the second payment growing the now-bound draw and
|
|
4193
|
-
* settling as revenue.
|
|
3848
|
+
* The pending rows are read locally first, so the common case (nothing
|
|
3849
|
+
* outstanding) costs one indexed SELECT and no network at all. Bounded to
|
|
3850
|
+
* {@link MAX_SETTLE_CHECKS} invoices on a short deadline, and **never throws
|
|
3851
|
+
* into the request**: a wallet failure here means the caller's balance is
|
|
3852
|
+
* merely not updated yet, which the ordinary `insufficient_credit` path
|
|
3853
|
+
* already states honestly.
|
|
4194
3854
|
*
|
|
4195
|
-
*
|
|
4196
|
-
*
|
|
4197
|
-
*
|
|
4198
|
-
*
|
|
4199
|
-
* the
|
|
3855
|
+
* **Each invoice gets its own failure boundary.** The sweep window is a few
|
|
3856
|
+
* rows wide, so a row that fails on its own terms — a `fund_id` the caller
|
|
3857
|
+
* reused on another rail, a credit someone else opened first — must not take
|
|
3858
|
+
* the rest of the pass down with it: the next invoice in the window may be
|
|
3859
|
+
* the paid one, and it would never be looked up. Only a transport failure
|
|
3860
|
+
* ends the pass early, because there the wallet itself is gone and the
|
|
3861
|
+
* remaining lookups would just spend the caller's latency confirming it.
|
|
4200
3862
|
*/
|
|
4201
|
-
|
|
3863
|
+
settlePending(ledger: CreditLedgerLike, args: {
|
|
3864
|
+
callerPubkey: string;
|
|
3865
|
+
creditId?: string;
|
|
3866
|
+
creditTtlMs: number;
|
|
3867
|
+
dvmId?: string;
|
|
3868
|
+
enqueueCreditDeposit?: CreditDepositEnqueue;
|
|
3869
|
+
nowMs?: number;
|
|
3870
|
+
}): Promise<InvoiceSettlement[]>;
|
|
4202
3871
|
/**
|
|
4203
|
-
*
|
|
4204
|
-
*
|
|
3872
|
+
* One invoice's settlement check. Returns the applied settlement, or
|
|
3873
|
+
* `undefined` when nothing changed (still unpaid, or retired unpaid).
|
|
4205
3874
|
*
|
|
4206
|
-
*
|
|
4207
|
-
*
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
*
|
|
3875
|
+
* Throws whatever the wallet or the ledger threw — classifying that is
|
|
3876
|
+
* {@link retireOrLog}'s job, so this stays one invoice's happy path.
|
|
3877
|
+
*/
|
|
3878
|
+
private applyOne;
|
|
3879
|
+
/**
|
|
3880
|
+
* Decide what one invoice's failure means, and retire the invoice when the
|
|
3881
|
+
* answer is "this can never succeed".
|
|
4212
3882
|
*
|
|
4213
|
-
*
|
|
4214
|
-
*
|
|
4215
|
-
*
|
|
4216
|
-
*
|
|
4217
|
-
*
|
|
4218
|
-
*
|
|
3883
|
+
* The split that matters is permanent-versus-transient, because it decides
|
|
3884
|
+
* whether the row keeps a slot in the sweep window. A transient failure — the
|
|
3885
|
+
* database blinked, the wallet answered oddly — leaves it `pending` and the
|
|
3886
|
+
* caller's next request retries it. A **permanent** one means the ledger will
|
|
3887
|
+
* refuse this invoice identically forever, so leaving it pending costs a
|
|
3888
|
+
* `lookup_invoice` on every subsequent request and, at
|
|
3889
|
+
* {@link MAX_SETTLE_CHECKS} of them, fills the window so a genuinely payable
|
|
3890
|
+
* invoice behind them is never even looked up. Those get `blocked`, which
|
|
3891
|
+
* drops them out of the sweep and hands the operator the payment hash.
|
|
3892
|
+
*/
|
|
3893
|
+
private retireOrLog;
|
|
3894
|
+
/**
|
|
3895
|
+
* The reason this invoice can never be credited, or `undefined` if the
|
|
3896
|
+
* failure was transient.
|
|
4219
3897
|
*
|
|
4220
|
-
*
|
|
4221
|
-
*
|
|
4222
|
-
* (
|
|
4223
|
-
*
|
|
4224
|
-
*
|
|
3898
|
+
* Every code here is decided by state a retry cannot move — a row the ledger
|
|
3899
|
+
* has already committed (a `credit_fundings` entry at this
|
|
3900
|
+
* `(credit_id, fund_id)`, or a `credits` row whose owner, denomination or
|
|
3901
|
+
* rail disagrees with the invoice), or the basis this module rebuilds
|
|
3902
|
+
* identically from the invoice row on every pass. None of them is a race.
|
|
4225
3903
|
*
|
|
4226
|
-
*
|
|
4227
|
-
*
|
|
4228
|
-
*
|
|
4229
|
-
*
|
|
4230
|
-
* the
|
|
4231
|
-
*
|
|
3904
|
+
* `funding_replayed` is the one that needs a second read to classify.
|
|
3905
|
+
* Usually it *is* benign — a concurrent check won the race and the money is
|
|
3906
|
+
* credited either way — but the key is `(credit_id, fund_id)` and `fund_id`
|
|
3907
|
+
* is **caller-chosen**: reuse it on cashu or x402 after this bolt11 was
|
|
3908
|
+
* minted and the row that collided is a different payment entirely, so the
|
|
3909
|
+
* sats this invoice received have nowhere to land.
|
|
4232
3910
|
*/
|
|
4233
|
-
private
|
|
3911
|
+
private blockingReason;
|
|
3912
|
+
private withBackend;
|
|
3913
|
+
/** Record activity without letting it promote an unvalidated connection. */
|
|
3914
|
+
private noteOperationOk;
|
|
3915
|
+
private notePermissionsOk;
|
|
3916
|
+
private notePermissionRefused;
|
|
4234
3917
|
/**
|
|
4235
|
-
*
|
|
4236
|
-
*
|
|
4237
|
-
* ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
|
|
4238
|
-
* there is no credit to name, so the 201 carries no `credit` block and the
|
|
4239
|
-
* 500 takes its words from `unappliedCopy` directly.
|
|
3918
|
+
* Only a *transport* failure from an ordinary invoice operation is a health
|
|
3919
|
+
* signal. Permission-probe refusals are classified by {@link refresh}.
|
|
4240
3920
|
*/
|
|
4241
|
-
private
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
3921
|
+
private noteFailure;
|
|
3922
|
+
private refresh;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
/**
|
|
3926
|
+
* Owner display identity surfaced on `/v1/info#owner` (internal-review). Personal orgs
|
|
3927
|
+
* resolve to the owner builder's profile; shared orgs resolve to the org's own
|
|
3928
|
+
* profile. Container DVMs read from env vars; isolate DVMs resolve from Postgres.
|
|
3929
|
+
*/
|
|
3930
|
+
interface OwnerDisplay {
|
|
3931
|
+
handle: string;
|
|
3932
|
+
displayName?: string | null;
|
|
3933
|
+
avatarUrl?: string | null;
|
|
3934
|
+
type: "builder" | "org";
|
|
3935
|
+
}
|
|
3936
|
+
/** Optional builder identity surfaced on `/v1/info` (forward-compatible stub). */
|
|
3937
|
+
interface BuilderIdentity {
|
|
3938
|
+
id?: string;
|
|
3939
|
+
name?: string;
|
|
3940
|
+
url?: string;
|
|
3941
|
+
/**
|
|
3942
|
+
* Builder identity x-only secp256k1 pubkey (internal-review). When populated, the
|
|
3943
|
+
* `/v1/info#builder` block also carries `attestation` + `signature` so
|
|
3944
|
+
* consumers can verify the deploy was signed by the holder of this key.
|
|
3945
|
+
* Populated at deploy time from `DVMKIT_BUILDER_PUBKEY` (Fly secret); the
|
|
3946
|
+
* SDK never re-signs at runtime.
|
|
4248
3947
|
*/
|
|
4249
|
-
|
|
3948
|
+
pubkey?: string;
|
|
3949
|
+
/** Canonical deploy-time attestation payload (internal-review). Served verbatim. */
|
|
3950
|
+
attestation?: AttestationPayload;
|
|
3951
|
+
/** Schnorr signature over `canonicaliseForSigning(attestation)` (internal-review). */
|
|
3952
|
+
signature?: string;
|
|
3953
|
+
}
|
|
3954
|
+
/** Platform reporter overrides — the host falls back to env when omitted. */
|
|
3955
|
+
interface PlatformReporterOpts {
|
|
3956
|
+
/** Bearer token. Defaults to `DVMKIT_PLATFORM_TOKEN` env. */
|
|
3957
|
+
token?: string;
|
|
3958
|
+
/** Platform internal URL. Defaults to `DVMKIT_PLATFORM_URL` env. */
|
|
3959
|
+
url?: string;
|
|
3960
|
+
}
|
|
3961
|
+
/** Options for {@link createDVMHost}. */
|
|
3962
|
+
interface DVMHostOpts {
|
|
4250
3963
|
/**
|
|
4251
|
-
*
|
|
4252
|
-
*
|
|
4253
|
-
*
|
|
4254
|
-
* The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
|
|
4255
|
-
* `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
|
|
4256
|
-
* from the same rail payment id — a fact about a row that was written once
|
|
4257
|
-
* and never moves. The tempting alternative, comparing `job.payment_tx_hash`
|
|
4258
|
-
* against `drawSettlementRef` (what the mid-job preflight does to inherit
|
|
4259
|
-
* `drawBasis`), silently rots: Tx C overwrites that column on every mid-job
|
|
4260
|
-
* credit, and a payment whose ledger leg was skipped — a cross-rail top-up,
|
|
4261
|
-
* an fx outage, a draw already resolved — leaves the rail's own reference
|
|
4262
|
-
* there, so an explicit-draw job would read as implicit from then on.
|
|
4263
|
-
*
|
|
4264
|
-
* Never throws, and never answers `rail` on a guess. `rail` is claimed only
|
|
4265
|
-
* where `withDrawBasis` demonstrably left `PaymentInfo.paidMsats` alone — no
|
|
4266
|
-
* draw at all, or a draw carrying no rail value to overlay it with. A job that
|
|
4267
|
-
* drew and whose pool this can't read or denominate answers `unpriced`, which
|
|
4268
|
-
* the gate declines to auto-credit from: the msat figure it would otherwise
|
|
4269
|
-
* fall back on is a slice of the credit's rail value at a ratio pinned
|
|
4270
|
-
* whenever that credit was funded, which is the exact figure this issue exists
|
|
4271
|
-
* to stop spending against.
|
|
3964
|
+
* Postgres connection string. Defaults to `DATABASE_URL` env.
|
|
3965
|
+
* Required at runtime (internal-review). Boot fails when missing unless `jobStore`
|
|
3966
|
+
* is explicitly supplied or `devMode` is true (test/dev escape hatches).
|
|
4272
3967
|
*/
|
|
4273
|
-
|
|
4274
|
-
/**
|
|
4275
|
-
|
|
3968
|
+
database?: string;
|
|
3969
|
+
/** Listen port. Defaults to `PORT` env or 8080. */
|
|
3970
|
+
port?: number;
|
|
3971
|
+
/** Environment variables. Defaults to `process.env`. */
|
|
3972
|
+
env?: Record<string, string>;
|
|
3973
|
+
/** Builder identity for `/v1/info` (forward-compatible). */
|
|
3974
|
+
builder?: BuilderIdentity;
|
|
3975
|
+
/** Owner display identity for `/v1/info#owner` (internal-review). Falls back to env vars. */
|
|
3976
|
+
owner?: OwnerDisplay;
|
|
3977
|
+
/** Platform reporter overrides. Defaults to env-derived values. */
|
|
3978
|
+
platformReporter?: PlatformReporterOpts;
|
|
3979
|
+
/** Override the SDK's fx fetcher. Defaults to env-configured CoinGecko. */
|
|
3980
|
+
fx?: FxFetcher;
|
|
4276
3981
|
/**
|
|
4277
|
-
*
|
|
4278
|
-
*
|
|
4279
|
-
*
|
|
4280
|
-
* two machines racing on the same job converge on identical bytes and burn
|
|
4281
|
-
* exactly one sequence number.
|
|
4282
|
-
*
|
|
4283
|
-
* Never throws. A receipt is evidence about a job, not part of delivering
|
|
4284
|
-
* it: if signing or persistence fails we log structured and serve the
|
|
4285
|
-
* response without one (same posture as the `cashu_refund_failed` branch in
|
|
4286
|
-
* `context.ts`). The gap is visible to callers — the `seq` series skips a
|
|
4287
|
-
* number — which is precisely the completeness signal receipts exist for.
|
|
3982
|
+
* Custom `/health` handler. When set, the SDK installs this instead of the
|
|
3983
|
+
* default `{ status: "ok" }` responder — useful for runtime-specific health
|
|
3984
|
+
* (pool depth, draining state, 503 while draining).
|
|
4288
3985
|
*/
|
|
4289
|
-
|
|
4290
|
-
/**
|
|
4291
|
-
|
|
3986
|
+
healthHandler?: (c: Context) => Response | Promise<Response>;
|
|
3987
|
+
/** KV store override. Defaults to PostgresKVStore (or MemoryKVStore in dev). */
|
|
3988
|
+
store?: KVStore;
|
|
3989
|
+
/** JobStore override. Takes precedence over `database`. */
|
|
3990
|
+
jobStore?: JobStore;
|
|
4292
3991
|
/**
|
|
4293
|
-
*
|
|
4294
|
-
*
|
|
4295
|
-
*
|
|
4296
|
-
*
|
|
4297
|
-
*
|
|
4298
|
-
* resolution returns the recorded state. Returns the `ReceiptCredit` block
|
|
4299
|
-
* for the receipt: `balance_after` is the recorded draw trajectory for a
|
|
4300
|
-
* settled draw, and the restored available balance for a released one.
|
|
3992
|
+
* Existing Postgres pool to reuse instead of opening one from `database`
|
|
3993
|
+
* (internal-review test seam). When set, the host builds its JobStore / KVStore /
|
|
3994
|
+
* cashu accumulator on this pool and leaves it open at shutdown — the caller
|
|
3995
|
+
* owns it. Lets the e2e harness run DVMs in `p2pk-accumulator` mode against
|
|
3996
|
+
* its single shared pool rather than spawning a pool per DVM.
|
|
4301
3997
|
*/
|
|
4302
|
-
|
|
3998
|
+
pool?: Pool;
|
|
3999
|
+
/** Cashu mints accepted. Defaults to env `DVMKIT_CASHU_MINTS`. */
|
|
4000
|
+
mints?: string[];
|
|
4001
|
+
/** Cashu receive mode override. */
|
|
4002
|
+
cashuMode?: CashuMode;
|
|
4003
|
+
/** MPP handle override. Defaults to env-resolved via `createMppFromOpts`. */
|
|
4004
|
+
mpp?: MppxServer;
|
|
4005
|
+
/** x402 stablecoin payment configuration. Defaults to env-resolved. */
|
|
4006
|
+
x402?: X402Config;
|
|
4007
|
+
/** Payment methods override. Defaults to derived from configured rails. */
|
|
4008
|
+
paymentMethods?: PaymentMethod[];
|
|
4303
4009
|
/**
|
|
4304
|
-
*
|
|
4305
|
-
* `
|
|
4306
|
-
*
|
|
4307
|
-
*
|
|
4308
|
-
*
|
|
4010
|
+
* Lightning receive leg for credit funding (internal-review). Defaults to the
|
|
4011
|
+
* env-resolved `DVMKIT_NWC_RECEIVE_URI` /
|
|
4012
|
+
* `DVMKIT_NWC_RECEIVE_INVOICE_TTL_SECONDS`. Pass `backend` to inject a
|
|
4013
|
+
* wallet directly — a test seam that skips the connect-time probe, so
|
|
4014
|
+
* production must always come through the URI.
|
|
4309
4015
|
*/
|
|
4310
|
-
|
|
4016
|
+
lightningReceive?: LightningReceiveConfig;
|
|
4017
|
+
/** When true, payment is skipped if no mints are configured (dev/test). */
|
|
4018
|
+
devMode?: boolean;
|
|
4019
|
+
/** Consumed-credential store override (test seam). */
|
|
4020
|
+
consumedCredentialStore?: ConsumedCredentialStore;
|
|
4021
|
+
/** Mint-health tracker override (test seam). */
|
|
4022
|
+
mintHealthTracker?: MintHealthTracker;
|
|
4023
|
+
/** Operator-alert boundary for failed terminal-job retention passes. */
|
|
4024
|
+
onJobRetentionSweepFailed?: JobManagerOpts["onJobRetentionSweepFailed"];
|
|
4025
|
+
/** Clear-on-recovery boundary invoked after a complete clean retention pass. */
|
|
4026
|
+
onJobRetentionSweepRecovered?: JobManagerOpts["onJobRetentionSweepRecovered"];
|
|
4311
4027
|
/**
|
|
4312
|
-
*
|
|
4313
|
-
*
|
|
4314
|
-
*
|
|
4028
|
+
* Wrap the SDK compatibility gate before it is exposed to descriptor custom
|
|
4029
|
+
* routes. Platform hosts use this to enforce their independently released
|
|
4030
|
+
* caller rollout policy; self-hosted SDK consumers receive the default gate.
|
|
4315
4031
|
*/
|
|
4316
|
-
|
|
4032
|
+
wrapRouteClientCompatibility?: (sdkGate: ClientCompatibilityGate) => ClientCompatibilityGate;
|
|
4033
|
+
}
|
|
4034
|
+
/** Per-mount options. Currently only `prefix` is supported. */
|
|
4035
|
+
interface MountOpts {
|
|
4036
|
+
/** Path prefix for this DVM's protocol routes (e.g. `/delete-feed`). */
|
|
4037
|
+
prefix?: string;
|
|
4038
|
+
}
|
|
4039
|
+
/** Live DVM host. Single wiring locus for the SDK (internal-review). */
|
|
4040
|
+
interface DVMHost {
|
|
4317
4041
|
/**
|
|
4318
|
-
*
|
|
4319
|
-
*
|
|
4320
|
-
*
|
|
4321
|
-
* only on the cases it exists for:
|
|
4322
|
-
*
|
|
4323
|
-
* - **A crash between the two store calls.** `claimReceiptSeq` commits the
|
|
4324
|
-
* sequence number to the job row before `saveReceipt` writes the bytes; a
|
|
4325
|
-
* process death in that window would otherwise strand that number
|
|
4326
|
-
* forever, and a permanent gap is indistinguishable from the deliberate
|
|
4327
|
-
* suppression `seq` exists to expose. Re-issuing here reuses the already
|
|
4328
|
-
* committed number (the claim is idempotent per job) rather than
|
|
4329
|
-
* allocating a second one.
|
|
4330
|
-
* - **A transient store failure** at the terminal: `issueReceipt` logs
|
|
4331
|
-
* `receipt_issue_failed` and returns nothing rather than failing the job,
|
|
4332
|
-
* so the next read retries.
|
|
4333
|
-
* - **Jobs that terminated before the DVM had a receipt key.** They pick one
|
|
4334
|
-
* up on first read, with `issued_at` reflecting when it was signed.
|
|
4042
|
+
* Underlying Hono app for custom non-protocol routes that belong to the
|
|
4043
|
+
* host (cross-DVM `/admin`, cron callbacks, etc.). For DVM-scoped routes,
|
|
4044
|
+
* declare `routes(app)` on the descriptor.
|
|
4335
4045
|
*/
|
|
4336
|
-
|
|
4046
|
+
readonly app: Hono;
|
|
4337
4047
|
/**
|
|
4338
|
-
*
|
|
4339
|
-
*
|
|
4340
|
-
*
|
|
4341
|
-
*
|
|
4342
|
-
*
|
|
4048
|
+
* Resolved Postgres pool — undefined before `host.serve()` completes
|
|
4049
|
+
* database resolution, set thereafter. Exposed so DVM-local Postgres
|
|
4050
|
+
* consumers (e.g. scrape's `ScrapeDb` per-fetch event store, internal-review)
|
|
4051
|
+
* can reuse the host's pool rather than constructing their own. Wire
|
|
4052
|
+
* inside descriptor `onBoot` (fires after pool init, before listener opens).
|
|
4343
4053
|
*/
|
|
4344
|
-
|
|
4054
|
+
readonly pool: Pool | undefined;
|
|
4345
4055
|
/**
|
|
4346
|
-
*
|
|
4347
|
-
*
|
|
4348
|
-
*
|
|
4349
|
-
*
|
|
4056
|
+
* Money-safe credit ledger (internal-review) — undefined before `host.serve()`
|
|
4057
|
+
* resolves stores, set thereafter. Postgres-backed when a pool exists;
|
|
4058
|
+
* pool-less hosts get a shared in-memory ledger so the internal-review fund+draw
|
|
4059
|
+
* semantics hold everywhere. Same wiring window as `pool` (descriptor
|
|
4060
|
+
* `onBoot` fires after init, before the listener opens).
|
|
4350
4061
|
*/
|
|
4351
|
-
|
|
4352
|
-
/** Handle job reaching terminal state (completed, failed, cancelled). */
|
|
4353
|
-
private handleJobTerminal;
|
|
4062
|
+
readonly creditLedger: CreditLedgerLike | undefined;
|
|
4354
4063
|
/**
|
|
4355
|
-
*
|
|
4356
|
-
*
|
|
4357
|
-
*
|
|
4358
|
-
* For a **credit-backed** job the revenue event is the *settled draw* (spec
|
|
4359
|
-
* §10), and since internal-review the sats figure corrects the job's mirror of that
|
|
4360
|
-
* draw against the draw itself: `withDrawBasis` stamps the job when the
|
|
4361
|
-
* payment lands, but a Bitcoin credit's settle re-prices the draw against
|
|
4362
|
-
* the funding lots it consumed. The correction is a delta, because a job's
|
|
4363
|
-
* counter can carry legs no draw ever absorbed — except where the draw's
|
|
4364
|
-
* forecast was zero and the counter therefore never held a component to
|
|
4365
|
-
* correct. The event is dated at settlement rather than at report time. A
|
|
4366
|
-
* released draw — the failed/cancelled path — is deliberately unreachable
|
|
4367
|
-
* here, which is what makes "no debit on job failure" true in the books as
|
|
4368
|
-
* well as the ledger.
|
|
4369
|
-
*
|
|
4370
|
-
* For everything else (free-then-paid jobs, isolate-shaped flows, a DVM
|
|
4371
|
-
* whose price couldn't be fiat-denominated) nothing changes: the pre-credits
|
|
4372
|
-
* payload is reported verbatim.
|
|
4373
|
-
*
|
|
4374
|
-
* Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
|
|
4375
|
-
* background sweep that holds no in-process job — books through this exact
|
|
4376
|
-
* path off the row it read. `paymentTxHash` is passed **verbatim**, never
|
|
4377
|
-
* recomputed as a `drawSettlementRef`: it is the same column the in-line
|
|
4378
|
-
* funnel reads, so the two bookings are byte-identical by construction and
|
|
4379
|
-
* the platform's `UNIQUE (rail, tx_hash)` dedupes them. Deriving it instead
|
|
4380
|
-
* would diverge on an implicit N=1 job (whose row carries the rail's own
|
|
4381
|
-
* reference) and on any job whose column a mid-job top-up overwrote — the
|
|
4382
|
-
* same rot `resolveDrawPool` documents as the reason not to read regime off
|
|
4383
|
-
* this column.
|
|
4384
|
-
*
|
|
4385
|
-
* Returns whether the report was handed to `onJobCompleted`, so a caller
|
|
4386
|
-
* that logs the booking says what actually happened rather than what it
|
|
4387
|
-
* assumed — the guards below still drop legitimate shapes (a free job, a
|
|
4388
|
-
* DVM with no reporter). Every drop that costs a booking logs first.
|
|
4389
|
-
*
|
|
4390
|
-
* The rail check sits **below** the draw re-read, not in the entry guard
|
|
4391
|
-
* (internal-review): a settled draw the terminal can't report under is a real debit
|
|
4392
|
-
* with no revenue event, permanently overstating outstanding liability
|
|
4393
|
-
* (`deposits − draw revenue`), and it used to return here in silence. Placed
|
|
4394
|
-
* after the re-read, `revenue_skipped_no_rail` can name the draw and its
|
|
4395
|
-
* amount, and can distinguish the four causes — see its `reason` below.
|
|
4064
|
+
* Add a DVM to this host. Multiple mounts compose at distinct prefixes;
|
|
4065
|
+
* a single mount with no prefix attaches at root.
|
|
4396
4066
|
*/
|
|
4397
|
-
|
|
4398
|
-
/**
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4067
|
+
mount<S, I extends ZodLike | undefined>(descriptor: DVMDescriptor<S, I>, opts?: MountOpts): void;
|
|
4068
|
+
/** Start listening. Returns the live server handle. */
|
|
4069
|
+
serve(opts?: {
|
|
4070
|
+
port?: number;
|
|
4071
|
+
}): Promise<{
|
|
4072
|
+
url: string;
|
|
4073
|
+
close: () => Promise<void>;
|
|
4074
|
+
}>;
|
|
4075
|
+
/** Graceful shutdown. Runs descriptor `onShutdown` hooks in reverse mount order. */
|
|
4076
|
+
shutdown(): Promise<void>;
|
|
4402
4077
|
}
|
|
4078
|
+
/**
|
|
4079
|
+
* Single wiring locus for SDK-side construction (internal-review). Replaces both the
|
|
4080
|
+
* `serve()` wrapper and the `createDVMServer` direct path. Reads env once,
|
|
4081
|
+
* resolves payment rails, owns the Postgres pool, and mounts each DVM
|
|
4082
|
+
* descriptor as a Hono sub-app on `host.app`.
|
|
4083
|
+
*/
|
|
4084
|
+
declare function createDVMHost(opts?: DVMHostOpts): DVMHost;
|
|
4403
4085
|
|
|
4404
4086
|
/**
|
|
4405
4087
|
* Cached liveness signal for the platform's central Tempo close observer
|
|
@@ -4685,12 +4367,12 @@ interface SDKServerOpts {
|
|
|
4685
4367
|
/** Cashu receive mode (internal-review). */
|
|
4686
4368
|
cashuMode?: CashuMode;
|
|
4687
4369
|
/**
|
|
4688
|
-
* Builder's NUT-11 P2PK lock pubkey (internal-review).
|
|
4689
|
-
*
|
|
4690
|
-
*
|
|
4691
|
-
*
|
|
4692
|
-
*
|
|
4693
|
-
*
|
|
4370
|
+
* Builder's NUT-11 P2PK lock pubkey (internal-review). Used as the seed pubkey for
|
|
4371
|
+
* `seedLockPubkeyState` on first boot, as the admin-auth identity, and as
|
|
4372
|
+
* part of the gate for whether `/v1/info` advertises `cashu`. After first
|
|
4373
|
+
* boot, the authoritative `[current, ...retired]` state lives in the DVM's
|
|
4374
|
+
* Postgres via `dvm_lock_pubkeys`; rotations (internal-review) mutate that store
|
|
4375
|
+
* directly.
|
|
4694
4376
|
*/
|
|
4695
4377
|
lockPubkey?: string;
|
|
4696
4378
|
/**
|
|
@@ -4708,7 +4390,7 @@ interface SDKServerOpts {
|
|
|
4708
4390
|
* without a separate env var.
|
|
4709
4391
|
*/
|
|
4710
4392
|
canonicalDvmId?: string;
|
|
4711
|
-
/** Postgres pool for wallet accumulator persistence. */
|
|
4393
|
+
/** Postgres pool for builder admin state and wallet accumulator persistence. */
|
|
4712
4394
|
db?: Pool;
|
|
4713
4395
|
/**
|
|
4714
4396
|
* General SDK strict-mode flag (internal-review), sourced from `DVMKIT_FAIL_FAST` at
|
|
@@ -4766,6 +4448,10 @@ interface SDKServerOpts {
|
|
|
4766
4448
|
* the platform reporter alongside the completion and paid-job-death paths.
|
|
4767
4449
|
*/
|
|
4768
4450
|
onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
|
|
4451
|
+
/** Advanced operator-alert callback for failed job-retention passes. */
|
|
4452
|
+
onJobRetentionSweepFailed?: JobManagerOpts["onJobRetentionSweepFailed"];
|
|
4453
|
+
/** Advanced clear-on-recovery callback for clean job-retention passes. */
|
|
4454
|
+
onJobRetentionSweepRecovered?: JobManagerOpts["onJobRetentionSweepRecovered"];
|
|
4769
4455
|
/**
|
|
4770
4456
|
* Advanced override: callback fired when a payment funds a credit (internal-review).
|
|
4771
4457
|
* Auto-wired to the `RevenueReporter`'s deposit report alongside
|
|
@@ -5122,4 +4808,4 @@ declare function attachCreditMenu(body: Record<string, unknown>, menu: CreditMen
|
|
|
5122
4808
|
*/
|
|
5123
4809
|
declare function toCreditTerms(menu: CreditMenu): CreditTerms;
|
|
5124
4810
|
|
|
5125
|
-
export {
|
|
4811
|
+
export { isTerminal as $, type AccumulatorPool as A, buildPaymentErrorResponse as B, type ConsumedCredentialStore as C, clearAccumulatorForDvm as D, createDVMServer as E, type FiatDenomination as F, creditDepositPayload as G, derivedFundCreditId as H, type IncomingPaymentOpts as I, JobCancelledError as J, devModeSkipsPaymentVerification as K, fromJobRecord as L, MintHealthTracker as M, fundedMicroFor as N, hasPaymentProof as O, PAYMENT_PROOF_KEYS as P, hashLockKey as Q, type ReporterBannerOpts as R, type SDKServerOpts as S, type TempoChannelReport as T, type UpfrontPaymentOpts as U, type VerifiedIncomingPayment as V, implicitCreditId as W, type X402BatchChannelObservation as X, initWalletAccumulatorTable as Y, insertAccumulatorRows as Z, isDerivedCreditId as _, type AccumulatorQuerier as a, X402ExactSettlementEvidenceMissingError as a$, isYieldMessage as a0, issueUpfrontChallenges as a1, msatsToFiatMicro as a2, paymentErrorBody as a3, pinAskFiat as a4, priceFiatMicro as a5, processIncomingPayment as a6, providerMessage as a7, repairX402ExactSettlementEffect as a8, resolveFxSnapshot as a9, type JobManagerOpts as aA, type LightningReceiveConfig as aB, MIN_INVOICE_TTL_SECONDS as aC, MemoryConsumedCredentialStore as aD, type MemoryConsumedCredentialStoreOpts as aE, MemoryProcessedPaymentStore as aF, MemoryX402ExactSettlementStore as aG, type MountOpts as aH, type OwnerDisplay as aI, type PlatformReporterOpts as aJ, PostgresProcessedPaymentStore as aK, PostgresX402ExactSettlementStore as aL, type PriceFiat as aM, type ProcessedPaymentQuerier as aN, type ProcessedPaymentRail as aO, type ProcessedPaymentRecord as aP, ProcessedPaymentReplayError as aQ, type ProcessedPaymentStore as aR, type X402BatchAcceptance as aS, type X402BatchFunding as aT, type X402BatchRefusal as aU, type X402BatchSettlementServer as aV, type X402ExactAcceptance as aW, X402ExactIntentConflictError as aX, type X402ExactSettlementAttempt as aY, type X402ExactSettlementChainEvidence as aZ, type X402ExactSettlementEffect as a_, resolvePriceFiat as aa, revenueReporterBannerState as ab, toCreditTerms as ac, toJobRecord as ad, unknownRouteNotFound as ae, verifyIncomingPayment as af, verifyTempoSessionManagementCredential as ag, verifyUpfrontPayment as ah, x402RequiredUsdcMicro as ai, x402SettledShare as aj, type CreditMenu as ak, ReceiptIssuer as al, LightningReceive as am, TempoSettlementReadiness as an, type DVMHostOpts as ao, type BuildCreditMenuArgs as ap, type BuilderIdentity as aq, CREDIT_ENVELOPE_KEYS as ar, type CreateX402BatchSettlementServerOpts as as, type CreditEnvelope as at, CreditEnvelopeError as au, type CreditFundCommitment as av, type CreditTerms as aw, DEFAULT_INVOICE_TTL_SECONDS as ax, type DVMHost as ay, JobManager as az, type AppEnv as b, type X402ExactSettlementEvidenceReader as b0, type X402ExactSettlementIntent as b1, X402ExactSettlementNotReadyError as b2, X402ExactSettlementServer as b3, type X402ExactSettlementServerOpts as b4, type X402ExactSettlementStatus as b5, type X402ExactSettlementStore as b6, type X402SettlementChainEvidence as b7, type X402SettlementEvidenceReader as b8, X402SettlementSubmissionError as b9, X402_BATCH_AUTO_SETTLEMENT as ba, X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS as bb, X402_BATCH_SETTLEMENT_NETWORK as bc, attachCreditMenu as bd, buildCreditMenu as be, createDVMHost as bf, createX402BatchSettlementServer as bg, creditEnvelopeIgnoreFields as bh, drawSettlementRef as bi, extractCreditEnvelope as bj, fundingCommitment as bk, selectPrimaryCredit as bl, stripCreditEnvelope as bm, toCreditView as bn, type CreditFundingReport as c, type FiatDenominationFailure as d, type FundOnlyRequest as e, type PaymentErrorCode as f, type PaymentErrorDetail as g, type PaymentInfo as h, type ResolvedFx as i, type RevenueBootCheckOpts as j, type ServerJob as k, type ShortPayForfeit as l, type TempoObserverHealth as m, TempoSessionChannelMismatchError as n, type VerifyIncomingSnapshot as o, X402FacilitatorHealth as p, type X402SettlementEvidenceOutcome as q, type X402SettlementRepair as r, type X402SettlementRepairRefusal as s, type X402SettlementRepaired as t, type X402WedgedSettlement as u, type X402WedgedSettlementPage as v, X402_BATCH_SETTLEMENT_MAINNET_NETWORK as w, abortJob as x, applyPaymentInfoToJob as y, assertRevenueReporterReady as z };
|