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
@@ -1,5 +1,5 @@
1
1
  import { isAddress, isAddressEqual, zeroAddress, } from 'viem';
2
- import { AmountExceedsDepositError, ChannelClosedError, ChannelNotFoundError, InsufficientBalanceError, InvalidSignatureError, VerificationFailedError, } from '../../../Errors.js';
2
+ import { AmountExceedsDepositError, ChannelClosedError, ChannelNotFoundError, DeltaTooSmallError, InsufficientBalanceError, InvalidSignatureError, VerificationFailedError, } from '../../../Errors.js';
3
3
  import * as Chain from '../precompile/Chain.js';
4
4
  import { readChannelClosedReceiptFields } from '../precompile/Chain.js';
5
5
  import * as Channel from '../precompile/Channel.js';
@@ -7,7 +7,7 @@ import { createSessionReceipt, uint96, } from '../precompile/Protocol.js';
7
7
  import * as Voucher from '../precompile/Voucher.js';
8
8
  import * as ChannelStore from './ChannelStore.js';
9
9
  import { getChallengePaymentFields } from './RequestState.js';
10
- import { assertSettlementSender, getClientAccount } from './Settlement.js';
10
+ import { assertSettlementSender, getClientAccount, maybeSettleScheduled, } from './Settlement.js';
11
11
  /** Returns the effective voucher signer for a TIP-1034 descriptor. */
12
12
  export function authorizedSigner(descriptor) {
13
13
  return isAddressEqual(descriptor.authorizedSigner, zeroAddress)
@@ -233,6 +233,7 @@ export function requireSessionCredentialPayload(payload) {
233
233
  descriptor: readDescriptor(candidate.descriptor),
234
234
  cumulativeAmount: readRawAmount(candidate.cumulativeAmount, 'cumulativeAmount'),
235
235
  signature: readHex(candidate.signature, 'signature'),
236
+ closeSignature: readHex(candidate.closeSignature, 'closeSignature'),
236
237
  };
237
238
  }
238
239
  }
@@ -375,6 +376,8 @@ async function validateVoucherCredential(parameters, payload) {
375
376
  escrow,
376
377
  lastOnChainVerified,
377
378
  });
379
+ if (parameters.allowPendingClose && channelState.closeRequestedAt !== 0)
380
+ return;
378
381
  await ChannelStore.validateVoucher({
379
382
  channel,
380
383
  channelState,
@@ -384,10 +387,12 @@ async function validateVoucherCredential(parameters, payload) {
384
387
  });
385
388
  }
