@dvmkit/sdk 0.2.1-rc.9 → 0.3.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1053,10 +1053,13 @@ var PostgresJobStore = class {
1053
1053
  );
1054
1054
  }
1055
1055
  cancelStaleJob(jobId, expectedActivityBefore, reason, terminalStatus) {
1056
- return this.terminateJob(jobId, reason, terminalStatus, expectedActivityBefore);
1056
+ return this.terminateJob(jobId, reason, terminalStatus, expectedActivityBefore, null);
1057
1057
  }
1058
1058
  cancelJob(jobId, reason) {
1059
- return this.terminateJob(jobId, reason, "cancelled", null);
1059
+ return this.terminateJob(jobId, reason, "cancelled", null, "caller");
1060
+ }
1061
+ cancelJobForHosting(jobId, reason) {
1062
+ return this.terminateJob(jobId, reason, "cancelled", null, "provider");
1060
1063
  }
1061
1064
  async getCounters(jobId) {
1062
1065
  const { rows } = await this.pool.query(
@@ -1209,7 +1212,7 @@ var PostgresJobStore = class {
1209
1212
  * machine running the handler wakes as soon as the terminal status is
1210
1213
  * visible.
1211
1214
  */
1212
- async terminateJob(jobId, reason, terminalStatus, expectedActivityBefore) {
1215
+ async terminateJob(jobId, reason, terminalStatus, expectedActivityBefore, endedBy) {
1213
1216
  const staleOnly = expectedActivityBefore !== null;
1214
1217
  const client = await this.pool.connect();
1215
1218
  try {
@@ -1241,7 +1244,7 @@ var PostgresJobStore = class {
1241
1244
  );
1242
1245
  await client.query(
1243
1246
  `UPDATE jobs SET
1244
- ended_by = CASE WHEN $7::boolean OR status = 'awaiting-input' THEN 'caller' ELSE 'provider' END,
1247
+ ended_by = COALESCE($7::text, CASE WHEN status = 'awaiting-input' THEN 'caller' ELSE 'provider' END),
1245
1248
  terminal_at_ms = $5,
1246
1249
  caller_wait_ms = caller_wait_ms + CASE WHEN status = 'awaiting-input'
1247
1250
  THEN GREATEST(0, $5 - COALESCE(awaiting_input_at_ms, last_activity_at)) ELSE 0 END,
@@ -1253,7 +1256,7 @@ var PostgresJobStore = class {
1253
1256
  next_seq = $4 + 1,
1254
1257
  last_activity_at = $5
1255
1258
  WHERE id = $1`,
1256
- [jobId, reason, JSON.stringify([cancelMsg]), nextSeq, now, terminalStatus, !staleOnly]
1259
+ [jobId, reason, JSON.stringify([cancelMsg]), nextSeq, now, terminalStatus, endedBy]
1257
1260
  );
1258
1261
  await client.query("SELECT pg_notify('job_messages', $1)", [jobId]);
1259
1262
  await client.query("COMMIT");
@@ -11,6 +11,9 @@ import {
11
11
  import {
12
12
  MemoryCreditLedger
13
13
  } from "./chunk-U5VNEALI.js";
14
+ import {
15
+ PostgresJobStore
16
+ } from "./chunk-HHTL3ZFT.js";
14
17
  import {
15
18
  SIGNED_ENVELOPE_FIELDS,
16
19
  SignedRequestError,
@@ -6756,6 +6759,18 @@ async function handleCreditRequest(c, deps) {
6756
6759
  await deps.settleLightning(callerPubkey, body.credit_id);
6757
6760
  return handleDrain(c, deps, { callerPubkey, body, envelope });
6758
6761
  case "fund":
6762
+ if (deps.withFundingAdmission && body.fund) {
6763
+ const creditId = body.credit_id ?? derivedFundCreditId(callerPubkey, body.fund.fund_id);
6764
+ const [prior, invoice] = await Promise.all([
6765
+ deps.ledger.getFunding({ creditId, fundId: body.fund.fund_id }),
6766
+ body.fund.method === "lightning" ? deps.ledger.getInvoice({ creditId, fundId: body.fund.fund_id }) : Promise.resolve(void 0)
6767
+ ]);
6768
+ const priorCredit = prior ? await deps.ledger.get(creditId) : void 0;
6769
+ if (priorCredit?.callerPubkey === callerPubkey || invoice?.callerPubkey === callerPubkey) {
6770
+ return handleFund(c, deps, { callerPubkey, body, envelope, menu });
6771
+ }
6772
+ return deps.withFundingAdmission(c, body, () => handleFund(c, deps, { callerPubkey, body, envelope, menu }));
6773
+ }
6759
6774
  return handleFund(c, deps, { callerPubkey, body, envelope, menu });
6760
6775
  }
6761
6776
  }
@@ -9071,6 +9086,269 @@ function safeError(err) {
9071
9086
  return "settlement_transport_error";
9072
9087
  }
9073
9088
 
9089
+ // src/sdk/server/hosting-control.ts
9090
+ import { createHmac, timingSafeEqual } from "crypto";
9091
+ function signHostingControl(secret, command) {
9092
+ return createHmac("sha256", secret).update(JSON.stringify([
9093
+ "dvmkit-hosting-v1",
9094
+ command.action,
9095
+ command.scope,
9096
+ command.dvmId,
9097
+ command.generation ?? null,
9098
+ command.deadlineMs ?? null,
9099
+ command.timestampMs,
9100
+ command.nonce
9101
+ ])).digest("hex");
9102
+ }
9103
+ var HostingControl = class {
9104
+ constructor(pool, dvmId, config) {
9105
+ this.pool = pool;
9106
+ this.dvmId = dvmId;
9107
+ this.config = config;
9108
+ if (Buffer.byteLength(config.secret) < 32 || !config.scope) {
9109
+ throw new Error("Hosting control requires a 32-byte secret and a non-empty scope");
9110
+ }
9111
+ }
9112
+ pool;
9113
+ dvmId;
9114
+ config;
9115
+ async init() {
9116
+ await withSdkInitLock(this.pool, async () => {
9117
+ await this.pool.query(`
9118
+ CREATE TABLE IF NOT EXISTS sdk_hosting_control (
9119
+ dvm_id TEXT NOT NULL, scope TEXT NOT NULL, state TEXT NOT NULL,
9120
+ generation BIGINT NOT NULL, deadline_ms BIGINT,
9121
+ PRIMARY KEY (dvm_id, scope)
9122
+ );
9123
+ CREATE TABLE IF NOT EXISTS sdk_hosting_nonces (
9124
+ dvm_id TEXT NOT NULL, scope TEXT NOT NULL, nonce TEXT NOT NULL,
9125
+ created_at_ms BIGINT NOT NULL,
9126
+ PRIMARY KEY (dvm_id, scope, nonce)
9127
+ );
9128
+ CREATE TABLE IF NOT EXISTS sdk_hosting_admissions (
9129
+ id TEXT PRIMARY KEY, dvm_id TEXT NOT NULL, scope TEXT NOT NULL,
9130
+ job_id TEXT NOT NULL, admitted_at_ms BIGINT NOT NULL,
9131
+ outcome TEXT NOT NULL DEFAULT 'pending', drain_deadline_ms BIGINT,
9132
+ credit_id TEXT, fund_id TEXT, payment_artifact_hash TEXT
9133
+ );
9134
+ ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS drain_deadline_ms BIGINT;
9135
+ ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS credit_id TEXT;
9136
+ ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS fund_id TEXT;
9137
+ ALTER TABLE sdk_hosting_admissions ADD COLUMN IF NOT EXISTS payment_artifact_hash TEXT;
9138
+ CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_pending
9139
+ ON sdk_hosting_admissions (dvm_id, scope) WHERE outcome = 'pending';
9140
+ CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_unresolved
9141
+ ON sdk_hosting_admissions (dvm_id, scope) WHERE outcome = 'unresolved';
9142
+ CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_artifact
9143
+ ON sdk_hosting_admissions (dvm_id, scope, payment_artifact_hash)
9144
+ WHERE payment_artifact_hash IS NOT NULL;
9145
+ CREATE INDEX IF NOT EXISTS sdk_hosting_admissions_job
9146
+ ON sdk_hosting_admissions (dvm_id, job_id, scope);
9147
+ CREATE INDEX IF NOT EXISTS sdk_hosting_nonces_age ON sdk_hosting_nonces (created_at_ms);
9148
+ `);
9149
+ await this.pool.query(
9150
+ `INSERT INTO sdk_hosting_control (dvm_id, scope, state, generation)
9151
+ VALUES ($1, $2, 'accepting', 0) ON CONFLICT DO NOTHING`,
9152
+ [this.dvmId, this.config.scope]
9153
+ );
9154
+ });
9155
+ }
9156
+ async authorize(command, signature) {
9157
+ if (command.scope !== this.config.scope || command.dvmId !== this.dvmId || !Number.isSafeInteger(command.timestampMs) || Math.abs(Date.now() - command.timestampMs) > 3e5 || typeof command.nonce !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(command.nonce) || !signature || !/^[0-9a-f]{64}$/i.test(signature)) return false;
9158
+ const actual = Buffer.from(signature, "hex");
9159
+ const expected = Buffer.from(signHostingControl(this.config.secret, command), "hex");
9160
+ if (!timingSafeEqual(actual, expected)) return false;
9161
+ const result = await this.pool.query(
9162
+ `INSERT INTO sdk_hosting_nonces (dvm_id, scope, nonce, created_at_ms)
9163
+ VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`,
9164
+ [this.dvmId, this.config.scope, command.nonce, Date.now()]
9165
+ );
9166
+ await this.pool.query("DELETE FROM sdk_hosting_nonces WHERE created_at_ms < $1", [Date.now() - 6e5]);
9167
+ return result.rowCount === 1;
9168
+ }
9169
+ async admit(id, jobId, details) {
9170
+ const client = await this.pool.connect();
9171
+ try {
9172
+ await client.query("BEGIN");
9173
+ const { rows } = await client.query(
9174
+ `SELECT state FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
9175
+ [this.dvmId, this.config.scope]
9176
+ );
9177
+ if (rows[0]?.state !== "accepting") {
9178
+ await client.query("ROLLBACK");
9179
+ return false;
9180
+ }
9181
+ await client.query(
9182
+ `INSERT INTO sdk_hosting_admissions
9183
+ (id, dvm_id, scope, job_id, admitted_at_ms, credit_id, fund_id, payment_artifact_hash)
9184
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
9185
+ [
9186
+ id,
9187
+ this.dvmId,
9188
+ this.config.scope,
9189
+ jobId,
9190
+ Date.now(),
9191
+ details?.creditId ?? null,
9192
+ details?.fundId ?? null,
9193
+ details?.paymentArtifactHash ?? null
9194
+ ]
9195
+ );
9196
+ await client.query("COMMIT");
9197
+ return true;
9198
+ } catch (error) {
9199
+ await client.query("ROLLBACK").catch(() => void 0);
9200
+ throw error;
9201
+ } finally {
9202
+ client.release();
9203
+ }
9204
+ }
9205
+ async acceptedJobByPaymentArtifactHash(hash2) {
9206
+ const { rows } = await this.pool.query(
9207
+ `SELECT a.job_id FROM sdk_hosting_admissions a
9208
+ JOIN jobs j ON j.id=a.job_id
9209
+ WHERE a.dvm_id=$1 AND a.scope=$2 AND a.payment_artifact_hash=$3
9210
+ AND a.outcome IN ('accepted', 'unresolved', 'pending')
9211
+ ORDER BY a.admitted_at_ms DESC LIMIT 1`,
9212
+ [this.dvmId, this.config.scope, hash2]
9213
+ );
9214
+ return rows[0]?.job_id;
9215
+ }
9216
+ async rebindAdmissionJob(id, jobId) {
9217
+ await this.pool.query(
9218
+ `UPDATE sdk_hosting_admissions SET job_id=$2 WHERE id=$1 AND outcome='pending'`,
9219
+ [id, jobId]
9220
+ );
9221
+ }
9222
+ async finishAdmission(id, outcome, jobId) {
9223
+ await this.pool.query(
9224
+ `UPDATE sdk_hosting_admissions SET outcome=$2, job_id=COALESCE($3, job_id)
9225
+ WHERE id=$1 AND outcome='pending'`,
9226
+ [id, outcome, jobId ?? null]
9227
+ );
9228
+ }
9229
+ async jobDeadline(jobId) {
9230
+ const { rows } = await this.pool.query(
9231
+ `SELECT MAX(drain_deadline_ms)::text AS drain_deadline_ms
9232
+ FROM sdk_hosting_admissions WHERE dvm_id=$1 AND scope=$2 AND job_id=$3`,
9233
+ [this.dvmId, this.config.scope, jobId]
9234
+ );
9235
+ return rows[0]?.drain_deadline_ms === null || !rows[0] ? null : Number(rows[0].drain_deadline_ms);
9236
+ }
9237
+ async transition(command) {
9238
+ if (command.action !== "drain" && command.action !== "resume" || !Number.isSafeInteger(command.generation) || command.generation < 1 || command.action === "drain" && !Number.isSafeInteger(command.deadlineMs) || command.action === "resume" && command.deadlineMs !== void 0) return "conflict";
9239
+ const client = await this.pool.connect();
9240
+ try {
9241
+ await client.query("BEGIN");
9242
+ const { rows } = await client.query(
9243
+ `SELECT state, generation, deadline_ms FROM sdk_hosting_control
9244
+ WHERE dvm_id=$1 AND scope=$2 FOR UPDATE`,
9245
+ [this.dvmId, this.config.scope]
9246
+ );
9247
+ const prior = rows[0];
9248
+ const generation = Number(prior.generation);
9249
+ const nextState = command.action === "drain" ? "draining" : "accepting";
9250
+ if (command.generation === generation && prior.state === nextState && Number(prior.deadline_ms ?? 0) === (command.deadlineMs ?? 0)) {
9251
+ await client.query("COMMIT");
9252
+ return "ok";
9253
+ }
9254
+ if (command.generation <= generation) {
9255
+ await client.query("ROLLBACK");
9256
+ return "conflict";
9257
+ }
9258
+ if (command.action === "drain" && (command.deadlineMs <= Date.now() || command.deadlineMs > Date.now() + 864e5)) {
9259
+ await client.query("ROLLBACK");
9260
+ return "conflict";
9261
+ }
9262
+ await client.query(
9263
+ `UPDATE sdk_hosting_control SET state=$3, generation=$4, deadline_ms=$5
9264
+ WHERE dvm_id=$1 AND scope=$2`,
9265
+ [this.dvmId, this.config.scope, nextState, command.generation, command.deadlineMs ?? null]
9266
+ );
9267
+ if (command.action === "drain") {
9268
+ await client.query(
9269
+ `UPDATE sdk_hosting_admissions SET drain_deadline_ms=$3
9270
+ WHERE dvm_id=$1 AND scope=$2 AND outcome='pending'`,
9271
+ [this.dvmId, this.config.scope, command.deadlineMs]
9272
+ );
9273
+ }
9274
+ await client.query("COMMIT");
9275
+ return "ok";
9276
+ } catch (error) {
9277
+ await client.query("ROLLBACK").catch(() => void 0);
9278
+ throw error;
9279
+ } finally {
9280
+ client.release();
9281
+ }
9282
+ }
9283
+ async status() {
9284
+ await this.pool.query(
9285
+ `UPDATE sdk_hosting_admissions a SET outcome='accepted'
9286
+ WHERE a.dvm_id=$1 AND a.scope=$2 AND a.outcome='pending'
9287
+ AND EXISTS (SELECT 1 FROM jobs j WHERE j.id=a.job_id)`,
9288
+ [this.dvmId, this.config.scope]
9289
+ );
9290
+ const creditTable = await this.pool.query(
9291
+ "SELECT to_regclass('credit_fundings') IS NOT NULL AS present"
9292
+ );
9293
+ await this.pool.query(
9294
+ `UPDATE sdk_hosting_admissions a SET outcome='accepted'
9295
+ WHERE a.dvm_id=$1 AND a.scope=$2 AND a.outcome='unresolved'
9296
+ AND (EXISTS (SELECT 1 FROM jobs j WHERE j.id=a.job_id)
9297
+ ${creditTable.rows[0].present ? `OR (a.credit_id IS NOT NULL AND EXISTS (
9298
+ SELECT 1 FROM credit_fundings f WHERE f.credit_id=a.credit_id AND f.fund_id=a.fund_id
9299
+ ))` : ""})`,
9300
+ [this.dvmId, this.config.scope]
9301
+ );
9302
+ const { rows } = await this.pool.query(
9303
+ `SELECT state, generation, deadline_ms FROM sdk_hosting_control WHERE dvm_id=$1 AND scope=$2`,
9304
+ [this.dvmId, this.config.scope]
9305
+ );
9306
+ const active = await this.pool.query(
9307
+ `SELECT j.id FROM jobs j WHERE j.dvm_id=$1
9308
+ AND j.status IN ('processing', 'working', 'awaiting-input')
9309
+ AND EXISTS (SELECT 1 FROM sdk_hosting_admissions a
9310
+ WHERE a.dvm_id=j.dvm_id AND a.job_id=j.id AND a.scope=$2)
9311
+ ORDER BY j.id`,
9312
+ [this.dvmId, this.config.scope]
9313
+ );
9314
+ const unowned = await this.pool.query(
9315
+ `SELECT j.id FROM jobs j WHERE j.dvm_id=$1
9316
+ AND j.status IN ('processing', 'working', 'awaiting-input')
9317
+ AND NOT EXISTS (SELECT 1 FROM sdk_hosting_admissions a
9318
+ WHERE a.dvm_id=j.dvm_id AND a.job_id=j.id)
9319
+ ORDER BY j.id`,
9320
+ [this.dvmId]
9321
+ );
9322
+ const pending = await this.pool.query(
9323
+ `SELECT COUNT(*) FILTER (WHERE outcome='pending')::text AS pending,
9324
+ COUNT(*) FILTER (WHERE outcome='unresolved')::text AS unresolved
9325
+ FROM sdk_hosting_admissions WHERE dvm_id=$1 AND scope=$2`,
9326
+ [this.dvmId, this.config.scope]
9327
+ );
9328
+ const unresolved = await this.pool.query(
9329
+ `SELECT id, job_id FROM sdk_hosting_admissions
9330
+ WHERE dvm_id=$1 AND scope=$2 AND outcome='unresolved' ORDER BY admitted_at_ms, id`,
9331
+ [this.dvmId, this.config.scope]
9332
+ );
9333
+ const inFlight = await this.pool.query(
9334
+ `SELECT id, job_id FROM sdk_hosting_admissions
9335
+ WHERE dvm_id=$1 AND scope=$2 AND outcome='pending' ORDER BY admitted_at_ms, id`,
9336
+ [this.dvmId, this.config.scope]
9337
+ );
9338
+ return {
9339
+ state: rows[0].state,
9340
+ generation: Number(rows[0].generation),
9341
+ deadlineMs: rows[0].deadline_ms === null ? null : Number(rows[0].deadline_ms),
9342
+ activeJobIds: active.rows.map((row) => row.id),
9343
+ unownedActiveJobIds: unowned.rows.map((row) => row.id),
9344
+ pendingAdmissionCount: Number(pending.rows[0].pending),
9345
+ pendingAdmissions: inFlight.rows.map((row) => ({ admissionId: row.id, workId: row.job_id })),
9346
+ unresolvedAdmissionCount: Number(pending.rows[0].unresolved),
9347
+ unresolvedAdmissions: unresolved.rows.map((row) => ({ admissionId: row.id, jobId: row.job_id }))
9348
+ };
9349
+ }
9350
+ };
9351
+
9074
9352
  // src/sdk/server/job-credential.ts
9075
9353
  function jobCredentialGate(opts) {
9076
9354
  const alternative = opts.acceptsBearer ? ", or the Authorization: Bearer token the job was submitted with" : "";
@@ -16730,6 +17008,7 @@ async function createDVMServer(descriptor, opts) {
16730
17008
  let x402ChannelStorage;
16731
17009
  let x402BatchSettlement;
16732
17010
  let server;
17011
+ let hostingControl;
16733
17012
  let shutdownPromise;
16734
17013
  const shutdown = () => shutdownPromise ??= (async () => {
16735
17014
  const errors = [];
@@ -16923,8 +17202,19 @@ async function createDVMServer(descriptor, opts) {
16923
17202
  });
16924
17203
  }
16925
17204
  }) : void 0;
17205
+ if (opts.hostingControl) {
17206
+ if (!opts.db || !opts.dvmId || !(opts.jobStore instanceof PostgresJobStore)) {
17207
+ throw new Error("Hosting control requires a Postgres pool, dvmId, and PostgresJobStore");
17208
+ }
17209
+ if (descriptor.credit && !opts.creditLedger?.durable) {
17210
+ throw new Error("Hosting control with credit requires a durable credit ledger");
17211
+ }
17212
+ hostingControl = new HostingControl(opts.db, opts.dvmId, opts.hostingControl);
17213
+ await hostingControl.init();
17214
+ }
16926
17215
  server = new DVMServer(descriptor, {
16927
17216
  ...opts,
17217
+ hostingControlInstance: hostingControl,
16928
17218
  creditLedger,
16929
17219
  processedPayments,
16930
17220
  onJobCompleted: effectiveOnJobCompleted,
@@ -17001,6 +17291,7 @@ var DVMServer = class {
17001
17291
  x402ExactSettlement;
17002
17292
  /** Withdraws this mount's Tempo session capability from the poll. */
17003
17293
  undeclareSessionCapability;
17294
+ hostingSweepTimer;
17004
17295
  constructor(descriptor, opts) {
17005
17296
  this.descriptor = descriptor;
17006
17297
  this.pricingCurrency = descriptor.currency ?? "usd";
@@ -17084,6 +17375,17 @@ var DVMServer = class {
17084
17375
  this.app = new Hono();
17085
17376
  try {
17086
17377
  this.registerRoutes();
17378
+ if (opts.hostingControlInstance) {
17379
+ this.hostingSweepTimer = setInterval(() => {
17380
+ void this.sweepHostingDeadline().catch((error) => {
17381
+ console.error("Hosting deadline sweep failed:", error);
17382
+ });
17383
+ }, 1e3);
17384
+ this.hostingSweepTimer.unref();
17385
+ void this.sweepHostingDeadline().catch((error) => {
17386
+ console.error("Hosting deadline sweep failed:", error);
17387
+ });
17388
+ }
17087
17389
  if (this.ownsMintHealthTracker) this.mintHealthTracker?.start();
17088
17390
  if (this.opts.tempoObserverHealth?.declareSessionCapability) {
17089
17391
  this.undeclareSessionCapability = this.opts.tempoObserverHealth.declareSessionCapability(
@@ -17117,6 +17419,7 @@ var DVMServer = class {
17117
17419
  * and host teardown layers can finish later cleanup before surfacing it.
17118
17420
  */
17119
17421
  shutdown() {
17422
+ if (this.hostingSweepTimer) clearInterval(this.hostingSweepTimer);
17120
17423
  this.jobManager.shutdown();
17121
17424
  const settlement = this.x402BatchSettlementInit ?? Promise.resolve(this.x402BatchSettlement);
17122
17425
  const stopped = settlement.then((s) => s?.stop());
@@ -17125,6 +17428,42 @@ var DVMServer = class {
17125
17428
  this.undeclareSessionCapability = void 0;
17126
17429
  return stopped;
17127
17430
  }
17431
+ async sweepHostingDeadline() {
17432
+ const control = this.opts.hostingControlInstance;
17433
+ if (!control) return;
17434
+ const status = await control.status();
17435
+ if (status.state !== "draining" || status.deadlineMs === null || Date.now() < status.deadlineMs)
17436
+ return;
17437
+ const store = this.jobManager.store;
17438
+ if (!(store instanceof PostgresJobStore)) return;
17439
+ for (const jobId of status.activeJobIds) {
17440
+ if (await store.cancelJobForHosting(jobId, "Hosting drain deadline reached")) {
17441
+ await this.jobManager.finalizeStoredTerminal(jobId);
17442
+ }
17443
+ }
17444
+ }
17445
+ async handleHostingCommand(c, statusOnly) {
17446
+ const control = this.opts.hostingControlInstance;
17447
+ if (!control) return c.json({ error: "not_found" }, 404);
17448
+ let command;
17449
+ try {
17450
+ command = await c.req.json();
17451
+ } catch {
17452
+ return c.json({ error: "invalid_hosting_control" }, 400);
17453
+ }
17454
+ if (!command || (statusOnly ? command.action !== "status" : command.action !== "drain" && command.action !== "resume")) {
17455
+ return c.json({ error: "invalid_hosting_control" }, 400);
17456
+ }
17457
+ if (!await control.authorize(command, c.req.header("X-DVM-Hosting-Signature"))) {
17458
+ return c.json({ error: "hosting_control_unauthorized" }, 401);
17459
+ }
17460
+ if (!statusOnly && command.action === "resume") await this.sweepHostingDeadline();
17461
+ if (!statusOnly && await control.transition(command) === "conflict") {
17462
+ return c.json({ error: "hosting_generation_conflict" }, 409);
17463
+ }
17464
+ if (!statusOnly) await this.sweepHostingDeadline();
17465
+ return c.json(await control.status());
17466
+ }
17128
17467
  registerRoutes() {
17129
17468
  const dvmSlug = this.descriptor.name;
17130
17469
  const builderPubkey = this.opts.builder?.pubkey;
@@ -17232,6 +17571,10 @@ var DVMServer = class {
17232
17571
  this.app.get("/v1/info", (c) => this.handleInfo(c));
17233
17572
  this.app.post("/v1/quote", (c) => this.handleQuote(c));
17234
17573
  this.app.post("/v1/job", (c) => this.handleCreateJob(c));
17574
+ if (this.opts.hostingControlInstance) {
17575
+ this.app.post("/_hosting/control", (c) => this.handleHostingCommand(c, false));
17576
+ this.app.post("/_hosting/status", (c) => this.handleHostingCommand(c, true));
17577
+ }
17235
17578
  this.app.post("/v1/request-resume/:requestId", (c) => this.handleCreateJob(c, true));
17236
17579
  this.app.post("/v1/request-result/:requestId", (c) => this.handleRequestResult(c));
17237
17580
  this.app.get("/v1/job/:id", (c) => this.handleGetJob(c));
@@ -17850,6 +18193,32 @@ var DVMServer = class {
17850
18193
  receiptIssuer: this.receiptIssuer,
17851
18194
  lightning: this.opts.lightningReceive ? { receive: this.opts.lightningReceive } : void 0,
17852
18195
  settleLightning: (callerPubkey, creditId) => this.settleLightningFunding(callerPubkey, creditId),
18196
+ withFundingAdmission: this.opts.hostingControlInstance ? async (ctx, body, run) => {
18197
+ const control = this.opts.hostingControlInstance;
18198
+ const admissionId = randomUUID4();
18199
+ const fundId = body.fund?.fund_id ?? admissionId;
18200
+ const creditId = body.credit_id ?? derivedFundCreditId(body.pubkey, fundId);
18201
+ const workId = `credit-fund:${creditId}:${fundId}`;
18202
+ if (!await control.admit(admissionId, workId, { creditId, fundId })) {
18203
+ return ctx.json({
18204
+ error: "hosting_draining",
18205
+ message: "This service is draining and is not accepting new credit funding.",
18206
+ hint: "Balance, reclaim, and already-issued funding recovery remain available."
18207
+ }, 503);
18208
+ }
18209
+ const authorization = ctx.req.header("Authorization");
18210
+ const paymentArtifact = Boolean(
18211
+ ctx.req.header("X-Cashu") || ctx.req.header("PAYMENT-SIGNATURE") || ctx.req.header("X-PAYMENT") || authorization && /^Payment\s/i.test(authorization)
18212
+ );
18213
+ let outcome = paymentArtifact ? "unresolved" : "failed";
18214
+ try {
18215
+ const response = await run();
18216
+ if (response.ok || body.fund?.method === "lightning" && await this.creditLedger.getInvoice({ creditId, fundId })) outcome = "accepted";
18217
+ return response;
18218
+ } finally {
18219
+ await control.finishAdmission(admissionId, outcome);
18220
+ }
18221
+ } : void 0,
17853
18222
  tempoSettlementReadiness: this.opts.tempoSettlementReadiness
17854
18223
  });
17855
18224
  }
@@ -18514,8 +18883,40 @@ var DVMServer = class {
18514
18883
  { capability: capabilityName, input: body.input, params: body.params, data: body.data },
18515
18884
  { stripAuthEnvelope: false }
18516
18885
  ) : void 0;
18886
+ const authorization = c.req.header("Authorization");
18887
+ const attachedPayment = c.req.header("X-Cashu") ?? c.req.header("PAYMENT-SIGNATURE") ?? c.req.header("X-PAYMENT") ?? (authorization && /^Payment\s/i.test(authorization) ? authorization : void 0);
18888
+ const hostingPaymentArtifactHash = attachedPayment ? createHash7("sha256").update(attachedPayment).digest("hex") : void 0;
18889
+ if (!resumeOnly && this.opts.hostingControlInstance && hostingPaymentArtifactHash) {
18890
+ const priorId = await this.opts.hostingControlInstance.acceptedJobByPaymentArtifactHash(
18891
+ hostingPaymentArtifactHash
18892
+ );
18893
+ const prior = priorId ? await this.jobManager.store.get(priorId) : void 0;
18894
+ if (prior && prior.requestFingerprint === fingerprint && prior.requesterId === c.get("requesterId") && (!this.descriptor.auth || prior.requesterPubkey === authPubkey)) {
18895
+ await this.jobManager.ensureReceipt(prior);
18896
+ return buildSubmitResponse(c, prior, prior.requesterToken);
18897
+ }
18898
+ }
18899
+ if (!resumeOnly && this.opts.hostingControlInstance && creditEnvelope && authPubkey) {
18900
+ const draw = await this.creditLedger.getDraw({
18901
+ creditId: creditEnvelope.creditId,
18902
+ drawId: creditEnvelope.drawId
18903
+ });
18904
+ if (draw?.jobId && await this.jobManager.store.get(draw.jobId)) {
18905
+ const recovered = await this.recoverDrawnJob(c, {
18906
+ creditId: creditEnvelope.creditId,
18907
+ drawId: creditEnvelope.drawId,
18908
+ jobId: draw.jobId,
18909
+ fingerprint,
18910
+ callerPubkey: authPubkey
18911
+ });
18912
+ if (recovered) return recovered;
18913
+ }
18914
+ }
18517
18915
  let publicRequestClaim;
18518
18916
  let publicRequestClaimResult;
18917
+ let hostingAdmissionId;
18918
+ let hostingAdmissionAccepted = false;
18919
+ let hostingPaymentMayHaveLanded = false;
18519
18920
  if (publicRequestId && authPubkey && recoveryStore) {
18520
18921
  publicRequestClaim = {
18521
18922
  requestId: publicRequestId,
@@ -18538,6 +18939,18 @@ var DVMServer = class {
18538
18939
  }
18539
18940
  }
18540
18941
  let jobId = publicRequestClaimResult?.jobId ?? this.jobManager.allocateJobId();
18942
+ if (!resumeOnly && this.opts.hostingControlInstance) {
18943
+ hostingAdmissionId = randomUUID4();
18944
+ if (!await this.opts.hostingControlInstance.admit(hostingAdmissionId, jobId, {
18945
+ paymentArtifactHash: hostingPaymentArtifactHash
18946
+ })) {
18947
+ return c.json({
18948
+ error: "hosting_draining",
18949
+ message: "This service is draining and is not accepting new jobs or payments.",
18950
+ hint: "Keep any existing job token to fetch its result or reclaim unused credit. Retry a new job after service resumes."
18951
+ }, 503);
18952
+ }
18953
+ }
18541
18954
  let payment = { paidMsats: 0, receivedProofs: [] };
18542
18955
  let resumedAcceptedPayment = false;
18543
18956
  if (resumeOnly) {
@@ -18594,6 +19007,9 @@ var DVMServer = class {
18594
19007
  const skipPayment = devModeSkipsPaymentVerification(this.opts);
18595
19008
  if (!skipPayment && !resumedAcceptedPayment) {
18596
19009
  const railOpts = await this.railVerifyOpts(c);
19010
+ hostingPaymentMayHaveLanded = Boolean(
19011
+ creditEnvelope || railOpts.cashuToken || railOpts.x402Payment || railOpts.mppCredential
19012
+ );
18597
19013
  try {
18598
19014
  payment = await loggers.isolate.span(
18599
19015
  "dvm.payment.validate",
@@ -18707,8 +19123,12 @@ var DVMServer = class {
18707
19123
  );
18708
19124
  }
18709
19125
  const exactAdmission = payment.x402ExactAdmission;
19126
+ hostingPaymentMayHaveLanded ||= resumedAcceptedPayment || payment.paidMsats > 0 || Boolean(payment.funding || exactAdmission);
18710
19127
  if (exactAdmission) {
18711
19128
  jobId = exactAdmission.jobId;
19129
+ if (hostingAdmissionId) {
19130
+ await this.opts.hostingControlInstance.rebindAdmissionJob(hostingAdmissionId, jobId);
19131
+ }
18712
19132
  const prior = await this.jobManager.store.get(jobId);
18713
19133
  if (prior) {
18714
19134
  if (prior.requestFingerprint !== fingerprint || prior.requesterPubkey !== authPubkey || prior.requesterId !== c.get("requesterId")) {
@@ -18769,6 +19189,21 @@ var DVMServer = class {
18769
19189
  }
18770
19190
  );
18771
19191
  await jm.persistJob(job);
19192
+ if (hostingAdmissionId) {
19193
+ await this.opts.hostingControlInstance.finishAdmission(hostingAdmissionId, "accepted", jobId);
19194
+ hostingAdmissionAccepted = true;
19195
+ }
19196
+ const reservedDrainDeadline = this.opts.hostingControlInstance ? await this.opts.hostingControlInstance.jobDeadline(jobId) : null;
19197
+ const hostingStatus = await this.opts.hostingControlInstance?.status();
19198
+ const currentDrainDeadline = hostingStatus?.state === "draining" ? hostingStatus.deadlineMs : null;
19199
+ if (reservedDrainDeadline !== null && Date.now() >= reservedDrainDeadline || currentDrainDeadline !== null && currentDrainDeadline !== void 0 && Date.now() >= currentDrainDeadline) {
19200
+ const store = jm.store;
19201
+ if (store instanceof PostgresJobStore && await store.cancelJobForHosting(jobId, "Hosting drain deadline reached")) {
19202
+ await jm.finalizeStoredTerminal(jobId);
19203
+ }
19204
+ const terminal = await store.get(jobId);
19205
+ if (terminal) return buildSubmitResponse(c, terminal, jobToken);
19206
+ }
18772
19207
  if (exactAdmission) {
18773
19208
  await this.x402ExactSettlement?.completeAdmission(
18774
19209
  this.opts.dvmId ?? "",
@@ -18790,6 +19225,12 @@ var DVMServer = class {
18790
19225
  }
18791
19226
  return response;
18792
19227
  } finally {
19228
+ if (hostingAdmissionId && !hostingAdmissionAccepted) {
19229
+ await this.opts.hostingControlInstance.finishAdmission(
19230
+ hostingAdmissionId,
19231
+ hostingPaymentMayHaveLanded ? "unresolved" : "failed"
19232
+ );
19233
+ }
18793
19234
  if (publicRequestClaim && recoveryStore) {
18794
19235
  await recoveryStore.releaseRequestIdClaim(publicRequestClaim);
18795
19236
  }
@@ -19921,6 +20362,7 @@ export {
19921
20362
  createAdminCashuNonceStore,
19922
20363
  X402ExactSettlementServer,
19923
20364
  X402ExactSettlementEvidenceMissingError,
20365
+ signHostingControl,
19924
20366
  jobCredentialGate,
19925
20367
  REQUEST_ID_FIELD,
19926
20368
  protocolEnvelopeIgnoreFields,
@@ -181,6 +181,41 @@ interface CreditExpirySweepOpts {
181
181
  limit?: number;
182
182
  }
183
183
 
184
+ interface HostingControlConfig {
185
+ /** Dedicated management key. Use at least 32 random bytes; never reuse a caller or builder key. */
186
+ secret: string;
187
+ /** Deployment scope shared by the platform and this DVM, independent of its caller origin. */
188
+ scope: string;
189
+ }
190
+ interface HostingControlCommand {
191
+ action: "drain" | "resume" | "status";
192
+ scope: string;
193
+ dvmId: string;
194
+ timestampMs: number;
195
+ nonce: string;
196
+ generation?: number;
197
+ deadlineMs?: number;
198
+ }
199
+ /** The ordered JSON array is the version-one wire signing contract. */
200
+ declare function signHostingControl(secret: string, command: HostingControlCommand): string;
201
+ interface HostingControlStatus {
202
+ state: "accepting" | "draining";
203
+ generation: number;
204
+ deadlineMs: number | null;
205
+ activeJobIds: string[];
206
+ unownedActiveJobIds: string[];
207
+ pendingAdmissionCount: number;
208
+ pendingAdmissions: {
209
+ admissionId: string;
210
+ workId: string;
211
+ }[];
212
+ unresolvedAdmissionCount: number;
213
+ unresolvedAdmissions: {
214
+ admissionId: string;
215
+ jobId: string;
216
+ }[];
217
+ }
218
+
184
219
  interface PromptResolver {
185
220
  resolve: (value: ResponseContent) => void;
186
221
  reject: (reason: Error) => void;
@@ -3676,6 +3711,8 @@ interface DVMServeResult {
3676
3711
  paymentModes: PaymentMode[];
3677
3712
  }
3678
3713
  interface DVMHostOpts {
3714
+ /** Dedicated hosting-management authority for each mounted DVM. Requires durable Postgres. */
3715
+ hostingControl?: HostingControlConfig;
3679
3716
  /**
3680
3717
  * Postgres connection string. Defaults to `DATABASE_URL` env.
3681
3718
  * Required at runtime. Boot fails when missing unless `jobStore`
@@ -4054,6 +4091,8 @@ interface SDKServerOpts {
4054
4091
  canonicalDvmId?: string;
4055
4092
  /** Postgres pool for builder admin state and wallet accumulator persistence. */
4056
4093
  db?: Pool;
4094
+ /** Enables authenticated, durable hosting admission and bounded drain. Requires Postgres and dvmId. */
4095
+ hostingControl?: HostingControlConfig;
4057
4096
  /**
4058
4097
  * Host strict-mode flag. Reporter readiness follows hosted context regardless
4059
4098
  * of this value.
@@ -4370,4 +4409,4 @@ declare function attachCreditMenu(body: Record<string, unknown>, menu: CreditMen
4370
4409
  */
4371
4410
  declare function toCreditTerms(menu: CreditMenu): CreditTerms;
4372
4411
 
4373
- 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, type X402ExactSettlementAttempt 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, JobManager as aA, type JobManagerOpts as aB, type LightningReceiveConfig as aC, MIN_INVOICE_TTL_SECONDS as aD, MemoryConsumedCredentialStore as aE, type MemoryConsumedCredentialStoreOpts as aF, MemoryProcessedPaymentStore as aG, MemoryX402ExactSettlementStore as aH, type MountOpts as aI, type OwnerDisplay as aJ, type PaidJobCompletion as aK, type PaymentMode as aL, type PlatformReporterOpts as aM, PostgresProcessedPaymentStore as aN, PostgresX402ExactSettlementStore as aO, type PriceFiat as aP, type ProcessedPaymentQuerier as aQ, type ProcessedPaymentRail as aR, type ProcessedPaymentRecord as aS, ProcessedPaymentReplayError as aT, type ProcessedPaymentStore as aU, type X402BatchAcceptance as aV, type X402BatchFunding as aW, type X402BatchRefusal as aX, type X402BatchSettlementServer as aY, type X402ExactAcceptance as aZ, X402ExactIntentConflictError 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 DVMServeResult as ap, type BuildCreditMenuArgs as aq, type BuilderIdentity as ar, CREDIT_ENVELOPE_KEYS as as, type CreateX402BatchSettlementServerOpts as at, type CreditEnvelope as au, CreditEnvelopeError as av, type CreditFundCommitment as aw, type CreditTerms as ax, DEFAULT_INVOICE_TTL_SECONDS as ay, type DVMHost as az, type AppEnv as b, type X402ExactSettlementChainEvidence as b0, type X402ExactSettlementEffect as b1, X402ExactSettlementEvidenceMissingError as b2, type X402ExactSettlementEvidenceReader as b3, type X402ExactSettlementIntent as b4, X402ExactSettlementNotReadyError as b5, X402ExactSettlementServer as b6, type X402ExactSettlementServerOpts as b7, type X402ExactSettlementStatus as b8, type X402ExactSettlementStore as b9, type X402SettlementChainEvidence as ba, type X402SettlementEvidenceReader as bb, X402SettlementSubmissionError as bc, X402_BATCH_AUTO_SETTLEMENT as bd, X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS as be, X402_BATCH_SETTLEMENT_NETWORK as bf, attachCreditMenu as bg, buildCreditMenu as bh, createDVMHost as bi, createX402BatchSettlementServer as bj, creditEnvelopeIgnoreFields as bk, drawSettlementRef as bl, extractCreditEnvelope as bm, fundingCommitment as bn, selectPrimaryCredit as bo, stripCreditEnvelope as bp, toCreditView as bq, 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 };
4412
+ 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, type X402BatchSettlementServer 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 HostingControlCommand as aA, type HostingControlConfig as aB, type HostingControlStatus as aC, JobManager as aD, type JobManagerOpts as aE, type LightningReceiveConfig as aF, MIN_INVOICE_TTL_SECONDS as aG, MemoryConsumedCredentialStore as aH, type MemoryConsumedCredentialStoreOpts as aI, MemoryProcessedPaymentStore as aJ, MemoryX402ExactSettlementStore as aK, type MountOpts as aL, type OwnerDisplay as aM, type PaidJobCompletion as aN, type PaymentMode as aO, type PlatformReporterOpts as aP, PostgresProcessedPaymentStore as aQ, PostgresX402ExactSettlementStore as aR, type PriceFiat as aS, type ProcessedPaymentQuerier as aT, type ProcessedPaymentRail as aU, type ProcessedPaymentRecord as aV, ProcessedPaymentReplayError as aW, type ProcessedPaymentStore as aX, type X402BatchAcceptance as aY, type X402BatchFunding as aZ, type X402BatchRefusal 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 DVMServeResult as ap, type BuildCreditMenuArgs as aq, type BuilderIdentity as ar, CREDIT_ENVELOPE_KEYS as as, type CreateX402BatchSettlementServerOpts as at, type CreditEnvelope as au, CreditEnvelopeError as av, type CreditFundCommitment as aw, type CreditTerms as ax, DEFAULT_INVOICE_TTL_SECONDS as ay, type DVMHost as az, type AppEnv as b, type X402ExactAcceptance as b0, X402ExactIntentConflictError as b1, type X402ExactSettlementAttempt as b2, type X402ExactSettlementChainEvidence as b3, type X402ExactSettlementEffect as b4, X402ExactSettlementEvidenceMissingError as b5, type X402ExactSettlementEvidenceReader as b6, type X402ExactSettlementIntent as b7, X402ExactSettlementNotReadyError as b8, X402ExactSettlementServer as b9, type X402ExactSettlementServerOpts as ba, type X402ExactSettlementStatus as bb, type X402ExactSettlementStore as bc, type X402SettlementChainEvidence as bd, type X402SettlementEvidenceReader as be, X402SettlementSubmissionError as bf, X402_BATCH_AUTO_SETTLEMENT as bg, X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS as bh, X402_BATCH_SETTLEMENT_NETWORK as bi, attachCreditMenu as bj, buildCreditMenu as bk, createDVMHost as bl, createX402BatchSettlementServer as bm, creditEnvelopeIgnoreFields as bn, drawSettlementRef as bo, extractCreditEnvelope as bp, fundingCommitment as bq, selectPrimaryCredit as br, signHostingControl as bs, stripCreditEnvelope as bt, toCreditView as bu, 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 };
@@ -6,8 +6,8 @@ import { Env, BlankSchema } from 'hono/types';
6
6
  import { Pool } from 'pg';
7
7
  export { Pool } from 'pg';
8
8
  import { g as getWallet } from '../wallet-Dwjs5n_M.js';
9
- import { A as AccumulatorPool, a as AccumulatorQuerier, C as ConsumedCredentialStore } from '../credit-menu-kDA9RVzD.js';
10
- export { b as AppEnv, c as CreditFundingReport, F as FiatDenomination, d as FiatDenominationFailure, e as FundOnlyRequest, I as IncomingPaymentOpts, J as JobCancelledError, M as MintHealthTracker, P as PAYMENT_PROOF_KEYS, f as PaymentErrorCode, g as PaymentErrorDetail, h as PaymentInfo, R as ReporterBannerOpts, i as ResolvedFx, j as RevenueBootCheckOpts, S as SDKServerOpts, k as ServerJob, l as ShortPayForfeit, T as TempoChannelReport, m as TempoObserverHealth, n as TempoSessionChannelMismatchError, U as UpfrontPaymentOpts, V as VerifiedIncomingPayment, o as VerifyIncomingSnapshot, X as X402BatchChannelObservation, p as X402FacilitatorHealth, q as X402SettlementEvidenceOutcome, r as X402SettlementRepair, s as X402SettlementRepairRefusal, t as X402SettlementRepaired, u as X402WedgedSettlement, v as X402WedgedSettlementPage, w as X402_BATCH_SETTLEMENT_MAINNET_NETWORK, x as abortJob, y as applyPaymentInfoToJob, z as assertRevenueReporterReady, B as buildPaymentErrorResponse, D as clearAccumulatorForDvm, E as createDVMServer, G as creditDepositPayload, H as derivedFundCreditId, K as devModeSkipsPaymentVerification, L as fromJobRecord, N as fundedMicroFor, O as hasPaymentProof, Q as hashLockKey, W as implicitCreditId, Y as initWalletAccumulatorTable, Z as insertAccumulatorRows, _ as isDerivedCreditId, $ as isTerminal, a0 as isYieldMessage, a1 as issueUpfrontChallenges, a2 as msatsToFiatMicro, a3 as paymentErrorBody, a4 as pinAskFiat, a5 as priceFiatMicro, a6 as processIncomingPayment, a7 as providerMessage, a8 as repairX402ExactSettlementEffect, a9 as resolveFxSnapshot, aa as resolvePriceFiat, ab as revenueReporterBannerState, ac as toCreditTerms, ad as toJobRecord, ae as unknownRouteNotFound, af as verifyIncomingPayment, ag as verifyTempoSessionManagementCredential, ah as verifyUpfrontPayment, ai as x402RequiredUsdcMicro, aj as x402SettledShare } from '../credit-menu-kDA9RVzD.js';
9
+ import { A as AccumulatorPool, a as AccumulatorQuerier, C as ConsumedCredentialStore } from '../credit-menu-BlCL7gZx.js';
10
+ export { b as AppEnv, c as CreditFundingReport, F as FiatDenomination, d as FiatDenominationFailure, e as FundOnlyRequest, I as IncomingPaymentOpts, J as JobCancelledError, M as MintHealthTracker, P as PAYMENT_PROOF_KEYS, f as PaymentErrorCode, g as PaymentErrorDetail, h as PaymentInfo, R as ReporterBannerOpts, i as ResolvedFx, j as RevenueBootCheckOpts, S as SDKServerOpts, k as ServerJob, l as ShortPayForfeit, T as TempoChannelReport, m as TempoObserverHealth, n as TempoSessionChannelMismatchError, U as UpfrontPaymentOpts, V as VerifiedIncomingPayment, o as VerifyIncomingSnapshot, X as X402BatchChannelObservation, p as X402FacilitatorHealth, q as X402SettlementEvidenceOutcome, r as X402SettlementRepair, s as X402SettlementRepairRefusal, t as X402SettlementRepaired, u as X402WedgedSettlement, v as X402WedgedSettlementPage, w as X402_BATCH_SETTLEMENT_MAINNET_NETWORK, x as abortJob, y as applyPaymentInfoToJob, z as assertRevenueReporterReady, B as buildPaymentErrorResponse, D as clearAccumulatorForDvm, E as createDVMServer, G as creditDepositPayload, H as derivedFundCreditId, K as devModeSkipsPaymentVerification, L as fromJobRecord, N as fundedMicroFor, O as hasPaymentProof, Q as hashLockKey, W as implicitCreditId, Y as initWalletAccumulatorTable, Z as insertAccumulatorRows, _ as isDerivedCreditId, $ as isTerminal, a0 as isYieldMessage, a1 as issueUpfrontChallenges, a2 as msatsToFiatMicro, a3 as paymentErrorBody, a4 as pinAskFiat, a5 as priceFiatMicro, a6 as processIncomingPayment, a7 as providerMessage, a8 as repairX402ExactSettlementEffect, a9 as resolveFxSnapshot, aa as resolvePriceFiat, ab as revenueReporterBannerState, ac as toCreditTerms, ad as toJobRecord, ae as unknownRouteNotFound, af as verifyIncomingPayment, ag as verifyTempoSessionManagementCredential, ah as verifyUpfrontPayment, ai as x402RequiredUsdcMicro, aj as x402SettledShare } from '../credit-menu-BlCL7gZx.js';
11
11
  import { g as LockPubkey } from '../lightning-backend-BhsyrBLa.js';
12
12
  export { t as lockPubkeyStoreToLockPubkey } from '../lightning-backend-BhsyrBLa.js';
13
13
  export { O as OutgoingMessage, S as StaleJobReapable, i as isStaleJobReapable } from '../job-store-D3CcZp_g.js';
@@ -65,13 +65,14 @@ import {
65
65
  verifyUpfrontPayment,
66
66
  x402RequiredUsdcMicro,
67
67
  x402SettledShare
68
- } from "../chunk-JNKQZVZX.js";
68
+ } from "../chunk-LTAIBLS2.js";
69
69
  import "../chunk-XRLKGCAW.js";
70
70
  import {
71
71
  settleWithFacilitator,
72
72
  verifyWithFacilitator
73
73
  } from "../chunk-BQ2NMWKE.js";
74
74
  import "../chunk-U5VNEALI.js";
75
+ import "../chunk-HHTL3ZFT.js";
75
76
  import {
76
77
  SIGNED_ENVELOPE_FIELDS,
77
78
  SIGNED_ENVELOPE_TYPES,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  PostgresJobStore
3
- } from "./chunk-JAHRF5X3.js";
3
+ } from "./chunk-HHTL3ZFT.js";
4
4
  import "./chunk-COYKJRMM.js";
5
5
  export {
6
6
  PostgresJobStore
@@ -1,8 +1,8 @@
1
1
  import { ab as CashuMode, bK as CreditDrainEnqueue, R as ResolvedCreditConfig, bL as DVMAuthScheme, z as SignedRequestDomain, bM as CreditLedgerLike, bN as X402RefundSettlementGate, bO as CreditSnapshot, bP as DrawResult, bQ as X402SettlementStatus, bR as X402UnresolvedRefund, bS as GrownDrawResult, bT as DrawResolution, bU as FundingRecord, X as FundingReceipt, bV as CreditInvoiceRecord, bW as InvoiceSettlement, bX as BlockedInvoiceCursor, bY as InvoiceReconciliation, bZ as InvoiceWriteOff, b_ as DrawRecord, b$ as StalePendingDrawCursor, c0 as TempoCreditLossEvidence, c1 as CreditLedgerQuerier, c2 as TempoCreditLoss, c3 as X402CreditLossEvidence, c4 as X402CreditLoss, c5 as DrainMethod, c6 as DrainRequestResult, c7 as BitcoinDepositLiability, bo as FundingLot, c8 as CreditDrainRecord, c9 as ChannelDrainCursor, ca as DrainWriteOff, cb as DrainReleaseResult, cc as DrainFulfilment, cd as DrainTransitionResult, Y as JobReceipt, a1 as Message, K as KVStore, F as SignedRequestReplayStore, Z as ZodLike, a as DVMDescriptor } from '../step-cache-v2yhyhSZ.js';
2
2
  export { ce as CLIENT_COMPATIBILITY_HEADERS, d as CanonicalEnvelope, cf as ClientCompatibility, cg as ClientCompatibilityEnv, ch as ClientCompatibilityGate, ci as ClientCompatibilityRequirement, cj as ClientSemVer, e as CreateSignedRequestVerifierOpts, ck as CreditFundingBasis, cl as CreditInvoiceStatus, cm as CreditLedger, cn as CreditLedgerError, co as CreditLedgerErrorCode, cp as CreditLedgerErrorDetails, cq as CreditLedgerPool, cr as CreditStatus, cs as DRAIN_DELIVERY_RESERVE_SATS, ct as DVM_PROTOCOL_VERSION, cu as DrainConflictReason, cv as DrawRailValue, cw as DrawStatus, cx as JobCostReportPayload, cy as JobTerminalReportPayload, cz as PostgresX402ChannelStorage, cA as ReplayStoreBackend, cB as RevenueSkippedNoRailPayload, cC as RevenueSkippedNoRailReason, w as SIGNED_REQUEST_AUTH_ID, x as SIGNED_REQUEST_STATEMENT_VERSION, cD as Secp256k1AuthOpts, y as SignedRequestAudience, B as SignedRequestError, E as SignedRequestFailure, G as SignedRequestSignOpts, H as SignedRequestStatementHeader, M as SignedRequestVerifier, cE as X402ChannelStorageOpts, cF as X402RelayLockHolder, cG as X402RelaySubmissionLock, cH as X402RelaySubmissionLockError, cI as allocateDrawValue, cJ as clientCompatibilityAttributes, cK as clientCompatibilityMiddleware, cL as clientUpgradeRequired, N as createSignedRequestVerifier, cM as parseClientCapabilities, cN as parseClientCompatibility, cO as parseDvmClient, cP as parseProtocolVersion, cQ as requireClientCompatibility, cR as secp256k1Auth, cS as signedRequestInput, T as signedRequestStatementHeader } from '../step-cache-v2yhyhSZ.js';
3
3
  import { Pool } from 'pg';
4
- import { ak as CreditMenu, b as AppEnv, U as UpfrontPaymentOpts, h as PaymentInfo, al as ReceiptIssuer, am as LightningReceive, an as TempoSettlementReadiness, ao as DVMHostOpts, ap as DVMServeResult } from '../credit-menu-kDA9RVzD.js';
5
- export { aq as BuildCreditMenuArgs, ar as BuilderIdentity, as as CREDIT_ENVELOPE_KEYS, C as ConsumedCredentialStore, at as CreateX402BatchSettlementServerOpts, au as CreditEnvelope, av as CreditEnvelopeError, aw as CreditFundCommitment, ax as CreditTerms, ay as DEFAULT_INVOICE_TTL_SECONDS, az as DVMHost, aA as JobManager, aB as JobManagerOpts, aC as LightningReceiveConfig, aD as MIN_INVOICE_TTL_SECONDS, aE as MemoryConsumedCredentialStore, aF as MemoryConsumedCredentialStoreOpts, aG as MemoryProcessedPaymentStore, aH as MemoryX402ExactSettlementStore, aI as MountOpts, aJ as OwnerDisplay, aK as PaidJobCompletion, aL as PaymentMode, aM as PlatformReporterOpts, aN as PostgresProcessedPaymentStore, aO as PostgresX402ExactSettlementStore, aP as PriceFiat, aQ as ProcessedPaymentQuerier, aR as ProcessedPaymentRail, aS as ProcessedPaymentRecord, aT as ProcessedPaymentReplayError, aU as ProcessedPaymentStore, aV as X402BatchAcceptance, aW as X402BatchFunding, aX as X402BatchRefusal, aY as X402BatchSettlementServer, aZ as X402ExactAcceptance, a_ as X402ExactIntentConflictError, a$ as X402ExactSettlementAttempt, b0 as X402ExactSettlementChainEvidence, b1 as X402ExactSettlementEffect, b2 as X402ExactSettlementEvidenceMissingError, b3 as X402ExactSettlementEvidenceReader, b4 as X402ExactSettlementIntent, b5 as X402ExactSettlementNotReadyError, b6 as X402ExactSettlementServer, b7 as X402ExactSettlementServerOpts, b8 as X402ExactSettlementStatus, b9 as X402ExactSettlementStore, ba as X402SettlementChainEvidence, bb as X402SettlementEvidenceReader, bc as X402SettlementSubmissionError, bd as X402_BATCH_AUTO_SETTLEMENT, be as X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS, bf as X402_BATCH_SETTLEMENT_NETWORK, bg as attachCreditMenu, bh as buildCreditMenu, bi as createDVMHost, bj as createX402BatchSettlementServer, bk as creditEnvelopeIgnoreFields, bl as drawSettlementRef, bm as extractCreditEnvelope, bn as fundingCommitment, bo as selectPrimaryCredit, bp as stripCreditEnvelope, bq as toCreditView } from '../credit-menu-kDA9RVzD.js';
4
+ import { ak as CreditMenu, b as AppEnv, U as UpfrontPaymentOpts, h as PaymentInfo, al as ReceiptIssuer, am as LightningReceive, an as TempoSettlementReadiness, ao as DVMHostOpts, ap as DVMServeResult } from '../credit-menu-BlCL7gZx.js';
5
+ export { aq as BuildCreditMenuArgs, ar as BuilderIdentity, as as CREDIT_ENVELOPE_KEYS, C as ConsumedCredentialStore, at as CreateX402BatchSettlementServerOpts, au as CreditEnvelope, av as CreditEnvelopeError, aw as CreditFundCommitment, ax as CreditTerms, ay as DEFAULT_INVOICE_TTL_SECONDS, az as DVMHost, aA as HostingControlCommand, aB as HostingControlConfig, aC as HostingControlStatus, aD as JobManager, aE as JobManagerOpts, aF as LightningReceiveConfig, aG as MIN_INVOICE_TTL_SECONDS, aH as MemoryConsumedCredentialStore, aI as MemoryConsumedCredentialStoreOpts, aJ as MemoryProcessedPaymentStore, aK as MemoryX402ExactSettlementStore, aL as MountOpts, aM as OwnerDisplay, aN as PaidJobCompletion, aO as PaymentMode, aP as PlatformReporterOpts, aQ as PostgresProcessedPaymentStore, aR as PostgresX402ExactSettlementStore, aS as PriceFiat, aT as ProcessedPaymentQuerier, aU as ProcessedPaymentRail, aV as ProcessedPaymentRecord, aW as ProcessedPaymentReplayError, aX as ProcessedPaymentStore, aY as X402BatchAcceptance, aZ as X402BatchFunding, a_ as X402BatchRefusal, a$ as X402BatchSettlementServer, b0 as X402ExactAcceptance, b1 as X402ExactIntentConflictError, b2 as X402ExactSettlementAttempt, b3 as X402ExactSettlementChainEvidence, b4 as X402ExactSettlementEffect, b5 as X402ExactSettlementEvidenceMissingError, b6 as X402ExactSettlementEvidenceReader, b7 as X402ExactSettlementIntent, b8 as X402ExactSettlementNotReadyError, b9 as X402ExactSettlementServer, ba as X402ExactSettlementServerOpts, bb as X402ExactSettlementStatus, bc as X402ExactSettlementStore, bd as X402SettlementChainEvidence, be as X402SettlementEvidenceReader, bf as X402SettlementSubmissionError, bg as X402_BATCH_AUTO_SETTLEMENT, bh as X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS, bi as X402_BATCH_SETTLEMENT_NETWORK, bj as attachCreditMenu, bk as buildCreditMenu, bl as createDVMHost, bm as createX402BatchSettlementServer, bn as creditEnvelopeIgnoreFields, bo as drawSettlementRef, bp as extractCreditEnvelope, bq as fundingCommitment, br as selectPrimaryCredit, bs as signHostingControl, bt as stripCreditEnvelope, bu as toCreditView } from '../credit-menu-BlCL7gZx.js';
6
6
  import { Context } from 'hono';
7
7
  import { z } from 'zod';
8
8
  import { F as FxFetcher } from '../fx-DhyT1Cjh.js';
@@ -129,6 +129,8 @@ interface CreditRouteDeps {
129
129
  * invoice on the same credit is swept with it.
130
130
  */
131
131
  settleLightning: (callerPubkey: string, creditId?: string) => Promise<void>;
132
+ /** Hosting gate around a fresh funding operation, after caller auth and before any rail effect. */
133
+ withFundingAdmission?: (c: Context<AppEnv>, body: CreditRequest, run: () => Promise<Response>) => Promise<Response>;
132
134
  tempoSettlementReadiness?: TempoSettlementReadiness;
133
135
  }
134
136
  declare function handleCreditRequest(c: Context<AppEnv>, deps: CreditRouteDeps): Promise<Response>;
@@ -401,6 +403,7 @@ declare class PostgresJobStore implements StreamableJobStore, ReceiptIssuingStor
401
403
  heartbeatActiveJobs(jobIds: string[], now: number): Promise<void>;
402
404
  cancelStaleJob(jobId: string, expectedActivityBefore: number, reason: string, terminalStatus: "failed" | "cancelled"): Promise<boolean>;
403
405
  cancelJob(jobId: string, reason: string): Promise<boolean>;
406
+ cancelJobForHosting(jobId: string, reason: string): Promise<boolean>;
404
407
  getCounters(jobId: string): Promise<JobCounters | undefined>;
405
408
  claimForProcessing(jobId: string): Promise<boolean>;
406
409
  tryReactivationLock<T>(jobId: string, fn: () => Promise<T>): Promise<{
@@ -1,9 +1,6 @@
1
1
  import {
2
2
  PostgresReplayStore
3
3
  } from "../chunk-AJNFXR7J.js";
4
- import {
5
- PostgresJobStore
6
- } from "../chunk-JAHRF5X3.js";
7
4
  import {
8
5
  PostgresKVStore
9
6
  } from "../chunk-V5ZRATJH.js";
@@ -74,11 +71,12 @@ import {
74
71
  revenueReporterBannerState,
75
72
  seedLockPubkeyState,
76
73
  selectPrimaryCredit,
74
+ signHostingControl,
77
75
  stripCreditEnvelope,
78
76
  stripProtocolEnvelope,
79
77
  toCreditView,
80
78
  unknownRouteNotFound
81
- } from "../chunk-JNKQZVZX.js";
79
+ } from "../chunk-LTAIBLS2.js";
82
80
  import {
83
81
  MemoryProcessedPaymentStore,
84
82
  PostgresProcessedPaymentStore,
@@ -88,6 +86,9 @@ import "../chunk-BQ2NMWKE.js";
88
86
  import {
89
87
  MemoryCreditLedger
90
88
  } from "../chunk-U5VNEALI.js";
89
+ import {
90
+ PostgresJobStore
91
+ } from "../chunk-HHTL3ZFT.js";
91
92
  import {
92
93
  SignedRequestError,
93
94
  createDefaultReplayStore,
@@ -1098,6 +1099,12 @@ function createDVMHost(opts = {}) {
1098
1099
  )
1099
1100
  );
1100
1101
  const envMaybe = env;
1102
+ const hostingSecret = envMaybe.DVMKIT_HOSTING_CONTROL_SECRET;
1103
+ const hostingScope = envMaybe.DVMKIT_HOSTING_CONTROL_SCOPE;
1104
+ if (!opts.hostingControl && Boolean(hostingSecret) !== Boolean(hostingScope)) {
1105
+ throw new Error("Hosting control requires both DVMKIT_HOSTING_CONTROL_SECRET and DVMKIT_HOSTING_CONTROL_SCOPE");
1106
+ }
1107
+ const hostingControl = opts.hostingControl ?? (hostingSecret && hostingScope ? { secret: hostingSecret, scope: hostingScope } : void 0);
1101
1108
  const port = serveOpts?.port ?? opts.port ?? Number(envMaybe.PORT ?? "8080");
1102
1109
  const bootStart = Date.now();
1103
1110
  const phaseMarks = [];
@@ -1127,7 +1134,7 @@ function createDVMHost(opts = {}) {
1127
1134
  });
1128
1135
  applyRetryToPool(pool, "dvm-host");
1129
1136
  pgPool = pool;
1130
- const { PostgresJobStore: PostgresJobStore2 } = await import("../postgres-job-store-MZZZMS2B.js");
1137
+ const { PostgresJobStore: PostgresJobStore2 } = await import("../postgres-job-store-VE2JRUEC.js");
1131
1138
  const pgJobStore = new PostgresJobStore2(pool);
1132
1139
  pendingInits.push(pgJobStore.init(), pgJobStore.initStreaming());
1133
1140
  jobStore = pgJobStore;
@@ -1547,7 +1554,8 @@ ${config.facilitatorAuth?.keyId ?? ""}`;
1547
1554
  lockPubkey: cashuOpts.lockPubkey,
1548
1555
  lockPubkeyGraceSeconds: cashuOpts.lockPubkeyGraceSeconds,
1549
1556
  canonicalDvmId: cashuOpts.canonicalDvmId,
1550
- db: cashuOpts.db,
1557
+ db: hostingControl ? pgPool : cashuOpts.db,
1558
+ hostingControl,
1551
1559
  dvmId: cashuOpts.dvmId,
1552
1560
  failFast,
1553
1561
  mintHealthTracker: opts.mintHealthTracker,
@@ -2116,6 +2124,7 @@ export {
2116
2124
  secp256k1Auth,
2117
2125
  selectPrimaryCredit,
2118
2126
  serve,
2127
+ signHostingControl,
2119
2128
  signedRequestInput,
2120
2129
  signedRequestStatementHeader,
2121
2130
  stripCreditEnvelope,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dvmkit/sdk",
3
- "version": "0.2.1-rc.9",
3
+ "version": "0.3.0-rc.9",
4
4
  "description": "SDK for building accountless, pay-per-use Digital Vending Machines",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {