mppx 0.11.0 → 0.13.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 (126) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +22 -1
  3. package/dist/Method.d.ts +7 -1
  4. package/dist/Method.js +7 -2
  5. package/dist/Receipt.d.ts +2 -0
  6. package/dist/Receipt.js +2 -0
  7. package/dist/cli/agent.d.ts +3 -0
  8. package/dist/cli/agent.js +20 -0
  9. package/dist/cli/cli.d.ts +9 -1
  10. package/dist/cli/cli.js +134 -136
  11. package/dist/cli/plugins/index.js +12 -14
  12. package/dist/cli/plugins/tempo.js +9 -2
  13. package/dist/cli/utils.d.ts +11 -1
  14. package/dist/cli/utils.js +15 -2
  15. package/dist/cli/validate/challenge.d.ts +2 -0
  16. package/dist/cli/validate/challenge.js +19 -4
  17. package/dist/cli/validate/discovery.d.ts +1 -0
  18. package/dist/cli/validate/discovery.js +10 -1
  19. package/dist/cli/validate/payment.js +63 -24
  20. package/dist/client/Methods.d.ts +0 -2
  21. package/dist/client/Methods.js +0 -2
  22. package/dist/client/Mppx.js +1 -0
  23. package/dist/client/index.d.ts +1 -1
  24. package/dist/client/index.js +1 -1
  25. package/dist/client/internal/Fetch.d.ts +2 -0
  26. package/dist/client/internal/Fetch.js +8 -2
  27. package/dist/client/internal/MethodChallenge.d.ts +1 -0
  28. package/dist/evm/Types.js +1 -1
  29. package/dist/evm/server/Charge.js +5 -0
  30. package/dist/evm/server/Methods.d.ts +164 -3
  31. package/dist/evm/server/Methods.js +50 -2
  32. package/dist/integrations/x402/Evm.js +4 -3
  33. package/dist/internal/HeaderCodec.d.ts +2 -2
  34. package/dist/internal/version.d.ts +2 -2
  35. package/dist/internal/version.js +1 -1
  36. package/dist/mcp/client/McpClient.js +4 -7
  37. package/dist/middlewares/elysia.d.ts +11 -8
  38. package/dist/middlewares/elysia.js +67 -14
  39. package/dist/middlewares/express.js +13 -6
  40. package/dist/middlewares/hono.js +4 -4
  41. package/dist/middlewares/nextjs.js +9 -5
  42. package/dist/proxy/internal/Headers.js +65 -0
  43. package/dist/proxy/services/stripe.js +1 -0
  44. package/dist/server/Mppx.d.ts +18 -2
  45. package/dist/server/Mppx.js +204 -92
  46. package/dist/server/NodeListener.js +71 -6
  47. package/dist/server/Request.d.ts +11 -1
  48. package/dist/server/Request.js +132 -13
  49. package/dist/server/Transport.d.ts +14 -1
  50. package/dist/server/Transport.js +1 -0
  51. package/dist/server/internal/html/compose.main.gen.d.ts +1 -1
  52. package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
  53. package/dist/stripe/server/Methods.d.ts +10 -3
  54. package/dist/stripe/server/Methods.js +19 -6
  55. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  56. package/dist/stripe/server/internal/html.gen.js +1 -1
  57. package/dist/tempo/Methods.d.ts +1 -0
  58. package/dist/tempo/Methods.js +1 -0
  59. package/dist/tempo/client/Charge.js +17 -13
  60. package/dist/tempo/client/Methods.d.ts +1 -5
  61. package/dist/tempo/client/Methods.js +10 -5
  62. package/dist/tempo/client/ResolveAccount.d.ts +3 -0
  63. package/dist/tempo/client/index.d.ts +0 -3
  64. package/dist/tempo/client/index.js +0 -2
  65. package/dist/tempo/internal/currencies.d.ts +19 -0
  66. package/dist/tempo/internal/currencies.js +41 -0
  67. package/dist/tempo/internal/defaults.d.ts +5 -3
  68. package/dist/tempo/internal/defaults.js +6 -2
  69. package/dist/tempo/internal/fee-payer.d.ts +11 -7
  70. package/dist/tempo/internal/fee-payer.js +30 -11
  71. package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
  72. package/dist/tempo/internal/machine-token-charge.js +2 -1
  73. package/dist/tempo/legacy/index.d.ts +1 -3
  74. package/dist/tempo/legacy/index.js +1 -3
  75. package/dist/tempo/server/Charge.d.ts +12 -1
  76. package/dist/tempo/server/Charge.js +109 -78
  77. package/dist/tempo/server/Methods.d.ts +5048 -40
  78. package/dist/tempo/server/Methods.js +104 -13
  79. package/dist/tempo/server/Subscription.d.ts +1 -1
  80. package/dist/tempo/server/Subscription.js +31 -4
  81. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  82. package/dist/tempo/server/internal/html.gen.js +1 -1
  83. package/dist/tempo/server/internal/transport.js +28 -11
  84. package/dist/tempo/session/client/ChannelOps.js +5 -0
  85. package/dist/tempo/session/client/Session.d.ts +1 -0
  86. package/dist/tempo/session/client/Session.js +7 -4
  87. package/dist/tempo/session/client/SessionManager.js +3 -0
  88. package/dist/tempo/session/client/Transports.d.ts +8 -1
  89. package/dist/tempo/session/client/Transports.js +12 -3
  90. package/dist/tempo/session/precompile/Chain.js +16 -1
  91. package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
  92. package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
  93. package/dist/tempo/session/precompile/Voucher.js +48 -0
  94. package/dist/tempo/session/server/ChannelStore.d.ts +22 -2
  95. package/dist/tempo/session/server/ChannelStore.js +37 -7
  96. package/dist/tempo/session/server/CredentialVerification.d.ts +6 -1
  97. package/dist/tempo/session/server/CredentialVerification.js +56 -19
  98. package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
  99. package/dist/tempo/session/server/MeteredStream.js +85 -29
  100. package/dist/tempo/session/server/RequestState.d.ts +6 -0
  101. package/dist/tempo/session/server/RequestState.js +11 -4
  102. package/dist/tempo/session/server/Session.d.ts +2 -1
  103. package/dist/tempo/session/server/Session.js +10 -2
  104. package/dist/tempo/session/server/Settlement.d.ts +23 -0
  105. package/dist/tempo/session/server/Settlement.js +105 -25
  106. package/dist/tempo/session/server/Sse.d.ts +3 -1
  107. package/dist/tempo/session/server/Sse.js +105 -32
  108. package/dist/tempo/session/server/Transports.d.ts +24 -19
  109. package/dist/tempo/session/server/Transports.js +169 -58
  110. package/dist/tempo/session/server/Ws.d.ts +5 -1
  111. package/dist/tempo/session/server/Ws.js +75 -20
  112. package/dist/tempo/subscription/KeyAuthorization.d.ts +70 -8
  113. package/dist/tempo/subscription/Store.d.ts +2 -0
  114. package/dist/tempo/subscription/Store.js +3 -0
  115. package/dist/validation/core.js +2 -1
  116. package/dist/x402/server/EvmCharge.d.ts +3 -1
  117. package/dist/x402/server/EvmCharge.js +14 -1
  118. package/package.json +8 -8
  119. package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
  120. package/dist/tempo/legacy/client/ChannelOps.js +0 -168
  121. package/dist/tempo/legacy/client/Session.d.ts +0 -185
  122. package/dist/tempo/legacy/client/Session.js +0 -279
  123. package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
  124. package/dist/tempo/legacy/client/SessionManager.js +0 -706
  125. package/dist/tempo/legacy/client/index.d.ts +0 -7
  126. package/dist/tempo/legacy/client/index.js +0 -5
@@ -49,6 +49,7 @@ export function sse(options) {
49
49
  const base = Transport.http();
50
50
  return Transport.from({
51
51
  name: 'sse',
52
+ supportsStreamingReceipts: true,
52
53
  captureRequest(request) {
53
54
  return (base.captureRequest?.(request) ?? {
54
55
  headers: new Headers(request.headers),
@@ -63,7 +64,7 @@ export function sse(options) {
63
64
  respondChallenge(options) {
64
65
  return base.respondChallenge(options);
65
66
  },
66
- respondReceipt({ credential, envelope, receipt, response, challengeId, input }) {
67
+ async respondReceipt({ credential, envelope, receipt, response, challengeId, input }) {
67
68
  const verifiedCredential = envelope?.credential ?? credential;
68
69
  const verifiedChallengeId = envelope?.challenge.id ?? challengeId;
69
70
  const verifiedRequest = envelope?.request ?? verifiedCredential.challenge.request;
@@ -112,8 +113,7 @@ export function sse(options) {
112
113
  if (hasPrepaidSessionTick(currentReceipt)) {
113
114
  return baseResponse;
114
115
  }
115
- const available = BigInt(currentReceipt.acceptedCumulative) - BigInt(currentReceipt.spent);
116
- if (available < tickCost) {
116
+ const paymentRequired = (available) => {
117
117
  const error = new Errors.InsufficientBalanceError({
118
118
  reason: `requested ${tickCost}, available ${available}`,
119
119
  });
@@ -125,7 +125,10 @@ export function sse(options) {
125
125
  'Content-Type': 'application/problem+json',
126
126
  },
127
127
  });
128
- }
128
+ };
129
+ const available = BigInt(currentReceipt.acceptedCumulative) - BigInt(currentReceipt.spent);
130
+ if (available < tickCost)
131
+ return paymentRequired(available);
129
132
  const chargedReceipt = {
130
133
  ...currentReceipt,
131
134
  spent: (BigInt(currentReceipt.spent) + tickCost).toString(),
@@ -150,7 +153,9 @@ export function sse(options) {
150
153
  // For null-body statuses, the request shape determines whether the
151
154
  // response is management (no charge) or plain content (charge one tick).
152
155
  if (isNullBodyStatus(chargedResponse.status)) {
153
- void chargePlainResponse();
156
+ const result = await chargePlainResponse();
157
+ if (!result.ok)
158
+ return paymentRequired(result.channel.highestVoucherAmount - result.channel.spent);
154
159
  return chargedResponse;
155
160
  }
156
161
  const stream = new ReadableStream({
@@ -193,20 +198,32 @@ export function sse(options) {
193
198
  /** Default SSE serve: iterates values and emits `event: message` per value. */
194
199
  export function defaultServe(options) {
195
200
  const iterable = typeof options.generate === 'function' ? options.generate() : options.generate;
201
+ const iterator = iterable[Symbol.asyncIterator]();
196
202
  const encoder = new TextEncoder();
203
+ let closed = false;
197
204
  const stream = new ReadableStream({
198
- async start(controller) {
205
+ async pull(controller) {
206
+ if (closed)
207
+ return;
199
208
  try {
200
- for await (const value of iterable) {
201
- controller.enqueue(encoder.encode(Sse_core.formatMessageEvent(value)));
209
+ const next = await iterator.next();
210
+ if (next.done) {
211
+ closed = true;
212
+ controller.close();
213
+ return;
202
214
  }
215
+ controller.enqueue(encoder.encode(Sse_core.formatMessageEvent(next.value)));
203
216
  }
204
217
  catch (e) {
218
+ closed = true;
205
219
  controller.error(e);
206
220
  }
207
- finally {
208
- controller.close();
209
- }
221
+ },
222
+ cancel() {
223
+ closed = true;
224
+ void Promise.resolve()
225
+ .then(() => iterator.return?.())
226
+ .catch(() => { });
210
227
  },
211
228
  });
212
229
  return new Response(stream, {
@@ -114,12 +114,17 @@ export async function createVoucherPayload(client, account, descriptor, cumulati
114
114
  */
115
115
  export async function createClosePayload(client, account, descriptor, cumulativeAmount, chainId, escrow = tip20ChannelEscrow) {
116
116
  const voucher = await createVoucherPayload(client, account, descriptor, cumulativeAmount, chainId, escrow);
117
+ const closeSignature = await Voucher.signCloseAuthorization(client, account, {
118
+ channelId: voucher.channelId,
119
+ cumulativeAmount: uint96(BigInt(voucher.cumulativeAmount)),
120
+ }, escrow, chainId);
117
121
  return {
118
122
  action: 'close',
119
123
  channelId: voucher.channelId,
120
124
  descriptor,
121
125
  cumulativeAmount: voucher.cumulativeAmount,
122
126
  signature: voucher.signature,
127
+ closeSignature,
123
128
  };
124
129
  }
125
130
  /**
@@ -45,6 +45,7 @@ export declare function session(parameters?: session.Parameters): Method.Client<
45
45
  }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
46
46
  action: import("zod/mini").ZodMiniLiteral<"close">;
47
47
  channelId: import("zod/mini").ZodMiniString<string>;
48
+ closeSignature: import("zod/mini").ZodMiniString<string>;
48
49
  cumulativeAmount: import("zod/mini").ZodMiniString<string>;
49
50
  descriptor: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniCustom<Channel.ChannelDescriptor, Channel.ChannelDescriptor>>;
50
51
  signature: import("zod/mini").ZodMiniString<string>;
@@ -195,7 +195,7 @@ export function session(parameters = {}) {
195
195
  }
196
196
  },
197
197
  });
198
- const topUpChannelIfNeeded = async ({ challenge, channel, deposit, fetch, input, requiredCumulative, }) => {
198
+ const topUpChannelIfNeeded = async ({ challenge, channel, deposit, fetch, input, requiredCumulative, signal, }) => {
199
199
  const knownDeposit = channel.deposit > deposit ? channel.deposit : deposit;
200
200
  const additionalDeposit = resolveAutomaticTopUp({
201
201
  deposit: knownDeposit,
@@ -215,6 +215,7 @@ export function session(parameters = {}) {
215
215
  createSessionCredential: (challenge, context) => method.createCredential({ challenge, context }),
216
216
  fetch,
217
217
  input,
218
+ signal,
218
219
  });
219
220
  const nextDeposit = knownDeposit + additionalDeposit;
220
221
  if (nextDeposit === channel.deposit)
@@ -228,7 +229,7 @@ export function session(parameters = {}) {
228
229
  await store.set(updated);
229
230
  sink.notifyUpdate(updated);
230
231
  };
231
- MethodChallenge.register(method, async ({ challenge, context, fetch, input }) => {
232
+ MethodChallenge.register(method, async ({ challenge, context, fetch, input, signal }) => {
232
233
  if (!isTip1034SessionChallenge(challenge))
233
234
  return;
234
235
  const sessionContext = context === undefined ? undefined : sessionContextSchema.parse(context);
@@ -264,6 +265,7 @@ export function session(parameters = {}) {
264
265
  fetch,
265
266
  input,
266
267
  requiredCumulative,
268
+ signal,
267
269
  });
268
270
  });
269
271
  return MethodResponse.register(method, async ({ challenge, credential, fetch, headers, input, refetch, response, signal }) => {
@@ -289,7 +291,7 @@ export function session(parameters = {}) {
289
291
  createSessionCredential: (challenge, context) => method.createCredential({ challenge, context }),
290
292
  fetch,
291
293
  getChannel: () => channel ?? null,
292
- async topUpIfNeeded({ deposit, requiredCumulative }) {
294
+ async topUpIfNeeded({ deposit, requiredCumulative, signal: topUpSignal }) {
293
295
  if (!channel)
294
296
  return;
295
297
  await topUpChannelIfNeeded({
@@ -299,11 +301,12 @@ export function session(parameters = {}) {
299
301
  fetch,
300
302
  input,
301
303
  requiredCumulative,
304
+ signal: topUpSignal,
302
305
  });
303
306
  },
304
307
  };
305
308
  return wrapSseResponse({
306
- onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
309
+ onNeedVoucher: (event, streamSignal) => handleSseNeedVoucher({ challenge, driver, input, signal: streamSignal }, event),
307
310
  onReceipt() { },
308
311
  response,
309
312
  signal,
@@ -235,6 +235,7 @@ export function sessionManager(parameters) {
235
235
  allowedChainIds: parameters.allowedChainIds,
236
236
  account: parameters.account,
237
237
  getClient: parameters.client ? () => parameters.client : parameters.getClient,
238
+ resolveAccount: parameters.resolveAccount,
238
239
  });
239
240
  const wrappedFetch = Fetch.from({
240
241
  fetch: config.fetch,
@@ -486,6 +487,7 @@ export function sessionManager(parameters) {
486
487
  channelId: parameters.channelId,
487
488
  additionalDeposit,
488
489
  knownDeposit: deposit,
490
+ signal: parameters.signal,
489
491
  });
490
492
  }
491
493
  async function restoreCumulative(channelId, cumulativeAmount) {
@@ -700,6 +702,7 @@ export function sessionManager(parameters) {
700
702
  context,
701
703
  fetch: config.fetch,
702
704
  input: probeUrl,
705
+ signal: init?.signal,
703
706
  });
704
707
  return createSessionCredential(challenge, context);
705
708
  },
@@ -80,6 +80,8 @@ export type TopUpRequirement = {
80
80
  input: RequestInfo | URL;
81
81
  /** Minimum cumulative voucher amount the server needs. */
82
82
  requiredCumulative: bigint;
83
+ /** Cancels nested management requests for this top-up. */
84
+ signal?: AbortSignal | undefined;
83
85
  };
84
86
  /** Inputs needed to validate a caller-requested manager top-up. */
85
87
  export type ResolveManualTopUpParameters = {
@@ -156,6 +158,8 @@ export type ResolveNeedVoucherContextParameters = {
156
158
  getChannel(): ChannelEntry | null;
157
159
  /** Original paid resource URL; converted to management URL by the caller. */
158
160
  input: RequestInfo | URL;
161
+ /** Cancels top-up work triggered by this event. */
162
+ signal?: AbortSignal | undefined;
159
163
  /** Performs the deposit top-up when server-required cumulative exceeds deposit. */
160
164
  topUpIfNeeded(parameters: TopUpRequirement): Promise<void>;
161
165
  };
@@ -207,6 +211,8 @@ export type PostTopUpParameters = {
207
211
  fetch: typeof globalThis.fetch;
208
212
  /** Original paid resource URL; normalized to a management URL before posting. */
209
213
  input: RequestInfo | URL;
214
+ /** Cancels the top-up management request. */
215
+ signal?: AbortSignal | undefined;
210
216
  };
211
217
  /** Inputs for retrying an HTTP 402 with a top-up/voucher management round trip. */
212
218
  export type RetryHttpPaymentRequiredParameters = {
@@ -334,7 +340,7 @@ export type SseResponseFrame = {
334
340
  /** Inputs for driving an open paid SSE response. */
335
341
  export type DriveSseResponseParameters = {
336
342
  /** Handles a request for the stream's next cumulative voucher. */
337
- onNeedVoucher(event: NeedVoucherEvent): Promise<void>;
343
+ onNeedVoucher(event: NeedVoucherEvent, signal?: AbortSignal | undefined): Promise<void>;
338
344
  /** Handles a payment receipt emitted by the stream. */
339
345
  onReceipt(receipt: SessionReceipt): void;
340
346
  /** Open SSE response to drive. */
@@ -351,6 +357,7 @@ export declare function handleSseNeedVoucher(parameters: {
351
357
  challenge: TempoSessionChallenge | null;
352
358
  driver: SsePaymentDriver;
353
359
  input: RequestInfo | URL;
360
+ signal?: AbortSignal | undefined;
354
361
  }, event: NeedVoucherEvent): Promise<void>;
355
362
  /** Options accepted by the auto-driving WebSocket session flow. */
356
363
  export type WebSocketDriverOptions = {
@@ -214,6 +214,7 @@ export async function resolveNeedVoucherContext(parameters) {
214
214
  channelId: parameters.expectedChannelId,
215
215
  deposit: eventAmounts.deposit,
216
216
  requiredCumulative: eventAmounts.requiredCumulative,
217
+ ...(parameters.signal ? { signal: parameters.signal } : {}),
217
218
  });
218
219
  const channel = parameters.getChannel();
219
220
  if (!channel || channel.channelId !== parameters.expectedChannelId) {
@@ -314,6 +315,7 @@ export async function postTopUp(parameters) {
314
315
  createSessionCredential: parameters.createSessionCredential,
315
316
  fetch: parameters.fetch,
316
317
  input: parameters.input,
318
+ signal: parameters.signal,
317
319
  });
318
320
  if (!response.ok)
319
321
  throw new Error(`Top-up POST failed with status ${response.status}`);
@@ -324,6 +326,7 @@ export async function postTopUp(parameters) {
324
326
  async function postManagementCredential(parameters) {
325
327
  const post = async (challenge) => parameters.fetch(managementInput(parameters.input), {
326
328
  method: 'POST',
329
+ ...(parameters.signal ? { signal: parameters.signal } : {}),
327
330
  headers: {
328
331
  [Constants.Headers.authorization]: await parameters.createSessionCredential(challenge, parameters.context),
329
332
  },
@@ -352,6 +355,8 @@ export async function retryHttpPaymentRequired(parameters) {
352
355
  channelId: snapshot.channelId,
353
356
  deposit: BigInt(snapshot.deposit),
354
357
  requiredCumulative,
358
+ signal: parameters.init?.signal ??
359
+ (parameters.input instanceof Request ? parameters.input.signal : undefined),
355
360
  });
356
361
  const currentChannel = parameters.getChannel();
357
362
  if (!currentChannel?.descriptor || currentChannel.channelId !== snapshot.channelId) {
@@ -453,7 +458,8 @@ export async function closeHttpSession(parameters) {
453
458
  * credential creation, and top-up policy stay in `SessionManager`.
454
459
  */
455
460
  export async function openSseSession(input, init, driver) {
456
- const { onReceipt, signal, ...fetchInit } = init ?? {};
461
+ const { onReceipt, signal: explicitSignal, ...fetchInit } = init ?? {};
462
+ const signal = explicitSignal ?? (input instanceof Request ? input.signal : undefined);
457
463
  const sseInit = {
458
464
  ...fetchInit,
459
465
  headers: {
@@ -475,7 +481,7 @@ export function consumeSseSessionResponse(input, response, options, driver) {
475
481
  throw new Error('Response has no body.');
476
482
  const challenge = driver.getChallenge();
477
483
  return iterateSseMessages({
478
- onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
484
+ onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input, signal: options?.signal }, event),
479
485
  onReceipt(receipt) {
480
486
  driver.acceptReceipt(receipt);
481
487
  options?.onReceipt?.(receipt);
@@ -535,7 +541,7 @@ export async function* driveSseResponse(parameters) {
535
541
  yield { data: event.data, raw };
536
542
  break;
537
543
  case 'payment-need-voucher':
538
- await parameters.onNeedVoucher(event.data);
544
+ await parameters.onNeedVoucher(event.data, parameters.signal);
539
545
  break;
540
546
  case 'payment-receipt':
541
547
  parameters.onReceipt(event.data);
@@ -632,6 +638,7 @@ export async function handleSseNeedVoucher(parameters, event) {
632
638
  expectedChannelId: channel.channelId,
633
639
  getChannel: parameters.driver.getChannel,
634
640
  input: parameters.input,
641
+ signal: parameters.signal,
635
642
  topUpIfNeeded: parameters.driver.topUpIfNeeded,
636
643
  });
637
644
  if (resolution.status !== 'ready')
@@ -642,6 +649,7 @@ export async function handleSseNeedVoucher(parameters, event) {
642
649
  createSessionCredential: parameters.driver.createSessionCredential,
643
650
  fetch: parameters.driver.fetch,
644
651
  input: parameters.input,
652
+ signal: parameters.signal,
645
653
  });
646
654
  if (!voucherResponse.ok) {
647
655
  throw new Error(`Voucher POST failed with status ${voucherResponse.status}`);
@@ -868,6 +876,7 @@ async function handleNeedVoucher(parameters, event) {
868
876
  expectedChannelId: parameters.socketState.channelId,
869
877
  getChannel: parameters.driver.getChannel,
870
878
  input: parameters.driver.httpUrl,
879
+ signal: parameters.driver.options?.signal,
871
880
  topUpIfNeeded: parameters.driver.topUpIfNeeded,
872
881
  });
873
882
  if (resolution.status === 'ignored') {
@@ -292,8 +292,23 @@ export async function simulateCredentialTransaction(parameters) {
292
292
  }
293
293
  function validateCredentialSponsorship(parameters) {
294
294
  const { allowedFeeTokens, challengeExpires, chainId, details, feePayer, feePayerPolicy, transaction, } = parameters;
295
- if (!feePayer || feePayer === true)
295
+ if (!feePayer)
296
296
  return;
297
+ if (feePayer === true) {
298
+ assertPrecompileFeePayerPolicy({ prepared: transaction, policy: feePayerPolicy });
299
+ if (feePayerPolicy?.allowKeyAuthorization === false &&
300
+ transaction.keyAuthorization !== undefined)
301
+ throw new BadRequestError({ reason: 'fee-payer policy keyAuthorization is not allowed' });
302
+ FeePayer.assertAllowedFeeToken(transaction, allowedFeeTokens);
303
+ FeePayer.assertTransactionPolicy({
304
+ challengeExpires,
305
+ chainId,
306
+ details,
307
+ policy: feePayerPolicy,
308
+ transaction,
309
+ });
310
+ return;
311
+ }
297
312
  FeePayer.prepareSponsoredTransaction({
298
313
  account: feePayer,
299
314
  allowedFeeTokens,
@@ -106,6 +106,8 @@ export type CloseCredentialPayload = {
106
106
  cumulativeAmount: RawAmountString;
107
107
  /** Voucher signature for `cumulativeAmount`. */
108
108
  signature: Hex;
109
+ /** Separate signature authorizing the cooperative close action. */
110
+ closeSignature: Hex;
109
111
  };
110
112
  /**
111
113
  * TIP20EscrowChannel precompile session credential payload (discriminated union).
@@ -22,10 +22,26 @@ export declare const voucherTypes: {
22
22
  readonly type: 'uint96';
23
23
  }];
24
24
  };
25
+ /** EIP-712 type that distinguishes cooperative close authorization from a voucher. */
26
+ export declare const closeAuthorizationTypes: {
27
+ readonly CloseAuthorization: readonly [{
28
+ readonly name: 'channelId';
29
+ readonly type: 'bytes32';
30
+ }, {
31
+ readonly name: 'cumulativeAmount';
32
+ readonly type: 'uint96';
33
+ }];
34
+ };
25
35
  /**
26
36
  * Sign a voucher with an account.
27
37
  */
28
38
  export declare function signVoucher(client: Client, account: Account, voucher: Voucher, verifyingContract: Address, chainId: number): Promise<Hex>;
39
+ /**
40
+ * Signs a distinct EIP-712 authorization for cooperatively closing a channel.
41
+ */
42
+ export declare function signCloseAuthorization(client: Client, account: Account, authorization: Voucher, verifyingContract: Address, chainId: number): Promise<Hex>;
43
+ /** Verifies that a cooperative-close signature belongs to a channel authority. */
44
+ export declare function verifyCloseAuthorization(escrowContract: Address, chainId: number, authorization: SignedVoucher, expectedSigners: readonly Address[]): boolean;
29
45
  /**
30
46
  * Verify a voucher signature matches the expected signer.
31
47
  *
@@ -29,6 +29,21 @@ export const voucherTypes = {
29
29
  { name: 'cumulativeAmount', type: 'uint96' },
30
30
  ],
31
31
  };
32
+ /** EIP-712 type that distinguishes cooperative close authorization from a voucher. */
33
+ export const closeAuthorizationTypes = {
34
+ CloseAuthorization: [
35
+ { name: 'channelId', type: 'bytes32' },
36
+ { name: 'cumulativeAmount', type: 'uint96' },
37
+ ],
38
+ };
39
+ function getCloseAuthorizationPayload(verifyingContract, chainId, authorization) {
40
+ return hashTypedData({
41
+ domain: getVoucherDomain(verifyingContract, chainId),
42
+ types: closeAuthorizationTypes,
43
+ primaryType: 'CloseAuthorization',
44
+ message: authorization,
45
+ });
46
+ }
32
47
  function getVoucherDigest(chainId, voucher) {
33
48
  return Channel.getVoucherSignPayload({
34
49
  chainId,
@@ -90,6 +105,39 @@ export async function signVoucher(client, account, voucher, verifyingContract, c
90
105
  throw new Error(`TIP-1034 vouchers require a TIP-1020 primitive signature; received "${envelope.type}".`);
91
106
  return SignatureEnvelope.serialize(envelope);
92
107
  }
108
+ /**
109
+ * Signs a distinct EIP-712 authorization for cooperatively closing a channel.
110
+ */
111
+ export async function signCloseAuthorization(client, account, authorization, verifyingContract, chainId) {
112
+ const signature = await signTypedData(client, {
113
+ account,
114
+ domain: getVoucherDomain(verifyingContract, chainId),
115
+ types: closeAuthorizationTypes,
116
+ primaryType: 'CloseAuthorization',
117
+ message: authorization,
118
+ });
119
+ const envelope = SignatureEnvelope.from(signature);
120
+ if (!isPrimitiveEnvelope(envelope.type))
121
+ throw new Error(`TIP-1034 close authorizations require a TIP-1020 primitive signature; received "${envelope.type}".`);
122
+ return SignatureEnvelope.serialize(envelope);
123
+ }
124
+ /** Verifies that a cooperative-close signature belongs to a channel authority. */
125
+ export function verifyCloseAuthorization(escrowContract, chainId, authorization, expectedSigners) {
126
+ try {
127
+ const envelope = SignatureEnvelope.from(authorization.signature);
128
+ if (!isPrimitiveEnvelope(envelope.type))
129
+ return false;
130
+ if (SignatureEnvelope.serialize(envelope).toLowerCase() !== authorization.signature.toLowerCase())
131
+ return false;
132
+ const payload = getCloseAuthorizationPayload(escrowContract, chainId, authorization);
133
+ const signer = SignatureEnvelope.extractAddress({ payload, signature: envelope });
134
+ const valid = SignatureEnvelope.verify(envelope, { address: signer, payload });
135
+ return valid && expectedSigners.some((expected) => TempoAddress.isEqual(signer, expected));
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
93
141
  /**
94
142
  * Verify a voucher signature matches the expected signer.
95
143
  *
@@ -189,6 +189,8 @@ export type VerifyAndAcceptVoucherParameters = {
189
189
  methodDetails: SessionMethodDetails;
190
190
  /** Minimum allowed voucher delta in raw units. */
191
191
  minVoucherDelta: bigint;
192
+ /** Whether this presentation must advance the accepted voucher. */
193
+ requireAdvance?: boolean | undefined;
192
194
  /** Server channel store. */
193
195
  store: ChannelStore;
194
196
  /** Signed cumulative voucher to verify and accept. */
@@ -255,6 +257,14 @@ export interface BaseState {
255
257
  token: Address;
256
258
  /** Number of charge operations (API requests) fulfilled in the current session. */
257
259
  units: number;
260
+ /** Uncommitted stream charges shared across workers. */
261
+ streamReservations?: Record<string, {
262
+ amount: bigint;
263
+ units: number;
264
+ expiresAt: number;
265
+ }> | undefined;
266
+ /** Active cross-worker claim for server-scheduled settlement. */
267
+ scheduledSettlementLease?: ScheduledSettlementLease | undefined;
258
268
  /** ISO 8601 timestamp of the last server-scheduled settlement. */
259
269
  lastSettlementAt?: string | undefined;
260
270
  /** Cumulative spent value when the last server-scheduled settlement ran. */
@@ -262,6 +272,11 @@ export interface BaseState {
262
272
  /** Charge operation count when the last server-scheduled settlement ran. */
263
273
  lastSettlementUnits?: number | undefined;
264
274
  }
275
+ /** Expiring ownership claim for one server-scheduled settlement attempt. */
276
+ export type ScheduledSettlementLease = {
277
+ expiresAt: number;
278
+ owner: string;
279
+ };
265
280
  /** Returns whether a channel is backed by the TIP20EscrowChannel precompile. */
266
281
  export declare function isPrecompileState(state: State): state is BaseState & PrecompileBackendState;
267
282
  /** Returns the greater bigint value without decreasing persisted monotonic state. */
@@ -315,6 +330,8 @@ export declare function verifyAndAcceptVoucher(parameters: VerifyAndAcceptVouche
315
330
  * - **D1 / SQL**: Database transactions
316
331
  */
317
332
  export type ChannelStore = {
333
+ /** False when the backing store only serializes mutations within this process. */
334
+ atomic?: boolean | undefined;
318
335
  getChannel(channelId: Hex): Promise<State | null>;
319
336
  /**
320
337
  * Atomic read-modify-write for channel state.
@@ -327,11 +344,12 @@ export type ChannelStore = {
327
344
  * Returns a `Promise` that resolves once `updateChannel` is called for
328
345
  * `channelId`. Implementations should resolve immediately if the channel
329
346
  * was updated between the call to `waitForUpdate` and the `Promise`
330
- * being awaited.
347
+ * being awaited. Implementations must reject and release the waiter when
348
+ * `signal` aborts.
331
349
  *
332
350
  * When not implemented, callers fall back to polling.
333
351
  */
334
- waitForUpdate?(channelId: Hex): Promise<void>;
352
+ waitForUpdate?(channelId: Hex, signal?: AbortSignal): Promise<void>;
335
353
  /**
336
354
  * Atomic read-modify-write that returns the callback's `result` directly.
337
355
  *
@@ -353,4 +371,6 @@ export declare function fromStore(store: Store.Store | Store.AtomicStore): Chann
353
371
  * when the channel exists but cannot currently be charged.
354
372
  */
355
373
  export declare function deductFromChannel(store: ChannelStore, channelId: State['channelId'], amount: bigint): Promise<DeductResult>;
374
+ /** Returns unexpired stream reservations that ordinary deductions must respect. */
375
+ export declare function reservedStreamAmount(state: Pick<State, 'streamReservations'>): bigint;
356
376
  //# sourceMappingURL=ChannelStore.d.ts.map
@@ -224,9 +224,11 @@ export async function validateVoucher(parameters) {
224
224
  }
225
225
  /** Verifies a cumulative voucher and returns a session receipt after store reconciliation. */
226
226
  export async function verifyAndAcceptVoucher(parameters) {
227
- const { store, challenge, channel, voucher, channelState } = parameters;
227
+ const { store, challenge, channel, voucher, channelState, requireAdvance } = parameters;
228
228
  const validation = await validateVoucher(parameters);
229
- if (validation.alreadyAccepted)
229
+ if (validation.alreadyAccepted) {
230
+ if (requireAdvance)
231
+ throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
230
232
  return createSessionReceipt({
231
233
  challengeId: challenge.id,
232
234
  channelId: voucher.channelId,
@@ -234,7 +236,14 @@ export async function verifyAndAcceptVoucher(parameters) {
234
236
  spent: channel.spent,
235
237
  units: channel.units,
236
238
  });
237
- const updated = await store.updateChannel(voucher.channelId, (current) => acceptVoucherStateUpdate({ channelState, current, voucher }));
239
+ }
240
+ const updated = await store.updateChannel(voucher.channelId, (current) => {
241
+ if (requireAdvance &&
242
+ current !== null &&
243
+ voucher.cumulativeAmount <= current.highestVoucherAmount)
244
+ throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
245
+ return acceptVoucherStateUpdate({ channelState, current, voucher });
246
+ });
238
247
  if (!updated)
239
248
  throw new ChannelNotFoundError({ reason: 'channel not found' });
240
249
  return createSessionReceipt({
@@ -348,6 +357,7 @@ export function fromStore(store) {
348
357
  }
349
358
  }
350
359
  const cs = {
360
+ atomic: typeof atomicUpdate === 'function',
351
361
  async getChannel(channelId) {
352
362
  const normalizedChannelId = normalizeChannelId(channelId);
353
363
  return normalizeMaybeState(normalizedChannelId, await stateStore.get(normalizedChannelId));
@@ -355,15 +365,30 @@ export function fromStore(store) {
355
365
  async updateChannel(channelId, fn) {
356
366
  return update(channelId, fn);
357
367
  },
358
- waitForUpdate(channelId) {
359
- return new Promise((resolve) => {
368
+ waitForUpdate(channelId, signal) {
369
+ return new Promise((resolve, reject) => {
360
370
  const normalizedChannelId = normalizeChannelId(channelId);
371
+ if (signal?.aborted) {
372
+ reject(signal.reason ?? new Error('aborted'));
373
+ return;
374
+ }
361
375
  let set = runtime.waiters.get(normalizedChannelId);
362
376
  if (!set) {
363
377
  set = new Set();
364
378
  runtime.waiters.set(normalizedChannelId, set);
365
379
  }
366
- set.add(resolve);
380
+ const settle = () => {
381
+ signal?.removeEventListener('abort', onAbort);
382
+ resolve();
383
+ };
384
+ const onAbort = () => {
385
+ set?.delete(settle);
386
+ if (set?.size === 0)
387
+ runtime.waiters.delete(normalizedChannelId);
388
+ reject(signal?.reason ?? new Error('aborted'));
389
+ };
390
+ signal?.addEventListener('abort', onAbort, { once: true });
391
+ set.add(settle);
367
392
  });
368
393
  },
369
394
  };
@@ -404,9 +429,14 @@ function planDeduction(current, amount) {
404
429
  return { op: 'noop', result: { ok: false, channel: current } };
405
430
  if (current.closeRequestedAt !== 0n)
406
431
  return { op: 'noop', result: { ok: false, channel: current } };
407
- if (current.highestVoucherAmount - current.spent < amount)
432
+ if (current.highestVoucherAmount - current.spent - reservedStreamAmount(current) < amount)
408
433
  return { op: 'noop', result: { ok: false, channel: current } };
409
434
  const next = { ...current, spent: current.spent + amount, units: current.units + 1 };
410
435
  return { op: 'set', value: next, result: { ok: true, channel: next } };
411
436
  }
437
+ /** Returns unexpired stream reservations that ordinary deductions must respect. */
438
+ export function reservedStreamAmount(state) {
439
+ const now = Date.now();
440
+ return Object.values(state.streamReservations ?? {}).reduce((total, reservation) => total + (reservation.expiresAt > now ? reservation.amount : 0n), 0n);
441
+ }
412
442
  //# sourceMappingURL=ChannelStore.js.map
@@ -79,6 +79,8 @@ export type BroadcastCredentialPayloadParameters = {
79
79
  onSessionSettlement?: OnSessionSettlement | undefined;
80
80
  /** Discriminated session credential payload to verify. */
81
81
  payload: SessionCredentialPayload;
82
+ /** Whether an open or voucher credential must add new funds for this request. */
83
+ requireVoucherAdvance?: boolean | undefined;
82
84
  /** Server-side channel store. */
83
85
  store: ChannelStore.ChannelStore;
84
86
  };
@@ -102,7 +104,10 @@ export type VoucherCredentialActionParameters = BroadcastCredentialActionParamet
102
104
  /** Inputs for broadcasting and settling a cooperative close credential. */
103
105
  export type CloseCredentialActionParameters = BroadcastCredentialActionParameters<'close'>;
104
106
  /** Inputs for validating a session credential without applying its state transition. */
105
- export type ValidateCredentialPayloadParameters = Pick<BroadcastCredentialPayloadParameters, 'account' | 'channelStateTtl' | 'chainId' | 'client' | 'credentialSource' | 'escrow' | 'expectedOperator' | 'feePayer' | 'feePayerPolicy' | 'feeToken' | 'lastOnChainVerified' | 'minVoucherDelta' | 'payload' | 'store' | 'challenge'>;
107
+ export type ValidateCredentialPayloadParameters = Pick<BroadcastCredentialPayloadParameters, 'account' | 'channelStateTtl' | 'chainId' | 'client' | 'credentialSource' | 'escrow' | 'expectedOperator' | 'feePayer' | 'feePayerPolicy' | 'feeToken' | 'lastOnChainVerified' | 'minVoucherDelta' | 'payload' | 'store' | 'challenge'> & {
108
+ /** Allows terminal broadcast to recover accepted funds before rejecting a pending close. */
109
+ allowPendingClose?: boolean | undefined;
110
+ };
106
111
  /** Non-mutating result produced by session credential validation. */
107
112
  export type CredentialPayloadValidation = {
108
113
  /** Session action validated from the credential. */