mppx 0.12.0 → 0.13.2

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 (110) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/Method.d.ts +7 -1
  3. package/dist/Method.js +7 -2
  4. package/dist/Receipt.d.ts +2 -0
  5. package/dist/Receipt.js +2 -0
  6. package/dist/cli/cli.d.ts +8 -0
  7. package/dist/cli/cli.js +134 -136
  8. package/dist/cli/plugins/index.js +12 -14
  9. package/dist/cli/plugins/tempo.js +9 -2
  10. package/dist/cli/sessions/request.d.ts +1 -1
  11. package/dist/cli/sessions/request.js +6 -8
  12. package/dist/cli/utils.d.ts +11 -1
  13. package/dist/cli/utils.js +15 -2
  14. package/dist/cli/validate/challenge.d.ts +2 -0
  15. package/dist/cli/validate/challenge.js +19 -4
  16. package/dist/cli/validate/discovery.d.ts +1 -0
  17. package/dist/cli/validate/discovery.js +10 -1
  18. package/dist/cli/validate/payment.js +34 -12
  19. package/dist/client/Methods.d.ts +0 -2
  20. package/dist/client/Methods.js +0 -2
  21. package/dist/client/Mppx.js +1 -0
  22. package/dist/client/index.d.ts +1 -1
  23. package/dist/client/index.js +1 -1
  24. package/dist/client/internal/Fetch.d.ts +2 -0
  25. package/dist/client/internal/Fetch.js +22 -7
  26. package/dist/client/internal/MethodChallenge.d.ts +1 -0
  27. package/dist/evm/Types.js +1 -1
  28. package/dist/evm/server/Charge.js +5 -0
  29. package/dist/integrations/x402/Evm.js +4 -3
  30. package/dist/internal/HeaderCodec.d.ts +2 -2
  31. package/dist/internal/version.d.ts +2 -2
  32. package/dist/internal/version.js +1 -1
  33. package/dist/mcp/client/McpClient.js +4 -7
  34. package/dist/middlewares/elysia.d.ts +11 -8
  35. package/dist/middlewares/elysia.js +67 -14
  36. package/dist/middlewares/express.js +13 -6
  37. package/dist/middlewares/hono.js +4 -4
  38. package/dist/middlewares/nextjs.js +9 -5
  39. package/dist/proxy/internal/Headers.js +65 -0
  40. package/dist/proxy/services/stripe.js +1 -0
  41. package/dist/server/Mppx.d.ts +14 -2
  42. package/dist/server/Mppx.js +85 -44
  43. package/dist/server/NodeListener.js +71 -6
  44. package/dist/server/Request.d.ts +11 -1
  45. package/dist/server/Request.js +132 -13
  46. package/dist/server/Transport.d.ts +14 -1
  47. package/dist/server/Transport.js +1 -0
  48. package/dist/stripe/server/Methods.d.ts +10 -3
  49. package/dist/stripe/server/Methods.js +17 -2
  50. package/dist/stripe/server/internal/html.gen.js +1 -1
  51. package/dist/tempo/Methods.d.ts +1 -0
  52. package/dist/tempo/Methods.js +1 -0
  53. package/dist/tempo/client/Charge.js +17 -13
  54. package/dist/tempo/client/Methods.d.ts +1 -5
  55. package/dist/tempo/client/Methods.js +10 -5
  56. package/dist/tempo/client/ResolveAccount.d.ts +3 -0
  57. package/dist/tempo/client/index.d.ts +0 -3
  58. package/dist/tempo/client/index.js +0 -2
  59. package/dist/tempo/internal/fee-payer.d.ts +6 -3
  60. package/dist/tempo/internal/fee-payer.js +23 -6
  61. package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
  62. package/dist/tempo/internal/machine-token-charge.js +2 -1
  63. package/dist/tempo/legacy/index.d.ts +1 -3
  64. package/dist/tempo/legacy/index.js +1 -3
  65. package/dist/tempo/server/Charge.js +87 -53
  66. package/dist/tempo/server/Methods.d.ts +55 -10
  67. package/dist/tempo/server/Subscription.d.ts +1 -1
  68. package/dist/tempo/server/Subscription.js +31 -4
  69. package/dist/tempo/server/internal/html.gen.js +1 -1
  70. package/dist/tempo/server/internal/transport.js +28 -11
  71. package/dist/tempo/session/client/ChannelOps.js +5 -0
  72. package/dist/tempo/session/client/Session.d.ts +1 -0
  73. package/dist/tempo/session/client/Session.js +7 -4
  74. package/dist/tempo/session/client/SessionManager.js +3 -0
  75. package/dist/tempo/session/client/Transports.d.ts +8 -1
  76. package/dist/tempo/session/client/Transports.js +12 -3
  77. package/dist/tempo/session/precompile/Chain.js +16 -1
  78. package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
  79. package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
  80. package/dist/tempo/session/precompile/Voucher.js +48 -0
  81. package/dist/tempo/session/server/ChannelStore.d.ts +20 -2
  82. package/dist/tempo/session/server/ChannelStore.js +25 -4
  83. package/dist/tempo/session/server/CredentialVerification.d.ts +4 -1
  84. package/dist/tempo/session/server/CredentialVerification.js +34 -6
  85. package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
  86. package/dist/tempo/session/server/MeteredStream.js +85 -29
  87. package/dist/tempo/session/server/Session.d.ts +2 -1
  88. package/dist/tempo/session/server/Session.js +2 -1
  89. package/dist/tempo/session/server/Settlement.d.ts +21 -0
  90. package/dist/tempo/session/server/Settlement.js +92 -17
  91. package/dist/tempo/session/server/Sse.d.ts +3 -1
  92. package/dist/tempo/session/server/Sse.js +105 -32
  93. package/dist/tempo/session/server/Transports.d.ts +24 -19
  94. package/dist/tempo/session/server/Transports.js +169 -58
  95. package/dist/tempo/session/server/Ws.d.ts +5 -1
  96. package/dist/tempo/session/server/Ws.js +75 -20
  97. package/dist/tempo/subscription/Store.d.ts +2 -0
  98. package/dist/tempo/subscription/Store.js +3 -0
  99. package/dist/validation/core.js +2 -1
  100. package/dist/x402/server/EvmCharge.d.ts +3 -1
  101. package/dist/x402/server/EvmCharge.js +14 -1
  102. package/package.json +6 -6
  103. package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
  104. package/dist/tempo/legacy/client/ChannelOps.js +0 -168
  105. package/dist/tempo/legacy/client/Session.d.ts +0 -185
  106. package/dist/tempo/legacy/client/Session.js +0 -279
  107. package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
  108. package/dist/tempo/legacy/client/SessionManager.js +0 -706
  109. package/dist/tempo/legacy/client/index.d.ts +0 -7
  110. 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
  *
