@dvmkit/sdk 0.0.0 → 0.1.0-rc.1

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 (41) hide show
  1. package/NOTICE +2 -0
  2. package/README.md +38 -2
  3. package/dist/chunk-27V2ILSR.js +291 -0
  4. package/dist/chunk-5GFED3GJ.js +955 -0
  5. package/dist/chunk-6JZIX5WW.js +1155 -0
  6. package/dist/chunk-7IH5SG2A.js +1038 -0
  7. package/dist/chunk-AT6V3SY7.js +102 -0
  8. package/dist/chunk-DCNT4PJS.js +733 -0
  9. package/dist/chunk-DMNLFNTW.js +135 -0
  10. package/dist/chunk-FROTD5XQ.js +70 -0
  11. package/dist/chunk-H25M54MI.js +149 -0
  12. package/dist/chunk-KQAJVVZT.js +712 -0
  13. package/dist/chunk-KXWROQGK.js +74 -0
  14. package/dist/chunk-L4OYF4DQ.js +67 -0
  15. package/dist/chunk-NTK5DJ6R.js +1256 -0
  16. package/dist/chunk-RPXHKMYE.js +3808 -0
  17. package/dist/chunk-S3XAHZQY.js +63 -0
  18. package/dist/chunk-YG7G4DPZ.js +25 -0
  19. package/dist/credit-ledger-EDMEZSA2.js +28 -0
  20. package/dist/index.d.ts +144 -0
  21. package/dist/index.js +303 -0
  22. package/dist/job-store-C5n6bhap.d.ts +5090 -0
  23. package/dist/memory-credit-ledger-7TTZDSRS.js +9 -0
  24. package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
  25. package/dist/mpp-setup-MOBWGTWJ.js +30 -0
  26. package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
  27. package/dist/postgres-job-store-J5F4GUWU.js +7 -0
  28. package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
  29. package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
  30. package/dist/pricing-4CEB34RM.js +48 -0
  31. package/dist/processed-payment-store-HAA4SFNK.js +11 -0
  32. package/dist/revenue-reporter-M35KP6V7.js +435 -0
  33. package/dist/server/index.d.ts +4108 -0
  34. package/dist/server/index.js +22538 -0
  35. package/dist/ssrf-BdHsrrIb.d.ts +325 -0
  36. package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
  37. package/dist/tempo-session-store-FTEEGZXA.js +467 -0
  38. package/dist/testing/index.d.ts +135 -0
  39. package/dist/testing/index.js +151 -0
  40. package/dist/x402-35VLYFKZ.js +1272 -0
  41. package/package.json +89 -6
