mppx 0.13.4 → 0.13.6

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 (73) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/Credential.d.ts +3 -0
  3. package/dist/Credential.js +9 -0
  4. package/dist/Errors.d.ts +63 -2
  5. package/dist/Errors.js +52 -2
  6. package/dist/Method.d.ts +10 -0
  7. package/dist/cli/cli.js +92 -92
  8. package/dist/cli/plugins/evm.js +2 -2
  9. package/dist/cli/plugins/index.js +12 -12
  10. package/dist/client/Mppx.d.ts +2 -0
  11. package/dist/client/Mppx.js +8 -10
  12. package/dist/client/internal/Fetch.js +107 -8
  13. package/dist/client/internal/MethodChallenge.d.ts +2 -0
  14. package/dist/client/internal/MethodChallenge.js +4 -0
  15. package/dist/client/internal/MethodResponse.d.ts +6 -0
  16. package/dist/client/internal/MethodResponse.js +16 -0
  17. package/dist/client/internal/protocols/Mcp.js +36 -5
  18. package/dist/evm/server/Charge.js +3 -1
  19. package/dist/internal/version.d.ts +2 -2
  20. package/dist/internal/version.js +1 -1
  21. package/dist/internal/warnOnce.d.ts +3 -0
  22. package/dist/internal/warnOnce.js +9 -0
  23. package/dist/server/Mppx.d.ts +5 -0
  24. package/dist/server/Mppx.js +45 -18
  25. package/dist/server/Transport.js +10 -0
  26. package/dist/stripe/server/Methods.js +4 -1
  27. package/dist/tempo/Attribution.d.ts +24 -9
  28. package/dist/tempo/Attribution.js +25 -5
  29. package/dist/tempo/client/Charge.d.ts +116 -10
  30. package/dist/tempo/client/Charge.js +117 -22
  31. package/dist/tempo/client/Methods.d.ts +88 -9
  32. package/dist/tempo/index.d.ts +1 -0
  33. package/dist/tempo/index.js +1 -0
  34. package/dist/tempo/internal/auto-swap.d.ts +10 -0
  35. package/dist/tempo/internal/auto-swap.js +35 -8
  36. package/dist/tempo/internal/fee-payer.d.ts +8 -0
  37. package/dist/tempo/internal/fee-payer.js +20 -8
  38. package/dist/tempo/legacy/session/Chain.js +8 -2
  39. package/dist/tempo/server/Charge.d.ts +11 -0
  40. package/dist/tempo/server/Charge.js +160 -29
  41. package/dist/tempo/server/Methods.js +28 -5
  42. package/dist/tempo/server/Relay.js +6 -22
  43. package/dist/tempo/server/Subscription.js +5 -1
  44. package/dist/tempo/server/internal/fallback-store.d.ts +9 -0
  45. package/dist/tempo/server/internal/fallback-store.js +13 -0
  46. package/dist/tempo/server/internal/html.gen.js +1 -1
  47. package/dist/tempo/server/internal/transport.d.ts +3 -0
  48. package/dist/tempo/server/internal/transport.js +5 -1
  49. package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
  50. package/dist/tempo/session/client/ChannelStore.d.ts +12 -1
  51. package/dist/tempo/session/client/ChannelStore.js +47 -0
  52. package/dist/tempo/session/client/CredentialState.js +8 -3
  53. package/dist/tempo/session/client/Session.js +50 -33
  54. package/dist/tempo/session/client/SessionManager.js +186 -65
  55. package/dist/tempo/session/client/Transports.d.ts +6 -0
  56. package/dist/tempo/session/client/Transports.js +52 -7
  57. package/dist/tempo/session/precompile/Chain.d.ts +2 -0
  58. package/dist/tempo/session/precompile/Chain.js +8 -3
  59. package/dist/tempo/session/server/ChannelStore.d.ts +34 -0
  60. package/dist/tempo/session/server/ChannelStore.js +69 -9
  61. package/dist/tempo/session/server/CredentialVerification.d.ts +7 -1
  62. package/dist/tempo/session/server/CredentialVerification.js +220 -48
  63. package/dist/tempo/session/server/MeteredStream.d.ts +6 -5
  64. package/dist/tempo/session/server/MeteredStream.js +10 -26
  65. package/dist/tempo/session/server/Session.d.ts +29 -2
  66. package/dist/tempo/session/server/Session.js +124 -44
  67. package/dist/tempo/session/server/Settlement.d.ts +9 -0
  68. package/dist/tempo/session/server/Settlement.js +23 -3
  69. package/dist/tempo/session/server/Sse.d.ts +4 -3
  70. package/dist/tempo/session/server/Sse.js +3 -3
  71. package/dist/tempo/session/server/Ws.d.ts +9 -1
  72. package/dist/tempo/session/server/Ws.js +1 -0
  73. package/package.json +1 -1