@@ -257,6 +257,14 @@ export interface BaseState {
257
257
  token: Address;
258
258
  /** Number of charge operations (API requests) fulfilled in the current session. */
259
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;
260
268
  /** ISO 8601 timestamp of the last server-scheduled settlement. */
261
269
  lastSettlementAt?: string | undefined;
262
270
  /** Cumulative spent value when the last server-scheduled settlement ran. */
@@ -264,6 +272,11 @@ export interface BaseState {
264
272
  /** Charge operation count when the last server-scheduled settlement ran. */
265
273
  lastSettlementUnits?: number | undefined;
266
274
  }
275
+ /** Expiring ownership claim for one server-scheduled settlement attempt. */
276
+ export type ScheduledSettlementLease = {
277
+ expiresAt: number;
278
+ owner: string;
279
+ };
267
280
  /** Returns whether a channel is backed by the TIP20EscrowChannel precompile. */
268
281
  export declare function isPrecompileState(state: State): state is BaseState & PrecompileBackendState;
269
282
  /** Returns the greater bigint value without decreasing persisted monotonic state. */
@@ -317,6 +330,8 @@ export declare function verifyAndAcceptVoucher(parameters: VerifyAndAcceptVouche
317
330
  * - **D1 / SQL**: Database transactions
318
331
  */
319
332
  export type ChannelStore = {
333
+ /** False when the backing store only serializes mutations within this process. */
334
+ atomic?: boolean | undefined;
320
335
  getChannel(channelId: Hex): Promise<State | null>;
321
336
  /**
322
337
  * Atomic read-modify-write for channel state.
@@ -329,11 +344,12 @@ export type ChannelStore = {
329
344
  * Returns a `Promise` that resolves once `updateChannel` is called for
330
345
  * `channelId`. Implementations should resolve immediately if the channel
331
346
  * was updated between the call to `waitForUpdate` and the `Promise`
332
- * being awaited.
347
+ * being awaited. Implementations must reject and release the waiter when
348
+ * `signal` aborts.
333
349
  *
334
350
  * When not implemented, callers fall back to polling.
335
351
  */
336
- waitForUpdate?(channelId: Hex): Promise<void>;
352
+ waitForUpdate?(channelId: Hex, signal?: AbortSignal): Promise<void>;
337
353
  /**
338
354
  * Atomic read-modify-write that returns the callback's `result` directly.
339
355
  *
@@ -355,4 +371,6 @@ export declare function fromStore(store: Store.Store | Store.AtomicStore): Chann
355
371
  * when the channel exists but cannot currently be charged.
356
372
  */
357
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;
358
376
  //# sourceMappingURL=ChannelStore.d.ts.map
@@ -357,6 +357,7 @@ export function fromStore(store) {
357
357
  }
358
358
  }
359
359
  const cs = {
360
+ atomic: typeof atomicUpdate === 'function',
360
361
  async getChannel(channelId) {
361
362
  const normalizedChannelId = normalizeChannelId(channelId);
362
363
  return normalizeMaybeState(normalizedChannelId, await stateStore.get(normalizedChannelId));
@@ -364,15 +365,30 @@ export function fromStore(store) {
364
365
  async updateChannel(channelId, fn) {
365
366
  return update(channelId, fn);
366
367
  },
367
- waitForUpdate(channelId) {
368
- return new Promise((resolve) => {
368
+ waitForUpdate(channelId, signal) {
369
+ return new Promise((resolve, reject) => {
369
370
  const normalizedChannelId = normalizeChannelId(channelId);
371
+ if (signal?.aborted) {
372
+ reject(signal.reason ?? new Error('aborted'));
373
+ return;
374
+ }
370
375
  let set = runtime.waiters.get(normalizedChannelId);
371
376
  if (!set) {
372
377
  set = new Set();
373
378
  runtime.waiters.set(normalizedChannelId, set);
374
379
  }
375
- 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);
376
392
  });
377
393
  },
378
394
  };
@@ -413,9 +429,14 @@ function planDeduction(current, amount) {
413
429
  return { op: 'noop', result: { ok: false, channel: current } };
414
430
  if (current.closeRequestedAt !== 0n)
415
431
  return { op: 'noop', result: { ok: false, channel: current } };
416
- if (current.highestVoucherAmount - current.spent < amount)
432
+ if (current.highestVoucherAmount - current.spent - reservedStreamAmount(current) < amount)
417
433
  return { op: 'noop', result: { ok: false, channel: current } };
418
434
  const next = { ...current, spent: current.spent + amount, units: current.units + 1 };
419
435
  return { op: 'set', value: next, result: { ok: true, channel: next } };
420
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
+ }
421
442
  //# sourceMappingURL=ChannelStore.js.map
@@ -104,7 +104,10 @@ export type VoucherCredentialActionParameters = BroadcastCredentialActionParamet
104
104
  /** Inputs for broadcasting and settling a cooperative close credential. */
105
105
  export type CloseCredentialActionParameters = BroadcastCredentialActionParameters<'close'>;
106
106
  /** Inputs for validating a session credential without applying its state transition. */
107
- 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
+ };
108
111
  /** Non-mutating result produced by session credential validation. */
109
112
  export type CredentialPayloadValidation = {
110
113
  /** Session action validated from the credential. */