@@ -0,0 +1,467 @@
1
+ import {
2
+ parseStoreValue,
3
+ stringifyStoreValue
4
+ } from "./chunk-YG7G4DPZ.js";
5
+ import {
6
+ withPostgresTransactionRecovery
7
+ } from "./chunk-H25M54MI.js";
8
+ import {
9
+ withSdkInitLock
10
+ } from "./chunk-S3XAHZQY.js";
11
+
12
+ // src/sdk/server/tempo-session-store.ts
13
+ import { AsyncLocalStorage } from "async_hooks";
14
+ var PostgresTempoSessionStore = class {
15
+ constructor(pool) {
16
+ this.pool = pool;
17
+ this.update = this.update.bind(this);
18
+ }
19
+ pool;
20
+ acceptance = new AsyncLocalStorage();
21
+ /** Create the channel table under the SDK-wide migration lock. */
22
+ async init() {
23
+ await withSdkInitLock(this.pool, async () => {
24
+ await this.pool.query(`
25
+ CREATE TABLE IF NOT EXISTS mpp_tempo_channels (
26
+ channel_key TEXT PRIMARY KEY,
27
+ state_json TEXT NOT NULL,
28
+ updated_at BIGINT NOT NULL
29
+ )
30
+ `);
31
+ await this.pool.query(
32
+ `CREATE INDEX IF NOT EXISTS idx_mpp_tempo_channels_updated
33
+ ON mpp_tempo_channels(updated_at)`
34
+ );
35
+ await this.pool.query(`
36
+ CREATE TABLE IF NOT EXISTS mpp_tempo_close_intents (
37
+ channel_id TEXT PRIMARY KEY,
38
+ drain_id TEXT NOT NULL,
39
+ reference TEXT,
40
+ forecast_pending BOOLEAN NOT NULL DEFAULT FALSE,
41
+ updated_at BIGINT NOT NULL
42
+ )
43
+ `);
44
+ await this.pool.query(
45
+ `ALTER TABLE mpp_tempo_close_intents
46
+ ADD COLUMN IF NOT EXISTS forecast_pending BOOLEAN NOT NULL DEFAULT FALSE`
47
+ );
48
+ });
49
+ }
50
+ /** Read one channel snapshot. */
51
+ async get(key) {
52
+ const client = this.acceptance.getStore()?.client;
53
+ const { rows } = await (client ?? this.pool).query(
54
+ `SELECT state_json FROM mpp_tempo_channels WHERE channel_key = $1`,
55
+ [key]
56
+ );
57
+ return rows[0] ? parseStoreValue(rows[0].state_json) : null;
58
+ }
59
+ /** Replace one channel snapshot. Used by mppx only for non-RMW maintenance. */
60
+ async put(key, value) {
61
+ await this.update(key, () => ({ op: "set", value, result: void 0 }));
62
+ }
63
+ /** Delete one channel snapshot. */
64
+ async delete(key) {
65
+ await this.update(key, () => ({ op: "delete", result: void 0 }));
66
+ }
67
+ /** Atomic read-modify-write using a row lock shared by every SDK replica. */
68
+ async update(key, fn) {
69
+ const acceptance = this.acceptance.getStore();
70
+ if (acceptance) {
71
+ const client = await this.acceptanceClient(acceptance);
72
+ return this.updateInTransaction(client, key, fn, acceptance);
73
+ }
74
+ return this.transaction(
75
+ "tempo_session.update",
76
+ (client) => this.updateInTransaction(client, key, fn)
77
+ );
78
+ }
79
+ /** Compose voucher acceptance and its ledger mutation under the channel-row lock. */
80
+ async withVoucherAcceptance(voucher, commit, operation) {
81
+ const context = {
82
+ commit,
83
+ channelId: voucher.channelId.toLowerCase(),
84
+ cumulativeAmount: voucher.cumulativeAmount
85
+ };
86
+ try {
87
+ const value = await this.acceptance.run(context, operation);
88
+ await context.client?.query("COMMIT");
89
+ return { value, result: context.result };
90
+ } catch (error) {
91
+ await context.client?.query("ROLLBACK").catch(() => void 0);
92
+ throw error;
93
+ } finally {
94
+ const pending = context.pendingClient;
95
+ context.client = void 0;
96
+ context.pendingClient = void 0;
97
+ (await pending?.catch(() => void 0))?.release();
98
+ }
99
+ }
100
+ /**
101
+ * Check out the acceptance transaction's client, opening it on first demand.
102
+ *
103
+ * An operation that never mutates the store therefore never takes a
104
+ * connection and never opens an empty transaction — the caller sees the same
105
+ * `result: undefined` it saw before, because nothing committed either way.
106
+ *
107
+ * The in-flight checkout is memoized, not just its result: mppx awaits each
108
+ * store call today, but two concurrent first mutations would otherwise open
109
+ * two transactions and leak the one nobody keeps a handle to.
110
+ */
111
+ acceptanceClient(acceptance) {
112
+ acceptance.pendingClient ??= (async () => {
113
+ const client = await this.pool.connect();
114
+ try {
115
+ await client.query("BEGIN");
116
+ } catch (error) {
117
+ client.release();
118
+ throw error;
119
+ }
120
+ acceptance.client = client;
121
+ return client;
122
+ })();
123
+ return acceptance.pendingClient;
124
+ }
125
+ /** Serialize a credit draw with the channel lifecycle that authorizes it. */
126
+ async withDrawPlacement(channelId, operation, tx) {
127
+ if (tx) return this.withLockedLifecycle(tx, channelId, operation);
128
+ return this.transaction(
129
+ "tempo_session.draw_placement",
130
+ (client) => this.withLockedLifecycle(client, channelId, operation)
131
+ );
132
+ }
133
+ async withLockedLifecycle(client, channelId, operation) {
134
+ const key = channelId.toLowerCase();
135
+ const { rows } = await client.query(
136
+ `SELECT state_json FROM mpp_tempo_channels WHERE channel_key = $1 FOR UPDATE`,
137
+ [key]
138
+ );
139
+ if (!rows[0]) throw new Error(`Tempo session channel ${key} not found`);
140
+ const current = parseStoreValue(rows[0].state_json);
141
+ if (!isRecord(current)) throw new Error(`Tempo session channel ${key} has invalid state`);
142
+ return operation(client, lifecycleFromState(current));
143
+ }
144
+ /**
145
+ * Compose a terminal ledger resolution with the channel's consumed value.
146
+ *
147
+ * Voucher acceptance records spending authority; only a successful job
148
+ * consumes it. Holding the channel row while the callback locks and settles
149
+ * the credit keeps that distinction atomic across machines and preserves the
150
+ * shared channel -> credit lock order.
151
+ */
152
+ async withTerminalConsumption(channelId, operation) {
153
+ const key = channelId.toLowerCase();
154
+ return this.transaction("tempo_session.terminal_consumption", async (client) => {
155
+ const { rows } = await client.query(
156
+ `SELECT state_json FROM mpp_tempo_channels WHERE channel_key = $1 FOR UPDATE`,
157
+ [key]
158
+ );
159
+ if (!rows[0]) throw new Error(`Tempo session channel ${key} not found`);
160
+ const current = parseStoreValue(rows[0].state_json);
161
+ if (!isRecord(current)) throw new Error(`Tempo session channel ${key} has invalid state`);
162
+ const lifecycle = lifecycleFromState(current);
163
+ const resolved = await operation(client, lifecycle);
164
+ if (resolved.consume) {
165
+ if (lifecycle.finalized || lifecycle.closeRequestedAt !== 0n) {
166
+ throw new Error("Tempo session channel is closing or finalized");
167
+ }
168
+ if (resolved.amount < 0n) throw new Error("Tempo session consumption cannot be negative");
169
+ const spent = lifecycle.spent;
170
+ const highestVoucherAmount = asBigInt(current.highestVoucherAmount);
171
+ if (highestVoucherAmount === void 0 || spent + resolved.amount > highestVoucherAmount) {
172
+ throw new Error(`Tempo session consumption exceeds accepted voucher authority`);
173
+ }
174
+ const next = {
175
+ ...current,
176
+ spent: spent + resolved.amount,
177
+ units: Number(current.units ?? 0) + 1
178
+ };
179
+ assertMonotonicTransition(current, next);
180
+ await client.query(
181
+ `UPDATE mpp_tempo_channels SET state_json = $2, updated_at = $3
182
+ WHERE channel_key = $1`,
183
+ [key, stringifyStoreValue(next, "Tempo session state"), Date.now()]
184
+ );
185
+ }
186
+ return resolved.value;
187
+ });
188
+ }
189
+ /** Retire lost backing while holding the channel row ahead of every credit lock. */
190
+ async withFinalizedCreditLoss(channelId, operation) {
191
+ const key = channelId.toLowerCase();
192
+ return this.transaction("tempo_session.finalized_credit_loss", async (client) => {
193
+ const { rows } = await client.query(
194
+ `SELECT state_json FROM mpp_tempo_channels WHERE channel_key = $1 FOR UPDATE`,
195
+ [key]
196
+ );
197
+ if (!rows[0]) throw new Error(`Tempo session channel ${key} not found`);
198
+ const current = parseStoreValue(rows[0].state_json);
199
+ if (!isRecord(current)) throw new Error(`Tempo session channel ${key} has invalid state`);
200
+ return operation(client, lifecycleFromState(current));
201
+ });
202
+ }
203
+ /** Page active channel snapshots for the close/settlement watcher. */
204
+ async listActive(limit = 100, cursor) {
205
+ const { rows } = await this.pool.query(
206
+ `SELECT channel_key, state_json, updated_at
207
+ FROM mpp_tempo_channels
208
+ WHERE (state_json::jsonb ->> 'finalized') IS DISTINCT FROM 'true'
209
+ AND ($2::BIGINT IS NULL OR (updated_at, channel_key) > ($2, $3))
210
+ ORDER BY updated_at, channel_key
211
+ LIMIT $1`,
212
+ [limit, cursor?.updatedAt ?? null, cursor?.key ?? null]
213
+ );
214
+ return rows.map((row) => ({
215
+ key: row.channel_key,
216
+ state: parseStoreValue(row.state_json),
217
+ updatedAt: Number(row.updated_at)
218
+ }));
219
+ }
220
+ /** Persist a drain's close intent before its credential is broadcast. */
221
+ async beginClose(channelId, drainId) {
222
+ return this.runCloseIntentTransaction(channelId, drainId, async (tx, record) => {
223
+ const { rows } = await tx.query(
224
+ `SELECT EXISTS (
225
+ SELECT 1
226
+ FROM credit_drains d
227
+ JOIN credits c ON c.credit_id = d.credit_id
228
+ WHERE c.tempo_channel_id = $1
229
+ AND d.drain_id = $2
230
+ AND d.status = 'pending'
231
+ ) AS pending,
232
+ (SELECT forecast_pending
233
+ FROM mpp_tempo_close_intents
234
+ WHERE channel_id = $1 AND drain_id = $2) AS forecast_pending`,
235
+ [channelId.toLowerCase(), drainId]
236
+ );
237
+ if (!rows[0]?.pending) {
238
+ throw new Error(`Tempo session drain ${drainId} is no longer pending`);
239
+ }
240
+ if (rows[0].forecast_pending !== false) {
241
+ throw new Error(`Tempo session drain ${drainId} refund forecast is still pending`);
242
+ }
243
+ return record;
244
+ });
245
+ }
246
+ /** Reserve a close intent and run its ledger debit in the same transaction. */
247
+ async withCloseIntent(channelId, drainId, operation) {
248
+ return this.runCloseIntentTransaction(channelId, drainId, operation);
249
+ }
250
+ /**
251
+ * Resolve a reserved close's forecast under the intent row lock.
252
+ *
253
+ * `proceed` opens the broadcast gate. `release` deletes the intent and runs
254
+ * the ledger restore in the same transaction, but only while the gate is
255
+ * still pending. A concurrent proceed wins by returning `proceed` to the
256
+ * releaser, which must not restore value that may already be broadcasting.
257
+ */
258
+ async resolveCloseForecast(channelId, drainId, decision, operation) {
259
+ return this.transaction("tempo_session.resolve_close_forecast", async (client) => {
260
+ const { rows } = await client.query(
261
+ `SELECT forecast_pending
262
+ FROM mpp_tempo_close_intents
263
+ WHERE channel_id = $1 AND drain_id = $2
264
+ FOR UPDATE`,
265
+ [channelId.toLowerCase(), drainId]
266
+ );
267
+ if (!rows[0]) {
268
+ return { outcome: "missing" };
269
+ }
270
+ if (!rows[0].forecast_pending) {
271
+ return { outcome: "proceed" };
272
+ }
273
+ if (decision === "proceed") {
274
+ await client.query(
275
+ `UPDATE mpp_tempo_close_intents
276
+ SET forecast_pending = FALSE, updated_at = $3
277
+ WHERE channel_id = $1 AND drain_id = $2`,
278
+ [channelId.toLowerCase(), drainId, Date.now()]
279
+ );
280
+ return { outcome: "proceed" };
281
+ }
282
+ if (!operation) {
283
+ throw new Error("A Tempo close forecast release requires a ledger operation");
284
+ }
285
+ await client.query(
286
+ `DELETE FROM mpp_tempo_close_intents
287
+ WHERE channel_id = $1 AND drain_id = $2`,
288
+ [channelId.toLowerCase(), drainId]
289
+ );
290
+ const value = await operation(client);
291
+ return { outcome: "released", value };
292
+ });
293
+ }
294
+ /** Delete a matching close intent and release its ledger debit atomically. */
295
+ async releaseCloseIntent(channelId, drainId, operation) {
296
+ return this.transaction("tempo_session.release_close_intent", async (client) => {
297
+ await client.query(
298
+ `DELETE FROM mpp_tempo_close_intents
299
+ WHERE channel_id = $1 AND drain_id = $2`,
300
+ [channelId.toLowerCase(), drainId]
301
+ );
302
+ return operation(client);
303
+ });
304
+ }
305
+ /** Read a persisted cooperative-close intent or confirmed result. */
306
+ async getClose(channelId) {
307
+ const { rows } = await this.pool.query(
308
+ `SELECT channel_id, drain_id, reference
309
+ FROM mpp_tempo_close_intents
310
+ WHERE channel_id = $1`,
311
+ [channelId.toLowerCase()]
312
+ );
313
+ return rows[0] ? closeRecord(rows[0]) : null;
314
+ }
315
+ /** Persist proof that a cooperative close reached terminal chain state. */
316
+ async confirmClose(channelId, drainId, reference) {
317
+ const { rows } = await this.pool.query(
318
+ `UPDATE mpp_tempo_close_intents
319
+ SET reference = COALESCE(reference, $3), updated_at = $4
320
+ WHERE channel_id = $1 AND drain_id = $2 AND forecast_pending = FALSE
321
+ RETURNING channel_id, drain_id, reference`,
322
+ [channelId.toLowerCase(), drainId, reference, Date.now()]
323
+ );
324
+ if (!rows[0]) throw new Error(`Tempo session close intent not found`);
325
+ return closeRecord(rows[0]);
326
+ }
327
+ async runCloseIntentTransaction(channelId, drainId, operation) {
328
+ return this.transaction("tempo_session.close_intent", async (client) => {
329
+ const record = await this.writeCloseIntent(client, channelId, drainId);
330
+ return operation(client, record);
331
+ });
332
+ }
333
+ transaction(operation, callback) {
334
+ return withPostgresTransactionRecovery(this.pool, "dvm-host", operation, callback);
335
+ }
336
+ async writeCloseIntent(query, channelId, drainId) {
337
+ const normalized = channelId.toLowerCase();
338
+ const { rows } = await query.query(
339
+ `INSERT INTO mpp_tempo_close_intents
340
+ (channel_id, drain_id, reference, forecast_pending, updated_at)
341
+ VALUES ($1, $2, NULL, TRUE, $3)
342
+ ON CONFLICT (channel_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
343
+ RETURNING channel_id, drain_id, reference`,
344
+ [normalized, drainId, Date.now()]
345
+ );
346
+ const row = rows[0];
347
+ if (row.drain_id !== drainId) {
348
+ throw new Error(`Tempo session channel ${normalized} is already closing for another drain`);
349
+ }
350
+ return closeRecord(row);
351
+ }
352
+ async updateInTransaction(client, key, fn, acceptance) {
353
+ await client.query(`SELECT pg_advisory_xact_lock(hashtextextended($1, 1762))`, [key]);
354
+ const { rows } = await client.query(
355
+ `SELECT state_json FROM mpp_tempo_channels WHERE channel_key = $1 FOR UPDATE`,
356
+ [key]
357
+ );
358
+ const current = rows[0] ? parseStoreValue(rows[0].state_json) : null;
359
+ const change = fn(current);
360
+ if (change.op === "set") {
361
+ const cumulativeAmount = acceptanceVoucherAmount(acceptance, key, current, change.value);
362
+ let value = change.value;
363
+ if (acceptance?.channelId === key.toLowerCase() && isRecord(current)) {
364
+ value = {
365
+ ...change.value,
366
+ spent: asBigInt(current.spent) ?? 0n,
367
+ units: Number(current.units ?? 0)
368
+ };
369
+ }
370
+ assertMonotonicTransition(current, value);
371
+ await client.query(
372
+ `INSERT INTO mpp_tempo_channels (channel_key, state_json, updated_at)
373
+ VALUES ($1, $2, $3)
374
+ ON CONFLICT (channel_key) DO UPDATE
375
+ SET state_json = EXCLUDED.state_json, updated_at = EXCLUDED.updated_at`,
376
+ [key, stringifyStoreValue(value, "Tempo session state"), Date.now()]
377
+ );
378
+ if (acceptance && cumulativeAmount !== void 0 && acceptance.result === void 0) {
379
+ const priorCumulativeAmount = isRecord(current) ? asBigInt(current.highestVoucherAmount) ?? 0n : 0n;
380
+ acceptance.result = await acceptance.commit(client, {
381
+ key,
382
+ cumulativeAmount,
383
+ priorCumulativeAmount,
384
+ delta: cumulativeAmount - priorCumulativeAmount
385
+ });
386
+ }
387
+ } else if (change.op === "delete") {
388
+ await client.query(`DELETE FROM mpp_tempo_channels WHERE channel_key = $1`, [key]);
389
+ }
390
+ return change.result;
391
+ }
392
+ };
393
+ function lifecycleFromState(state) {
394
+ return {
395
+ finalized: state.finalized === true,
396
+ closeRequestedAt: asBigInt(state.closeRequestedAt) ?? 0n,
397
+ spent: asBigInt(state.spent) ?? 0n,
398
+ settledOnChain: asBigInt(state.settledOnChain) ?? 0n,
399
+ highestVoucherAmount: asBigInt(state.highestVoucherAmount) ?? 0n
400
+ };
401
+ }
402
+ function closeRecord(row) {
403
+ return {
404
+ channelId: row.channel_id,
405
+ drainId: row.drain_id,
406
+ reference: row.reference
407
+ };
408
+ }
409
+ function assertMonotonicTransition(current, next) {
410
+ if (!isRecord(next)) throw new Error("Tempo session state must be an object");
411
+ if (!isRecord(current)) return;
412
+ for (const field of [
413
+ "channelId",
414
+ "chainId",
415
+ "escrowContract",
416
+ "payee",
417
+ "payer",
418
+ "token",
419
+ "authorizedSigner",
420
+ "operator"
421
+ ]) {
422
+ if (current[field] !== void 0 && next[field] !== current[field]) {
423
+ throw new Error(`Tempo session immutable field changed: ${field}`);
424
+ }
425
+ }
426
+ for (const field of ["highestVoucherAmount", "settledOnChain", "spent", "deposit"]) {
427
+ const before = asBigInt(current[field]);
428
+ const after = asBigInt(next[field]);
429
+ const terminalReset = next.finalized === true && after === 0n && field === "deposit";
430
+ if (before !== void 0 && after !== void 0 && after < before && !terminalReset) {
431
+ throw new Error(`Tempo session monotonic field decreased: ${field}`);
432
+ }
433
+ }
434
+ const closeRequestedAt = asBigInt(next.closeRequestedAt);
435
+ if (closeRequestedAt !== void 0 && closeRequestedAt < 0n) {
436
+ throw new Error("Tempo session closeRequestedAt cannot be negative");
437
+ }
438
+ if (current.finalized === true && next.finalized !== true) {
439
+ throw new Error("Tempo session finalized state cannot be reopened");
440
+ }
441
+ }
442
+ function isRecord(value) {
443
+ return !!value && typeof value === "object" && !Array.isArray(value);
444
+ }
445
+ function asBigInt(value) {
446
+ if (typeof value === "bigint") return value;
447
+ if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
448
+ if (typeof value === "string" && /^\d+$/.test(value)) return BigInt(value);
449
+ return void 0;
450
+ }
451
+ function increasedVoucherAmount(current, next) {
452
+ if (!isRecord(next)) return void 0;
453
+ const after = asBigInt(next.highestVoucherAmount);
454
+ const before = isRecord(current) ? asBigInt(current.highestVoucherAmount) ?? 0n : 0n;
455
+ return after !== void 0 && after > before ? after : void 0;
456
+ }
457
+ function acceptanceVoucherAmount(acceptance, key, current, next) {
458
+ if (key.toLowerCase() !== acceptance?.channelId) return void 0;
459
+ const increased = increasedVoucherAmount(current, next);
460
+ if (increased !== void 0) return increased;
461
+ const authorized = isRecord(current) ? asBigInt(current.highestVoucherAmount) : void 0;
462
+ return authorized !== void 0 && authorized >= acceptance.cumulativeAmount ? acceptance.cumulativeAmount : void 0;
463
+ }
464
+ export {
465
+ PostgresTempoSessionStore,
466
+ assertMonotonicTransition
467
+ };
@@ -0,0 +1,135 @@
1
+ import { aX as StreamableJobStore, aY as ReceiptIssuingStore, J as JobRecord, aZ as RequestIdClaim, a_ as RequestIdClaimResult, a9 as JobReceipt, a$ as OutgoingMessage, aa as AppendOutgoingOptions, b0 as PaymentCreditDelta, b1 as VerifyAndCreditResult, b2 as JobCounters, a6 as Message, av as MessageType, K as KVStore, L as Logger, v as ResponseContent, P as PaymentContent, S as SDKJobContext } from '../job-store-C5n6bhap.js';
2
+ import '@cashu/cashu-ts';
3
+ import 'mppx';
4
+ import 'hono';
5
+ import '@x402/core/server';
6
+ import '@x402/evm/batch-settlement/server';
7
+ import 'pg';
8
+ import '@x402/core/types';
9
+ import 'zod';
10
+
11
+ /**
12
+ * In-memory JobStore implementing the transactional + streaming surface
13
+ * (internal-review). Used by unit tests; not production-safe — there's no real
14
+ * concurrency control beyond JS's single-threaded execution.
15
+ */
16
+ declare class MemoryJobStore implements StreamableJobStore, ReceiptIssuingStore {
17
+ private data;
18
+ private messages;
19
+ private nextSeq;
20
+ private messageSubscribers;
21
+ private notifySubscribers;
22
+ private reactivationLocks;
23
+ private requestIdClaims;
24
+ /** Per-DVM monotonic receipt counter (internal-review) — the `receipt_counter` row. */
25
+ private receiptCounter;
26
+ /** Receipt sequence already allocated to a job, keyed by job id. */
27
+ private receiptSeqs;
28
+ get(id: string): Promise<JobRecord | undefined>;
29
+ findJobByPaymentTxHash(txHash: string): Promise<JobRecord | undefined>;
30
+ findJobByRequestId(requestId: string, requesterPubkey: string): Promise<JobRecord | undefined>;
31
+ claimRequestId(claim: RequestIdClaim): Promise<RequestIdClaimResult | undefined>;
32
+ resumeRequestId(claim: RequestIdClaim): Promise<RequestIdClaimResult | undefined>;
33
+ releaseRequestIdClaim(claim: RequestIdClaim): Promise<void>;
34
+ save(record: JobRecord): Promise<void>;
35
+ delete(id: string): Promise<void>;
36
+ claimReceiptSeq(jobId: string): Promise<number>;
37
+ saveReceipt(jobId: string, receipt: JobReceipt): Promise<JobReceipt | undefined>;
38
+ appendOutgoing(jobId: string, message: OutgoingMessage, opts?: AppendOutgoingOptions): Promise<number>;
39
+ recordInbound(jobId: string, message: OutgoingMessage): Promise<number>;
40
+ verifyAndCredit(jobId: string, seq: number, credit: PaymentCreditDelta | null): Promise<VerifyAndCreditResult>;
41
+ getVerifiedInbound(jobId: string, seq: number): Promise<VerifyAndCreditResult | undefined>;
42
+ markInboundFailed(jobId: string, seq: number, _reason: string): Promise<void>;
43
+ findStaleJobs(processingThresholdMs: number, awaitingThresholdMs: number, limit: number, capabilities: string[] | null): Promise<JobRecord[]>;
44
+ heartbeatActiveJobs(jobIds: string[], now: number): Promise<void>;
45
+ cancelStaleJob(jobId: string, expectedActivityBefore: number, reason: string, terminalStatus: "failed" | "cancelled"): Promise<boolean>;
46
+ cancelJob(jobId: string, reason: string): Promise<boolean>;
47
+ getCounters(jobId: string): Promise<JobCounters | undefined>;
48
+ claimForProcessing(jobId: string): Promise<boolean>;
49
+ tryReactivationLock<T>(jobId: string, fn: () => Promise<T>): Promise<{
50
+ acquired: true;
51
+ result: T;
52
+ } | {
53
+ acquired: false;
54
+ }>;
55
+ subscribeMessages(jobId: string, afterSeq: number, onMessage: (msg: Message) => void): Promise<() => void>;
56
+ subscribeNotifications(jobId: string, onNotify: () => void): Promise<() => void>;
57
+ getMessages(jobId: string, afterSeq: number): Promise<Message[]>;
58
+ initStreaming(): Promise<void>;
59
+ shutdownStreaming(): Promise<void>;
60
+ /**
61
+ * Shared body of `cancelStaleJob` (activity cutoff supplied) and `cancelJob`
62
+ * (`null` cutoff — unconditional). Mirrors `PostgresJobStore.terminateJob`:
63
+ * the terminal status and the final `cancel` message land together and the
64
+ * notify fires before the incremental log is cleaned up.
65
+ */
66
+ private terminateJob;
67
+ private allocateSeq;
68
+ private messagesFor;
69
+ private notify;
70
+ private fireNotifyOnly;
71
+ }
72
+
73
+ /** Recorded outbound message. */
74
+ interface RecordedMessage {
75
+ /** Message type (e.g. "text", "artifact", "prompt", "complete"). */
76
+ type: MessageType;
77
+ /** Message content payload. */
78
+ content: Record<string, unknown>;
79
+ }
80
+ /** Options for creating a test context. */
81
+ interface TestContextOpts<State = Record<string, unknown>, Input = string> {
82
+ /** Override the job ID. Default: "test-job-1". */
83
+ jobId?: string;
84
+ /** Freeform tags describing the job type. Default: []. */
85
+ tags?: string[];
86
+ /** Job input — string for unstructured, or the parsed type for schema-based DVMs. */
87
+ input?: Input;
88
+ /** Job parameters as key-value pairs. */
89
+ params?: Record<string, string>;
90
+ /** Opaque requester identifier. */
91
+ requesterId?: string;
92
+ /** Total msats paid so far. */
93
+ paidMsats?: number;
94
+ /** Initial per-job state (cloned to avoid test pollution). */
95
+ state?: State;
96
+ /** Environment variables available via ctx.env. */
97
+ env?: Record<string, string>;
98
+ /** KV store instance. Default: in-memory MemoryKVStore. */
99
+ store?: KVStore;
100
+ /** Logger instance. Default: no-op logger. */
101
+ log?: Logger;
102
+ /** Pre-programmed responses for prompt() calls, keyed by prompt ID. */
103
+ responses?: Record<string, ResponseContent>;
104
+ /** Pre-programmed payment for requestPayment() calls. */
105
+ payment?: PaymentContent;
106
+ }
107
+ /** Extended SDKJobContext exposing test inspection properties. */
108
+ interface TestJobContext<State = Record<string, unknown>, Input = string> extends SDKJobContext<State, Input> {
109
+ /** All outbound messages sent during the handler. */
110
+ readonly messages: RecordedMessage[];
111
+ /** Whether complete() was called. */
112
+ readonly completed: boolean;
113
+ /** Whether fail() was called. */
114
+ readonly failed: boolean;
115
+ /** Whether fail() was called with `{ refund: true }`. */
116
+ readonly refundRequested: boolean;
117
+ /** The summary string passed to complete(). */
118
+ readonly summary: string | undefined;
119
+ /** The error string passed to fail(). */
120
+ readonly failError: string | undefined;
121
+ /**
122
+ * Cancel the job under test (internal-review) — aborts `ctx.signal` exactly as a
123
+ * caller cancel, idle timeout, or stale-sweep would in the real runtime, and
124
+ * puts the context into the same post-terminal shape: every emitter
125
+ * (`text` / `artifact` / `sendMessage` / `working` / `progress` /
126
+ * `complete` / `fail`) becomes a no-op, and `prompt` / `requestPayment`
127
+ * reject with `JobCancelledError` rather than hanging. Use it to assert your
128
+ * handler stops work after cancellation.
129
+ */
130
+ cancel(reason?: string): void;
131
+ }
132
+ /** Create a test-only SDKJobContext for unit testing DVM handlers. */
133
+ declare function createTestContext<State = Record<string, unknown>, Input = string>(opts?: TestContextOpts<State, Input>): TestJobContext<State, Input>;
134
+
135
+ export { MemoryJobStore, type RecordedMessage, type TestContextOpts, type TestJobContext, createTestContext };