@dvmkit/sdk 0.1.2-rc.7 → 0.1.4-rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +121 -8
  2. package/dist/chunk-BIFLRKMO.js +87 -0
  3. package/dist/chunk-BQ2NMWKE.js +160 -0
  4. package/dist/{chunk-LDTWX7JW.js → chunk-BTZY7VPH.js} +13 -1
  5. package/dist/{chunk-FJDCFHW5.js → chunk-C6JHBLMW.js} +3 -81
  6. package/dist/{chunk-EXHBXA4U.js → chunk-DBCLBYHP.js} +13 -1
  7. package/dist/chunk-E4EVGPDX.js +391 -0
  8. package/dist/chunk-EDDYHZ6W.js +1010 -0
  9. package/dist/{chunk-2ABMGUDS.js → chunk-EVBK675R.js} +88 -10
  10. package/dist/{chunk-P4RUVDU7.js → chunk-H2MEFVH6.js} +12 -141
  11. package/dist/chunk-KXZUCCEY.js +142 -0
  12. package/dist/{chunk-LWUR4CGG.js → chunk-MLRCSJYX.js} +11 -3
  13. package/dist/{chunk-N4VTG3KH.js → chunk-QK3VJNCK.js} +930 -3011
  14. package/dist/chunk-RW5LP57K.js +44 -0
  15. package/dist/{chunk-6JZIX5WW.js → chunk-SSSZUVWM.js} +178 -8
  16. package/dist/{chunk-TKA6ZP4M.js → chunk-U6M3ATSG.js} +56 -426
  17. package/dist/chunk-VRQDX5P4.js +1742 -0
  18. package/dist/{chunk-JGGI65I3.js → chunk-Z4BNLUZF.js} +1 -150
  19. package/dist/{credit-ledger-ED6JXKVD.js → credit-ledger-2DFQHNLB.js} +2 -2
  20. package/dist/{credit-menu-BM4qCD5U.d.ts → credit-menu-C1ezIFlJ.d.ts} +1699 -1941
  21. package/dist/{fx-C-liI3oY.d.ts → fx-C6dl2LVI.d.ts} +1 -1
  22. package/dist/index.d.ts +7 -6
  23. package/dist/index.js +8 -4
  24. package/dist/internal/caller.d.ts +10441 -0
  25. package/dist/internal/caller.js +10078 -0
  26. package/dist/internal/index.d.ts +2 -10816
  27. package/dist/internal/index.js +2 -10130
  28. package/dist/internal/server.d.ts +404 -0
  29. package/dist/internal/server.js +202 -0
  30. package/dist/job-store-DHnW4Cg_.d.ts +591 -0
  31. package/dist/lightning-backend-Ci1nogk_.d.ts +367 -0
  32. package/dist/{memory-credit-ledger-XJ5VQEVP.js → memory-credit-ledger-OP24Z2KO.js} +3 -3
  33. package/dist/{postgres-job-store-J5F4GUWU.js → postgres-job-store-3RAXMNSY.js} +1 -1
  34. package/dist/{revenue-reporter-JIKUPXOK.js → revenue-reporter-ASZ7SHHH.js} +1 -1
  35. package/dist/server/index.d.ts +41 -11
  36. package/dist/server/index.js +93 -69
  37. package/dist/{job-store-C53VQ5uu.d.ts → step-cache-BLPZNizw.d.ts} +176 -585
  38. package/dist/{tempo-session-store-DALMRIWN.js → tempo-session-store-2JNOKJGX.js} +2 -2
  39. package/dist/testing/index.d.ts +25 -3
  40. package/dist/testing/index.js +36 -3
  41. package/dist/{usd-gLcJB1ps.d.ts → usd-BgOfZlk6.d.ts} +1 -1
  42. package/dist/wallet-CJC8lwxx.d.ts +29 -0
  43. package/dist/{x402-FTG2GRAQ.js → x402-T2C5MX3T.js} +6 -3
  44. package/package.json +11 -5
  45. 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 { X as JsonValue, bR as CreditLedgerLike, b_ as CreditInvoiceRecord, d3 as CreditDepositEnqueue, b$ as InvoiceSettlement, Z as ZodLike, a as DVMDescriptor, K as KVStore, o as JobStore, ah as CashuMode, at as MppxServer, aa as X402Config, p as PaymentMethod, cu as ClientCompatibilityGate, a2 as Message, ap as FundingMethod, Y as FundingReceipt, bT as CreditSnapshot, d4 as TopUpCapUnenforcedReason, _ as JobReceipt, cn as AppendOutgoingOptions, S as SDKJobContext, aQ as StepCache, v as ResponseContent, P as PaymentContent, J as JobRecord, a3 as MessageType, ac as X402Receipt, ab as X402ExactVersionSupport, d5 as X402SettlementIntent, aE as PaymentRequirementsV2, c6 as CreditLedgerQuerier, d6 as X402SettlementCursor, bV as X402SettlementStatus, d7 as X402SettlementWriteOff, bS as X402RefundSettlementGate, d8 as X402FacilitatorAuth, d9 as X402BatchSettlementConfig, cK as PostgresX402ChannelStorage, da as X402PayoutObserver, db as X402SettlementReconciliationReason, aC as PaymentRequirements, a1 as MppxCredential, aj as CreditDepositPayload, bU as DrawResult, cA as CreditLedgerError, a7 as ReceiptCredit, al as DrainReceiptEvent, ak as DrainReceipt, z as SignedRequestAudience, dc as CreditDrawReleaseEnqueue, cM as RevenueSkippedNoRailPayload, cs as ClientCompatibility, aF as PayoutReporter, R as ResolvedCreditConfig, g as CreditView } from './job-store-C53VQ5uu.js';
4
- import { F as FxFetcher, b as FxRateSnapshot } from './fx-C-liI3oY.js';
3
+ import { a1 as Message, ah as FundingMethod, X as FundingReceipt, bO as CreditSnapshot, Y as JobReceipt, S as SDKJobContext, aw as StepCache, u as ResponseContent, P as PaymentContent, a2 as MessageType, bj as X402Receipt, aE as X402Config, bi as X402ExactVersionSupport, cS as X402SettlementIntent, aq as PaymentRequirementsV2, c1 as CreditLedgerQuerier, cT as X402SettlementCursor, bQ as X402SettlementStatus, cU as X402SettlementWriteOff, bN as X402RefundSettlementGate, cV as X402FacilitatorAuth, cW as X402BatchSettlementConfig, cy as PostgresX402ChannelStorage, cX as X402PayoutObserver, br as MppxServer, ab as CashuMode, bM as CreditLedgerLike, cY as CreditDepositEnqueue, cZ as X402SettlementReconciliationReason, ao as PaymentRequirements, a0 as MppxCredential, o as PaymentMethod, bm as CreditDepositPayload, bP as DrawResult, cn as CreditLedgerError, a6 as ReceiptCredit, ae as DrainReceiptEvent, ad as DrainReceipt, K as KVStore, y as SignedRequestAudience, c_ as CreditDrawReleaseEnqueue, cx as JobCostReportPayload, cA as RevenueSkippedNoRailPayload, Z as ZodLike, a as DVMDescriptor, bV as CreditInvoiceRecord, bW as InvoiceSettlement, ch as ClientCompatibilityGate, cf as ClientCompatibility, bu as PayoutReporter, R as ResolvedCreditConfig, g as CreditView } from './step-cache-BLPZNizw.js';
4
+ import { b as FxRateSnapshot, F as FxFetcher } from './fx-C6dl2LVI.js';
5
+ import { g as LockPubkey, f as CheckMintHealthOptions, b as LightningBackend, A as AttestationPayload } from './lightning-backend-Ci1nogk_.js';
6
+ import { T as TopUpCapUnenforcedReason, A as AppendOutgoingOptions, J as JobRecord, b as JobStore } from './job-store-DHnW4Cg_.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 captured at claim time so a crashed-mid-melt
54
- * row can be auto-resolved on next scheduler boot via the mint's
55
- * `checkMeltQuoteBolt11` endpoint (internal-review). Null on rows from before the
56
- * column existed, or rows that crashed mid-melt before the quote id was
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 scheduler can run reads on a single
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,248 @@ interface CreditExpirySweepOpts {
525
162
  limit?: number;
526
163
  }
527
164
 
528
- /** Cached liveness signal for a platform-hosted DVM's Lightning receive rail. */
529
- interface LightningRailHealth {
530
- /** Whether the receive rail may be advertised at this instant. */
531
- available(): boolean;
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
- /** Builder-facing wiring for the Lightning receive leg (internal-review). */
537
- interface LightningReceiveConfig {
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
- * Receive-only NWC connection URI (`DVMKIT_NWC_RECEIVE_URI`). Validated at
540
- * boot: a connection that can *spend* is refused outright.
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
- uri?: string;
185
+ capability: string;
186
+ input: string;
187
+ params: Record<string, string>;
188
+ requesterId: string;
543
189
  /**
544
- * Pre-built backend, bypassing URI parsing and the connect-time probe. Test
545
- * seam only — production always goes through `uri` so the receive-only
546
- * property is actually checked.
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
- backend?: LightningBackend;
549
- /** Invoice lifetime in seconds. Defaults to {@link DEFAULT_INVOICE_TTL_SECONDS}. */
550
- invoiceTtlSeconds?: number;
194
+ requesterTokenHash?: string;
551
195
  /**
552
- * Smallest funding this deployment can receive over Lightning, in sats
553
- * (`DVMKIT_LIGHTNING_FUNDING_MIN_SATS`). The floor is a property of the
554
- * receive wallet's channel policy (`htlc_minimum_msat` upstream), so it is
555
- * per-deployment and sats-physical — the menu renders it into fiat with a
556
- * margin, but everything that *enforces* it compares raw sats. Defaults to
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
- fundingMinSats?: number;
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
- * Optional platform-derived channel-liveness gate. Platform-hosted DVMs use
562
- * it alongside their NWC reachability probe; self-hosted DVMs omit it.
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
- railHealth?: LightningRailHealth;
565
- }
566
- /** Default bolt11 lifetime — long enough to pay by hand, short enough to retire. */
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
- * Validate the configured connection and build the receive leg, or return
608
- * `undefined` when this DVM didn't configure one.
609
- *
610
- * **Throws on a credential that is wrong, degrades on one that is merely
611
- * unreachable.** A wallet outage at boot must not stop a DVM whose other
612
- * rails are fine — the menu simply omits `lightning` until a later probe
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
- static create(config: LightningReceiveConfig): Promise<LightningReceive | undefined>;
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";
619
242
  /**
620
- * Whether `lightning` belongs on the funding menu right now.
243
+ * What this job cost the **builder** to serve, in 1e-6 of
244
+ * {@link ServerJob.costCurrency} — the running total the handler declared
245
+ * through `ctx.cost()` (internal-review). Absent until a handler declares
246
+ * something; absent is "unreported", which is not the same fact as a
247
+ * declared zero and is reported differently.
621
248
  *
622
- * Reads the cached observation and never blocks; a stale one kicks off a
623
- * background refresh and answers with what we last knew. Advertising a rail
624
- * whose wallet is down would hand the caller an option that 402s on use —
625
- * the §4 posture is to drop it and let the sale survive on the others.
626
- */
627
- available(): boolean;
249
+ * Persisted on the row rather than held in the context because the report
250
+ * has to survive a crash between the job finishing and the report landing:
251
+ * the terminal funnel writes this row before it books, and Postgres cost
252
+ * reconciliation retries any revision whose durable-enqueue marker still
253
+ * lags. Never netted against `paidMsats` or the draw — the builder's cost
254
+ * and the caller's payment are two questions.
255
+ */
256
+ costAmountMicro?: number;
257
+ /** Currency {@link ServerJob.costAmountMicro} is 1e-6 of (lowercase ISO-4217). */
258
+ costCurrency?: string;
259
+ /** Number of `ctx.cost()` declarations incorporated into the current cost state. */
260
+ costRevision?: number;
261
+ /** Credit the upfront payment funded/drew (internal-review). See `JobRecord.creditId`. */
262
+ creditId?: string;
263
+ /** The draw placed for this job on `creditId` (internal-review). */
264
+ drawId?: string;
265
+ /** Original funding artifact returned by an explicit fund-and-draw. */
266
+ fundingReceipt?: FundingReceipt;
267
+ fundingCredit?: CreditSnapshot;
268
+ receivedProofs: ProofLike[];
269
+ pendingPaymentMsats?: number;
628
270
  /**
629
- * Issue (or re-issue) the bolt11 funding `(creditId, fundId)`.
630
- *
631
- * The invoice is minted **for** the credit and amount named in the caller's
632
- * signed body — that binding is the internal-review condition-3 commitment on this
633
- * rail, and it is why no artifact hash rides the request: there is no
634
- * caller-supplied artifact to hash. A re-poll returns the stored row rather
635
- * than minting again; a second bolt11 for one `fund_id` would leave two
636
- * payable invoices against a funding that can only be credited once.
271
+ * Per-job binding for MPP credentials. Contains the `challenge.id` of every
272
+ * mppx challenge issued in the most-recent `requestPayment` yield. mppx HMAC
273
+ * binds challenges to realm/method/amount/expiry, not to the dvmkit job-id;
274
+ * verifying the incoming credential's `challenge.id` is in this set blocks
275
+ * a credential lifted from a sibling job that requested the same fields.
276
+ * Cleared on successful credit (internal-review).
637
277
  */
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>;
278
+ pendingMppChallengeIds?: string[];
647
279
  /**
648
- * Ask the wallet whether one payment hash is paid, and when (internal-review).
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.
280
+ * Per-job binding for x402 mid-job credentials (internal-review). 32-byte 0x-hex
281
+ * nonce issued by `requestPayment` and stamped onto the outbound x402
282
+ * envelope; the caller signs `TransferWithAuthorization` against this exact
283
+ * `bytes32`. Verification rejects an incoming `x402_payment` whose decoded
284
+ * authorization nonce differs, blocking a cross-job replay before the
285
+ * facilitator settles. Cleared on successful credit.
666
286
  */
667
- lookupSettlement(paymentHash: string): Promise<{
668
- settled: boolean;
669
- settledAt?: number;
670
- known: boolean;
671
- }>;
287
+ pendingX402Nonce?: string;
672
288
  /**
673
- * Consult the wallet about this caller's outstanding invoices and credit the
674
- * ones that settled — the pull half of pull-based crediting.
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.
289
+ * Exact USDC microunit amount paired with `pendingX402Nonce`. Kept as a
290
+ * decimal string so the EIP-3009 uint256 value remains lossless.
690
291
  */
691
- settlePending(ledger: CreditLedgerLike, args: {
692
- callerPubkey: string;
693
- creditId?: string;
694
- creditTtlMs: number;
695
- dvmId?: string;
696
- enqueueCreditDeposit?: CreditDepositEnqueue;
697
- nowMs?: number;
698
- }): Promise<InvoiceSettlement[]>;
292
+ pendingX402AmountUsdcMicro?: string;
699
293
  /**
700
- * One invoice's settlement check. Returns the applied settlement, or
701
- * `undefined` when nothing changed (still unpaid, or retired unpaid).
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.
294
+ * Fiat micro-units the outstanding `requestPayment` ask is worth, pinned when
295
+ * the payment-request was emitted (internal-review). See `JobRecord`.
705
296
  */
706
- private applyOne;
297
+ pendingPaymentFiatMicro?: number;
298
+ /** Currency of {@link pendingPaymentFiatMicro} (lowercase ISO-4217). */
299
+ pendingPaymentFiatCurrency?: string;
707
300
  /**
708
- * Decide what one invoice's failure means, and retire the invoice when the
709
- * answer is "this can never succeed".
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.
301
+ * Cumulative fiat micro this job has asked for mid-job, the ceiling on its
302
+ * draw growth (internal-review). See `JobRecord.askedTopUpMicro` for the sentinel.
720
303
  */
721
- private retireOrLog;
304
+ askedTopUpMicro?: number;
722
305
  /**
723
- * The reason this invoice can never be credited, or `undefined` if the
724
- * failure was transient.
725
- *
726
- * Every code here is decided by state a retry cannot move — a row the ledger
727
- * has already committed (a `credit_fundings` entry at this
728
- * `(credit_id, fund_id)`, or a `credits` row whose owner, denomination or
729
- * rail disagrees with the invoice), or the basis this module rebuilds
730
- * identically from the invoice row on every pass. None of them is a race.
306
+ * Currency of {@link askedTopUpMicro} — and this job's sticky ask
307
+ * denomination (internal-review). See `JobRecord.askedTopUpCurrency`.
308
+ */
309
+ askedTopUpCurrency?: string;
310
+ /**
311
+ * Why this job's draw growth is not capped, when it isn't (internal-review). Mirror
312
+ * of `JobRecord.topUpCapUnenforcedReason`.
313
+ */
314
+ topUpCapUnenforcedReason?: TopUpCapUnenforcedReason;
315
+ /** Price the job was charged at (msats). Used to detect overpayment for change/melt gating. */
316
+ requiredMsats?: number;
317
+ /**
318
+ * The signed receipt for this job's terminal outcome (internal-review). Mirror of
319
+ * `JobRecord.receipt`, set once the terminal funnel has signed and persisted
320
+ * it so the local read paths (which serve from `activeJobs` during the
321
+ * cleanup window) hand back the same bytes as a cross-machine re-read.
322
+ */
323
+ receipt?: JobReceipt;
324
+ /**
325
+ * The in-flight terminal chain — persist, then sign + store the receipt
326
+ * (internal-review). Assigned synchronously by `ctx.complete()` / `ctx.fail()` at
327
+ * the instant the job goes terminal, so the `POST /v1/job` fast path can
328
+ * await it (bounded) and put the receipt on the synchronous 200 rather than
329
+ * making the caller poll for it. Never rejects — the chain swallows its own
330
+ * failures.
331
+ */
332
+ terminalWork?: Promise<void>;
333
+ listeners: Set<(msg: Message) => void>;
334
+ /**
335
+ * Cancellation signal for the running handler (internal-review). Aborted whenever
336
+ * the job is cancelled on this process — caller cancel, idle timeout,
337
+ * stale-sweep teardown, supersession. Surfaced to builders as `ctx.signal`
338
+ * and auto-threaded into `ctx.fetch`, so an in-flight provider call
339
+ * (ElevenLabs, Scrapfly, …) is torn down instead of billing the caller for
340
+ * work they just cancelled. Per-process runtime state like `listeners` —
341
+ * never persisted, and re-created fresh on reactivation.
342
+ */
343
+ abort: AbortController;
344
+ /**
345
+ * Hook for cross-machine message persistence (Tx A, internal-review). Set by
346
+ * `JobManager` when the store is streamable. The appender runs the
347
+ * outgoing-message tx atomically with any payment-request counter bump so
348
+ * `(message, pending_payment_msats)` land together — never separately.
731
349
  *
732
- * `funding_replayed` is the one that needs a second read to classify.
733
- * Usually it *is* benign — a concurrent check won the race and the money is
734
- * credited either way — but the key is `(credit_id, fund_id)` and `fund_id`
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.
350
+ * Per-job serialised (internal-review): each call chains onto `messageAppenderTail`
351
+ * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
352
+ * `complete`) can't race for the DB-side seq allocation.
738
353
  */
739
- private blockingReason;
740
- private withBackend;
741
- /** Record activity without letting it promote an unvalidated connection. */
742
- private noteOperationOk;
743
- private notePermissionsOk;
744
- private notePermissionRefused;
354
+ messageAppender?: (msg: Message, opts?: AppendOutgoingOptions) => void;
745
355
  /**
746
- * Only a *transport* failure from an ordinary invoice operation is a health
747
- * signal. Permission-probe refusals are classified by {@link refresh}.
356
+ * Tail of the per-job appender chain (internal-review). `wireMessageAppender` updates
357
+ * this on every call so `persistJob` can await it before the terminal snapshot
358
+ * save runs `DELETE FROM job_messages`.
748
359
  */
749
- private noteFailure;
750
- private refresh;
360
+ messageAppenderTail?: Promise<unknown>;
361
+ sdkCtx?: SDKJobContext<unknown, unknown>;
362
+ stepCache: StepCache;
363
+ state: unknown;
364
+ pendingPrompts: Map<string, PromptResolver>;
365
+ pendingPayment: PaymentResolver | null;
366
+ /** When not null, indicates replay mode — prompt/payment scan message history for cached responses. */
367
+ replayHighSeq: number | null;
368
+ /** Number of provider messages to suppress during replay. Cleared on handler error. */
369
+ replayProviderSkip: number;
370
+ /** Index of the next payment to match during replay (for multi-payment handlers). */
371
+ replayPaymentIndex: number;
372
+ /**
373
+ * Set once Tx C applies the current payment ask during this reactivation.
374
+ * Runtime-only: replayed prompts may mutate `status`, but never clear this
375
+ * marker before the matching payment is consumed.
376
+ */
377
+ reactivationPaymentApplied?: true;
378
+ /** Unix ms timestamp of job creation. */
379
+ createdAt: number;
380
+ /** Unix ms timestamp of last activity (message, yield, terminal). */
381
+ lastActivityAt: number;
382
+ }
383
+ /** Extract the persistent fields from a ServerJob into a JobRecord. */
384
+ declare function toJobRecord(job: ServerJob): JobRecord;
385
+ /** Reconstitute a ServerJob from a persisted JobRecord with fresh runtime fields. */
386
+ declare function fromJobRecord(record: JobRecord): ServerJob;
387
+ /**
388
+ * Signal cancellation to a running handler (internal-review). Idempotent — aborting an
389
+ * already-aborted controller is a no-op, so the overlapping teardown paths
390
+ * (caller cancel then stale sweep, say) can each call it unconditionally.
391
+ */
392
+ declare function abortJob(job: ServerJob, reason: string): void;
393
+ /**
394
+ * Reason carried on `ctx.signal.reason` and thrown by post-terminal `ctx.prompt`
395
+ * / `ctx.requestPayment` calls. Builders can `instanceof`-check it to tell a
396
+ * cancellation apart from a provider-side abort.
397
+ */
398
+ declare class JobCancelledError extends Error {
399
+ constructor(message?: string);
751
400
  }
401
+ /** Append a provider-sent message to the job and notify SSE listeners. */
402
+ declare function providerMessage(job: ServerJob, type: MessageType, content: Record<string, unknown> | object): void;
403
+ /** Check if a job status is terminal (completed, failed, cancelled). */
404
+ declare function isTerminal(status: ServerJob["status"]): boolean;
405
+ /** Check if a message is a yield point (ends the provider's turn). */
406
+ declare function isYieldMessage(msg: Message): boolean;
752
407
 
753
408
  /**
754
409
  * Per-DVM runtime mint-health tracker (internal-review). Alongside the platform's
@@ -894,419 +549,38 @@ declare class MintHealthTracker {
894
549
  }
895
550
 
896
551
  /**
897
- * Owner display identity surfaced on `/v1/info#owner` (internal-review). Personal orgs
898
- * resolve to the owner builder's profile; shared orgs resolve to the org's own
899
- * profile. Container DVMs read from env vars; isolate DVMs resolve from Postgres.
552
+ * Credit fields riding the signed request body (internal-review, spec §2).
553
+ *
554
+ * An explicit draw references the credit INSIDE the secp256k1/Schnorr-signed
555
+ * `body.data` — no new caller crypto. Wire field names (snake_case, matching
556
+ * the rest of the wire surface):
557
+ *
558
+ * - `credit_id` — the credit to draw against.
559
+ * - `draw_id` — client-generated draw id; the ledger's idempotency key.
560
+ * - `fund` — optional `{ amount_micro, commitment }` funding commitment:
561
+ * present iff a funding artifact (X-Cashu / X-PAYMENT / mpp credential)
562
+ * rides the same request. `commitment` is the SHA-256 hex over the raw
563
+ * artifact bytes, binding the header-borne proof — which sits OUTSIDE the
564
+ * signed body — to the credit its sender intended (spec §2 condition 3).
565
+ *
566
+ * These are envelope-level fields, not capability input: **the SDK** removes
567
+ * them before any builder-owned schema parses ({@link stripCreditEnvelope} in
568
+ * `JobManager.parseInput` and on the quote path; {@link
569
+ * creditEnvelopeIgnoreFields} feeding the auth verifier's own schema check), so
570
+ * handlers never see them and a top-level `.strict()` capability schema is
571
+ * fine (internal-review). Relying on Zod's default strip mode instead was the trap:
572
+ * `.strict()` rejects unknown keys, so an explicit draw 400'd at parse before
573
+ * the envelope was ever extracted.
574
+ *
575
+ * The strip never touches the canonical signing bytes. Every auth call site
576
+ * verifies the raw wire body (`signedRequestInput`, internal-review), so the credit
577
+ * fields are in the verified bytes because they were never taken out of them —
578
+ * the strip is only ever applied to what a schema is about to parse.
900
579
  */
901
- interface OwnerDisplay {
902
- handle: string;
903
- displayName?: string | null;
904
- avatarUrl?: string | null;
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;
580
+ interface CreditEnvelope {
581
+ creditId: string;
582
+ drawId: string;
583
+ fund?: CreditFundCommitment;
1310
584
  }
1311
585
  /** The signed funding commitment for a fund-and-draw request. */
1312
586
  interface CreditFundCommitment {
@@ -1620,16 +894,11 @@ declare function fundedMicroFor(priceFiat: PriceFiat, paidMsats: number, require
1620
894
  * quote is 11 sats and the old advertisement demanded 10,718 µUSDC — 7% above
1621
895
  * the price the caller was quoted and the ledger recorded.
1622
896
  *
1623
- * Both call sites — `buildX402Requirements` and the upfront verify branch —
1624
- * MUST derive through here rather than recomputing. Two independent derivations
1625
- * is exactly the shape that produced this bug.
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.
897
+ * Every caller gates x402 on a USD-priced DVM before reaching this helper.
898
+ * Keeping the helper USD-only makes a future advertisement call site fail
899
+ * closed instead of quietly restoring the rate-derived non-USD path.
1631
900
  */
1632
- declare function x402RequiredUsdcMicro(priceFiat: PriceFiat | undefined, requiredMsats: number, btcUsdRate: number): number;
901
+ declare function x402RequiredUsdcMicro(priceFiat: PriceFiat): number;
1633
902
  /**
1634
903
  * A settled x402 authorization's share of a quoted figure, taken against the
1635
904
  * `maxAmountRequired` the 402 advertised (internal-review) — `quoted` unmodified when
@@ -1639,7 +908,7 @@ declare function x402RequiredUsdcMicro(priceFiat: PriceFiat | undefined, require
1639
908
  *
1640
909
  * x402 must NOT reach {@link fundedMicroFor} through `usdcToMsats`. The
1641
910
  * advertised figure is stated in µUSDC by {@link x402RequiredUsdcMicro} — the
1642
- * price verbatim for a USD job, a `Math.ceil` off the rate otherwise — and the
911
+ * price verbatim for a USD job — and the
1643
912
  * receipt converts back with `usdcToMsats`, a `Math.round`, which is the
1644
913
  * inverse of neither. The round trip lands at or above `requiredMsats`, which
1645
914
  * took the pro-rata branch and valued a payment of exactly the advertised
@@ -2524,6 +1793,8 @@ interface PaymentInfo {
2524
1793
  */
2525
1794
  interface CreditFundingReport {
2526
1795
  creditId: string;
1796
+ /** Canonical owner returned by the ledger after applying the funding. */
1797
+ callerPubkey: string;
2527
1798
  /** The rail's own settlement reference — the deposit's idempotency key. */
2528
1799
  fundingId: string;
2529
1800
  rail: FundingMethod;
@@ -2931,7 +2202,7 @@ declare function verifyTempoSessionManagementCredential(args: {
2931
2202
  * here: the former is already complete, the latter is a challenge envelope, not
2932
2203
  * an error report.
2933
2204
  */
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;
2205
+ 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
2206
  /**
2936
2207
  * Build a flat payment-error body carrying the four agent-facing fields every
2937
2208
  * payment error must have (internal-review): the machine `code`, the operator-facing
@@ -2984,6 +2255,8 @@ declare function buildPaymentErrorResponse(body: Record<string, unknown>, challe
2984
2255
  interface IncomingPaymentOpts {
2985
2256
  mints?: string[];
2986
2257
  x402Config?: X402Config;
2258
+ /** Declared DVM pricing currency. x402 is accepted only when this is USD. */
2259
+ pricingCurrency?: string;
2987
2260
  /** Durable exact-settlement coordinator shared with the upfront path. */
2988
2261
  x402ExactSettlement?: X402ExactSettlementServer;
2989
2262
  /** Exact generations the facilitator currently advertises. Defaults to both. */
@@ -3312,1094 +2585,1569 @@ interface X402FacilitatorHealthOpts extends X402FacilitatorProbeOpts {
3312
2585
  refreshTtlMs?: number;
3313
2586
  maxStalenessMs?: number;
3314
2587
  }
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;
2588
+ /** Live, synchronous capability view consumed while payment responses are rendered. */
2589
+ interface X402FacilitatorHealthLike {
2590
+ /** Prime or refresh the capability observation. */
2591
+ refresh(): Promise<void>;
2592
+ /** Return current support and begin a background refresh when its TTL has elapsed. */
2593
+ support(): X402FacilitatorSupport;
2594
+ /** Last successful raw response, reused by batch initialization without another request. */
2595
+ supportedResponse?(): SupportedResponse | undefined;
2596
+ }
2597
+ /**
2598
+ * TTL-cached facilitator capability gate. Failed refreshes retain the last
2599
+ * successful observation until the same three-hour ceiling used by the
2600
+ * Lightning rail; a cold-start failure temporarily assumes exact dual-serve
2601
+ * so a facilitator blip does not suppress a healthy payment rail for a whole
2602
+ * process lifetime. Batch settlement is never assumed without an observation.
2603
+ */
2604
+ declare class X402FacilitatorHealth implements X402FacilitatorHealthLike {
2605
+ private readonly config;
2606
+ private readonly opts;
2607
+ private readonly now;
2608
+ private readonly refreshTtlMs;
2609
+ private readonly maxStalenessMs;
2610
+ private readonly startedAtMs;
2611
+ private observation;
2612
+ private lastAttemptAtMs;
2613
+ private inFlight;
2614
+ private ceilingLoggedForMs;
2615
+ private lastSupportedResponse;
2616
+ constructor(config: X402Config, opts?: X402FacilitatorHealthOpts);
2617
+ /** Refresh the observation, coalescing concurrent requests. */
2618
+ refresh(): Promise<void>;
2619
+ /** Answer synchronously from cache and start a non-blocking refresh when due. */
2620
+ support(): X402FacilitatorSupport;
2621
+ /** Return the raw response paired with the last successful observation. */
2622
+ supportedResponse(): SupportedResponse | undefined;
2623
+ private get facilitator();
2624
+ private get network();
2625
+ private refreshOnce;
2626
+ }
2627
+
2628
+ /** Options for creating a JobManager. */
2629
+ interface JobManagerOpts {
2630
+ /** Environment variables for ctx.env. */
2631
+ env: Record<string, string>;
2632
+ /** KV store for ctx.store. */
2633
+ store: KVStore;
2634
+ /** Cashu mints accepted for payment. */
2635
+ mints?: string[];
2636
+ /** Persistent backing store for job records. Default: in-memory. */
2637
+ jobStore?: JobStore;
2638
+ /** When true, payment messages auto-credit without verification. */
2639
+ devMode?: boolean;
2640
+ /** Payment methods this DVM accepts. Default: derived by the server from configured rails. */
2641
+ paymentMethods?: PaymentMethod[];
2642
+ /** x402 stablecoin payment configuration. */
2643
+ x402?: X402Config;
2644
+ /** Durable exact-settlement coordinator shared with upfront verification. */
2645
+ x402ExactSettlement?: X402ExactSettlementServer;
2646
+ /** Live facilitator capabilities for version-specific mid-job payment handling. */
2647
+ x402FacilitatorHealth?: X402FacilitatorHealthLike;
2648
+ /** MPP multi-rail payment handle (built via `createMppFromOpts`). */
2649
+ mpp?: MppxServer;
2650
+ /** Cashu receive mode (internal-review). */
2651
+ cashuMode?: CashuMode;
2652
+ /** Builder's NUT-11 P2PK lock pubkey (internal-review). Required for accumulator mode. */
2653
+ lockPubkey?: string;
2654
+ /** Postgres pool for wallet accumulator persistence. */
2655
+ db?: Pool;
2656
+ /**
2657
+ * Agent-wallet (internal-review): per-DVM identifier used for the scheduler advisory
2658
+ * lock and the `(dvm_id, request_id)` replay key. Defaults to `DVMKIT_DVM_ID`
2659
+ * env at boot in `createDVMHost()`.
2660
+ */
2661
+ dvmId?: string;
2662
+ /** Audience expected by persisted v2 caller statements. */
2663
+ authAudience?: SignedRequestAudience;
2664
+ /**
2665
+ * Shared fx fetcher used to convert USD-string descriptor prices and
2666
+ * fiat-form `requestPayment` calls into sats (internal-review). The SDK server owns
2667
+ * one per process so concurrent quotes share the 60s cache window.
2668
+ */
2669
+ fxFetcher: FxFetcher;
2670
+ /**
2671
+ * Signs a receipt at every terminal transition (internal-review). Absent when the
2672
+ * DVM has no receipt key wired — jobs then terminate exactly as before and
2673
+ * `/v1/info` doesn't advertise `receipts`.
2674
+ */
2675
+ receiptIssuer?: ReceiptIssuer;
2676
+ /**
2677
+ * Credit ledger for the terminal funnel (internal-review): a job carrying a
2678
+ * `creditId`/`drawId` settles its draw on success and releases it on
2679
+ * failure/cancel — this is how "no debit on job failure" is mechanically
2680
+ * real. The resolution runs before receipt issuance so the receipt's
2681
+ * `credit` block countersigns the post-resolution balance.
2682
+ */
2683
+ creditLedger?: CreditLedgerLike;
2684
+ /**
2685
+ * Durable processed-payment markers (internal-review), threaded through to the
2686
+ * mid-job top-up path (internal-review) so an x402/mpp payment's marker, `fund`, and
2687
+ * `growDraw` commit together.
2688
+ */
2689
+ processedPayments?: ProcessedPaymentStore;
2690
+ /**
2691
+ * Reporter-owned transactional deposit enqueue threaded into mid-job rail
2692
+ * commits (internal-review).
2693
+ */
2694
+ enqueueCreditDeposit?: CreditDepositEnqueue;
2695
+ /** Reporter outbox inserted atomically with a terminal draw release (internal-review). */
2696
+ enqueueCreditDrawRelease?: CreditDrawReleaseEnqueue;
2697
+ /** Credit lifetime a top-up mints with, from the DVM's advertised `credit.ttl`. */
2698
+ creditTtlMs?: number;
2699
+ /**
2700
+ * The DVM's declared pricing currency (internal-review), which is the currency of
2701
+ * every credit it opens. Threaded into the job context so a mid-job ask on a
2702
+ * job that holds no credit yet is still pinned in the denomination its first
2703
+ * payment will open the credit in (internal-review). `DVMServer` passes its resolved
2704
+ * `pricingCurrency`; a host building a `JobManager` directly falls back to the
2705
+ * descriptor, and then to `"usd"`.
2706
+ */
2707
+ pricingCurrency?: string;
2708
+ /**
2709
+ * Callback fired when a mid-job top-up moves money onto a credit (internal-review).
2710
+ * The platform books it as a **deposit** — a liability until drawn, never
2711
+ * summed into revenue — so a top-up that skipped this would leave the
2712
+ * outstanding-liability view short. Mirrors `DVMServer.reportCreditDeposit`,
2713
+ * which handles the upfront and `/v1/credit` legs.
2714
+ */
2715
+ onCreditFunded?: (info: CreditDepositPayload) => void | Promise<void>;
2716
+ /**
2717
+ * Callback fired when a paid job completes (internal-review). Container-runtime DVMs
2718
+ * use this to report revenue to the platform via `RevenueReporter`. Mirrors
2719
+ * the isolate path's `IsolateJobManager.opts.onJobCompleted`.
2720
+ */
2721
+ onJobCompleted?: (info: {
2722
+ dvmId: string;
2723
+ jobId: string;
2724
+ /** Dispatched capability. Omitted only for legacy persisted jobs that predate dispatch. */
2725
+ capability?: string;
2726
+ paidMsats: number;
2727
+ paymentMint?: string;
2728
+ rail: string;
2729
+ paymentTxHash?: string;
2730
+ nativeAmount?: number;
2731
+ nativeAsset?: string;
2732
+ cashuFlow?: string;
2733
+ cost?: {
2734
+ amountMicro: number;
2735
+ currency: string;
2736
+ };
2737
+ creditId?: string;
2738
+ drawId?: string;
2739
+ drawAmountMicro?: number;
2740
+ creditCurrency?: string;
2741
+ settledAt?: number;
2742
+ fundingRef?: string;
2743
+ kind?: string;
2744
+ }) => void | Promise<void>;
2745
+ /**
2746
+ * Callback fired when a terminal job declared a builder cost but emitted no
2747
+ * revenue report (internal-review). Container-runtime DVMs use the reporter's
2748
+ * durable `job-cost` outbox path. The receiver de-duplicates by DVM + job,
2749
+ * independently of the rail-keyed revenue ledger.
2750
+ */
2751
+ onJobCost?: (info: JobCostReportPayload) => void | Promise<void>;
2752
+ /**
2753
+ * Callback fired when the stale-job reaper force-fails a *paid* job — its
2754
+ * pending credit draw is released rather than settled, because only a
2755
+ * completed job settles a draw (internal-review). Container-runtime DVMs wire this
2756
+ * to a paid-job-death report → operator `notice` (internal-review). Never fires for
2757
+ * free jobs (`paidMsats === 0`). Fired fire-and-forget so a throw or a slow
2758
+ * report can't wedge the sweep.
2759
+ */
2760
+ onPaidJobDeath?: (info: {
2761
+ dvmId: string;
2762
+ jobId: string;
2763
+ capability: string;
2764
+ paidMsats: number;
2765
+ rail: string;
2766
+ reason: "worker_died_mid_job" | "stale_no_terminal_status";
2767
+ nativeAmount?: number;
2768
+ nativeAsset?: string;
2769
+ paymentMint?: string;
2770
+ }) => void | Promise<void>;
2771
+ /**
2772
+ * Callback fired when a settled credit draw cannot book a revenue event
2773
+ * because no usable payment rail is available (internal-review). Container-runtime
2774
+ * DVMs auto-wire this to the platform's deduped operator notice. Fired
2775
+ * fire-and-forget so alert delivery cannot block terminal handling or draw
2776
+ * reconciliation.
2777
+ */
2778
+ onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
2779
+ /**
2780
+ * Stale-job sweep threshold in ms (internal-review). Jobs in a non-terminal status
2781
+ * (`processing` / `working` / `awaiting-input`) whose `lastActivityAt` is
2782
+ * older than this are force-cancelled with reason `stale_no_terminal_status`,
2783
+ * so callers always reach a terminal status instead of polling forever after
2784
+ * a worker crash, OOM, or silent `messageAppender` failure. Default:
2785
+ * `2 × descriptor.idleTimeout + STALE_JOB_WATCHDOG_HEADROOM_MS`, matching
2786
+ * the internal-review acceptance criterion against scrape's `JOB_TIMEOUT_MS`.
2787
+ * Builders whose per-handler wall-clock bound exceeds 90s must override
2788
+ * this — otherwise the watchdog fires on a legitimate in-flight handler.
2789
+ * Set to `0` to disable (test seam).
2790
+ */
2791
+ staleJobTimeoutMs?: number;
2792
+ /**
2793
+ * Sweep interval for the stale-job watchdog. Default: `min(60s, max(15s,
2794
+ * tightestActiveTimeout / 4))`. Tests override to fire deterministically.
2795
+ */
2796
+ staleJobSweepIntervalMs?: number;
2797
+ /**
2798
+ * Worker-liveness watchdog in ms (internal-review). Jobs in `processing`/`working`
2799
+ * whose `lastActivityAt` is older than this are marked `failed` (dead
2800
+ * worker), separately from the longer `awaiting-input` idle timeout above.
2801
+ * Default: `descriptor.processingWatchdog * 1000` or
2802
+ * `DEFAULT_PROCESSING_WATCHDOG_MS`. Set to `0` to disable (test seam).
2803
+ */
2804
+ processingWatchdogMs?: number;
2805
+ /**
2806
+ * Interval at which locally-active `processing`/`working` jobs have their
2807
+ * `lastActivityAt` bumped so the processing watchdog only fires on a truly
2808
+ * dead worker (internal-review). Default: `DEFAULT_HEARTBEAT_INTERVAL_MS`. Set to
2809
+ * `0` to disable (test seam). Also floors how far the internal-review clock guard
2810
+ * may compensate a backwards step on the processing arm — a cadence close to
2811
+ * `processingWatchdogMs` leaves it no room and it compensates nothing.
2812
+ */
2813
+ heartbeatIntervalMs?: number;
2814
+ /**
2815
+ * Age in ms at which a `pending` credit draw whose `job_id` matches no row in
2816
+ * the job store is released as orphaned (internal-review). The draw commits before
2817
+ * the job row is persisted, so a crash or a fail-closed refusal in that
2818
+ * window strands a hold no terminal path can ever reach. Must stay
2819
+ * comfortably above the submit path's draw→persist latency. Set to `0` to
2820
+ * disable the watchdog entirely — stranded holds then stay stranded, so
2821
+ * treat it as a money knob, not a tuning one. Default:
2822
+ * {@link DEFAULT_ORPHAN_DRAW_AGE_MS}.
2823
+ */
2824
+ orphanDrawAgeMs?: number;
2825
+ /**
2826
+ * Age in ms at which a `pending` credit draw whose job row **is** terminal is
2827
+ * reconciled against that row's outcome (internal-review) — settled for a
2828
+ * `completed` job, released for a `failed`/`cancelled` one. Covers the hole
2829
+ * `orphanDrawAgeMs` cannot: `resolveCreditDraw` throwing on a ledger error
2830
+ * leaves a terminal job whose hold no later path retries.
2831
+ *
2832
+ * Rides the same scan as the orphan arm, which has two consequences for the
2833
+ * value you set here. Setting `orphanDrawAgeMs` to `0` disables this arm too.
2834
+ * And the effective gate is `max(orphanDrawAgeMs, terminalDrawReconcileAgeMs)`
2835
+ * — the scan never returns a row younger than its own cutoff, so anything
2836
+ * below `orphanDrawAgeMs` is silently clamped up to it. That floor is
2837
+ * deliberate rather than a wart: honouring a narrower value would mean
2838
+ * widening the scan, which hands the orphan arm rows younger than
2839
+ * `orphanDrawAgeMs` that it must not release. It can only ever delay a
2840
+ * settle, never advance one, which is the safe direction for a debit.
2841
+ *
2842
+ * Set to `0` to disable this arm alone — a money knob, not a tuning one.
2843
+ * Default: {@link DEFAULT_TERMINAL_DRAW_RECONCILE_AGE_MS}.
2844
+ */
2845
+ terminalDrawReconcileAgeMs?: number;
2846
+ /**
2847
+ * Sweep interval for the orphan-draw watchdog. Default:
2848
+ * {@link DEFAULT_ORPHAN_DRAW_SWEEP_INTERVAL_MS}. Set to `0` to disable — the
2849
+ * test seam, since suites drive `sweepOrphanDraws()` directly.
2850
+ */
2851
+ orphanDrawSweepIntervalMs?: number;
2852
+ /** Retention sweep cadence. Default: daily; `0` disables the interval. */
2853
+ jobRetentionSweepIntervalMs?: number;
2854
+ /** Delay before the first retention pass. Default: 30s; `0` disables it. */
2855
+ jobRetentionSweepBootDelayMs?: number;
2856
+ /** Operator-alert boundary invoked whenever a retention pass fails. */
2857
+ onJobRetentionSweepFailed?: (info: {
2858
+ dvmId: string;
2859
+ dvmName: string;
2860
+ retentionDays: number;
2861
+ error: string;
2862
+ }) => void | Promise<void>;
2863
+ /** Clear-on-recovery boundary invoked after every complete, clean pass. */
2864
+ onJobRetentionSweepRecovered?: (info: {
2865
+ dvmId: string;
2866
+ dvmName: string;
2867
+ retentionDays: number;
2868
+ examined: number;
2869
+ redacted: number;
2870
+ }) => void | Promise<void>;
2871
+ /**
2872
+ * Grace window in ms a reactivating machine waits for a live original handler
2873
+ * to win the single-execution claim before replaying an `awaiting-input` job
2874
+ * itself (internal-review). Default: `DEFAULT_REACTIVATION_CLAIM_GRACE_MS`. Widen if
2875
+ * cross-machine NOTIFY latency causes spurious double executions; lower in
2876
+ * tests for speed.
2877
+ */
2878
+ reactivationClaimGraceMs?: number;
2879
+ }
2880
+ type ResolvedInput<I extends ZodLike | undefined> = I extends ZodLike<infer O> ? O : string;
2881
+ /**
2882
+ * A deposit this payment funded that the job row did **not** take (internal-review).
2883
+ *
2884
+ * Reached when two mid-job top-ups race on a free-then-paid job: each opens its
2885
+ * own implicit credit, and `verifyAndCredit`'s set-if-null bind keeps one. The
2886
+ * loser's money is real, owned by the caller, and reclaimable — but nothing
2887
+ * named it, so the caller would have to derive `imp:<rail>:<payment_id>` to
2888
+ * reach their own balance. This rides the 201 so they don't have to.
2889
+ *
2890
+ * The response is the fast surface, not the only one: the credit is opened
2891
+ * under the caller's verified pubkey, so `POST /v1/credit` op `balance` lists
2892
+ * it and op `drain` reclaims it once the hold resolves.
2893
+ */
2894
+ interface UnappliedCredit {
2895
+ credit_id: string;
2896
+ credited_micro: number;
2897
+ credit_currency: string;
2898
+ display: string;
2899
+ hint: string;
3323
2900
  }
3324
2901
  /**
3325
- * TTL-cached facilitator capability gate. Failed refreshes retain the last
3326
- * successful observation until the same three-hour ceiling used by the
3327
- * Lightning rail; a cold-start failure temporarily assumes exact dual-serve
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.
2902
+ * What {@link JobManager.processPayment} tells the route. `error` is a refusal
2903
+ * to serialise verbatim; otherwise the payment was accepted, optionally
2904
+ * carrying an {@link UnappliedCredit} to fold into the 201.
3330
2905
  */
3331
- declare class X402FacilitatorHealth implements X402FacilitatorHealthLike {
3332
- private readonly config;
2906
+ type ProcessPaymentOutcome = {
2907
+ error: {
2908
+ body: Record<string, unknown>;
2909
+ status: number;
2910
+ };
2911
+ unappliedCredit?: undefined;
2912
+ } | {
2913
+ error?: undefined;
2914
+ unappliedCredit?: UnappliedCredit;
2915
+ };
2916
+ /**
2917
+ * Transport-agnostic job lifecycle manager.
2918
+ *
2919
+ * Handles job creation, handler invocation, message dispatch, persistence,
2920
+ * idle timeout, and durable replay. Shared by both the HTTP server and
2921
+ * relay provider.
2922
+ */
2923
+ declare class JobManager<State, InputSchema extends ZodLike | undefined> {
2924
+ private readonly descriptor;
3333
2925
  private readonly opts;
3334
- private readonly now;
3335
- private readonly refreshTtlMs;
3336
- private readonly maxStalenessMs;
3337
- private readonly startedAtMs;
3338
- private observation;
3339
- private lastAttemptAtMs;
3340
- private inFlight;
3341
- private ceilingLoggedForMs;
3342
- private lastSupportedResponse;
3343
- constructor(config: X402Config, opts?: X402FacilitatorHealthOpts);
3344
- /** Refresh the observation, coalescing concurrent requests. */
3345
- refresh(): Promise<void>;
3346
- /** Answer synchronously from cache and start a non-blocking refresh when due. */
3347
- support(): X402FacilitatorSupport;
3348
- /** Return the raw response paired with the last successful observation. */
3349
- supportedResponse(): SupportedResponse | undefined;
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;
2926
+ private readonly _activeJobs;
2927
+ private readonly jobStore;
2928
+ private readonly idleTimers;
2929
+ private readonly cleanupTimers;
2930
+ /** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
2931
+ private readonly terminalCostHandoffRetryTimers;
2932
+ /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
2933
+ private readonly statusWatchers;
2934
+ /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
2935
+ private readonly statusChecksInFlight;
2936
+ /** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
2937
+ private readonly statusChecksQueued;
2938
+ private readonly sessionId;
2939
+ private nextJobId;
2940
+ private accumulatorMonitor?;
2941
+ private readonly staleJobTimeoutMs;
2942
+ private readonly processingWatchdogMs;
2943
+ private readonly reactivationClaimGraceMs;
3383
2944
  /**
3384
- * Agent-wallet (internal-review): per-DVM identifier used for the scheduler advisory
3385
- * lock and the `(dvm_id, request_id)` replay key. Defaults to `DVMKIT_DVM_ID`
3386
- * env at boot in `createDVMHost()`.
2945
+ * Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
2946
+ * guard's compensation on the processing arm — see
2947
+ * {@link staleSweepCompensationCapMs}.
3387
2948
  */
3388
- dvmId?: string;
3389
- /** Audience expected by persisted v2 caller statements. */
3390
- authAudience?: SignedRequestAudience;
2949
+ private readonly heartbeatIntervalMs;
2950
+ private readonly orphanDrawAgeMs;
2951
+ private readonly terminalDrawReconcileAgeMs;
2952
+ private staleSweepTimer?;
2953
+ private heartbeatTimer?;
2954
+ private orphanDrawSweepTimer?;
2955
+ private terminalCostRecoveryTimer?;
2956
+ private jobRetentionSweepTimer?;
2957
+ private jobRetentionBootTimer?;
2958
+ /** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
2959
+ private orphanSweepClaimed;
2960
+ /** Where a page-capped tick left off; cleared once a tick reaches the end. */
2961
+ private orphanSweepResumeFrom?;
2962
+ private jobRetentionResumeFrom?;
3391
2963
  /**
3392
- * Shared fx fetcher used to convert USD-string descriptor prices and
3393
- * fiat-form `requestPayment` calls into sats (internal-review). The SDK server owns
3394
- * one per process so concurrent quotes share the 60s cache window.
2964
+ * A `Date.now()` reading that cannot regress within this manager's lifetime
2965
+ * (internal-review). Anchored at construction, so a manager already running when
2966
+ * the clock stepped is covered. The stale-job sweep's two cutoffs, the
2967
+ * orphan-draw age gate and the reactivation claim grace all read it; see
2968
+ * {@link createMonotonicClock} for what it costs and where it is clamped.
3395
2969
  */
3396
- fxFetcher: FxFetcher;
2970
+ private readonly monotonicNowMs;
2971
+ private sweepInFlight;
2972
+ private orphanSweepInFlight;
2973
+ private terminalCostRecoveryInFlight;
2974
+ private terminalCostRecoveryResumeAfter?;
2975
+ private jobRetentionSweepInFlight;
2976
+ private heartbeatInFlight;
3397
2977
  /**
3398
- * Signs a receipt at every terminal transition (internal-review). Absent when the
3399
- * DVM has no receipt key wired — jobs then terminate exactly as before and
3400
- * `/v1/info` doesn't advertise `receipts`.
2978
+ * The denomination of every credit this DVM opens (internal-review). Resolved once,
2979
+ * with the same boundary fallback `DVMServer` applies for direct JavaScript
2980
+ * callers that force an incomplete value past the descriptor contract.
3401
2981
  */
3402
- receiptIssuer?: ReceiptIssuer;
2982
+ private readonly pricingCurrency;
2983
+ constructor(descriptor: DVMDescriptor<State, InputSchema>, opts: JobManagerOpts);
2984
+ /** Active in-memory jobs. */
2985
+ get activeJobs(): Map<string, ServerJob>;
2986
+ /** The backing job store. */
2987
+ get store(): JobStore;
3403
2988
  /**
3404
- * Credit ledger for the terminal funnel (internal-review): a job carrying a
3405
- * `creditId`/`drawId` settles its draw on success and releases it on
3406
- * failure/cancel — this is how "no debit on job failure" is mechanically
3407
- * real. The resolution runs before receipt issuance so the receipt's
3408
- * `credit` block countersigns the post-resolution balance.
2989
+ * True when this DVM will actually issue receipts (internal-review) — a key *and*
2990
+ * a store that can allocate sequence numbers and persist bytes write-once.
2991
+ * `/v1/info#receipts` reads this rather than the key alone, so the flag can
2992
+ * never promise something `issueReceipt` silently declines to do.
3409
2993
  */
3410
- creditLedger?: CreditLedgerLike;
2994
+ get receiptsEnabled(): boolean;
2995
+ /** True when the named capability exists on this descriptor. */
2996
+ hasCapability(name: string): boolean;
2997
+ /** Names of every capability this descriptor exposes — used for error envelopes. */
2998
+ capabilityNames(): string[];
3411
2999
  /**
3412
- * Durable processed-payment markers (internal-review), threaded through to the
3413
- * mid-job top-up path (internal-review) so an x402/mpp payment's marker, `fund`, and
3414
- * `growDraw` commit together.
3000
+ * Resolve a capability's static `price` to msats (internal-review).
3001
+ *
3002
+ * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
3003
+ * Returns `undefined` for dynamic-priced capabilities (those that declare
3004
+ * `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
3005
+ */
3006
+ currentPriceMsats(capability: string): Promise<number | undefined>;
3007
+ /**
3008
+ * Translate a capability's static `price` into the fiat envelope used by
3009
+ * the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
3010
+ * Returns `undefined` for dynamic-priced capabilities and for unknown
3011
+ * names (the route handler validates the name before invoking this).
3012
+ */
3013
+ currentPricingMax(capability: string): {
3014
+ amount: number;
3015
+ currency: string;
3016
+ } | undefined;
3017
+ /**
3018
+ * Parse a request body through a capability's input schema (internal-review).
3019
+ *
3020
+ * Resolution order when a capability declares an `input` schema:
3021
+ * 1. `body.data` is preferred — agents pass structured fields directly
3022
+ * (CLI builds this from `--param k=v`).
3023
+ * 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
3024
+ * JSON-string contract.
3025
+ * 3. Neither usable → throw `MissingStructuredInputError` so the route can
3026
+ * return `invalid_input` with a hint pointing at `/v1/info`.
3027
+ *
3028
+ * When the capability has no `input` schema, returns `body.input` raw
3029
+ * (primitive path).
3030
+ *
3031
+ * The internal-review credit envelope is removed before the schema runs, on both
3032
+ * branches (internal-review) — it rides the signed body but is protocol-level, so a
3033
+ * top-level `.strict()` capability schema would otherwise reject every
3034
+ * explicit draw as an unknown key, before the envelope was even extracted.
3035
+ * The persisted `record.input` the internal-review reactivation path re-reads is the
3036
+ * pre-Zod wire form, so it comes through the second branch carrying them too.
3037
+ */
3038
+ parseInput(body: {
3039
+ input?: string;
3040
+ data?: unknown;
3041
+ }, capability: string): unknown;
3042
+ /**
3043
+ * Look up the per-capability descriptor by name (internal-review). Throws when the
3044
+ * name doesn't exist — the route layer validates body.capability first, so
3045
+ * reaching this with an unknown name is a programmer error.
3415
3046
  */
3416
- processedPayments?: ProcessedPaymentStore;
3047
+ private requireCapability;
3417
3048
  /**
3418
- * Reporter-owned transactional deposit enqueue threaded into mid-job rail
3419
- * commits (internal-review).
3049
+ * Soft capability lookup — `undefined` when the name isn't defined. The
3050
+ * single place the `descriptor.capabilities` index cast lives; callers that
3051
+ * tolerate a missing capability (the route layer pre-validates, or the cap
3052
+ * was deleted from the descriptor after a job was persisted) route through
3053
+ * here instead of re-casting inline.
3420
3054
  */
3421
- enqueueCreditDeposit?: CreditDepositEnqueue;
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;
3055
+ private getCapability;
3426
3056
  /**
3427
- * The DVM's declared pricing currency (internal-review), which is the currency of
3428
- * every credit it opens. Threaded into the job context so a mid-job ask on a
3429
- * job that holds no credit yet is still pinned in the denomination its first
3430
- * payment will open the credit in (internal-review). `DVMServer` passes its resolved
3431
- * `pricingCurrency`; a host building a `JobManager` directly falls back to the
3432
- * descriptor, and then to `"usd"`.
3057
+ * Allocate the id the next job will be created under (internal-review). The
3058
+ * submit path calls this BEFORE payment verification so the ledger draw
3059
+ * commits with its job linkage, then passes the id back through
3060
+ * `createJob`'s provenance. Ids allocated for requests whose payment is
3061
+ * refused are simply never used — the sequence has gaps, which nothing
3062
+ * reads meaning into.
3433
3063
  */
3434
- pricingCurrency?: string;
3064
+ allocateJobId(): string;
3435
3065
  /**
3436
- * Callback fired when a mid-job top-up moves money onto a credit (internal-review).
3437
- * The platform books it as a **deposit** — a liability until drawn, never
3438
- * summed into revenue — so a top-up that skipped this would leave the
3439
- * outstanding-liability view short. Mirrors `DVMServer.reportCreditDeposit`,
3440
- * which handles the upfront and `/v1/credit` legs.
3066
+ * Create a new job from a request body. `provenance` (internal-review) is what the
3067
+ * idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
3068
+ * to re-issue, and the fingerprint + caller pubkey a retry must reproduce
3069
+ * to be given it.
3441
3070
  */
3442
- onCreditFunded?: (info: CreditDepositPayload) => void | Promise<void>;
3071
+ createJob(body: {
3072
+ input: string;
3073
+ params?: Record<string, string>;
3074
+ }, requesterId: string, payment: PaymentInfo, requiredMsats: number | undefined, capability: string, requesterTokenHash?: string, provenance?: {
3075
+ requesterToken?: string;
3076
+ requestFingerprint?: string;
3077
+ requesterPubkey?: string;
3078
+ requestId?: string;
3079
+ authRequestPath?: string;
3080
+ /**
3081
+ * Pre-allocated id from {@link allocateJobId} (internal-review) — the submit
3082
+ * path allocates before payment verification so the ledger draw can
3083
+ * record the job it pays for.
3084
+ */
3085
+ jobId?: string;
3086
+ }): ServerJob;
3087
+ /** Build an SDKJobContext and attach it to the job. */
3088
+ buildAndAttachContext(job: ServerJob, parsedInput: unknown): SDKJobContext<State, ResolvedInput<InputSchema>>;
3089
+ /** Start the handler for a job. */
3090
+ runHandler(job: ServerJob, sdkCtx: SDKJobContext<unknown, unknown>): void;
3091
+ /** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
3092
+ dispatchMessage(job: ServerJob, type: MessageType, content: unknown): Promise<void>;
3443
3093
  /**
3444
- * Callback fired when a paid job completes (internal-review). Container-runtime DVMs
3445
- * use this to report revenue to the platform via `RevenueReporter`. Mirrors
3446
- * the isolate path's `IsolateJobManager.opts.onJobCompleted`.
3094
+ * Persist a job to the backing store.
3095
+ *
3096
+ * Awaits the per-job appender tail (internal-review) so that:
3097
+ * 1. The snapshot's `messages` is consistent with the `job_messages` table
3098
+ * — no row is still in flight at snapshot time.
3099
+ * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
3100
+ * can't race a still-pending `appendOutgoing` for the final yield
3101
+ * message (which would otherwise wipe the row before an in-flight SSE
3102
+ * subscriber's NOTIFY-driven fetch can see it).
3447
3103
  */
3448
- onJobCompleted?: (info: {
3449
- dvmId: string;
3450
- jobId: string;
3451
- paidMsats: number;
3452
- paymentMint?: string;
3453
- rail: string;
3454
- paymentTxHash?: string;
3455
- nativeAmount?: number;
3456
- nativeAsset?: string;
3457
- cashuFlow?: string;
3458
- creditId?: string;
3459
- drawId?: string;
3460
- drawAmountMicro?: number;
3461
- creditCurrency?: string;
3462
- settledAt?: number;
3463
- fundingRef?: string;
3464
- kind?: string;
3465
- }) => void | Promise<void>;
3104
+ persistJob(job: ServerJob): Promise<void>;
3466
3105
  /**
3467
- * Callback fired when the stale-job reaper force-fails a *paid* job — its
3468
- * pending credit draw is released rather than settled, because only a
3469
- * completed job settles a draw (internal-review). Container-runtime DVMs wire this
3470
- * to a paid-job-death report → operator `notice` (internal-review). Never fires for
3471
- * free jobs (`paidMsats === 0`). Fired fire-and-forget so a throw or a slow
3472
- * report can't wedge the sweep.
3106
+ * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
3107
+ * in-memory `job.status`, so on its own it only protects the machine running
3108
+ * the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
3109
+ * on a machine that isn't running the job — `app.ts` cancels it in the store
3110
+ * and this process learns of it only through the durable row.
3111
+ *
3112
+ * Adopting the durable status blocks the rest of the handler's writes via the
3113
+ * local guard, makes `handleJobTerminal` see a non-completed job so it skips
3114
+ * the revenue report, rejects a suspended handler's prompt/payment yields, and
3115
+ * fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
3116
+ * the durable state won and the caller must skip its save.
3117
+ *
3118
+ * Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
3119
+ * this within a round-trip of the remote cancel committing, which is what
3120
+ * actually stops the provider spend; `persistJob` calls it again on every save
3121
+ * as the backstop for the window where a cancel commits between a handler's
3122
+ * read and its write (the check-then-write it can still lose is covered by the
3123
+ * stores' terminal-sticky `save`, which keeps the row itself correct).
3473
3124
  */
3474
- onPaidJobDeath?: (info: {
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>;
3125
+ private adoptDurableTerminal;
3485
3126
  /**
3486
- * Callback fired when a settled credit draw cannot book a revenue event
3487
- * because no usable payment rail is available (internal-review). Container-runtime
3488
- * DVMs auto-wire this to the platform's deduped operator notice. Fired
3489
- * fire-and-forget so alert delivery cannot block terminal handling or draw
3490
- * reconciliation.
3127
+ * Persist an adopted terminal cost, retaining the in-memory revision for a
3128
+ * later retry when the durable store is transiently unavailable.
3491
3129
  */
3492
- onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
3130
+ private tryPersistAdoptedTerminalCost;
3131
+ /** Retry outside the status watcher, which correctly stops once the job is terminal. */
3132
+ private scheduleTerminalCostHandoffRetry;
3133
+ private retryTerminalCostHandoff;
3134
+ /** Durably merge a terminal handler owner's latest cost before reporting it. */
3135
+ private persistAdoptedTerminalCost;
3493
3136
  /**
3494
- * Stale-job sweep threshold in ms (internal-review). Jobs in a non-terminal status
3495
- * (`processing` / `working` / `awaiting-input`) whose `lastActivityAt` is
3496
- * older than this are force-cancelled with reason `stale_no_terminal_status`,
3497
- * so callers always reach a terminal status instead of polling forever after
3498
- * a worker crash, OOM, or silent `messageAppender` failure. Default:
3499
- * `2 × descriptor.idleTimeout + STALE_JOB_WATCHDOG_HEADROOM_MS`, matching
3500
- * the internal-review acceptance criterion against scrape's `JOB_TIMEOUT_MS`.
3501
- * Builders whose per-handler wall-clock bound exceeds 90s must override
3502
- * this — otherwise the watchdog fires on a legitimate in-flight handler.
3503
- * Set to `0` to disable (test seam).
3137
+ * Watch the durable status of a locally-active job (internal-review).
3138
+ *
3139
+ * `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
3140
+ * holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
3141
+ * lands somewhere else, and that machine's store-only cancel (`cancelJob`)
3142
+ * commits the terminal status, appends the `cancel` message, and `pg_notify`s
3143
+ * in one transaction. This subscription is how the handler's machine hears it:
3144
+ * on each notify, re-read the durable status and adopt it if it went terminal.
3145
+ * Without it, the handler only notices at its next store write, and the
3146
+ * in-flight provider call — the spend the cancel was meant to stop — runs to
3147
+ * completion.
3148
+ *
3149
+ * Every locally-emitted message notifies this machine too, so the re-read is
3150
+ * coalesced: at most one `getCounters` in flight per job, and none once the
3151
+ * job is locally terminal.
3504
3152
  */
3505
- staleJobTimeoutMs?: number;
3153
+ private watchDurableStatus;
3506
3154
  /**
3507
- * Sweep interval for the stale-job watchdog. Default: `min(60s, max(15s,
3508
- * tightestActiveTimeout / 4))`. Tests override to fire deterministically.
3155
+ * Tear down a job's durable-status subscription (internal-review). Must be called
3156
+ * wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
3157
+ * the store's shared LISTEN connection. Idempotent.
3509
3158
  */
3510
- staleJobSweepIntervalMs?: number;
3159
+ unwatchDurableStatus(jobId: string): void;
3511
3160
  /**
3512
- * Worker-liveness watchdog in ms (internal-review). Jobs in `processing`/`working`
3513
- * whose `lastActivityAt` is older than this are marked `failed` (dead
3514
- * worker), separately from the longer `awaiting-input` idle timeout above.
3515
- * Default: `descriptor.processingWatchdog * 1000` or
3516
- * `DEFAULT_PROCESSING_WATCHDOG_MS`. Set to `0` to disable (test seam).
3161
+ * NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
3162
+ *
3163
+ * A notify that arrives while a re-read is in flight is queued rather than
3164
+ * dropped: the in-flight read may have observed the row a moment *before* the
3165
+ * cancel committed, and dropping its notify would put the abort back where
3166
+ * this issue found it — waiting for the handler's next store write.
3517
3167
  */
3518
- processingWatchdogMs?: number;
3168
+ private checkDurableTerminal;
3519
3169
  /**
3520
- * Interval at which locally-active `processing`/`working` jobs have their
3521
- * `lastActivityAt` bumped so the processing watchdog only fires on a truly
3522
- * dead worker (internal-review). Default: `DEFAULT_HEARTBEAT_INTERVAL_MS`. Set to
3523
- * `0` to disable (test seam). Also floors how far the internal-review clock guard
3524
- * may compensate a backwards step on the processing arm — a cadence close to
3525
- * `processingWatchdogMs` leaves it no room and it compensates nothing.
3170
+ * Wire the Tx A appender when the store supports streaming.
3171
+ *
3172
+ * Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
3173
+ * the same transaction. The DB allocates the row's seq via
3174
+ * `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
3175
+ * traffic on different machines can never collide on the `(job_id, seq)` PK.
3176
+ * In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
3177
+ *
3178
+ * Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
3179
+ * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
3180
+ * `complete`) commit their `appendOutgoing` transactions in call order.
3181
+ * Without this, the two transactions race for the `jobs` row lock and the
3182
+ * DB-side seq can be allocated in the opposite order — which both renumbers
3183
+ * the persisted messages and reorders the NOTIFY events feeding the
3184
+ * cross-machine SSE subscriber. The subscriber closes the stream on the
3185
+ * first yield message it sees, so a NOTIFY for `complete` arriving before
3186
+ * `artifact`'s NOTIFY silently drops the artifact.
3526
3187
  */
3527
- heartbeatIntervalMs?: number;
3188
+ private wireMessageAppender;
3189
+ /** Start (or restart) the idle timer for a job. */
3190
+ startIdleTimer(job: ServerJob): void;
3191
+ /** Clear the idle timer for a job. */
3192
+ clearIdleTimer(id: string): void;
3193
+ /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
3194
+ cancelJobIdle(job: ServerJob): Promise<void>;
3528
3195
  /**
3529
- * Age in ms at which a `pending` credit draw whose `job_id` matches no row in
3530
- * the job store is released as orphaned (internal-review). The draw commits before
3531
- * the job row is persisted, so a crash or a fail-closed refusal in that
3532
- * window strands a hold no terminal path can ever reach. Must stay
3533
- * comfortably above the submit path's draw→persist latency. Set to `0` to
3534
- * disable the watchdog entirely — stranded holds then stay stranded, so
3535
- * treat it as a money knob, not a tuning one. Default:
3536
- * {@link DEFAULT_ORPHAN_DRAW_AGE_MS}.
3196
+ * Force-terminate stale non-terminal jobs. Two status-aware arms:
3197
+ * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
3198
+ * (`stale_no_terminal_status`) — the caller never paid / responded.
3199
+ * • `processing`/`working` past `processingWatchdogMs` → `failed`
3200
+ * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
3201
+ * live workers fresh, so a stale row here means a dead process.
3202
+ *
3203
+ * Fires on a timer (wired in the constructor) and also callable on demand
3204
+ * (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
3205
+ * concurrently from multiple DVM machines. Scoped to capabilities this
3206
+ * JobManager owns so multi-mount hosts don't steal stuck rows from sibling
3207
+ * descriptors — the local activeJobs teardown only makes sense on the
3208
+ * JobManager that actually hosted the zombie handler.
3209
+ *
3210
+ * **Both cutoffs float off the wall clock (internal-review).** Each is one
3211
+ * `Date.now()` sample compared against `last_activity_at`, stamped from a
3212
+ * different sample at a different moment, so a backwards step between the
3213
+ * two makes every row look more recently active than it is: the query
3214
+ * matches nothing and both arms go silent for the length of the skew. The
3215
+ * dead worker's job keeps its `processing` status, its credit hold stays
3216
+ * `pending`, and the internal-review paid-job-death alert — wired to this reaper —
3217
+ * never fires. Same defect, same host class, as internal-review's orphan sweep.
3218
+ *
3219
+ * **The compensation is capped** — see {@link staleSweepCompensationCapMs}
3220
+ * for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
3221
+ * of an unclaimed hold, refused outright for any live job row; ours
3222
+ * force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
3223
+ * `expectedActivityBefore` is the same compensated threshold the query used,
3224
+ * and a live worker's post-step heartbeat writes the low, post-step
3225
+ * `Date.now()`, so `last_activity_at <= expectedActivityBefore` still holds.
3226
+ * The CAS guards a job checking in *between* the find and the claim with a
3227
+ * stamp above the cutoff — a race, not a clock.
3228
+ *
3229
+ * **Some blindness is inherent, and no cap choice removes it.** A backwards
3230
+ * step of `S` inverts the stamp ordering: everything stamped after it reads
3231
+ * `S` older than everything stamped before. A live worker heartbeating right
3232
+ * now stamps `wall - S`; a job genuinely idle for `age` stamps `wall - age`.
3233
+ * The stale row is the lower of the two — the one a cutoff reaches first —
3234
+ * only once `age` exceeds `S`. Below that the reaper cannot tell them apart,
3235
+ * so any cutoff that claimed the stale job would force-fail every live
3236
+ * worker on the host with it. The cap picks where on that curve to sit: on
3237
+ * the defaults a row must have been silent for `S + window / 2`, and a live
3238
+ * worker keeps five missed heartbeats of margin. The
3239
+ * alternative to a missed reap is mild and self-healing — the orphan sweep
3240
+ * backstops the hold, and the arm recovers when the wall catches up. The
3241
+ * alternative to a false reap is an irreversible force-fail of paid work.
3242
+ *
3243
+ * The stamps themselves stay wall-clock epoch-ms on purpose — they are
3244
+ * written by whichever machine touched the job and read by every other one,
3245
+ * so only the reading side can float.
3537
3246
  */
3538
- orphanDrawAgeMs?: number;
3247
+ sweepStaleJobs(): Promise<{
3248
+ swept: number;
3249
+ }>;
3539
3250
  /**
3540
- * Age in ms at which a `pending` credit draw whose job row **is** terminal is
3541
- * reconciled against that row's outcome (internal-review) — settled for a
3542
- * `completed` job, released for a `failed`/`cancelled` one. Covers the hole
3543
- * `orphanDrawAgeMs` cannot: `resolveCreditDraw` throwing on a ledger error
3544
- * leaves a terminal job whose hold no later path retries.
3251
+ * How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
3252
+ * clock to cover a backwards step (internal-review). The compensation is capped,
3253
+ * not free: a claimed row must still have been silent for
3254
+ * `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
3255
+ * force-fails a job that may well be alive.
3545
3256
  *
3546
- * Rides the same scan as the orphan arm, which has two consequences for the
3547
- * value you set here. Setting `orphanDrawAgeMs` to `0` disables this arm too.
3548
- * And the effective gate is `max(orphanDrawAgeMs, terminalDrawReconcileAgeMs)`
3549
- * — the scan never returns a row younger than its own cutoff, so anything
3550
- * below `orphanDrawAgeMs` is silently clamped up to it. That floor is
3551
- * deliberate rather than a wart: honouring a narrower value would mean
3552
- * widening the scan, which hands the orphan arm rows younger than
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.
3257
+ * **Half the window** is the floor that governs a wide window — five missed
3258
+ * heartbeats on the processing defaults. On the `awaiting-input` arm it also
3259
+ * preserves a real invariant: with the derived
3260
+ * `2 x idleTimeout + headroom`, half is `idleTimeout + 45s`, so the sweeper
3261
+ * still cannot fire before the in-process idle timer would have. (A builder
3262
+ * who overrides `staleJobTimeoutMs` below `2 x idleTimeout` has already
3263
+ * opted out of that, guard or no guard.)
3555
3264
  *
3556
- * Set to `0` to disable this arm alone — a money knob, not a tuning one.
3557
- * Default: {@link DEFAULT_TERMINAL_DRAW_RECONCILE_AGE_MS}.
3265
+ * **Two heartbeat intervals** is the floor that governs a window configured
3266
+ * close to the beat cadence — `processingWatchdog: 40` against the 30s
3267
+ * default beat, where half the window is less than one beat and a worker
3268
+ * heartbeating exactly on schedule would be reaped the moment the host's
3269
+ * clock stepped. That pair is marginal already; the guard must not make it
3270
+ * deterministic. The cap collapses to zero there, which is today's
3271
+ * behaviour: late, never wrong. The `awaiting-input` arm takes no such term
3272
+ * — nothing heartbeats it by design, and its refresh is an inbound caller
3273
+ * message on no cadence at all.
3274
+ *
3275
+ * **Why a magnitude and not a predicate.** The tempting sharper rule is to
3276
+ * compensate per row, fully, for any stamp that reads ahead of our wall —
3277
+ * one our own post-step heartbeat could not have written. It is unsafe: a
3278
+ * future-stamped row is equally what a live job heartbeated by a peer whose
3279
+ * clock runs fast looks like, and from the stamp alone the two are
3280
+ * indistinguishable. The same reasoning bounds the cap from the other side —
3281
+ * while our clock is stepped, the guard force-fails the live jobs of any
3282
+ * peer slower than `windowMs - cap`, which is 2.5 minutes of tolerated
3283
+ * disagreement on the defaults, far outside anything NTP-managed hosts show.
3558
3284
  */
3559
- terminalDrawReconcileAgeMs?: number;
3285
+ private staleSweepCompensationCapMs;
3560
3286
  /**
3561
- * Sweep interval for the orphan-draw watchdog. Default:
3562
- * {@link DEFAULT_ORPHAN_DRAW_SWEEP_INTERVAL_MS}. Set to `0` to disable — the
3563
- * test seam, since suites drive `sweepOrphanDraws()` directly.
3564
- */
3565
- orphanDrawSweepIntervalMs?: number;
3287
+ * Redact terminal job content beyond this DVM's configured window.
3288
+ *
3289
+ * The scan advances over every candidate, including rows that fail draw
3290
+ * resolution, so one money-path fault cannot starve everything behind it.
3291
+ * Any failed row remains unredacted and the next tick restarts from the head;
3292
+ * a page-capped clean pass instead retains its cursor and resumes at the
3293
+ * tail. Failure and recovery callbacks are awaited but never allowed to
3294
+ * change the sweep result.
3295
+ */
3296
+ sweepJobRetention(): Promise<{
3297
+ examined: number;
3298
+ redacted: number;
3299
+ complete: boolean;
3300
+ }>;
3301
+ /** Resolve every hold and persist any configured receipt before content disappears. */
3302
+ private prepareJobForRedaction;
3303
+ private notifyJobRetentionFailed;
3304
+ private notifyJobRetentionRecovered;
3566
3305
  /**
3567
- * Grace window in ms a reactivating machine waits for a live original handler
3568
- * to win the single-execution claim before replaying an `awaiting-input` job
3569
- * itself (internal-review). Default: `DEFAULT_REACTIVATION_CLAIM_GRACE_MS`. Widen if
3570
- * cross-machine NOTIFY latency causes spurious double executions; lower in
3571
- * tests for speed.
3306
+ * Resolve `pending` credit draws the terminal funnel can no longer reach.
3307
+ * Two arms over one scan, distinguished by whether the draw's job row exists.
3308
+ *
3309
+ * **No job row (internal-review) — release.** internal-review allocates the job id and
3310
+ * commits the ledger draw, with its `job_id` linkage, before `recordReplay`
3311
+ * and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
3312
+ * fail-closed `replay_detected` 401 in that window leaves a committed hold
3313
+ * that **no** terminal path can ever reach: `issueReceipt` →
3314
+ * `resolveCreditDraw` keys off the `creditId`/`drawId` persisted on the job
3315
+ * row, so with no row there is no release path at all and the caller's
3316
+ * available balance stays reduced forever. Cosmetic for an implicit N=1
3317
+ * credit, a silent balance shrink for an explicit N>1 one.
3318
+ *
3319
+ * **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
3320
+ * demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
3321
+ * `issueReceipt` catches it, logs `credit_resolve_failed` and returns
3322
+ * nothing, and until now nothing retried. Same stranded hold, reached through
3323
+ * a different door — and for a `completed` job it strands the platform's
3324
+ * books too, since draws *are* the revenue events (internal-review): the caller is
3325
+ * charged nothing, the balance never moves, and outstanding liability
3326
+ * (`deposits − draw revenue`) overstates forever. So a late settle books, off
3327
+ * the committed draw row, through the ordinary reporter path.
3328
+ *
3329
+ * **A non-terminal job row is still never touched** — the stale-job sweep
3330
+ * above drives those to terminal first, and its `issueReceiptForStoredJob`
3331
+ * resolves the draw on the way. Those skipped holds are why the scan
3332
+ * keyset-paginates rather than re-reading one batch: they stay `pending`
3333
+ * indefinitely (an `awaiting-input` job on a 30-minute idle timeout sits well
3334
+ * past the orphan cutoff), so a fixed first page of them would starve every
3335
+ * real orphan behind it, on every tick, forever. The cursor advances over
3336
+ * every row examined; a tick that hits {@link ORPHAN_DRAW_SWEEP_MAX_PAGES}
3337
+ * logs `orphan_draw_sweep_truncated` and the next one resumes where it
3338
+ * stopped, so progress is bounded per tick but never blocked.
3339
+ *
3340
+ * Fires on a timer (wired in the constructor) and also callable on demand
3341
+ * (tests, ops). Both `settle` and `release` are idempotent under the credit
3342
+ * row's `FOR UPDATE`, so concurrent sweepers on several machines — and a
3343
+ * sweeper racing the in-line funnel — resolve the hold exactly once; the
3344
+ * losers see `replayed: true` and book nothing.
3572
3345
  */
3573
- reactivationClaimGraceMs?: number;
3574
- }
3575
- type ResolvedInput<I extends ZodLike | undefined> = I extends ZodLike<infer O> ? O : string;
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;
3346
+ sweepOrphanDraws(): Promise<{
3347
+ released: number;
3348
+ reconciled: number;
3349
+ }>;
3637
3350
  /**
3638
- * Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
3639
- * guard's compensation on the processing arm — see
3640
- * {@link staleSweepCompensationCapMs}.
3351
+ * The end-of-scan line for a tick that resolved nothing (internal-review, extended
3352
+ * by internal-review). Two shapes are worth a warning, and neither is the ordinary
3353
+ * one — the timer runs every 5 minutes per ledger in production.
3354
+ *
3355
+ * `examined > 0`: aged candidates were looked at and none moved. Usually
3356
+ * legitimate (their jobs are still running, or terminal inside the reconcile
3357
+ * grace period), but it is also what a silently skipped hold looks like.
3358
+ *
3359
+ * `examined === 0`: the query returned nothing. Ordinarily that means there
3360
+ * is nothing to do — but it is equally what a blinded scan looks like, which
3361
+ * is the case the internal-review gate could not reach. Probe for the oldest
3362
+ * `pending` hold at any age and speak up when one exists the scan should
3363
+ * have seen and didn't: already past the age gate outright, stamped ahead of
3364
+ * our clock (some other process's clock is fast), or hidden while our own
3365
+ * clock is held forward over a backwards step. A ledger holding nothing but
3366
+ * young draws stays quiet.
3367
+ *
3368
+ * The probe is diagnostics, so it is wrapped: a tick that swept cleanly must
3369
+ * never report failure because the extra read fell over. A probe that throws
3370
+ * says so on the same line rather than replacing the tick's outcome.
3641
3371
  */
3642
- private readonly heartbeatIntervalMs;
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?;
3372
+ private reportSweepOutcome;
3652
3373
  /**
3653
- * A `Date.now()` reading that cannot regress within this manager's lifetime
3654
- * (internal-review). Anchored at construction, so a manager already running when
3655
- * the clock stepped is covered. The stale-job sweep's two cutoffs, the
3656
- * orphan-draw age gate and the reactivation claim grace all read it; see
3657
- * {@link createMonotonicClock} for what it costs and where it is clamped.
3374
+ * Resolve one aged `pending` draw whose job row reached terminal (internal-review),
3375
+ * mirroring what `resolveCreditDraw` would have done in line. Returns true
3376
+ * when this call is the one that moved the draw — the caller counts it, and
3377
+ * only it books.
3378
+ *
3379
+ * **The settle is gated on the job row naming _this_ draw.** The scan finds
3380
+ * the draw by `credit_draws.job_id`, but the job's own `credit_id`/`draw_id`
3381
+ * are written by a *later* transaction than the one that committed the draw
3382
+ * (`verifyIncomingPayment` commits fund+draw; `verifyAndCredit` binds the
3383
+ * row), and a free-then-paid job binds set-if-null. A store failure or a
3384
+ * second concurrent top-up therefore leaves a hold tagged with the job whose
3385
+ * row points somewhere else, or nowhere. Settling that would debit the caller
3386
+ * a second time for one job — and invisibly, since `revenue_events`'
3387
+ * `UNIQUE (dvm_id, job_id, rail)` would drop the booking. So an unbound hold
3388
+ * is **released**, never settled: the job's payment was the draw it is bound
3389
+ * to, and this one bought nothing.
3390
+ *
3391
+ * Concurrency needs no guard of its own. `settle`/`release` run under the
3392
+ * credit row's `FOR UPDATE`, so exactly one caller — a sibling reconciler,
3393
+ * or the in-line funnel arriving late — sees `replayed: false`, which makes
3394
+ * the booking gate a true mutex rather than a hopeful one. The one race left
3395
+ * open is another machine's `handleJobTerminal` stalled between resolving the
3396
+ * draw and booking it, which would double-report; the platform's
3397
+ * `(dvm_id, credit_id, draw_id)` and `(dvm_id, job_id, rail)` unique indexes
3398
+ * absorb that.
3399
+ *
3400
+ * `invalid_draw_state` — the funnel having already resolved the draw to the
3401
+ * other state — is the expected loss, logged and swallowed like the orphan
3402
+ * arm's, so one contended hold can't strand the rest of the batch.
3658
3403
  */
3659
- private readonly monotonicNowMs;
3660
- private sweepInFlight;
3661
- private orphanSweepInFlight;
3662
- private heartbeatInFlight;
3404
+ private reconcileTerminalDraw;
3663
3405
  /**
3664
- * The denomination of every credit this DVM opens (internal-review). Resolved once,
3665
- * with the same boundary fallback `DVMServer` applies for direct JavaScript
3666
- * callers that force an incomplete value past the descriptor contract.
3406
+ * Refresh `lastActivityAt` for jobs this process is actively running
3407
+ * (`processing`/`working`) so the processing watchdog only fires once the
3408
+ * worker is genuinely dead (internal-review). `awaiting-input` jobs are
3409
+ * deliberately excluded — their idle timeout must still elapse. Fires on the
3410
+ * heartbeat timer; also callable on demand for tests.
3667
3411
  */
3668
- private readonly pricingCurrency;
3669
- constructor(descriptor: DVMDescriptor<State, InputSchema>, opts: JobManagerOpts);
3670
- /** Active in-memory jobs. */
3671
- get activeJobs(): Map<string, ServerJob>;
3672
- /** The backing job store. */
3673
- get store(): JobStore;
3412
+ heartbeatActiveJobs(): Promise<{
3413
+ beat: number;
3414
+ }>;
3674
3415
  /**
3675
- * True when this DVM will actually issue receipts (internal-review) — a key *and*
3676
- * a store that can allocate sequence numbers and persist bytes write-once.
3677
- * `/v1/info#receipts` reads this rather than the key alone, so the flag can
3678
- * never promise something `issueReceipt` silently declines to do.
3416
+ * Single-execution claim for a reactivating machine (internal-review). Before
3417
+ * replaying an `awaiting-input` job, wait a bounded grace window for a live
3418
+ * original handler to win the `awaiting-input → processing` CAS via its
3419
+ * NOTIFY wake. Resolves:
3420
+ * • `false` — the row left `awaiting-input` during the window (the original
3421
+ * handler claimed it / drove it terminal). Stand down; do not replay.
3422
+ * • `true` — the window elapsed still `awaiting-input` (the original worker
3423
+ * is gone or has no live resolver) AND this machine won the claim CAS.
3424
+ * Replay for dead-worker recovery.
3425
+ *
3426
+ * Biasing the original handler to win kills the double execution (double
3427
+ * substrate spend, clobbered artifact) without heartbeating `awaiting-input`
3428
+ * (which would break the caller-input idle timeout). Non-streamable stores
3429
+ * have no cross-machine race, so the claim is a no-op `true`.
3679
3430
  */
3680
- get receiptsEnabled(): boolean;
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[];
3431
+ claimAwaitingInputForReactivation(jobId: string): Promise<boolean>;
3685
3432
  /**
3686
- * Resolve a capability's static `price` to msats (internal-review).
3433
+ * Record an inbound client message durably via Tx B (internal-review) and append
3434
+ * it to the in-memory `job.messages` array. Returns the DB-allocated seq
3435
+ * (`null` when the store isn't streamable — non-Postgres test fallback).
3687
3436
  *
3688
- * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
3689
- * Returns `undefined` for dynamic-priced capabilities (those that declare
3690
- * `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
3437
+ * Status starts as `pending-verification`; a follow-up Tx C call
3438
+ * (`processPayment` for payments, `markInboundVerified` for everything
3439
+ * else) transitions it to `verified` once the inbound is accepted.
3691
3440
  */
3692
- currentPriceMsats(capability: string): Promise<number | undefined>;
3441
+ recordInboundPending(job: ServerJob, msgType: MessageType, msgContent: Record<string, unknown>): Promise<number | null>;
3693
3442
  /**
3694
- * Translate a capability's static `price` into the fiat envelope used by
3695
- * the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
3696
- * Returns `undefined` for dynamic-priced capabilities and for unknown
3697
- * names (the route handler validates the name before invoking this).
3443
+ * Tx C complement for non-payment inbound messages — flip the row to
3444
+ * `verified` so cross-machine readers see it via `subscribeMessages`.
3698
3445
  */
3699
- currentPricingMax(capability: string): {
3700
- amount: number;
3701
- currency: string;
3702
- } | undefined;
3446
+ markInboundVerified(jobId: string, inboundSeq: number | null): Promise<void>;
3703
3447
  /**
3704
- * Parse a request body through a capability's input schema (internal-review).
3705
- *
3706
- * Resolution order when a capability declares an `input` schema:
3707
- * 1. `body.data` is preferred — agents pass structured fields directly
3708
- * (CLI builds this from `--param k=v`).
3709
- * 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
3710
- * JSON-string contract.
3711
- * 3. Neither usable → throw `MissingStructuredInputError` so the route can
3712
- * return `invalid_input` with a hint pointing at `/v1/info`.
3448
+ * Reactivate a suspended/event-driven job from the store. Builds the
3449
+ * in-memory `ServerJob`, wires the appender, builds the context, starts
3450
+ * the idle timer. The caller has already recorded the inbound via Tx B
3451
+ * and run Tx C (verify + credit); reactivation is now decoupled from
3452
+ * credit (internal-review, internal-review). The caller decides whether to run the
3453
+ * handler (Pattern A: status was `awaiting-input`) or to dispatch the
3454
+ * message to a custom handler (Pattern B).
3713
3455
  *
3714
- * When the capability has no `input` schema, returns `body.input` raw
3715
- * (primitive path).
3456
+ * For DVMs that declare descriptor-level auth, the persisted
3457
+ * `record.input` carries the original signed envelope. We re-verify the
3458
+ * signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
3459
+ * injection, malicious operator with DB access — can't silently feed an
3460
+ * attacker-supplied pubkey/envelope into the handler. The drift window
3461
+ * and replay store are deliberately skipped: the persisted timestamp is
3462
+ * from the original signing instant (long past the 5-min drift bound for
3463
+ * any long-running job), and the nonce was already committed at submission.
3464
+ * On failure: throws `SignedRequestError` ({@link AuthSchemaError} when
3465
+ * the capability has no input schema to verify against); the caller is
3466
+ * expected to mark the row failed and surface a 401 / 500 to the inbound
3467
+ * caller.
3716
3468
  *
3717
- * The internal-review credit envelope is removed before the schema runs, on both
3718
- * branches (internal-review) — it rides the signed body but is protocol-level, so a
3719
- * top-level `.strict()` capability schema would otherwise reject every
3720
- * explicit draw as an unknown key, before the envelope was even extracted.
3721
- * The persisted `record.input` the internal-review reactivation path re-reads is the
3722
- * pre-Zod wire form, so it comes through the second branch carrying them too.
3469
+ * What gets re-verified is the persisted row read back through
3470
+ * {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
3471
+ * those are the caller's own signed bytes, the same ones `/v1/quote` and the
3472
+ * submit checked (internal-review). That makes the tamper check strictly stronger
3473
+ * than the parsed form it replaced: an injected key the capability schema
3474
+ * doesn't declare used to be stripped before the signature was checked, so
3475
+ * it re-verified clean.
3723
3476
  */
3724
- parseInput(body: {
3725
- input?: string;
3726
- data?: unknown;
3727
- }, capability: string): unknown;
3477
+ reactivateJob(record: JobRecord): {
3478
+ job: ServerJob;
3479
+ sdkCtx: SDKJobContext<unknown, unknown>;
3480
+ };
3728
3481
  /**
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.
3482
+ * Verify an inbound payment and apply the credit via Tx C (internal-review).
3483
+ * The inbound message must have been recorded via Tx B already
3484
+ * (`recordInboundPending` returned `inboundSeq`). For non-streamable
3485
+ * stores (`inboundSeq === null`), falls back to in-memory mutation via
3486
+ * `applyPaymentInfoToJob`.
3487
+ *
3488
+ * Dev-mode short-circuit: auto-credits the outstanding
3489
+ * `pendingPaymentMsats` without external verification. Gated on
3490
+ * `devModeSkipsPaymentVerification` — the same predicate the upfront path
3491
+ * reads, so a dev server wired to a mint verifies mid-job payments for real
3492
+ * (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
3493
+ * which is the one caller that has no wallet to pay from. `dev_auto` loses to
3494
+ * any proof riding the same message: money on the wire always takes the rail,
3495
+ * so the flag can never leave a real token unspent against a credited job.
3496
+ *
3497
+ * Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
3498
+ * commits the proofs *and* the ledger leg in one transaction and returns;
3499
+ * only then does `verifyAndCredit` write the job row, on its own connection.
3500
+ * Everything after that call therefore runs with the caller's money already
3501
+ * moved, which is why the write is retried rather than left to throw, and why
3502
+ * an exhausted retry answers with the credit it landed on instead of a bare
3503
+ * 500. See `creditInbound`.
3732
3504
  */
3733
- private requireCapability;
3505
+ processPayment(job: ServerJob, body: {
3506
+ type: MessageType;
3507
+ content: Record<string, unknown>;
3508
+ }, inboundSeq: number | null, extra?: {
3509
+ lockPubkeys?: LockPubkey[];
3510
+ mintHealthTracker?: MintHealthTracker;
3511
+ }): Promise<ProcessPaymentOutcome>;
3734
3512
  /**
3735
- * Soft capability lookup — `undefined` when the name isn't defined. The
3736
- * single place the `descriptor.capabilities` index cast lives; callers that
3737
- * tolerate a missing capability (the route layer pre-validates, or the cap
3738
- * was deleted from the descriptor after a job was persisted) route through
3739
- * here instead of re-casting inline.
3513
+ * Tx C with a bounded retry (internal-review).
3514
+ *
3515
+ * By the time this runs on the verified path, the rail commit and the ledger
3516
+ * leg have already committed in a transaction this call is not part of — so a
3517
+ * throw here is money moved against a job row that records none of it, and
3518
+ * letting it propagate was the bug. A transient store error is the ordinary
3519
+ * cause and a second attempt clears it.
3520
+ *
3521
+ * The retry is safe by construction, not by convention: `verifyAndCredit`
3522
+ * CASes on `job_messages.status = 'pending-verification'` inside its own
3523
+ * transaction, so a call whose COMMIT actually landed before the ack was lost
3524
+ * finds the row already `verified` and returns `alreadyVerified: true`
3525
+ * carrying the counters and binding that first attempt committed. Re-applying
3526
+ * the delta is impossible either way.
3527
+ *
3528
+ * That argument covers every attempt but the last, whose error nothing
3529
+ * re-checks — and a connection-level fault is exactly the shape that loses
3530
+ * the ack on *all* of them, idempotent replays included (the no-op path
3531
+ * COMMITs too). So the exhausted path asks the row before reporting a
3532
+ * failure: `getVerifiedInbound` reads, transaction-free, whether a Tx C for
3533
+ * this seq committed. Without it a fully credited, fully bound job answers
3534
+ * `payment_unapplied` and invites a retry, and a caller who takes it pays
3535
+ * twice for one ask — the second payment growing the now-bound draw and
3536
+ * settling as revenue.
3537
+ *
3538
+ * The read runs on the pool that just failed those COMMITs, so it is retried
3539
+ * too, and a read that fails all the way through is reported as *unknown*
3540
+ * rather than as nothing: `CreditInboundExhaustedError.readError` is what
3541
+ * separates "checked the row, nothing landed" from "couldn't check", both on
3542
+ * the operator's log line and in the copy the caller acts on.
3740
3543
  */
3741
- private getCapability;
3544
+ private creditInbound;
3742
3545
  /**
3743
- * Allocate the id the next job will be created under (internal-review). The
3744
- * submit path calls this BEFORE payment verification so the ledger draw
3745
- * commits with its job linkage, then passes the id back through
3746
- * `createJob`'s provenance. Ids allocated for requests whose payment is
3747
- * refused are simply never used — the sequence has gaps, which nothing
3748
- * reads meaning into.
3546
+ * Answer a payment whose rail and ledger legs committed but whose job row
3547
+ * could not be written (internal-review) — every retry spent, the money real.
3548
+ *
3549
+ * The refusal names the credit the money landed on so the caller can act on
3550
+ * it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
3551
+ * 500 rather than a 402 on purpose: the payment was valid and was accepted,
3552
+ * so telling the caller it was insufficient would be a lie, and this is a
3553
+ * fault an operator should see in their 5xx rate. `retryable` is true because
3554
+ * the ask is genuinely still outstanding.
3555
+ *
3556
+ * Nothing is released here. The hold is left to the internal-review reconciler,
3557
+ * which makes the same decision — unbound draw ⇒ release, never settle — but
3558
+ * under the credit row's `FOR UPDATE`, after the job is terminal, where it
3559
+ * cannot race an in-flight COMMIT. Releasing from here would also be
3560
+ * irreversible: a released draw can be neither re-grown nor re-opened, so one
3561
+ * bad call would silently de-ledger every later top-up on the job.
3562
+ *
3563
+ * Whether the row was *read* is load-bearing on both surfaces. When the
3564
+ * post-exhaustion read-back failed as well, `credit_job_row_unwritten` says so
3565
+ * (`row_checked: false` plus the read's own error) instead of implying an
3566
+ * operator can trust the row is clean, and the caller gets the `"unconfirmed"`
3567
+ * copy rather than a flat "pay it again" it could be charged twice for.
3568
+ *
3569
+ * That copy overrides `display` as well as `hint`, on every branch including
3570
+ * the ledger-less one. `display` is the sentence an agent relays to its human
3571
+ * (repo convention: JSON carries the words, not just the facts), so a body
3572
+ * whose `hint` says the outcome is unknown while its `display` still asserts
3573
+ * the ask is outstanding is read as "pay again" by the audience that acts on
3574
+ * it — and a payment that did land is charged on top.
3749
3575
  */
3750
- allocateJobId(): string;
3576
+ private reportUnappliedPayment;
3751
3577
  /**
3752
- * Create a new job from a request body. `provenance` (internal-review) is what the
3753
- * idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
3754
- * to re-issue, and the fingerprint + caller pubkey a retry must reproduce
3755
- * to be given it.
3756
- */
3757
- createJob(body: {
3758
- input: string;
3759
- params?: Record<string, string>;
3760
- }, requesterId: string, payment: PaymentInfo, requiredMsats: number | undefined, capability: string, requesterTokenHash?: string, provenance?: {
3761
- requesterToken?: string;
3762
- requestFingerprint?: string;
3763
- requesterPubkey?: string;
3764
- requestId?: string;
3765
- authRequestPath?: string;
3766
- /**
3767
- * Pre-allocated id from {@link allocateJobId} (internal-review) — the submit
3768
- * path allocates before payment verification so the ledger draw can
3769
- * record the job it pays for.
3770
- */
3771
- jobId?: string;
3772
- }): ServerJob;
3773
- /** Build an SDKJobContext and attach it to the job. */
3774
- buildAndAttachContext(job: ServerJob, parsedInput: unknown): SDKJobContext<State, ResolvedInput<InputSchema>>;
3775
- /** Start the handler for a job. */
3776
- runHandler(job: ServerJob, sdkCtx: SDKJobContext<unknown, unknown>): void;
3777
- /** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
3778
- dispatchMessage(job: ServerJob, type: MessageType, content: unknown): Promise<void>;
3578
+ * Describe a deposit the job row never took, in the shape both the 201 and
3579
+ * the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
3580
+ * ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
3581
+ * there is no credit to name, so the 201 carries no `credit` block and the
3582
+ * 500 takes its words from `unappliedCopy` directly.
3583
+ */
3584
+ private unappliedCredit;
3585
+ /** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
3586
+ private reportCreditDeposit;
3779
3587
  /**
3780
- * Persist a job to the backing store.
3781
- *
3782
- * Awaits the per-job appender tail (internal-review) so that:
3783
- * 1. The snapshot's `messages` is consistent with the `job_messages` table
3784
- * — no row is still in flight at snapshot time.
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).
3588
+ * Internal: build the BuildContextOpts shared between `createJob` and
3589
+ * `reactivateJob`. Centralised so persistence/terminal callbacks and the
3590
+ * internal-review NOTIFY-driven cross-machine wake stay in one place.
3789
3591
  */
3790
- persistJob(job: ServerJob): Promise<void>;
3592
+ private buildContextOpts;
3791
3593
  /**
3792
- * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
3793
- * in-memory `job.status`, so on its own it only protects the machine running
3794
- * the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
3795
- * on a machine that isn't running the job — `app.ts` cancels it in the store
3796
- * and this process learns of it only through the durable row.
3594
+ * Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
3595
+ * job's remaining pool in (internal-review).
3797
3596
  *
3798
- * Adopting the durable status blocks the rest of the handler's writes via the
3799
- * local guard, makes `handleJobTerminal` see a non-completed job so it skips
3800
- * the revenue report, rejects a suspended handler's prompt/payment yields, and
3801
- * fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
3802
- * the durable state won and the caller must skip its save.
3597
+ * The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
3598
+ * `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
3599
+ * from the same rail payment id — a fact about a row that was written once
3600
+ * and never moves. The tempting alternative, comparing `job.payment_tx_hash`
3601
+ * against `drawSettlementRef` (what the mid-job preflight does to inherit
3602
+ * `drawBasis`), silently rots: Tx C overwrites that column on every mid-job
3603
+ * credit, and a payment whose ledger leg was skipped — a cross-rail top-up,
3604
+ * an fx outage, a draw already resolved — leaves the rail's own reference
3605
+ * there, so an explicit-draw job would read as implicit from then on.
3803
3606
  *
3804
- * Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
3805
- * this within a round-trip of the remote cancel committing, which is what
3806
- * actually stops the provider spend; `persistJob` calls it again on every save
3807
- * as the backstop for the window where a cancel commits between a handler's
3808
- * read and its write (the check-then-write it can still lose is covered by the
3809
- * stores' terminal-sticky `save`, which keeps the row itself correct).
3607
+ * Never throws, and never answers `rail` on a guess. `rail` is claimed only
3608
+ * where `withDrawBasis` demonstrably left `PaymentInfo.paidMsats` alone — no
3609
+ * draw at all, or a draw carrying no rail value to overlay it with. A job that
3610
+ * drew and whose pool this can't read or denominate answers `unpriced`, which
3611
+ * the gate declines to auto-credit from: the msat figure it would otherwise
3612
+ * fall back on is a slice of the credit's rail value at a ratio pinned
3613
+ * whenever that credit was funded, which is the exact figure this issue exists
3614
+ * to stop spending against.
3810
3615
  */
3811
- private adoptDurableTerminal;
3616
+ private resolveDrawPool;
3617
+ /** Internal: structured warn for the one path that can't price a drawn job's pool. */
3618
+ private warnDrawPool;
3812
3619
  /**
3813
- * Watch the durable status of a locally-active job (internal-review).
3814
- *
3815
- * `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
3816
- * holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
3817
- * lands somewhere else, and that machine's store-only cancel (`cancelJob`)
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.
3620
+ * Issue the signed receipt for a job that has reached a terminal status
3621
+ * (internal-review). Idempotent and safe to call from every terminal path — the
3622
+ * store owns both the sequence allocation and the write-once persist, so
3623
+ * two machines racing on the same job converge on identical bytes and burn
3624
+ * exactly one sequence number.
3824
3625
  *
3825
- * Every locally-emitted message notifies this machine too, so the re-read is
3826
- * coalesced: at most one `getCounters` in flight per job, and none once the
3827
- * job is locally terminal.
3626
+ * Never throws. A receipt is evidence about a job, not part of delivering
3627
+ * it: if signing or persistence fails we log structured and serve the
3628
+ * response without one (same posture as the `cashu_refund_failed` branch in
3629
+ * `context.ts`). The gap is visible to callers — the `seq` series skips a
3630
+ * number — which is precisely the completeness signal receipts exist for.
3828
3631
  */
3829
- private watchDurableStatus;
3632
+ issueReceipt(record: JobRecord): Promise<JobReceipt | undefined>;
3633
+ /** Build the common settle/release args, adding the hosted release outbox when wired. */
3634
+ private drawResolutionArgs;
3830
3635
  /**
3831
- * Tear down a job's durable-status subscription (internal-review). Must be called
3832
- * wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
3833
- * the store's shared LISTEN connection. Idempotent.
3636
+ * Settle or release a terminal job's credit draw (internal-review): `completed`
3637
+ * settles (the hold becomes a real debit); `failed`/`cancelled` releases
3638
+ * (the hold evaporates — "no debit on job failure", mechanically
3639
+ * superseding the internal-review/974 no-op refund for credit-paid jobs, including
3640
+ * the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
3641
+ * resolution returns the recorded state. Returns the `ReceiptCredit` block
3642
+ * for the receipt: `balance_after` is the recorded draw trajectory for a
3643
+ * settled draw, and the restored available balance for a released one.
3834
3644
  */
3835
- unwatchDurableStatus(jobId: string): void;
3645
+ private resolveCreditDraw;
3836
3646
  /**
3837
- * NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
3838
- *
3839
- * A notify that arrives while a re-read is in flight is queued rather than
3840
- * dropped: the in-flight read may have observed the row a moment *before* the
3841
- * cancel committed, and dropping its notify would put the abort back where
3842
- * this issue found it — waiting for the handler's next store write.
3647
+ * The one place a terminal job's status becomes a ledger verb (internal-review):
3648
+ * `completed` settles the hold into a real debit, `failed`/`cancelled`
3649
+ * release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
3650
+ * late reconciler (`reconcileTerminalDraw`) — two paths that must never
3651
+ * disagree about what an outcome means for the caller's money.
3843
3652
  */
3844
- private checkDurableTerminal;
3653
+ private settlesDraw;
3845
3654
  /**
3846
- * Wire the Tx A appender when the store supports streaming.
3847
- *
3848
- * Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
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.
3655
+ * Sign a locally-held terminal job's receipt and mirror it onto the
3656
+ * in-memory job, so the read paths served out of `activeJobs` during the
3657
+ * cleanup window hand back the same bytes as a cross-machine re-read.
3863
3658
  */
3864
- private wireMessageAppender;
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>;
3659
+ private attachReceipt;
3871
3660
  /**
3872
- * Force-terminate stale non-terminal jobs. Two status-aware arms:
3873
- * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
3874
- * (`stale_no_terminal_status`) — the caller never paid / responded.
3875
- * • `processing`/`working` past `processingWatchdogMs` → `failed`
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.
3661
+ * Read-path repair for a terminal job carrying no receipt (internal-review), for
3662
+ * either an in-memory job or a store record. Free in the steady state — it
3663
+ * returns on the `receipt` check without touching the store — so it costs
3664
+ * only on the cases it exists for:
3918
3665
  *
3919
- * The stamps themselves stay wall-clock epoch-ms on purpose — they are
3920
- * written by whichever machine touched the job and read by every other one,
3921
- * so only the reading side can float.
3666
+ * - **A crash between the two store calls.** `claimReceiptSeq` commits the
3667
+ * sequence number to the job row before `saveReceipt` writes the bytes; a
3668
+ * process death in that window would otherwise strand that number
3669
+ * forever, and a permanent gap is indistinguishable from the deliberate
3670
+ * suppression `seq` exists to expose. Re-issuing here reuses the already
3671
+ * committed number (the claim is idempotent per job) rather than
3672
+ * allocating a second one.
3673
+ * - **A transient store failure** at the terminal: `issueReceipt` logs
3674
+ * `receipt_issue_failed` and returns nothing rather than failing the job,
3675
+ * so the next read retries.
3676
+ * - **Jobs that terminated before the DVM had a receipt key.** They pick one
3677
+ * up on first read, with `issued_at` reflecting when it was signed.
3922
3678
  */
3923
- sweepStaleJobs(): Promise<{
3924
- swept: number;
3925
- }>;
3679
+ ensureReceipt(target: ServerJob | JobRecord): Promise<void>;
3926
3680
  /**
3927
- * How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
3928
- * clock to cover a backwards step (internal-review). The compensation is capped,
3929
- * not free: a claimed row must still have been silent for
3930
- * `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
3931
- * force-fails a job that may well be alive.
3932
- *
3933
- * **Half the window** is the floor that governs a wide window — five missed
3934
- * heartbeats on the processing defaults. On the `awaiting-input` arm it also
3935
- * preserves a real invariant: with the derived
3936
- * `2 x idleTimeout + headroom`, half is `idleTimeout + 45s`, so the sweeper
3937
- * still cannot fire before the in-process idle timer would have. (A builder
3938
- * who overrides `staleJobTimeoutMs` below `2 x idleTimeout` has already
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.
3681
+ * Close out a terminal job whose caller hasn't already persisted it: sign
3682
+ * the receipt, then save the snapshot. The snapshot save never writes the
3683
+ * receipt column, so ordering only affects how soon a reader sees the
3684
+ * receipt — this way a caller polling immediately after the terminal
3685
+ * already finds it.
3686
+ */
3687
+ private finalizeTerminal;
3688
+ /**
3689
+ * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
3690
+ * cross-machine cancel, or a row the stale sweeper just reaped. Re-reads the
3691
+ * record so the receipt is built from the committed terminal row rather than
3692
+ * from whatever the caller happened to have in hand.
3960
3693
  */
3961
- private staleSweepCompensationCapMs;
3694
+ issueReceiptForStoredJob(jobId: string): Promise<JobReceipt | undefined>;
3962
3695
  /**
3963
- * Resolve `pending` credit draws the terminal funnel can no longer reach.
3964
- * Two arms over one scan, distinguished by whether the draw's job row exists.
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.
3985
- *
3986
- * **A non-terminal job row is still never touched** — the stale-job sweep
3987
- * above drives those to terminal first, and its `issueReceiptForStoredJob`
3988
- * resolves the draw on the way. Those skipped holds are why the scan
3989
- * keyset-paginates rather than re-reading one batch: they stay `pending`
3990
- * indefinitely (an `awaiting-input` job on a 30-minute idle timeout sits well
3991
- * past the orphan cutoff), so a fixed first page of them would starve every
3992
- * real orphan behind it, on every tick, forever. The cursor advances over
3993
- * every row examined; a tick that hits {@link ORPHAN_DRAW_SWEEP_MAX_PAGES}
3994
- * logs `orphan_draw_sweep_truncated` and the next one resumes where it
3995
- * stopped, so progress is bounded per tick but never blocked.
3996
- *
3997
- * Fires on a timer (wired in the constructor) and also callable on demand
3998
- * (tests, ops). Both `settle` and `release` are idempotent under the credit
3999
- * row's `FOR UPDATE`, so concurrent sweepers on several machines — and a
4000
- * sweeper racing the in-line funnel — resolve the hold exactly once; the
4001
- * losers see `replayed: true` and book nothing.
3696
+ * Finish accounting for a terminal written directly to the store — the
3697
+ * stale reaper and a cancel handled on a different machine (internal-review).
4002
3698
  */
4003
- sweepOrphanDraws(): Promise<{
4004
- released: number;
4005
- reconciled: number;
3699
+ finalizeStoredTerminal(jobId: string): Promise<JobReceipt | undefined>;
3700
+ /** Handle job reaching terminal state (completed, failed, cancelled). */
3701
+ private handleJobTerminal;
3702
+ /**
3703
+ * Re-enqueue terminal cost-only rows left between the job commit and outbox
3704
+ * insert. The marker advances only after `onJobCost` resolves, which for the
3705
+ * SDK reporter means the local durable insert has committed; a crash after
3706
+ * that insert but before the marker can duplicate a payload, but the
3707
+ * revisioned platform contract makes that replay harmless.
3708
+ */
3709
+ recoverTerminalCosts(): Promise<{
3710
+ examined: number;
3711
+ queued: number;
3712
+ complete: boolean;
4006
3713
  }>;
3714
+ /** Report the latest declared-cost state only when no revenue event carried it. */
3715
+ private reportJobCost;
4007
3716
  /**
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.
4011
- *
4012
- * `examined > 0`: aged candidates were looked at and none moved. Usually
4013
- * legitimate (their jobs are still running, or terminal inside the reconcile
4014
- * grace period), but it is also what a silently skipped hold looks like.
3717
+ * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
3718
+ * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
4015
3719
  *
4016
- * `examined === 0`: the query returned nothing. Ordinarily that means there
4017
- * is nothing to do — but it is equally what a blinded scan looks like, which
4018
- * is the case the internal-review gate could not reach. Probe for the oldest
4019
- * `pending` hold at any age and speak up when one exists the scan should
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.
3720
+ * For a **credit-backed** job the revenue event is the *settled draw* (spec
3721
+ * §10), and since internal-review the sats figure corrects the job's mirror of that
3722
+ * draw against the draw itself: `withDrawBasis` stamps the job when the
3723
+ * payment lands, but a Bitcoin credit's settle re-prices the draw against
3724
+ * the funding lots it consumed. The correction is a delta, because a job's
3725
+ * counter can carry legs no draw ever absorbed — except where the draw's
3726
+ * forecast was zero and the counter therefore never held a component to
3727
+ * correct. The event is dated at settlement rather than at report time. A
3728
+ * released draw — the failed/cancelled path — is deliberately unreachable
3729
+ * here, which is what makes "no debit on job failure" true in the books as
3730
+ * well as the ledger.
4024
3731
  *
4025
- * The probe is diagnostics, so it is wrapped: a tick that swept cleanly must
4026
- * never report failure because the extra read fell over. A probe that throws
4027
- * says so on the same line rather than replacing the tick's outcome.
4028
- */
4029
- private reportSweepOutcome;
4030
- /**
4031
- * Resolve one aged `pending` draw whose job row reached terminal (internal-review),
4032
- * mirroring what `resolveCreditDraw` would have done in line. Returns true
4033
- * when this call is the one that moved the draw — the caller counts it, and
4034
- * only it books.
3732
+ * For everything else (free-then-paid jobs, isolate-shaped flows, a DVM
3733
+ * whose price couldn't be fiat-denominated) nothing changes: the pre-credits
3734
+ * payload is reported verbatim.
4035
3735
  *
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.
3736
+ * Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
3737
+ * background sweep that holds no in-process job — books through this exact
3738
+ * path off the row it read. `paymentTxHash` is passed **verbatim**, never
3739
+ * recomputed as a `drawSettlementRef`: it is the same column the in-line
3740
+ * funnel reads, so the two bookings are byte-identical by construction and
3741
+ * the platform's `UNIQUE (rail, tx_hash)` dedupes them. Deriving it instead
3742
+ * would diverge on an implicit N=1 job (whose row carries the rail's own
3743
+ * reference) and on any job whose column a mid-job top-up overwrote — the
3744
+ * same rot `resolveDrawPool` documents as the reason not to read regime off
3745
+ * this column.
4047
3746
  *
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.
3747
+ * Returns whether the report was handed to `onJobCompleted`, so a caller
3748
+ * that logs the booking says what actually happened rather than what it
3749
+ * assumed — the guards below still drop legitimate shapes (a free job, a
3750
+ * DVM with no reporter). Every drop that costs a booking logs first.
4056
3751
  *
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.
3752
+ * The rail check sits **below** the draw re-read, not in the entry guard
3753
+ * (internal-review): a settled draw the terminal can't report under is a real debit
3754
+ * with no revenue event, permanently overstating outstanding liability
3755
+ * (`deposits − draw revenue`), and it used to return here in silence. Placed
3756
+ * after the re-read, `revenue_skipped_no_rail` can name the draw and its
3757
+ * amount, and can distinguish the four causes — see its `reason` below.
4060
3758
  */
4061
- private reconcileTerminalDraw;
3759
+ private bookRevenue;
3760
+ /** Schedule job removal from activeJobs after a delay so clients can still poll final status. */
3761
+ scheduleCleanup(job: ServerJob): void;
3762
+ /** Clear all timers. Called on server shutdown to allow clean exit. */
3763
+ shutdown(): void;
3764
+ }
3765
+
3766
+ /** Cached liveness signal for a platform-hosted DVM's Lightning receive rail. */
3767
+ interface LightningRailHealth {
3768
+ /** Whether the receive rail may be advertised at this instant. */
3769
+ available(): boolean;
3770
+ /** Prime the cached signal at boot when the source supports it. */
3771
+ refresh?(): Promise<void>;
3772
+ }
3773
+
3774
+ /** Builder-facing wiring for the Lightning receive leg (internal-review). */
3775
+ interface LightningReceiveConfig {
4062
3776
  /**
4063
- * Refresh `lastActivityAt` for jobs this process is actively running
4064
- * (`processing`/`working`) so the processing watchdog only fires once the
4065
- * worker is genuinely dead (internal-review). `awaiting-input` jobs are
4066
- * deliberately excluded — their idle timeout must still elapse. Fires on the
4067
- * heartbeat timer; also callable on demand for tests.
3777
+ * Receive-only NWC connection URI (`DVMKIT_NWC_RECEIVE_URI`). Validated at
3778
+ * boot: a connection that can *spend* is refused outright.
4068
3779
  */
4069
- heartbeatActiveJobs(): Promise<{
4070
- beat: number;
4071
- }>;
3780
+ uri?: string;
4072
3781
  /**
4073
- * Single-execution claim for a reactivating machine (internal-review). Before
4074
- * replaying an `awaiting-input` job, wait a bounded grace window for a live
4075
- * original handler to win the `awaiting-input → processing` CAS via its
4076
- * NOTIFY wake. Resolves:
4077
- * • `false` — the row left `awaiting-input` during the window (the original
4078
- * handler claimed it / drove it terminal). Stand down; do not replay.
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`.
3782
+ * Pre-built backend, bypassing URI parsing and the connect-time probe. Test
3783
+ * seam only — production always goes through `uri` so the receive-only
3784
+ * property is actually checked.
4087
3785
  */
4088
- claimAwaitingInputForReactivation(jobId: string): Promise<boolean>;
3786
+ backend?: LightningBackend;
3787
+ /** Invoice lifetime in seconds. Defaults to {@link DEFAULT_INVOICE_TTL_SECONDS}. */
3788
+ invoiceTtlSeconds?: number;
4089
3789
  /**
4090
- * Record an inbound client message durably via Tx B (internal-review) and append
4091
- * it to the in-memory `job.messages` array. Returns the DB-allocated seq
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.
3790
+ * Smallest funding this deployment can receive over Lightning, in sats
3791
+ * (`DVMKIT_LIGHTNING_FUNDING_MIN_SATS`). The floor is a property of the
3792
+ * receive wallet's channel policy (`htlc_minimum_msat` upstream), so it is
3793
+ * per-deployment and sats-physical — the menu renders it into fiat with a
3794
+ * margin, but everything that *enforces* it compares raw sats. Defaults to
3795
+ * {@link DEFAULT_LIGHTNING_FUNDING_MIN_SATS}.
4097
3796
  */
4098
- recordInboundPending(job: ServerJob, msgType: MessageType, msgContent: Record<string, unknown>): Promise<number | null>;
3797
+ fundingMinSats?: number;
4099
3798
  /**
4100
- * Tx C complement for non-payment inbound messages — flip the row to
4101
- * `verified` so cross-machine readers see it via `subscribeMessages`.
3799
+ * Optional platform-derived channel-liveness gate. Platform-hosted DVMs use
3800
+ * it alongside their NWC reachability probe; self-hosted DVMs omit it.
3801
+ */
3802
+ railHealth?: LightningRailHealth;
3803
+ }
3804
+ /** Default bolt11 lifetime — long enough to pay by hand, short enough to retire. */
3805
+ declare const DEFAULT_INVOICE_TTL_SECONDS = 900;
3806
+ /**
3807
+ * Floor on the invoice lifetime: twice the signed-request drift window, so the
3808
+ * bolt11 always outlives the funding negotiation that produced it (the issue's
3809
+ * "invoice expiry ≥ the funding-negotiation window"). A shorter one would
3810
+ * expire inside the caller's own retry budget and strand them mid-top-up.
3811
+ */
3812
+ declare const MIN_INVOICE_TTL_SECONDS = 600;
3813
+ /**
3814
+ * The builder-side Lightning receive leg (internal-review, credits spec §4).
3815
+ *
3816
+ * Issues a bolt11 over a **receive-only** NWC connection while the DVM is
3817
+ * awake serving the 402, and credits the ledger when a later request observes
3818
+ * settlement. Two properties are load-bearing:
3819
+ *
3820
+ * - **It never holds a send credential.** `pay_invoice` on this connection is
3821
+ * refused at construction, so a compromised DVM can mint invoices and
3822
+ * nothing else. The drain/refund sender (internal-review) is a separate, budgeted
3823
+ * connection by rule.
3824
+ * - **Crediting is pull-based — there is no settlement watcher.** The caller's
3825
+ * next request drives `lookup_invoice`, which is what makes this work on a
3826
+ * suspend-to-zero fleet: a machine that is asleep has nothing to miss.
3827
+ * Wallet downtime at that moment delays crediting; it never loses money,
3828
+ * because the invoice→credit binding is a durable row.
3829
+ *
3830
+ * Reachability is **cached**, never probed per request: the funding menu is
3831
+ * assembled on every quote and every 402, so a live NIP-47 round trip there
3832
+ * would put a nostr relay in the latency path of every priced call.
3833
+ */
3834
+ declare class LightningReceive {
3835
+ private readonly source;
3836
+ readonly invoiceTtlSeconds: number;
3837
+ readonly fundingMinSats: number;
3838
+ private health;
3839
+ private checkedAtMs;
3840
+ private refreshing;
3841
+ private permissionRefusal;
3842
+ private constructor();
3843
+ private readonly railHealth;
3844
+ /**
3845
+ * Validate the configured connection and build the receive leg, or return
3846
+ * `undefined` when this DVM didn't configure one.
3847
+ *
3848
+ * **Throws on a credential that is wrong, degrades on one that is merely
3849
+ * unreachable.** A wallet outage at boot must not stop a DVM whose other
3850
+ * rails are fine — the menu simply omits `lightning` until a later probe
3851
+ * succeeds. A connection that can spend, or one that can't state what it can
3852
+ * do, is a different thing entirely: it is a standing money risk that no
3853
+ * amount of retrying fixes, so it fails the boot loudly with the fix in the
3854
+ * message.
4102
3855
  */
4103
- markInboundVerified(jobId: string, inboundSeq: number | null): Promise<void>;
3856
+ static create(config: LightningReceiveConfig): Promise<LightningReceive | undefined>;
4104
3857
  /**
4105
- * Reactivate a suspended/event-driven job from the store. Builds the
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.
3858
+ * Whether `lightning` belongs on the funding menu right now.
4125
3859
  *
4126
- * What gets re-verified is the persisted row read back through
4127
- * {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
4128
- * those are the caller's own signed bytes, the same ones `/v1/quote` and the
4129
- * submit checked (internal-review). That makes the tamper check strictly stronger
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.
3860
+ * Reads the cached observation and never blocks; a stale one kicks off a
3861
+ * background refresh and answers with what we last knew. Advertising a rail
3862
+ * whose wallet is down would hand the caller an option that 402s on use —
3863
+ * the §4 posture is to drop it and let the sale survive on the others.
4133
3864
  */
4134
- reactivateJob(record: JobRecord): {
4135
- job: ServerJob;
4136
- sdkCtx: SDKJobContext<unknown, unknown>;
4137
- };
3865
+ available(): boolean;
4138
3866
  /**
4139
- * Verify an inbound payment and apply the credit via Tx C (internal-review).
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.
3867
+ * Issue (or re-issue) the bolt11 funding `(creditId, fundId)`.
4153
3868
  *
4154
- * Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
4155
- * commits the proofs *and* the ledger leg in one transaction and returns;
4156
- * only then does `verifyAndCredit` write the job row, on its own connection.
4157
- * Everything after that call therefore runs with the caller's money already
4158
- * moved, which is why the write is retried rather than left to throw, and why
4159
- * an exhausted retry answers with the credit it landed on instead of a bare
4160
- * 500. See `creditInbound`.
3869
+ * The invoice is minted **for** the credit and amount named in the caller's
3870
+ * signed body — that binding is the internal-review condition-3 commitment on this
3871
+ * rail, and it is why no artifact hash rides the request: there is no
3872
+ * caller-supplied artifact to hash. A re-poll returns the stored row rather
3873
+ * than minting again; a second bolt11 for one `fund_id` would leave two
3874
+ * payable invoices against a funding that can only be credited once.
4161
3875
  */
4162
- processPayment(job: ServerJob, body: {
4163
- type: MessageType;
4164
- content: Record<string, unknown>;
4165
- }, inboundSeq: number | null, extra?: {
4166
- lockPubkeys?: LockPubkey[];
4167
- mintHealthTracker?: MintHealthTracker;
4168
- }): Promise<ProcessPaymentOutcome>;
3876
+ issue(ledger: CreditLedgerLike, args: {
3877
+ creditId: string;
3878
+ fundId: string;
3879
+ callerPubkey: string;
3880
+ currency: string;
3881
+ amountMicro: number;
3882
+ amountMsats: number;
3883
+ description?: string;
3884
+ }): Promise<CreditInvoiceRecord>;
4169
3885
  /**
4170
- * Tx C with a bounded retry (internal-review).
3886
+ * Ask the wallet whether one payment hash is paid, and when (internal-review).
4171
3887
  *
4172
- * By the time this runs on the verified path, the rail commit and the ledger
4173
- * leg have already committed in a transaction this call is not part of — so a
4174
- * throw here is money moved against a job row that records none of it, and
4175
- * letting it propagate was the bug. A transient store error is the ordinary
4176
- * cause and a second attempt clears it.
3888
+ * The operator's reconcile verb runs this before it credits anything. A
3889
+ * `blocked` row implies payment — {@link applyOne} returns above the block
3890
+ * classification when the lookup says unpaid — but that is *this fleet's
3891
+ * belief, recorded possibly weeks ago*, and the verb it gates mints balance
3892
+ * against it. One round trip turns the belief into a fact and recovers the
3893
+ * true settlement instant for `settled_at`, which a blocked row never got to
3894
+ * write.
4177
3895
  *
4178
- * The retry is safe by construction, not by convention: `verifyAndCredit`
4179
- * CASes on `job_messages.status = 'pending-verification'` inside its own
4180
- * transaction, so a call whose COMMIT actually landed before the ack was lost
4181
- * finds the row already `verified` and returns `alreadyVerified: true`
4182
- * carrying the counters and binding that first attempt committed. Re-applying
4183
- * the delta is impossible either way.
3896
+ * Unlike {@link settlePending} this **throws**: there is no request whose
3897
+ * latency it protects, and crediting on an unverifiable wallet is exactly
3898
+ * what it exists to prevent. `NOT_FOUND` is the one exception, and it is not
3899
+ * an outage — it is the wallet saying it has never seen this hash, the same
3900
+ * reading {@link applyOne} and the caller-side reconcile in `src/lib/nwc.ts`
3901
+ * take. It comes back as `known: false` because the operator's repair for it
3902
+ * ("you are pointed at a different wallet than the one that minted this")
3903
+ * is not the repair for an unpaid invoice, and certainly not for a retry.
3904
+ */
3905
+ lookupSettlement(paymentHash: string): Promise<{
3906
+ settled: boolean;
3907
+ settledAt?: number;
3908
+ known: boolean;
3909
+ }>;
3910
+ /**
3911
+ * Consult the wallet about this caller's outstanding invoices and credit the
3912
+ * ones that settled — the pull half of pull-based crediting.
4184
3913
  *
4185
- * That argument covers every attempt but the last, whose error nothing
4186
- * re-checks — and a connection-level fault is exactly the shape that loses
4187
- * the ack on *all* of them, idempotent replays included (the no-op path
4188
- * COMMITs too). So the exhausted path asks the row before reporting a
4189
- * failure: `getVerifiedInbound` reads, transaction-free, whether a Tx C for
4190
- * this seq committed. Without it a fully credited, fully bound job answers
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.
3914
+ * The pending rows are read locally first, so the common case (nothing
3915
+ * outstanding) costs one indexed SELECT and no network at all. Bounded to
3916
+ * {@link MAX_SETTLE_CHECKS} invoices on a short deadline, and **never throws
3917
+ * into the request**: a wallet failure here means the caller's balance is
3918
+ * merely not updated yet, which the ordinary `insufficient_credit` path
3919
+ * already states honestly.
4194
3920
  *
4195
- * The read runs on the pool that just failed those COMMITs, so it is retried
4196
- * too, and a read that fails all the way through is reported as *unknown*
4197
- * rather than as nothing: `CreditInboundExhaustedError.readError` is what
4198
- * separates "checked the row, nothing landed" from "couldn't check", both on
4199
- * the operator's log line and in the copy the caller acts on.
3921
+ * **Each invoice gets its own failure boundary.** The sweep window is a few
3922
+ * rows wide, so a row that fails on its own terms — a `fund_id` the caller
3923
+ * reused on another rail, a credit someone else opened first — must not take
3924
+ * the rest of the pass down with it: the next invoice in the window may be
3925
+ * the paid one, and it would never be looked up. Only a transport failure
3926
+ * ends the pass early, because there the wallet itself is gone and the
3927
+ * remaining lookups would just spend the caller's latency confirming it.
4200
3928
  */
4201
- private creditInbound;
3929
+ settlePending(ledger: CreditLedgerLike, args: {
3930
+ callerPubkey: string;
3931
+ creditId?: string;
3932
+ creditTtlMs: number;
3933
+ dvmId?: string;
3934
+ enqueueCreditDeposit?: CreditDepositEnqueue;
3935
+ nowMs?: number;
3936
+ }): Promise<InvoiceSettlement[]>;
4202
3937
  /**
4203
- * Answer a payment whose rail and ledger legs committed but whose job row
4204
- * could not be written (internal-review) — every retry spent, the money real.
3938
+ * One invoice's settlement check. Returns the applied settlement, or
3939
+ * `undefined` when nothing changed (still unpaid, or retired unpaid).
4205
3940
  *
4206
- * The refusal names the credit the money landed on so the caller can act on
4207
- * it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
4208
- * 500 rather than a 402 on purpose: the payment was valid and was accepted,
4209
- * so telling the caller it was insufficient would be a lie, and this is a
4210
- * fault an operator should see in their 5xx rate. `retryable` is true because
4211
- * the ask is genuinely still outstanding.
3941
+ * Throws whatever the wallet or the ledger threw — classifying that is
3942
+ * {@link retireOrLog}'s job, so this stays one invoice's happy path.
3943
+ */
3944
+ private applyOne;
3945
+ /**
3946
+ * Decide what one invoice's failure means, and retire the invoice when the
3947
+ * answer is "this can never succeed".
4212
3948
  *
4213
- * Nothing is released here. The hold is left to the internal-review reconciler,
4214
- * which makes the same decision — unbound draw ⇒ release, never settle — but
4215
- * under the credit row's `FOR UPDATE`, after the job is terminal, where it
4216
- * cannot race an in-flight COMMIT. Releasing from here would also be
4217
- * irreversible: a released draw can be neither re-grown nor re-opened, so one
4218
- * bad call would silently de-ledger every later top-up on the job.
3949
+ * The split that matters is permanent-versus-transient, because it decides
3950
+ * whether the row keeps a slot in the sweep window. A transient failure — the
3951
+ * database blinked, the wallet answered oddly — leaves it `pending` and the
3952
+ * caller's next request retries it. A **permanent** one means the ledger will
3953
+ * refuse this invoice identically forever, so leaving it pending costs a
3954
+ * `lookup_invoice` on every subsequent request and, at
3955
+ * {@link MAX_SETTLE_CHECKS} of them, fills the window so a genuinely payable
3956
+ * invoice behind them is never even looked up. Those get `blocked`, which
3957
+ * drops them out of the sweep and hands the operator the payment hash.
3958
+ */
3959
+ private retireOrLog;
3960
+ /**
3961
+ * The reason this invoice can never be credited, or `undefined` if the
3962
+ * failure was transient.
4219
3963
  *
4220
- * Whether the row was *read* is load-bearing on both surfaces. When the
4221
- * post-exhaustion read-back failed as well, `credit_job_row_unwritten` says so
4222
- * (`row_checked: false` plus the read's own error) instead of implying an
4223
- * operator can trust the row is clean, and the caller gets the `"unconfirmed"`
4224
- * copy rather than a flat "pay it again" it could be charged twice for.
3964
+ * Every code here is decided by state a retry cannot move — a row the ledger
3965
+ * has already committed (a `credit_fundings` entry at this
3966
+ * `(credit_id, fund_id)`, or a `credits` row whose owner, denomination or
3967
+ * rail disagrees with the invoice), or the basis this module rebuilds
3968
+ * identically from the invoice row on every pass. None of them is a race.
4225
3969
  *
4226
- * That copy overrides `display` as well as `hint`, on every branch including
4227
- * the ledger-less one. `display` is the sentence an agent relays to its human
4228
- * (repo convention: JSON carries the words, not just the facts), so a body
4229
- * whose `hint` says the outcome is unknown while its `display` still asserts
4230
- * the ask is outstanding is read as "pay again" by the audience that acts on
4231
- * it — and a payment that did land is charged on top.
3970
+ * `funding_replayed` is the one that needs a second read to classify.
3971
+ * Usually it *is* benign — a concurrent check won the race and the money is
3972
+ * credited either way — but the key is `(credit_id, fund_id)` and `fund_id`
3973
+ * is **caller-chosen**: reuse it on cashu or x402 after this bolt11 was
3974
+ * minted and the row that collided is a different payment entirely, so the
3975
+ * sats this invoice received have nowhere to land.
4232
3976
  */
4233
- private reportUnappliedPayment;
3977
+ private blockingReason;
3978
+ private withBackend;
3979
+ /** Record activity without letting it promote an unvalidated connection. */
3980
+ private noteOperationOk;
3981
+ private notePermissionsOk;
3982
+ private notePermissionRefused;
4234
3983
  /**
4235
- * Describe a deposit the job row never took, in the shape both the 201 and
4236
- * the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
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.
3984
+ * Only a *transport* failure from an ordinary invoice operation is a health
3985
+ * signal. Permission-probe refusals are classified by {@link refresh}.
4240
3986
  */
4241
- private unappliedCredit;
4242
- /** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
4243
- private reportCreditDeposit;
3987
+ private noteFailure;
3988
+ private refresh;
3989
+ }
3990
+
3991
+ /**
3992
+ * Owner display identity surfaced on `/v1/info#owner` (internal-review). Personal orgs
3993
+ * resolve to the owner builder's profile; shared orgs resolve to the org's own
3994
+ * profile. Container DVMs read from env vars; isolate DVMs resolve from Postgres.
3995
+ */
3996
+ interface OwnerDisplay {
3997
+ handle: string;
3998
+ displayName?: string | null;
3999
+ avatarUrl?: string | null;
4000
+ type: "builder" | "org";
4001
+ }
4002
+ /** Optional builder identity surfaced on `/v1/info` (forward-compatible stub). */
4003
+ interface BuilderIdentity {
4004
+ id?: string;
4005
+ name?: string;
4006
+ url?: string;
4244
4007
  /**
4245
- * Internal: build the BuildContextOpts shared between `createJob` and
4246
- * `reactivateJob`. Centralised so persistence/terminal callbacks and the
4247
- * internal-review NOTIFY-driven cross-machine wake stay in one place.
4008
+ * Builder identity x-only secp256k1 pubkey (internal-review). When populated, the
4009
+ * `/v1/info#builder` block also carries `attestation` + `signature` so
4010
+ * consumers can verify the deploy was signed by the holder of this key.
4011
+ * Populated at deploy time from `DVMKIT_BUILDER_PUBKEY` (Fly secret); the
4012
+ * SDK never re-signs at runtime.
4248
4013
  */
4249
- private buildContextOpts;
4014
+ pubkey?: string;
4015
+ /** Canonical deploy-time attestation payload (internal-review). Served verbatim. */
4016
+ attestation?: AttestationPayload;
4017
+ /** Schnorr signature over `canonicaliseForSigning(attestation)` (internal-review). */
4018
+ signature?: string;
4019
+ }
4020
+ /** Platform reporter overrides — the host falls back to env when omitted. */
4021
+ interface PlatformReporterOpts {
4022
+ /** Bearer token. Defaults to `DVMKIT_PLATFORM_TOKEN` env. */
4023
+ token?: string;
4024
+ /** Platform internal URL. Defaults to `DVMKIT_PLATFORM_URL` env. */
4025
+ url?: string;
4026
+ }
4027
+ /** Options for {@link createDVMHost}. */
4028
+ interface DVMHostOpts {
4250
4029
  /**
4251
- * Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
4252
- * job's remaining pool in (internal-review).
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.
4030
+ * Postgres connection string. Defaults to `DATABASE_URL` env.
4031
+ * Required at runtime (internal-review). Boot fails when missing unless `jobStore`
4032
+ * is explicitly supplied or `devMode` is true (test/dev escape hatches).
4272
4033
  */
4273
- private resolveDrawPool;
4274
- /** Internal: structured warn for the one path that can't price a drawn job's pool. */
4275
- private warnDrawPool;
4034
+ database?: string;
4035
+ /** Listen port. Defaults to `PORT` env or 8080. */
4036
+ port?: number;
4037
+ /** Environment variables. Defaults to `process.env`. */
4038
+ env?: Record<string, string>;
4039
+ /** Builder identity for `/v1/info` (forward-compatible). */
4040
+ builder?: BuilderIdentity;
4041
+ /** Owner display identity for `/v1/info#owner` (internal-review). Falls back to env vars. */
4042
+ owner?: OwnerDisplay;
4043
+ /** Platform reporter overrides. Defaults to env-derived values. */
4044
+ platformReporter?: PlatformReporterOpts;
4045
+ /** Override the SDK's fx fetcher. Defaults to env-configured CoinGecko. */
4046
+ fx?: FxFetcher;
4276
4047
  /**
4277
- * Issue the signed receipt for a job that has reached a terminal status
4278
- * (internal-review). Idempotent and safe to call from every terminal path — the
4279
- * store owns both the sequence allocation and the write-once persist, so
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.
4048
+ * Custom `/health` handler. When set, the SDK installs this instead of the
4049
+ * default `{ status: "ok" }` responder — useful for runtime-specific health
4050
+ * (pool depth, draining state, 503 while draining).
4288
4051
  */
4289
- issueReceipt(record: JobRecord): Promise<JobReceipt | undefined>;
4290
- /** Build the common settle/release args, adding the hosted release outbox when wired. */
4291
- private drawResolutionArgs;
4052
+ healthHandler?: (c: Context) => Response | Promise<Response>;
4053
+ /** KV store override. Defaults to PostgresKVStore (or MemoryKVStore in dev). */
4054
+ store?: KVStore;
4055
+ /** JobStore override. Takes precedence over `database`. */
4056
+ jobStore?: JobStore;
4292
4057
  /**
4293
- * Settle or release a terminal job's credit draw (internal-review): `completed`
4294
- * settles (the hold becomes a real debit); `failed`/`cancelled` releases
4295
- * (the hold evaporates — "no debit on job failure", mechanically
4296
- * superseding the internal-review/974 no-op refund for credit-paid jobs, including
4297
- * the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
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.
4058
+ * Existing Postgres pool to reuse instead of opening one from `database`
4059
+ * (internal-review test seam). When set, the host builds its JobStore / KVStore /
4060
+ * cashu accumulator on this pool and leaves it open at shutdown — the caller
4061
+ * owns it. Lets the e2e harness run DVMs in `p2pk-accumulator` mode against
4062
+ * its single shared pool rather than spawning a pool per DVM.
4301
4063
  */
4302
- private resolveCreditDraw;
4064
+ pool?: Pool;
4065
+ /** Cashu mints accepted. Defaults to env `DVMKIT_CASHU_MINTS`. */
4066
+ mints?: string[];
4067
+ /** Cashu receive mode override. */
4068
+ cashuMode?: CashuMode;
4069
+ /** MPP handle override. Defaults to env-resolved via `createMppFromOpts`. */
4070
+ mpp?: MppxServer;
4071
+ /** x402 stablecoin payment configuration. Defaults to env-resolved. */
4072
+ x402?: X402Config;
4073
+ /** Payment methods override. Defaults to derived from configured rails. */
4074
+ paymentMethods?: PaymentMethod[];
4303
4075
  /**
4304
- * The one place a terminal job's status becomes a ledger verb (internal-review):
4305
- * `completed` settles the hold into a real debit, `failed`/`cancelled`
4306
- * release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
4307
- * late reconciler (`reconcileTerminalDraw`) — two paths that must never
4308
- * disagree about what an outcome means for the caller's money.
4076
+ * Lightning receive leg for credit funding (internal-review). Defaults to the
4077
+ * env-resolved `DVMKIT_NWC_RECEIVE_URI` /
4078
+ * `DVMKIT_NWC_RECEIVE_INVOICE_TTL_SECONDS`. Pass `backend` to inject a
4079
+ * wallet directly — a test seam that skips the connect-time probe, so
4080
+ * production must always come through the URI.
4309
4081
  */
4310
- private settlesDraw;
4082
+ lightningReceive?: LightningReceiveConfig;
4083
+ /** When true, payment is skipped if no mints are configured (dev/test). */
4084
+ devMode?: boolean;
4085
+ /** Consumed-credential store override (test seam). */
4086
+ consumedCredentialStore?: ConsumedCredentialStore;
4087
+ /** Mint-health tracker override (test seam). */
4088
+ mintHealthTracker?: MintHealthTracker;
4089
+ /** Operator-alert boundary for failed terminal-job retention passes. */
4090
+ onJobRetentionSweepFailed?: JobManagerOpts["onJobRetentionSweepFailed"];
4091
+ /** Clear-on-recovery boundary invoked after a complete clean retention pass. */
4092
+ onJobRetentionSweepRecovered?: JobManagerOpts["onJobRetentionSweepRecovered"];
4311
4093
  /**
4312
- * Sign a locally-held terminal job's receipt and mirror it onto the
4313
- * in-memory job, so the read paths served out of `activeJobs` during the
4314
- * cleanup window hand back the same bytes as a cross-machine re-read.
4094
+ * Wrap the SDK compatibility gate before it is exposed to descriptor custom
4095
+ * routes. Platform hosts use this to enforce their independently released
4096
+ * caller rollout policy; self-hosted SDK consumers receive the default gate.
4315
4097
  */
4316
- private attachReceipt;
4098
+ wrapRouteClientCompatibility?: (sdkGate: ClientCompatibilityGate) => ClientCompatibilityGate;
4099
+ }
4100
+ /** Per-mount options. Currently only `prefix` is supported. */
4101
+ interface MountOpts {
4102
+ /** Path prefix for this DVM's protocol routes (e.g. `/delete-feed`). */
4103
+ prefix?: string;
4104
+ }
4105
+ /** Live DVM host. Single wiring locus for the SDK (internal-review). */
4106
+ interface DVMHost {
4317
4107
  /**
4318
- * Read-path repair for a terminal job carrying no receipt (internal-review), for
4319
- * either an in-memory job or a store record. Free in the steady state — it
4320
- * returns on the `receipt` check without touching the store — so it costs
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.
4108
+ * Underlying Hono app for custom non-protocol routes that belong to the
4109
+ * host (cross-DVM `/admin`, cron callbacks, etc.). For DVM-scoped routes,
4110
+ * declare `routes(app)` on the descriptor.
4335
4111
  */
4336
- ensureReceipt(target: ServerJob | JobRecord): Promise<void>;
4112
+ readonly app: Hono;
4337
4113
  /**
4338
- * Close out a terminal job whose caller hasn't already persisted it: sign
4339
- * the receipt, then save the snapshot. The snapshot save never writes the
4340
- * receipt column, so ordering only affects how soon a reader sees the
4341
- * receipt — this way a caller polling immediately after the terminal
4342
- * already finds it.
4114
+ * Resolved Postgres pool — undefined before `host.serve()` completes
4115
+ * database resolution, set thereafter. Exposed so DVM-local Postgres
4116
+ * consumers (e.g. scrape's `ScrapeDb` per-fetch event store, internal-review)
4117
+ * can reuse the host's pool rather than constructing their own. Wire
4118
+ * inside descriptor `onBoot` (fires after pool init, before listener opens).
4343
4119
  */
4344
- private finalizeTerminal;
4120
+ readonly pool: Pool | undefined;
4345
4121
  /**
4346
- * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
4347
- * cross-machine cancel, or a row the stale sweeper just reaped. Re-reads the
4348
- * record so the receipt is built from the committed terminal row rather than
4349
- * from whatever the caller happened to have in hand.
4122
+ * Money-safe credit ledger (internal-review) — undefined before `host.serve()`
4123
+ * resolves stores, set thereafter. Postgres-backed when a pool exists;
4124
+ * pool-less hosts get a shared in-memory ledger so the internal-review fund+draw
4125
+ * semantics hold everywhere. Same wiring window as `pool` (descriptor
4126
+ * `onBoot` fires after init, before the listener opens).
4350
4127
  */
4351
- issueReceiptForStoredJob(jobId: string): Promise<JobReceipt | undefined>;
4352
- /** Handle job reaching terminal state (completed, failed, cancelled). */
4353
- private handleJobTerminal;
4128
+ readonly creditLedger: CreditLedgerLike | undefined;
4354
4129
  /**
4355
- * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
4356
- * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
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.
4130
+ * Add a DVM to this host. Multiple mounts compose at distinct prefixes;
4131
+ * a single mount with no prefix attaches at root.
4396
4132
  */
4397
- private bookRevenue;
4398
- /** Schedule job removal from activeJobs after a delay so clients can still poll final status. */
4399
- scheduleCleanup(job: ServerJob): void;
4400
- /** Clear all timers. Called on server shutdown to allow clean exit. */
4401
- shutdown(): void;
4133
+ mount<S, I extends ZodLike | undefined>(descriptor: DVMDescriptor<S, I>, opts?: MountOpts): void;
4134
+ /** Start listening. Returns the live server handle. */
4135
+ serve(opts?: {
4136
+ port?: number;
4137
+ }): Promise<{
4138
+ url: string;
4139
+ close: () => Promise<void>;
4140
+ }>;
4141
+ /** Graceful shutdown. Runs descriptor `onShutdown` hooks in reverse mount order. */
4142
+ shutdown(): Promise<void>;
4402
4143
  }
4144
+ /**
4145
+ * Single wiring locus for SDK-side construction (internal-review). Replaces both the
4146
+ * `serve()` wrapper and the `createDVMServer` direct path. Reads env once,
4147
+ * resolves payment rails, owns the Postgres pool, and mounts each DVM
4148
+ * descriptor as a Hono sub-app on `host.app`.
4149
+ */
4150
+ declare function createDVMHost(opts?: DVMHostOpts): DVMHost;
4403
4151
 
4404
4152
  /**
4405
4153
  * Cached liveness signal for the platform's central Tempo close observer
@@ -4685,12 +4433,12 @@ interface SDKServerOpts {
4685
4433
  /** Cashu receive mode (internal-review). */
4686
4434
  cashuMode?: CashuMode;
4687
4435
  /**
4688
- * Builder's NUT-11 P2PK lock pubkey (internal-review). Required to enable accumulator
4689
- * mode — used as the seed pubkey for `seedLockPubkeyState` on first boot, and
4690
- * as the gate for whether `/v1/info` advertises `cashu` and the admin routes
4691
- * mount. After first boot, the authoritative `[current, ...retired]` state
4692
- * lives in the DVM's Postgres via `dvm_lock_pubkeys`; rotations (internal-review)
4693
- * mutate that store directly.
4436
+ * Builder's NUT-11 P2PK lock pubkey (internal-review). Used as the seed pubkey for
4437
+ * `seedLockPubkeyState` on first boot, as the admin-auth identity, and as
4438
+ * part of the gate for whether `/v1/info` advertises `cashu`. After first
4439
+ * boot, the authoritative `[current, ...retired]` state lives in the DVM's
4440
+ * Postgres via `dvm_lock_pubkeys`; rotations (internal-review) mutate that store
4441
+ * directly.
4694
4442
  */
4695
4443
  lockPubkey?: string;
4696
4444
  /**
@@ -4708,7 +4456,7 @@ interface SDKServerOpts {
4708
4456
  * without a separate env var.
4709
4457
  */
4710
4458
  canonicalDvmId?: string;
4711
- /** Postgres pool for wallet accumulator persistence. */
4459
+ /** Postgres pool for builder admin state and wallet accumulator persistence. */
4712
4460
  db?: Pool;
4713
4461
  /**
4714
4462
  * General SDK strict-mode flag (internal-review), sourced from `DVMKIT_FAIL_FAST` at
@@ -4753,6 +4501,12 @@ interface SDKServerOpts {
4753
4501
  * tests and advanced integrations only.
4754
4502
  */
4755
4503
  onJobCompleted?: JobManagerOpts["onJobCompleted"];
4504
+ /**
4505
+ * Advanced override for a declared cost on a terminal job that emitted no
4506
+ * revenue report (internal-review). Auto-wired to the reporter's durable
4507
+ * job-cost path; intended for tests and advanced integrations only.
4508
+ */
4509
+ onJobCost?: JobManagerOpts["onJobCost"];
4756
4510
  /**
4757
4511
  * Advanced override: callback fired when the stale-job reaper force-fails a
4758
4512
  * *paid* job (internal-review). Auto-wired to the `RevenueReporter`'s paid-job-death
@@ -4766,6 +4520,10 @@ interface SDKServerOpts {
4766
4520
  * the platform reporter alongside the completion and paid-job-death paths.
4767
4521
  */
4768
4522
  onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
4523
+ /** Advanced operator-alert callback for failed job-retention passes. */
4524
+ onJobRetentionSweepFailed?: JobManagerOpts["onJobRetentionSweepFailed"];
4525
+ /** Advanced clear-on-recovery callback for clean job-retention passes. */
4526
+ onJobRetentionSweepRecovered?: JobManagerOpts["onJobRetentionSweepRecovered"];
4769
4527
  /**
4770
4528
  * Advanced override: callback fired when a payment funds a credit (internal-review).
4771
4529
  * Auto-wired to the `RevenueReporter`'s deposit report alongside
@@ -5122,4 +4880,4 @@ declare function attachCreditMenu(body: Record<string, unknown>, menu: CreditMen
5122
4880
  */
5123
4881
  declare function toCreditTerms(menu: CreditMenu): CreditTerms;
5124
4882
 
5125
- export { assertRevenueReporterReady as $, type AccumulatorPool as A, type BuilderIdentityKeypair as B, type CreatedInvoice as C, X402FacilitatorHealth as D, type X402SettlementEvidenceOutcome as E, type FiatDenomination as F, type X402SettlementRepair as G, type X402SettlementRepairRefusal as H, type InvoiceStatus as I, JobCancelledError as J, type X402SettlementRepaired as K, type LightningTransactionListOptions as L, type MintAmountBounds as M, type X402WedgedSettlement as N, type X402WedgedSettlementPage as O, PAYMENT_PROOF_KEYS as P, X402_BATCH_SETTLEMENT_MAINNET_NETWORK as Q, type ReporterBannerOpts as R, type SDKServerOpts as S, type TempoChannelReport as T, type UpfrontPaymentOpts as U, type VerifiedIncomingPayment as V, abortJob as W, type X402BatchChannelObservation as X, amountBoundsVerdict as Y, applyPaymentInfoToJob as Z, assertNutSupport as _, type LightningTransactionSnapshot as a, MIN_INVOICE_TTL_SECONDS as a$, buildAttestation as a0, buildPaymentErrorResponse as a1, canSwap as a2, checkMintHealth as a3, clearAccumulatorForDvm as a4, createDVMServer as a5, creditDepositPayload as a6, derivedFundCreditId as a7, devModeSkipsPaymentVerification as a8, fromJobRecord as a9, toJobRecord as aA, unknownRouteNotFound as aB, verifyAttestation as aC, verifyIncomingPayment as aD, verifyTempoSessionManagementCredential as aE, verifyUpfrontPayment as aF, writeIdentity as aG, x402RequiredUsdcMicro as aH, x402SettledShare as aI, type CreditMenu as aJ, ReceiptIssuer as aK, LightningReceive as aL, TempoSettlementReadiness as aM, type DVMHostOpts as aN, type BuildCreditMenuArgs as aO, type BuilderIdentity as aP, CREDIT_ENVELOPE_KEYS as aQ, type CreateX402BatchSettlementServerOpts as aR, type CreditEnvelope as aS, CreditEnvelopeError as aT, type CreditFundCommitment as aU, type CreditTerms as aV, DEFAULT_INVOICE_TTL_SECONDS as aW, type DVMHost as aX, JobManager as aY, type JobManagerOpts as aZ, type LightningReceiveConfig as a_, fundedMicroFor as aa, generateIdentity as ab, hasPaymentProof as ac, hashCapabilities as ad, hashLockKey as ae, implicitCreditId as af, initWalletAccumulatorTable as ag, insertAccumulatorRows as ah, isDerivedCreditId as ai, isTerminal as aj, isYieldMessage as ak, issueUpfrontChallenges as al, loadIdentitySecret as am, toLockPubkey as an, msatsToFiatMicro as ao, paymentErrorBody as ap, pinAskFiat as aq, priceFiatMicro as ar, processIncomingPayment as as, providerMessage as at, repairX402ExactSettlementEffect as au, resolveFxSnapshot as av, resolvePriceFiat as aw, revenueReporterBannerState as ax, signAttestation as ay, toCreditTerms as az, type LightningBackend as b, MemoryConsumedCredentialStore as b0, type MemoryConsumedCredentialStoreOpts as b1, MemoryProcessedPaymentStore as b2, MemoryX402ExactSettlementStore as b3, type MountOpts as b4, type OwnerDisplay as b5, type PlatformReporterOpts as b6, PostgresProcessedPaymentStore as b7, PostgresX402ExactSettlementStore as b8, type PriceFiat as b9, X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS as bA, X402_BATCH_SETTLEMENT_NETWORK as bB, attachCreditMenu as bC, buildCreditMenu as bD, createDVMHost as bE, createX402BatchSettlementServer as bF, creditEnvelopeIgnoreFields as bG, drawSettlementRef as bH, extractCreditEnvelope as bI, fundingCommitment as bJ, selectPrimaryCredit as bK, stripCreditEnvelope as bL, toCreditView as bM, type ProcessedPaymentQuerier as ba, type ProcessedPaymentRail as bb, type ProcessedPaymentRecord as bc, ProcessedPaymentReplayError as bd, type ProcessedPaymentStore as be, type X402BatchAcceptance as bf, type X402BatchFunding as bg, type X402BatchRefusal as bh, type X402BatchSettlementServer as bi, type X402ExactAcceptance as bj, X402ExactIntentConflictError as bk, type X402ExactSettlementAttempt as bl, type X402ExactSettlementChainEvidence as bm, type X402ExactSettlementEffect as bn, X402ExactSettlementEvidenceMissingError as bo, type X402ExactSettlementEvidenceReader as bp, type X402ExactSettlementIntent as bq, X402ExactSettlementNotReadyError as br, X402ExactSettlementServer as bs, type X402ExactSettlementServerOpts as bt, type X402ExactSettlementStatus as bu, type X402ExactSettlementStore as bv, type X402SettlementChainEvidence as bw, type X402SettlementEvidenceReader as bx, X402SettlementSubmissionError as by, X402_BATCH_AUTO_SETTLEMENT as bz, type LightningPayment as c, type LightningWalletInfo as d, type LockPubkey as e, type AccumulatorQuerier as f, type ConsumedCredentialStore as g, type MintHealthCheckResult as h, type AppEnv as i, type AttestationPayload as j, type CheckMintHealthOptions as k, type CreditFundingReport as l, type FiatDenominationFailure as m, type FundOnlyRequest as n, type IncomingPaymentOpts as o, MintHealthTracker as p, type PaymentErrorCode as q, type PaymentErrorDetail as r, type PaymentInfo as s, type ResolvedFx as t, type RevenueBootCheckOpts as u, type ServerJob as v, type ShortPayForfeit as w, type TempoObserverHealth as x, TempoSessionChannelMismatchError as y, type VerifyIncomingSnapshot as z };
4883
+ 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 };