386
389
  async function resolveVoucherChannelState(parameters) {
387
- const { channel, channelId, channelStateTtl, client, escrow, lastOnChainVerified } = parameters;
388
- const isStale = Date.now() - (lastOnChainVerified.get(channelId) ?? 0) > channelStateTtl;
389
- const state = isStale ? await Chain.getChannelState(client, channelId, escrow) : undefined;
390
- if (state)
390
+ const { channel, channelId, channelStateTtl, client, escrow, forceRefresh, lastOnChainVerified } = parameters;
391
+ const shouldRefresh = forceRefresh || Date.now() - (lastOnChainVerified.get(channelId) ?? 0) > channelStateTtl;
392
+ const state = shouldRefresh ? await Chain.getChannelState(client, channelId, escrow) : undefined;
393
+ if (state?.closeRequestedAt)
394
+ lastOnChainVerified.delete(channelId);
395
+ else if (state)
391
396
  lastOnChainVerified.set(channelId, Date.now());
392
397
  return {
393
398
  deposit: state?.deposit ?? uint96(channel.deposit),
@@ -410,6 +415,9 @@ async function validateCloseCredential(parameters, payload) {
410
415
  escrow,
411
416
  store,
412
417
  });
418
+ const closeAuthorized = Voucher.verifyCloseAuthorization(escrow, chainId, { channelId, cumulativeAmount, signature: payload.closeSignature }, [channel.payer, channel.authorizedSigner]);
419
+ if (!closeAuthorized)
420
+ throw new InvalidSignatureError({ reason: 'invalid close authorization signature' });
413
421
  if (channel.finalized)
414
422
  throw new ChannelClosedError({ reason: 'channel is already finalized' });
415
423
  const state = await Chain.getChannelState(client, channelId, escrow);
@@ -513,18 +521,26 @@ async function handleOpenCredential(parameters) {
513
521
  const { descriptor, state } = result;
514
522
  assertSameDescriptor(descriptor, payload.descriptor);
515
523
  validateChannelState(state, request.amount);
516
- const updated = await store.updateChannel(channelId, (current) => ChannelStore.openChannelState({
517
- authorizedSigner: authorizedSigner(descriptor),
518
- chainId,
519
- channelId,
520
- current,
521
- descriptor,
522
- escrow,
523
- expiringNonceHash: result.expiringNonceHash,
524
- cumulativeAmount,
525
- signature: payload.signature,
526
- state,
527
- }));
524
+ const updated = await store.updateChannel(channelId, (current) => {
525
+ if (parameters.requireVoucherAdvance &&
526
+ current !== null &&
527
+ cumulativeAmount <= current.highestVoucherAmount)
528
+ throw new DeltaTooSmallError({
529
+ reason: 'voucher does not add new funds for this request',
530
+ });
531
+ return ChannelStore.openChannelState({
532
+ authorizedSigner: authorizedSigner(descriptor),
533
+ chainId,
534
+ channelId,
535
+ current,
536
+ descriptor,
537
+ escrow,
538
+ expiringNonceHash: result.expiringNonceHash,
539
+ cumulativeAmount,
540
+ signature: payload.signature,
541
+ state,
542
+ });
543
+ });
528
544
  if (!updated)
529
545
  throw new VerificationFailedError({ reason: 'failed to create channel' });
530
546
  return createSessionReceipt({
@@ -603,21 +619,39 @@ async function handleVoucherCredential(parameters) {
603
619
  channelStateTtl,
604
620
  client,
605
621
  escrow,
622
+ forceRefresh: true,
606
623
  lastOnChainVerified,
607
624
  });
608
625
  if (channelState.closeRequestedAt !== 0) {
609
- await store.updateChannel(channelId, (current) => current
626
+ const closing = await store.updateChannel(channelId, (current) => current
610
627
  ? {
611
628
  ...current,
629
+ settledOnChain: channelState.settled > current.settledOnChain
630
+ ? channelState.settled
631
+ : current.settledOnChain,
612
632
  closeRequestedAt: BigInt(channelState.closeRequestedAt) > current.closeRequestedAt
613
633
  ? BigInt(channelState.closeRequestedAt)
614
634
  : current.closeRequestedAt,
615
635
  }
616
636
  : current);
637
+ if (closing && !closing.finalized && channelState.deposit > 0n)
638
+ await maybeSettleScheduled({
639
+ account: parameters.account,
640
+ channel: closing,
641
+ client,
642
+ feePayer: parameters.feePayer,
643
+ feePayerPolicy: parameters.feePayerPolicy,
644
+ feeToken: parameters.feeToken,
645
+ onSessionSettlement: parameters.onSessionSettlement,
646
+ schedule: {},
647
+ store,
648
+ });
649
+ throw new ChannelClosedError({ reason: 'channel has a pending close request' });
617
650
  }
618
651
  return ChannelStore.verifyAndAcceptVoucher({
619
652
  store,
620
653
  minVoucherDelta,
654
+ requireAdvance: parameters.requireVoucherAdvance,
621
655
  challenge,
622
656
  channel,
623
657
  voucher,
@@ -640,6 +674,9 @@ async function handleCloseCredential(parameters) {
640
674
  escrow,
641
675
  store,
642
676
  });
677
+ const closeAuthorized = Voucher.verifyCloseAuthorization(escrow, chainId, { channelId, cumulativeAmount, signature: payload.closeSignature }, [channel.payer, channel.authorizedSigner]);
678
+ if (!closeAuthorized)
679
+ throw new InvalidSignatureError({ reason: 'invalid close authorization signature' });
643
680
  if (channel.finalized)
644
681
  throw new ChannelClosedError({ reason: 'channel is already finalized' });
645
682
  const state = await Chain.getChannelState(client, channelId, escrow);
@@ -8,8 +8,9 @@ export type SessionController = {
8
8
  * Reserve voucher coverage for the next emitted chunk.
9
9
  *
10
10
  * The reservation blocks until sufficient voucher headroom exists, but the
11
- * charge is only committed once a chunk is actually emitted. If the stream
12
- * ends or aborts before that emission, the reservation is dropped.
11
+ * charge is committed when a chunk is emitted or when the generator finishes
12
+ * successfully. A reservation is only dropped when the stream aborts or the
13
+ * generator fails before the charge can be committed.
13
14
  *
14
15
  * Pass an explicit raw-unit `amount` for request-aware or otherwise dynamic
15
16
  * pricing. When omitted, the session challenge's configured tick cost is
@@ -1,46 +1,102 @@
1
- import { commitReservedCharges, reserveChargeOrWait } from './Transports.js';
1
+ import { commitReservedCharges, releaseReservedCharges, renewReservedCharges, reserveChargeOrWait, streamReservationLeaseMs, } from './Transports.js';
2
2
  /** Applies voucher reservation and spend commits to an async session stream. */
3
3
  export async function* meterIterable(options) {
4
4
  let prepaidUnits = options.prepaidUnits ?? 0;
5
- let reservedAmount = 0n;
6
- let reservedUnits = 0;
5
+ const controller = new AbortController();
6
+ const signal = controller.signal;
7
+ const reservation = {
8
+ store: options.store,
9
+ channelId: options.channelId,
10
+ reservationId: globalThis.crypto.randomUUID(),
11
+ };
12
+ let pending = false;
13
+ let cancelled = false;
14
+ const onAbort = () => {
15
+ if (signal.aborted)
16
+ return;
17
+ cancelled = true;
18
+ controller.abort(options.signal?.reason);
19
+ };
20
+ options.signal?.addEventListener('abort', onAbort, { once: true });
21
+ if (options.signal?.aborted)
22
+ onAbort();
23
+ const renewal = setInterval(() => {
24
+ if (pending)
25
+ void renewReservedCharges(reservation).catch((error) => controller.abort(error));
26
+ }, streamReservationLeaseMs / 3);
27
+ renewal.unref?.();
28
+ const releaseOnAbort = () => {
29
+ clearInterval(renewal);
30
+ void releaseReservedCharges(reservation).catch(() => { });
31
+ };
32
+ signal.addEventListener('abort', releaseOnAbort, { once: true });
33
+ if (signal.aborted)
34
+ releaseOnAbort();
7
35
  const charge = async (amount) => {
36
+ signal.throwIfAborted();
8
37
  if (amount === undefined && prepaidUnits > 0) {
9
38
  prepaidUnits -= 1;
10
39
  return;
11
40
  }
12
- const resolvedAmount = amount ?? options.tickCost;
41
+ if ((amount ?? options.tickCost) === 0n)
42
+ return;
13
43
  await reserveChargeOrWait({
14
- store: options.store,
15
- channelId: options.channelId,
16
- amount: resolvedAmount,
17
- reservedAmount,
44
+ ...reservation,
45
+ requireExisting: pending,
46
+ amount: amount ?? options.tickCost,
18
47
  emit: options.emitNeedVoucher,
19
48
  formatNeedVoucher: options.formatNeedVoucher,
20
49
  pollIntervalMs: options.pollIntervalMs,
21
- signal: options.signal,
50
+ signal,
22
51
  });
23
- reservedAmount += resolvedAmount;
24
- reservedUnits += 1;
52
+ pending = true;
53
+ signal.throwIfAborted();
25
54
  };
26
- const signal = options.signal ?? new AbortController().signal;
27
- const iterable = typeof options.generate === 'function' ? options.generate({ charge, signal }) : options.generate;
28
- for await (const value of iterable) {
29
- if (options.signal?.aborted)
30
- break;
31
- if (typeof options.generate !== 'function')
32
- await charge();
33
- const channel = await commitReservedCharges({
34
- store: options.store,
35
- channelId: options.channelId,
36
- amount: reservedAmount,
37
- units: reservedUnits,
38
- });
39
- reservedAmount = 0n;
40
- reservedUnits = 0;
41
- if (channel)
42
- await options.onChargeCommitted?.(channel);
43
- yield value;
55
+ const commit = async () => {
56
+ signal.throwIfAborted();
57
+ if (!pending)
58
+ return;
59
+ const channel = await commitReservedCharges(reservation);
60
+ if (!channel)
61
+ throw new Error('stream reservation was lost');
62
+ pending = false;
63
+ await options.onChargeCommitted?.(channel);
64
+ };
65
+ try {
66
+ const manual = typeof options.generate === 'function';
67
+ const iterable = typeof options.generate === 'function'
68
+ ? options.generate({ charge, signal })
69
+ : options.generate;
70
+ const iterator = iterable[Symbol.asyncIterator]();
71
+ try {
72
+ while (!signal.aborted) {
73
+ const { done, value } = await iterator.next();
74
+ if (signal.aborted)
75
+ break;
76
+ if (done) {
77
+ if (manual)
78
+ await commit();
79
+ break;
80
+ }
81
+ if (!manual)
82
+ await charge();
83
+ await commit();
84
+ yield value;
85
+ }
86
+ if (!cancelled)
87
+ signal.throwIfAborted();
88
+ }
89
+ finally {
90
+ clearInterval(renewal);
91
+ await releaseReservedCharges(reservation);
92
+ await iterator.return?.();
93
+ }
94
+ }
95
+ finally {
96
+ clearInterval(renewal);
97
+ options.signal?.removeEventListener('abort', onAbort);
98
+ signal.removeEventListener('abort', releaseOnAbort);
99
+ await releaseReservedCharges(reservation);
44
100
  }
45
101
  }
46
102
  //# sourceMappingURL=MeteredStream.js.map
@@ -18,6 +18,10 @@ export type ResolveSessionSnapshotParameters = {
18
18
  channelId: Hex | undefined;
19
19
  /** Payment fields the reusable channel must match before it is advertised. */
20
20
  expected?: SessionSnapshotPaymentFields | undefined;
21
+ /** Minimum raw-unit increase required when the next voucher must advance. */
22
+ minimumVoucherAdvance?: bigint | undefined;
23
+ /** Whether the next voucher must advance beyond the accepted cumulative amount. */
24
+ requireVoucherAdvance?: boolean | undefined;
21
25
  /** Server channel store. */
22
26
  store: ChannelStore.ChannelStore;
23
27
  };
@@ -149,12 +153,14 @@ export type ResolveSessionPaymentRequestParameters = {
149
153
  decimals: number;
150
154
  defaultFeePayer?: viem_Account | undefined;
151
155
  getClient: ResolveRequestChainIdParameters['getClient'];
156
+ minVoucherDelta?: string | undefined;
152
157
  parameterChainId?: number | undefined;
153
158
  parameterEscrowContract?: Address | undefined;
154
159
  parameterFeePayer?: ParameterFeePayer;
155
160
  request: SessionPaymentRequestInput;
156
161
  resolveChannelId?: ResolveSessionChannelId | undefined;
157
162
  source?: string | undefined;
163
+ sseEnabled?: boolean | undefined;
158
164
  store: ChannelStore.ChannelStore;
159
165
  };
160
166
  /** Inputs used to resolve shared context for credential verification. */
@@ -47,7 +47,7 @@ export async function resolveSessionChannelId(parameters) {
47
47
  }
48
48
  /** Builds server bootstrap hints for a reusable precompile session channel. */
49
49
  export async function resolveSessionSnapshot(parameters) {
50
- const { amount, channelId, expected, store } = parameters;
50
+ const { amount, channelId, expected, minimumVoucherAdvance = 0n, requireVoucherAdvance = false, store, } = parameters;
51
51
  if (!channelId)
52
52
  return undefined;
53
53
  const channel = await store.getChannel(ChannelStore.normalizeChannelId(channelId));
@@ -63,7 +63,11 @@ export async function resolveSessionSnapshot(parameters) {
63
63
  return undefined;
64
64
  if (expected && !matchesSnapshotPaymentFields(channel, expected))
65
65
  return undefined;
66
- const requiredCumulative = channel.spent + amount;
66
+ const spendBoundary = channel.spent + amount;
67
+ const advanceBoundary = requireVoucherAdvance
68
+ ? channel.highestVoucherAmount + (minimumVoucherAdvance > 0n ? minimumVoucherAdvance : 1n)
69
+ : 0n;
70
+ const requiredCumulative = spendBoundary > advanceBoundary ? spendBoundary : advanceBoundary;
67
71
  return {
68
72
  acceptedCumulative: channel.highestVoucherAmount.toString(),
69
73
  chainId: channel.chainId,
@@ -158,7 +162,7 @@ export async function resolveRequestChainId(parameters) {
158
162
  }
159
163
  /** Resolves request-time TIP-1034 details and server bootstrap hints for a challenge. */
160
164
  export async function resolveSessionPaymentRequest(parameters) {
161
- const { capturedRequest, credential, decimals, defaultFeePayer, getClient, parameterChainId, parameterEscrowContract, parameterFeePayer, request, resolveChannelId, source, store, } = parameters;
165
+ const { capturedRequest, credential, decimals, defaultFeePayer, getClient, minVoucherDelta, parameterChainId, parameterEscrowContract, parameterFeePayer, request, resolveChannelId, source, sseEnabled, store, } = parameters;
162
166
  const chainId = await resolveRequestChainId({
163
167
  getClient,
164
168
  parameterChainId,
@@ -172,6 +176,7 @@ export async function resolveSessionPaymentRequest(parameters) {
172
176
  const escrowContract = resolveRequestEscrowContract(request.escrowContract, parameterEscrowContract);
173
177
  const operator = resolveRequestOperator(request.operator);
174
178
  const requestAmount = parseUnits(request.amount, decimals);
179
+ const isContentRequest = capturedRequest ? isSessionContentRequest(capturedRequest) : false;
175
180
  const channelId = await resolveSessionChannelId({
176
181
  capturedRequest,
177
182
  credential,
@@ -181,7 +186,7 @@ export async function resolveSessionPaymentRequest(parameters) {
181
186
  store,
182
187
  });
183
188
  const sessionSnapshot = await resolveSessionSnapshot({
184
- amount: capturedRequest && !isSessionContentRequest(capturedRequest) ? 0n : requestAmount,
189
+ amount: capturedRequest && !isContentRequest ? 0n : requestAmount,
185
190
  channelId,
186
191
  expected: {
187
192
  chainId,
@@ -189,6 +194,8 @@ export async function resolveSessionPaymentRequest(parameters) {
189
194
  escrowContract,
190
195
  recipient: readChallengeAddress(request.recipient, 'recipient'),
191
196
  },
197
+ minimumVoucherAdvance: parseUnits(minVoucherDelta ?? '0', decimals),
198
+ requireVoucherAdvance: Boolean(capturedRequest) && !sseEnabled && isContentRequest,
192
199
  store,
193
200
  });
194
201
  const { operator: _operator, ...requestWithoutOperator } = request;
@@ -59,6 +59,7 @@ export declare function session<const parameters extends session.Parameters>(p?:
59
59
  }, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
60
60
  action: import("zod/mini").ZodMiniLiteral<"close">;
61
61
  channelId: import("zod/mini").ZodMiniString<string>;
62
+ closeSignature: import("zod/mini").ZodMiniString<string>;
62
63
  cumulativeAmount: import("zod/mini").ZodMiniString<string>;
63
64
  descriptor: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniCustom<import("../precompile/Protocol.js").ChannelDescriptor, import("../precompile/Protocol.js").ChannelDescriptor>>;
64
65
  signature: import("zod/mini").ZodMiniString<string>;
@@ -137,7 +138,7 @@ export declare namespace session {
137
138
  type FeePayerPolicy = Partial<FeePayer.Policy>;
138
139
  /** Parameters accepted by the TIP-1034 server session payment method. */
139
140
  type Parameters = {
140
- /** TTL in milliseconds for cached on-chain channel state. After this duration, the server re-queries on-chain state during voucher handling to detect forced close requests. @default 5_000 */
141
+ /** TTL in milliseconds for cached on-chain channel state during non-mutating validation. Voucher acceptance always refreshes on-chain state. @default 5_000 */
141
142
  channelStateTtl?: number | undefined;
142
143
  /** Override the fee-sponsor policy used for sponsored open/topUp transactions and server-driven close transactions. */
143
144
  feePayerPolicy?: FeePayerPolicy | undefined;
@@ -26,7 +26,7 @@ import { broadcastCredentialPayload, validateCredentialPayload } from './Credent
26
26
  import { requireSessionCredentialPayload } from './CredentialVerification.js';
27
27
  import { resolveCredentialVerificationContext, resolveSessionChannelId, resolveSessionSnapshot, resolveSessionPaymentRequest, } from './RequestState.js';
28
28
  import { respondToSessionCredential } from './RequestState.js';
29
- import { applyVerifiedHttpAccounting, chargeSessionChannel, } from './Settlement.js';
29
+ import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, } from './Settlement.js';
30
30
  import { isSettlementDue, maybeSettleScheduled } from './Settlement.js';
31
31
  import { resolveSettlementSchedule, } from './Settlement.js';
32
32
  import * as Ws from './Ws.js';
@@ -243,7 +243,7 @@ export function session(p) {
243
243
  ...(typeof parameters.sse === 'object' ? parameters.sse : undefined),
244
244
  })
245
245
  : undefined;
246
- const validateCredential = async ({ credential, request, }) => {
246
+ const validateCredential = async ({ credential, operation, request, }) => {
247
247
  const payload = requireSessionCredentialPayload(credential.payload);
248
248
  const context = await resolveCredentialVerificationContext({
249
249
  decimals,
@@ -254,6 +254,7 @@ export function session(p) {
254
254
  });
255
255
  const details = await validateCredentialPayload({
256
256
  account,
257
+ allowPendingClose: operation === 'broadcast',
257
258
  challenge: credential.challenge,
258
259
  channelStateTtl,
259
260
  chainId: context.chainId,
@@ -305,6 +306,11 @@ export function session(p) {
305
306
  minVoucherDelta: context.minVoucherDelta,
306
307
  onSessionSettlement,
307
308
  payload,
309
+ requireVoucherAdvance: shouldApplyVerifiedHttpAccounting({
310
+ capturedRequest: envelope?.capturedRequest,
311
+ payloadAction: payload.action,
312
+ sseEnabled: Boolean(parameters.sse),
313
+ }),
308
314
  store,
309
315
  });
310
316
  return applyVerifiedHttpAccounting({
@@ -372,11 +378,13 @@ export function session(p) {
372
378
  decimals,
373
379
  defaultFeePayer: feePayer,
374
380
  getClient,
381
+ minVoucherDelta: parameters.minVoucherDelta,
375
382
  parameterChainId: parameters.chainId,
376
383
  parameterEscrowContract: parameters.escrowContract,
377
384
  parameterFeePayer: configuredFeePayer,
378
385
  request,
379
386
  resolveChannelId: parameters.resolveChannelId,
387
+ sseEnabled: Boolean(parameters.sse),
380
388
  store,
381
389
  });
382
390
  return {
@@ -89,6 +89,7 @@ export type OnSessionSettlement = (context: SessionSettlementContext) => MaybePr
89
89
  /** Inputs used to mark a channel after automatic scheduled settlement succeeds. */
90
90
  export type MarkSettlementCompleteParameters = {
91
91
  channelId: ChannelStore.State['channelId'];
92
+ leaseOwner: string;
92
93
  settledAt?: string | undefined;
93
94
  store: ChannelStore.ChannelStore;
94
95
  };
@@ -106,6 +107,26 @@ export declare function resolveSettlementProgress(channel: ChannelStore.State):
106
107
  export declare function isSettlementDue(channel: ChannelStore.State, schedule: ResolvedSettlementSchedule | undefined): boolean;
107
108
  /** Records the channel spend/unit counters that a scheduled settlement captured. */
108
109
  export declare function markSettlementComplete(parameters: MarkSettlementCompleteParameters): Promise<void>;
110
+ /** Atomically claims one due scheduled settlement across server workers. */
111
+ export declare function claimScheduledSettlement(parameters: {
112
+ channelId: Hex;
113
+ leaseMs?: number | undefined;
114
+ schedule: ResolvedSettlementSchedule;
115
+ store: ChannelStore.ChannelStore;
116
+ }): Promise<string | undefined>;
117
+ /** Extends an owned scheduled settlement lease while its transaction is in flight. */
118
+ export declare function renewScheduledSettlement(parameters: {
119
+ channelId: Hex;
120
+ leaseMs?: number | undefined;
121
+ leaseOwner: string;
122
+ store: ChannelStore.ChannelStore;
123
+ }): Promise<void>;
124
+ /** Releases a scheduled settlement claim after a failed attempt. */
125
+ export declare function releaseScheduledSettlement(parameters: {
126
+ channelId: Hex;
127
+ leaseOwner: string;
128
+ store: ChannelStore.ChannelStore;
129
+ }): Promise<void>;
109
130
  /** Callback used by post-verification accounting to deduct spend from a channel. */
110
131
  export type ChargeSessionChannel = (channelId: Hex, amount: bigint) => Promise<ChannelStore.State>;
111
132
  /** Callback used by post-verification accounting to run server-owned settlement policy. */
@@ -138,6 +159,8 @@ export type ApplyVerifiedHttpAccountingParameters = {
138
159
  /** Runs optional server settlement policy after a successful content charge. */
139
160
  settleCharged: SettleChargedSessionChannel;
140
161
  };
162
+ /** Returns whether an accepted credential will authorize and charge an HTTP content response. */
163
+ export declare function shouldApplyVerifiedHttpAccounting(parameters: Pick<ApplyVerifiedHttpAccountingParameters, 'capturedRequest' | 'payloadAction' | 'sseEnabled'>): boolean;
141
164
  /** Applies the default HTTP content charge after a session credential has been accepted. */
142
165
  export declare function applyVerifiedHttpAccounting(parameters: ApplyVerifiedHttpAccountingParameters): Promise<SessionReceipt>;
143
166
  /** Atomically deducts spend from a channel and maps store failures to typed session errors. */
@@ -46,8 +46,9 @@ export function resolveCredentialFeePayer(parameters) {
46
46
  return requestFeePayer;
47
47
  if (typeof feePayer === 'object')
48
48
  return feePayer;
49
- return typeof feePayer === 'string' ? true : undefined;
49
+ return feePayer === true || typeof feePayer === 'string' ? true : undefined;
50
50
  }
51
+ const scheduledSettlementLeaseMs = 5 * 60_000;
51
52
  /** Converts a public settlement schedule into raw-unit thresholds. */
52
53
  export function resolveSettlementSchedule(schedule, decimals) {
53
54
  if (!schedule)
@@ -93,6 +94,9 @@ export function isSettlementDue(channel, schedule) {
93
94
  const progress = resolveSettlementProgress(channel);
94
95
  if (!progress)
95
96
  return false;
97
+ // A detected force-close makes outstanding vouchers immediately due.
98
+ if (!channel.finalized && channel.closeRequestedAt > 0n)
99
+ return true;
96
100
  if (schedule.units !== undefined && progress.units >= schedule.units)
97
101
  return true;
98
102
  if (schedule.amount !== undefined && progress.amount >= schedule.amount)
@@ -103,26 +107,79 @@ export function isSettlementDue(channel, schedule) {
103
107
  }
104
108
  /** Records the channel spend/unit counters that a scheduled settlement captured. */
105
109
  export async function markSettlementComplete(parameters) {
106
- const { channelId, store, settledAt = new Date().toISOString() } = parameters;
107
- await store.updateChannel(channelId, (current) => current
108
- ? {
109
- ...current,
110
+ const { channelId, leaseOwner, store, settledAt = new Date().toISOString() } = parameters;
111
+ await store.updateChannel(channelId, (current) => {
112
+ if (!current)
113
+ return current;
114
+ if (current.scheduledSettlementLease?.owner !== leaseOwner)
115
+ return current;
116
+ const { scheduledSettlementLease: _, ...channel } = current;
117
+ return {
118
+ ...channel,
110
119
  lastSettlementAt: settledAt,
111
120
  lastSettlementSpent: current.spent,
112
121
  lastSettlementUnits: current.units,
113
- }
114
- : current);
122
+ };
123
+ });
115
124
  }
116
- /** Applies the default HTTP content charge after a session credential has been accepted. */
117
- export async function applyVerifiedHttpAccounting(parameters) {
118
- const { capturedRequest, payloadAction, receipt, sseEnabled } = parameters;
125
+ /** Atomically claims one due scheduled settlement across server workers. */
126
+ export async function claimScheduledSettlement(parameters) {
127
+ const { channelId, leaseMs = scheduledSettlementLeaseMs, schedule, store } = parameters;
128
+ const now = Date.now();
129
+ const owner = globalThis.crypto.randomUUID();
130
+ let claimed = false;
131
+ await store.updateChannel(channelId, (current) => {
132
+ claimed = false;
133
+ if (!current || !isSettlementDue(current, schedule))
134
+ return current;
135
+ const lease = current.scheduledSettlementLease;
136
+ if (lease && lease.expiresAt > now)
137
+ return current;
138
+ claimed = true;
139
+ return {
140
+ ...current,
141
+ scheduledSettlementLease: { expiresAt: now + leaseMs, owner },
142
+ };
143
+ });
144
+ return claimed ? owner : undefined;
145
+ }
146
+ /** Extends an owned scheduled settlement lease while its transaction is in flight. */
147
+ export async function renewScheduledSettlement(parameters) {
148
+ const { channelId, leaseMs = scheduledSettlementLeaseMs, leaseOwner, store } = parameters;
149
+ await store.updateChannel(channelId, (current) => {
150
+ if (!current || current.scheduledSettlementLease?.owner !== leaseOwner)
151
+ return current;
152
+ return {
153
+ ...current,
154
+ scheduledSettlementLease: { expiresAt: Date.now() + leaseMs, owner: leaseOwner },
155
+ };
156
+ });
157
+ }
158
+ /** Releases a scheduled settlement claim after a failed attempt. */
159
+ export async function releaseScheduledSettlement(parameters) {
160
+ const { channelId, leaseOwner, store } = parameters;
161
+ await store.updateChannel(channelId, (current) => {
162
+ if (!current || current.scheduledSettlementLease?.owner !== leaseOwner)
163
+ return current;
164
+ const { scheduledSettlementLease: _, ...channel } = current;
165
+ return channel;
166
+ });
167
+ }
168
+ /** Returns whether an accepted credential will authorize and charge an HTTP content response. */
169
+ export function shouldApplyVerifiedHttpAccounting(parameters) {
170
+ const { capturedRequest, payloadAction, sseEnabled } = parameters;
119
171
  if (!capturedRequest)
120
- return receipt;
172
+ return false;
121
173
  if (payloadAction !== 'open' && payloadAction !== 'voucher')
122
- return receipt;
174
+ return false;
123
175
  if (sseEnabled && capturedRequest.method === 'POST')
124
- return receipt;
125
- if (!isSessionContentRequest(capturedRequest))
176
+ return false;
177
+ return isSessionContentRequest(capturedRequest);
178
+ }
179
+ /** Applies the default HTTP content charge after a session credential has been accepted. */
180
+ export async function applyVerifiedHttpAccounting(parameters) {
181
+ const { receipt, sseEnabled } = parameters;
182
+ if (!shouldApplyVerifiedHttpAccounting(parameters))
126
183
  return receipt;
127
184
  const requestAmount = parameters.getRequestAmount();
128
185
  const charged = await parameters.charge(receipt.channelId, requestAmount);
@@ -185,19 +242,42 @@ export function assertSettlementSender(parameters) {
185
242
  /** Applies automatic settlement when the server-owned schedule is due. */
186
243
  export async function maybeSettleScheduled(parameters) {
187
244
  const { channel, schedule, store } = parameters;
188
- if (!isSettlementDue(channel, schedule))
245
+ if (!schedule || !isSettlementDue(channel, schedule))
189
246
  return undefined;
190
- const txHash = await settle(store, parameters.client, channel.channelId, {
191
- account: parameters.account,
192
- ...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
193
- ...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
194
- ...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
195
- onSessionSettlement: parameters.onSessionSettlement
196
- ? (ctx) => parameters.onSessionSettlement({ ...ctx, trigger: 'scheduled' })
197
- : undefined,
247
+ const leaseOwner = await claimScheduledSettlement({
248
+ channelId: channel.channelId,
249
+ schedule,
250
+ store,
198
251
  });
199
- await markSettlementComplete({ channelId: channel.channelId, store });
200
- return txHash;
252
+ if (!leaseOwner)
253
+ return undefined;
254
+ const renewal = setInterval(() => {
255
+ void renewScheduledSettlement({
256
+ channelId: channel.channelId,
257
+ leaseOwner,
258
+ store,
259
+ }).catch(() => undefined);
260
+ }, scheduledSettlementLeaseMs / 2);
261
+ try {
262
+ const txHash = await settle(store, parameters.client, channel.channelId, {
263
+ account: parameters.account,
264
+ ...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
265
+ ...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
266
+ ...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
267
+ onSessionSettlement: parameters.onSessionSettlement
268
+ ? (ctx) => parameters.onSessionSettlement({ ...ctx, trigger: 'scheduled' })
269
+ : undefined,
270
+ });
271
+ await markSettlementComplete({ channelId: channel.channelId, leaseOwner, store });
272
+ return txHash;
273
+ }
274
+ catch (error) {
275
+ await releaseScheduledSettlement({ channelId: channel.channelId, leaseOwner, store }).catch(() => undefined);
276
+ throw error;
277
+ }
278
+ finally {
279
+ clearInterval(renewal);
280
+ }
201
281
  }
202
282
  /** Settles the highest accepted voucher for a precompile-backed session channel. */
203
283
  export async function settle(store_, client, channelId_, options) {