@@ -368,14 +368,22 @@ async function validateVoucherCredential(parameters, payload) {
368
368
  assertCredentialSourceCanSpend({ chainId, channel, source: credentialSource });
369
369
  if (channel.finalized)
370
370
  throw new ChannelClosedError({ reason: 'channel is finalized' });
371
- const channelState = await resolveVoucherChannelState({
371
+ const channelState = canUseStoredChannelState({
372
372
  channel,
373
373
  channelId,
374
- channelStateTtl,
375
- client,
376
- escrow,
377
374
  lastOnChainVerified,
378
- });
375
+ voucher,
376
+ voucherStateMaxAge: parameters.voucherStateMaxAge ?? 0,
377
+ })
378
+ ? storedChannelState(channel)
379
+ : await resolveVoucherChannelState({
380
+ channel,
381
+ channelId,
382
+ channelStateTtl,
383
+ client,
384
+ escrow,
385
+ lastOnChainVerified,
386
+ });
379
387
  if (parameters.allowPendingClose && channelState.closeRequestedAt !== 0)
380
388
  return;
381
389
  await ChannelStore.validateVoucher({
@@ -387,17 +395,36 @@ async function validateVoucherCredential(parameters, payload) {
387
395
  });
388
396
  }
389
397
  async function resolveVoucherChannelState(parameters) {
390
- const { channel, channelId, channelStateTtl, client, escrow, forceRefresh, lastOnChainVerified } = parameters;
398
+ const { channelId, channelStateTtl, client, escrow, forceRefresh, lastOnChainVerified } = parameters;
391
399
  const shouldRefresh = forceRefresh || Date.now() - (lastOnChainVerified.get(channelId) ?? 0) > channelStateTtl;
392
400
  const state = shouldRefresh ? await Chain.getChannelState(client, channelId, escrow) : undefined;
393
- if (state?.closeRequestedAt)
401
+ if (state && (state.closeRequestedAt !== 0 || state.deposit === 0n))
394
402
  lastOnChainVerified.delete(channelId);
395
403
  else if (state)
396
404
  lastOnChainVerified.set(channelId, Date.now());
405
+ if (state)
406
+ return state;
407
+ const { channel } = parameters;
408
+ return storedChannelState(typeof channel === 'function' ? await channel() : channel);
409
+ }
410
+ /**
411
+ * Whether a voucher may be checked against the stored channel instead of waiting for a chain read.
412
+ * A payer can withdraw only after the escrow's close grace period, so a voucher within the deposit
413
+ * of recently verified open state stays settleable while a fresh read finishes.
414
+ */
415
+ function canUseStoredChannelState(parameters) {
416
+ const { channel, channelId, lastOnChainVerified, voucher, voucherStateMaxAge } = parameters;
417
+ const verifiedAt = Math.max(lastOnChainVerified.get(channelId) ?? 0, channel.onChainVerifiedAt ?? 0);
418
+ return (voucherStateMaxAge > 0 &&
419
+ channel.closeRequestedAt === 0n &&
420
+ Date.now() - verifiedAt <= voucherStateMaxAge &&
421
+ voucher.cumulativeAmount <= channel.deposit);
422
+ }
423
+ function storedChannelState(channel) {
397
424
  return {
398
- deposit: state?.deposit ?? uint96(channel.deposit),
399
- settled: state?.settled ?? uint96(channel.settledOnChain),
400
- closeRequestedAt: state?.closeRequestedAt ?? Number(channel.closeRequestedAt),
425
+ deposit: uint96(channel.deposit),
426
+ settled: uint96(channel.settledOnChain),
427
+ closeRequestedAt: Number(channel.closeRequestedAt),
401
428
  };
402
429
  }
403
430
  async function validateCloseCredential(parameters, payload) {
@@ -602,26 +629,81 @@ async function handleVoucherCredential(parameters) {
602
629
  const voucher = Voucher.parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature);
603
630
  assertDescriptor(payload);
604
631
  validateChannelDescriptor(payload.descriptor, channelId, chainId, escrow, request.recipient, request.currency, expectedOperator);
605
- const channel = await ChannelStore.loadPrecompileChannel({
606
- descriptor: payload.descriptor,
607
- channelId,
608
- chainId,
609
- escrow,
610
- store,
611
- validateDescriptor: true,
612
- });
632
+ const voucherStateMaxAge = parameters.voucherStateMaxAge ?? 0;
633
+ const { chargeAmount, onCharged } = parameters;
634
+ let loadingChannel;
635
+ const loadChannel = () => {
636
+ loadingChannel ??= ChannelStore.loadPrecompileChannel({
637
+ descriptor: payload.descriptor,
638
+ channelId,
639
+ chainId,
640
+ escrow,
641
+ store,
642
+ validateDescriptor: true,
643
+ });
644
+ return loadingChannel;
645
+ };
646
+ // The on-chain read does not depend on the stored channel, so it overlaps the store read.
647
+ // Store errors still take precedence because the stored channel is awaited first.
648
+ let reading;
649
+ const readChannelState = () => {
650
+ if (reading)
651
+ return reading;
652
+ const startedAt = Date.now();
653
+ const state = resolveVoucherChannelState({
654
+ channel: loadChannel,
655
+ channelId,
656
+ channelStateTtl,
657
+ client,
658
+ escrow,
659
+ forceRefresh: true,
660
+ lastOnChainVerified,
661
+ });
662
+ state.catch(() => { });
663
+ reading = { startedAt, state };
664
+ return reading;
665
+ };
666
+ const recentlyRead = voucherStateMaxAge > 0 &&
667
+ Date.now() - (lastOnChainVerified.get(channelId) ?? 0) <=
668
+ Math.min(channelStateTtl, voucherStateMaxAge);
669
+ if (!recentlyRead)
670
+ readChannelState();
671
+ if (voucherStateMaxAge > 0 && store.updateChannelResult) {
672
+ const receipt = await acceptVoucherInOneUpdate(parameters, voucher, payload.descriptor);
673
+ if (receipt) {
674
+ if (reading)
675
+ recordOnChainReadInBackground(parameters, channelId, reading);
676
+ return receipt;
677
+ }
678
+ }
679
+ const channel = await loadChannel();
613
680
  assertCredentialSourceCanSpend({ chainId, channel, source: credentialSource });
614
681
  if (channel.finalized)
615
682
  throw new ChannelClosedError({ reason: 'channel is finalized' });
616
- const channelState = await resolveVoucherChannelState({
683
+ if (canUseStoredChannelState({
617
684
  channel,
618
685
  channelId,
619
- channelStateTtl,
620
- client,
621
- escrow,
622
- forceRefresh: true,
623
686
  lastOnChainVerified,
624
- });
687
+ voucher,
688
+ voucherStateMaxAge,
689
+ })) {
690
+ if (reading)
691
+ recordOnChainReadInBackground(parameters, channelId, reading);
692
+ return ChannelStore.verifyAndAcceptVoucher({
693
+ store,
694
+ minVoucherDelta,
695
+ requireAdvance: parameters.requireVoucherAdvance,
696
+ challenge,
697
+ channel,
698
+ voucher,
699
+ channelState: storedChannelState(channel),
700
+ methodDetails: { chainId, escrowContract: escrow },
701
+ chargeAmount,
702
+ onCharged,
703
+ });
704
+ }
705
+ const { startedAt, state } = readChannelState();
706
+ const channelState = await state;
625
707
  const reconciled = channel.closeRequestedAt === 0n
626
708
  ? channel
627
709
  : ((await store.updateChannel(channelId, (current) => ChannelStore.reconcileExpiredPendingClose({
@@ -630,30 +712,7 @@ async function handleVoucherCredential(parameters) {
630
712
  state: channelState,
631
713
  }))) ?? channel);
632
714
  if (channelState.closeRequestedAt !== 0) {
633
- const closing = await store.updateChannel(channelId, (current) => current
634
- ? {
635
- ...current,
636
- settledOnChain: channelState.settled > current.settledOnChain
637
- ? channelState.settled
638
- : current.settledOnChain,
639
- closeRequestedAt: BigInt(channelState.closeRequestedAt) > current.closeRequestedAt
640
- ? BigInt(channelState.closeRequestedAt)
641
- : current.closeRequestedAt,
642
- }
643
- : current);
644
- if (closing && !closing.finalized && channelState.deposit > 0n)
645
- await maybeSettleScheduled({
646
- account: parameters.account,
647
- channel: closing,
648
- client,
649
- feePayer: parameters.feePayer,
650
- feePayerPolicy: parameters.feePayerPolicy,
651
- feeToken: parameters.feeToken,
652
- onSessionSettlement: parameters.onSessionSettlement,
653
- onSessionSettlementFailure: parameters.onSessionSettlementFailure,
654
- schedule: {},
655
- store,
656
- });
715
+ await recordPendingClose(parameters, channelId, channelState);
657
716
  throw new ChannelClosedError({ reason: 'channel has a pending close request' });
658
717
  }
659
718
  return ChannelStore.verifyAndAcceptVoucher({
@@ -665,8 +724,121 @@ async function handleVoucherCredential(parameters) {
665
724
  voucher,
666
725
  channelState,
667
726
  methodDetails: { chainId, escrowContract: escrow },
727
+ verifiedAt: startedAt,
728
+ chargeAmount,
729
+ onCharged,
668
730
  });
669
731
  }
732
+ /**
733
+ * Accepts a voucher, and charges its request, in one store update with no separate read: the
734
+ * update checks the stored channel itself. Returns `undefined` without writing when the stored
735
+ * on-chain state was not verified recently enough to accept against.
736
+ */
737
+ async function acceptVoucherInOneUpdate(parameters, voucher, descriptor) {
738
+ const { challenge, chainId, chargeAmount, credentialSource, escrow, lastOnChainVerified, minVoucherDelta, requireVoucherAdvance, store, } = parameters;
739
+ const voucherStateMaxAge = parameters.voucherStateMaxAge ?? 0;
740
+ const valid = await Voucher.verifyVoucher(escrow, chainId, voucher, authorizedSigner(descriptor));
741
+ if (!valid)
742
+ throw new InvalidSignatureError({ reason: 'invalid voucher signature' });
743
+ const outcome = await store.updateChannelResult(voucher.channelId, (current) => {
744
+ if (!current)
745
+ throw new ChannelNotFoundError({ reason: 'channel not found' });
746
+ if (!ChannelStore.isPrecompileState(current))
747
+ throw new VerificationFailedError({ reason: 'channel is not precompile-backed' });
748
+ assertSameDescriptor(descriptor, current.descriptor);
749
+ validateChannelDescriptor(descriptor, voucher.channelId, chainId, escrow, current.payee, current.token);
750
+ assertCredentialSourceCanSpend({ chainId, channel: current, source: credentialSource });
751
+ if (current.finalized)
752
+ throw new ChannelClosedError({ reason: 'channel is finalized' });
753
+ if (!canUseStoredChannelState({
754
+ channel: current,
755
+ channelId: voucher.channelId,
756
+ lastOnChainVerified,
757
+ voucher,
758
+ voucherStateMaxAge,
759
+ }))
760
+ return { op: 'noop', result: undefined };
761
+ if (requireVoucherAdvance && voucher.cumulativeAmount <= current.highestVoucherAmount)
762
+ throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
763
+ const channelState = storedChannelState(current);
764
+ const { alreadyAccepted } = ChannelStore.assertVoucherAmounts({
765
+ channel: current,
766
+ channelState,
767
+ minVoucherDelta,
768
+ voucher,
769
+ });
770
+ if (alreadyAccepted && chargeAmount === undefined)
771
+ return { op: 'noop', result: { channel: current, charged: false } };
772
+ const acceptance = ChannelStore.planVoucherAcceptance({
773
+ accepted: ChannelStore.acceptVoucherStateUpdate({ channelState, current, voucher }),
774
+ chargeAmount,
775
+ });
776
+ return { op: 'set', value: acceptance.channel, result: acceptance };
777
+ });
778
+ if (!outcome)
779
+ return undefined;
780
+ if (chargeAmount !== undefined) {
781
+ if (!outcome.charged)
782
+ throw ChannelStore.insufficientBalanceForCharge(outcome.channel, chargeAmount);
783
+ parameters.onCharged?.(outcome.channel);
784
+ }
785
+ return createSessionReceipt({
786
+ challengeId: challenge.id,
787
+ channelId: voucher.channelId,
788
+ acceptedCumulative: outcome.channel.highestVoucherAmount,
789
+ spent: outcome.channel.spent,
790
+ units: outcome.channel.units,
791
+ });
792
+ }
793
+ /** Persists a pending close seen on chain and settles the channel while the payee still can. */
794
+ async function recordPendingClose(parameters, channelId, channelState) {
795
+ const { client, store } = parameters;
796
+ const closing = await store.updateChannel(channelId, (current) => current
797
+ ? {
798
+ ...current,
799
+ settledOnChain: channelState.settled > current.settledOnChain
800
+ ? channelState.settled
801
+ : current.settledOnChain,
802
+ closeRequestedAt: BigInt(channelState.closeRequestedAt) > current.closeRequestedAt
803
+ ? BigInt(channelState.closeRequestedAt)
804
+ : current.closeRequestedAt,
805
+ }
806
+ : current);
807
+ if (closing && !closing.finalized && channelState.deposit > 0n)
808
+ await maybeSettleScheduled({
809
+ account: parameters.account,
810
+ channel: closing,
811
+ client,
812
+ feePayer: parameters.feePayer,
813
+ feePayerPolicy: parameters.feePayerPolicy,
814
+ feeToken: parameters.feeToken,
815
+ onSessionSettlement: parameters.onSessionSettlement,
816
+ onSessionSettlementFailure: parameters.onSessionSettlementFailure,
817
+ schedule: {},
818
+ store,
819
+ });
820
+ }
821
+ /**
822
+ * Records an on-chain read that a voucher did not wait for: a pending close is persisted so later
823
+ * vouchers are rejected, and open state extends the verified age of the stored channel.
824
+ */
825
+ function recordOnChainReadInBackground(parameters, channelId, read) {
826
+ read.state
827
+ .then(async (state) => {
828
+ if (state.closeRequestedAt !== 0)
829
+ return recordPendingClose(parameters, channelId, state);
830
+ if (state.deposit === 0n)
831
+ return;
832
+ await parameters.store.updateChannel(channelId, (current) => current && !current.finalized && current.closeRequestedAt === 0n
833
+ ? {
834
+ ...current,
835
+ ...ChannelStore.mergeActiveOnChainState(current, state),
836
+ onChainVerifiedAt: Math.max(read.startedAt, current.onChainVerifiedAt ?? 0),
837
+ }
838
+ : current);
839
+ })
840
+ .catch(() => { });
841
+ }
670
842
  const sendMethods = new Set([
671
843
  'eth_sendRawTransaction',
672
844
  'eth_sendRawTransactionSync',
@@ -5,12 +5,11 @@ import * as ChannelStore from './ChannelStore.js';
5
5
  /** Controller passed to manual-charge streaming generators. */
6
6
  export type SessionController = {
7
7
  /**
8
- * Reserve voucher coverage for the next emitted chunk.
8
+ * Commit a charge before performing billable work.
9
9
  *
10
- * The reservation blocks until sufficient voucher headroom exists, but the
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.
10
+ * Waits for sufficient voucher coverage and commits the charge before resolving.
11
+ * Once resolved, the charge is retained even if the stream is canceled or the
12
+ * generator fails. Await this method before starting the corresponding work.
14
13
  *
15
14
  * Pass an explicit raw-unit `amount` for request-aware or otherwise dynamic
16
15
  * pricing. When omitted, the session challenge's configured tick cost is
@@ -32,6 +31,8 @@ export type MeteredStreamOptions = {
32
31
  formatNeedVoucher(parameters: NeedVoucherEvent): string;
33
32
  /** Async source or manual-charge source. */
34
33
  generate: SessionStreamGenerator;
34
+ /** Verifies that the channel can still fund work and delivery; failures stop the stream. */
35
+ beforeCharge?: (() => MaybePromise<void>) | undefined;
35
36
  /** Runs after a nonzero reserved charge has been committed. */
36
37
  onChargeCommitted?: ((channel: ChannelStore.State) => MaybePromise<unknown>) | undefined;
37
38
  /** Store polling interval when `waitForUpdate` is unavailable. */
@@ -1,38 +1,24 @@
1
- import { commitReservedCharges, releaseReservedCharges, renewReservedCharges, reserveChargeOrWait, streamReservationLeaseMs, } from './Transports.js';
1
+ import { commitReservedCharges, releaseReservedCharges, reserveChargeOrWait } from './Transports.js';
2
2
  /** Applies voucher reservation and spend commits to an async session stream. */
3
3
  export async function* meterIterable(options) {
4
+ const manual = typeof options.generate === 'function';
4
5
  let prepaidUnits = options.prepaidUnits ?? 0;
5
- const controller = new AbortController();
6
- const signal = controller.signal;
6
+ const signal = options.signal ?? new AbortController().signal;
7
7
  const reservation = {
8
8
  store: options.store,
9
9
  channelId: options.channelId,
10
10
  reservationId: globalThis.crypto.randomUUID(),
11
11
  };
12
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
13
  const releaseOnAbort = () => {
29
- clearInterval(renewal);
30
14
  void releaseReservedCharges(reservation).catch(() => { });
31
15
  };
32
16
  signal.addEventListener('abort', releaseOnAbort, { once: true });
33
17
  if (signal.aborted)
34
18
  releaseOnAbort();
35
19
  const charge = async (amount) => {
20
+ signal.throwIfAborted();
21
+ await options.beforeCharge?.();
36
22
  signal.throwIfAborted();
37
23
  if (amount === undefined && prepaidUnits > 0) {
38
24
  prepaidUnits -= 1;
@@ -50,9 +36,13 @@ export async function* meterIterable(options) {
50
36
  signal,
51
37
  });
52
38
  pending = true;
39
+ if (manual)
40
+ await commit();
53
41
  signal.throwIfAborted();
54
42
  };
55
43
  const commit = async () => {
44
+ signal.throwIfAborted();
45
+ await options.beforeCharge?.();
56
46
  signal.throwIfAborted();
57
47
  if (!pending)
58
48
  return;
@@ -63,7 +53,6 @@ export async function* meterIterable(options) {
63
53
  await options.onChargeCommitted?.(channel);
64
54
  };
65
55
  try {
66
- const manual = typeof options.generate === 'function';
67
56
  const iterable = typeof options.generate === 'function'
68
57
  ? options.generate({ charge, signal })
69
58
  : options.generate;
@@ -74,7 +63,7 @@ export async function* meterIterable(options) {
74
63
  if (signal.aborted)
75
64
  break;
76
65
  if (done) {
77
- if (manual)
66
+ if (manual && pending)
78
67
  await commit();
79
68
  break;
80
69
  }
@@ -83,18 +72,13 @@ export async function* meterIterable(options) {
83
72
  await commit();
84
73
  yield value;
85
74
  }
86
- if (!cancelled)
87
- signal.throwIfAborted();
88
75
  }
89
76
  finally {
90
- clearInterval(renewal);
91
77
  await releaseReservedCharges(reservation);
92
78
  await iterator.return?.();
93
79
  }
94
80
  }
95
81
  finally {
96
- clearInterval(renewal);
97
- options.signal?.removeEventListener('abort', onAbort);
98
82
  signal.removeEventListener('abort', releaseOnAbort);
99
83
  await releaseReservedCharges(reservation);
100
84
  }
@@ -21,6 +21,8 @@ import { type ResolveSessionChannelId } from './RequestState.js';
21
21
  import { type SettleChargedSessionChannel } from './Settlement.js';
22
22
  import { type OnSessionSettlement, type OnSessionSettlementFailure, type SettlementSchedule } from './Settlement.js';
23
23
  import * as Ws from './Ws.js';
24
+ /** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
25
+ export declare function verifyStream(method: object, channelId: Hex): Promise<void>;
24
26
  /** Server-side automatic settlement schedule. */
25
27
  export type { SettlementSchedule } from './Settlement.js';
26
28
  /** Server-side settlement event hook types. */
@@ -131,15 +133,27 @@ export declare namespace session {
131
133
  serveWebSocket: (options: ServeWebSocketOptions) => Promise<void>;
132
134
  };
133
135
  /** WebSocket server options supplied after the session binds shared dependencies. */
134
- type ServeWebSocketOptions = Omit<Ws.serve.Options, 'onChargeCommitted' | 'settleScheduled' | 'store'>;
136
+ type ServeWebSocketOptions = Omit<Ws.serve.Options, 'beforeCharge' | 'onChargeCommitted' | 'settleScheduled' | 'store'>;
135
137
  /** Request defaults inferred from the Tempo session method schema. */
136
138
  type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.session>, 'escrowContract' | 'feePayer' | 'recipient'>;
137
139
  /** Partial fee-sponsor policy used for server-submitted session transactions. */
138
140
  type FeePayerPolicy = Partial<FeePayer.Policy>;
139
141
  /** Parameters accepted by the TIP-1034 server session payment method. */
140
142
  type Parameters = {
141
- /** TTL in milliseconds for cached on-chain channel state during non-mutating validation. Voucher acceptance always refreshes on-chain state. @default 5_000 */
143
+ /** TTL in milliseconds for cached on-chain channel state during non-mutating validation, and before a voucher accepted under `voucherStateMaxAge` starts a fresh read. @default 5_000 */
142
144
  channelStateTtl?: number | undefined;
145
+ /**
146
+ * Maximum age in milliseconds of verified on-chain channel state that a voucher may be validated
147
+ * and accepted against without waiting for a fresh read. The fresh read then finishes in the background and
148
+ * records a pending close, so later vouchers are rejected. `0` reads on-chain state before every
149
+ * acceptance.
150
+ *
151
+ * A payer can withdraw only after the escrow's `CLOSE_GRACE_PERIOD` (900 s on Tempo) following a
152
+ * close request, so vouchers accepted this way stay settleable as long as the payee settles them
153
+ * within the grace period minus this age. Keep it well below that bound.
154
+ * @default 0
155
+ */
156
+ voucherStateMaxAge?: number | undefined;
143
157
  /** Override the fee-sponsor policy used for sponsored open/topUp transactions and server-driven close transactions. */
144
158
  feePayerPolicy?: FeePayerPolicy | undefined;
145
159
  /** Minimum voucher delta to accept (numeric string, default: "0"). */
@@ -159,6 +173,19 @@ export declare namespace session {
159
173
  * containing the signed highest voucher for client rehydration.
160
174
  */
161
175
  bootstrap?: boolean | undefined;
176
+ /**
177
+ * Lets a session challenge pay for later requests to the same route until it expires. The
178
+ * challenge is bound to its route, currency, recipient and channel terms but not to the amount
179
+ * it quoted, and every request is charged its own price from the channel's accepted balance.
180
+ * A client can then send a voucher with a request instead of waiting for a `402`.
181
+ */
182
+ reusableChallenges?: boolean | undefined;
183
+ /**
184
+ * Charges request-priced content (`unitType: 'request'`) in the same store update that accepts
185
+ * its voucher, before the response, instead of in a second update when the response is
186
+ * delivered. The server must then refund requests it cannot serve.
187
+ */
188
+ chargeOnAcceptance?: boolean | undefined;
162
189
  /**
163
190
  * Atomic store backend for channel state.
164
191
  *