mppx 0.5.13 → 0.5.16

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 (83) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/Method.d.ts +5 -2
  3. package/dist/Method.d.ts.map +1 -1
  4. package/dist/Method.js.map +1 -1
  5. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  6. package/dist/mcp-sdk/server/Transport.js +8 -2
  7. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  8. package/dist/server/Mppx.d.ts.map +1 -1
  9. package/dist/server/Mppx.js +17 -10
  10. package/dist/server/Mppx.js.map +1 -1
  11. package/dist/server/Request.js +5 -1
  12. package/dist/server/Request.js.map +1 -1
  13. package/dist/server/Transport.d.ts.map +1 -1
  14. package/dist/server/Transport.js +4 -0
  15. package/dist/server/Transport.js.map +1 -1
  16. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  17. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  18. package/dist/stripe/server/internal/html.gen.js +1 -1
  19. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  20. package/dist/tempo/Methods.d.ts.map +1 -1
  21. package/dist/tempo/Methods.js +4 -2
  22. package/dist/tempo/Methods.js.map +1 -1
  23. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  24. package/dist/tempo/client/SessionManager.js +20 -10
  25. package/dist/tempo/client/SessionManager.js.map +1 -1
  26. package/dist/tempo/internal/fee-payer.d.ts +4 -1
  27. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  28. package/dist/tempo/internal/fee-payer.js +99 -23
  29. package/dist/tempo/internal/fee-payer.js.map +1 -1
  30. package/dist/tempo/server/Charge.d.ts.map +1 -1
  31. package/dist/tempo/server/Charge.js +6 -0
  32. package/dist/tempo/server/Charge.js.map +1 -1
  33. package/dist/tempo/server/Session.d.ts +4 -0
  34. package/dist/tempo/server/Session.d.ts.map +1 -1
  35. package/dist/tempo/server/Session.js +79 -48
  36. package/dist/tempo/server/Session.js.map +1 -1
  37. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  38. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  39. package/dist/tempo/server/internal/html.gen.js +1 -1
  40. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  41. package/dist/tempo/server/internal/transport.d.ts +0 -7
  42. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  43. package/dist/tempo/server/internal/transport.js +84 -13
  44. package/dist/tempo/server/internal/transport.js.map +1 -1
  45. package/dist/tempo/session/Chain.d.ts +5 -0
  46. package/dist/tempo/session/Chain.d.ts.map +1 -1
  47. package/dist/tempo/session/Chain.js +202 -63
  48. package/dist/tempo/session/Chain.js.map +1 -1
  49. package/dist/tempo/session/ChannelStore.d.ts +1 -0
  50. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  51. package/dist/tempo/session/ChannelStore.js +38 -15
  52. package/dist/tempo/session/ChannelStore.js.map +1 -1
  53. package/package.json +2 -2
  54. package/src/Method.ts +5 -2
  55. package/src/internal/changeset.test.ts +106 -0
  56. package/src/mcp-sdk/client/McpClient.integration.test.ts +634 -0
  57. package/src/mcp-sdk/server/Transport.test.ts +1 -0
  58. package/src/mcp-sdk/server/Transport.ts +10 -2
  59. package/src/proxy/Proxy.test.ts +149 -1
  60. package/src/server/Mppx.test.ts +120 -0
  61. package/src/server/Mppx.ts +27 -11
  62. package/src/server/Request.test.ts +46 -1
  63. package/src/server/Request.ts +6 -1
  64. package/src/server/Transport.test.ts +2 -0
  65. package/src/server/Transport.ts +4 -0
  66. package/src/stripe/server/internal/html.gen.ts +1 -1
  67. package/src/tempo/Methods.test.ts +13 -0
  68. package/src/tempo/Methods.ts +23 -16
  69. package/src/tempo/client/SessionManager.ts +32 -9
  70. package/src/tempo/internal/fee-payer.test.ts +88 -16
  71. package/src/tempo/internal/fee-payer.ts +118 -23
  72. package/src/tempo/server/Charge.test.ts +73 -0
  73. package/src/tempo/server/Charge.ts +6 -0
  74. package/src/tempo/server/Session.test.ts +934 -47
  75. package/src/tempo/server/Session.ts +100 -52
  76. package/src/tempo/server/internal/html.gen.ts +1 -1
  77. package/src/tempo/server/internal/transport.test.ts +321 -10
  78. package/src/tempo/server/internal/transport.ts +101 -14
  79. package/src/tempo/session/Chain.test.ts +225 -2
  80. package/src/tempo/session/Chain.ts +250 -65
  81. package/src/tempo/session/ChannelStore.test.ts +23 -0
  82. package/src/tempo/session/ChannelStore.ts +46 -13
  83. package/src/viem/Client.test.ts +52 -1
@@ -37,6 +37,7 @@ import * as Store from '../../Store.js'
37
37
  import * as Client from '../../viem/Client.js'
38
38
  import * as Account from '../internal/account.js'
39
39
  import * as defaults from '../internal/defaults.js'
40
+ import * as FeePayer from '../internal/fee-payer.js'
40
41
  import type * as types from '../internal/types.js'
41
42
  import * as Methods from '../Methods.js'
42
43
  import {
@@ -92,6 +93,7 @@ export function session<const parameters extends session.Parameters>(
92
93
  channelStateTtl = 5_000,
93
94
  currency = defaults.resolveCurrency(parameters),
94
95
  decimals = defaults.decimals,
96
+ feePayerPolicy,
95
97
  store: rawStore = Store.memory(),
96
98
  suggestedDeposit,
97
99
  unitType,
@@ -179,7 +181,7 @@ export function session<const parameters extends session.Parameters>(
179
181
  }
180
182
  },
181
183
 
182
- async verify({ credential, request }) {
184
+ async verify({ credential, envelope, request }) {
183
185
  const { challenge, payload } = credential as Credential.Credential<SessionCredentialPayload>
184
186
 
185
187
  const resolvedRequest = Methods.session.schema.request.parse(request)
@@ -203,9 +205,10 @@ export function session<const parameters extends session.Parameters>(
203
205
  payload,
204
206
  methodDetails,
205
207
  resolvedFeePayer,
208
+ feePayerPolicy,
206
209
  waitForConfirmation,
207
210
  )
208
- lastOnChainVerified.set(payload.channelId, Date.now())
211
+ lastOnChainVerified.set(sessionReceipt.channelId, Date.now())
209
212
  break
210
213
 
211
214
  case 'topUp':
@@ -216,8 +219,9 @@ export function session<const parameters extends session.Parameters>(
216
219
  payload,
217
220
  methodDetails,
218
221
  resolvedFeePayer,
222
+ feePayerPolicy,
219
223
  )
220
- lastOnChainVerified.set(payload.channelId, Date.now())
224
+ lastOnChainVerified.set(sessionReceipt.channelId, Date.now())
221
225
  break
222
226
 
223
227
  case 'voucher':
@@ -251,6 +255,28 @@ export function session<const parameters extends session.Parameters>(
251
255
  })
252
256
  }
253
257
 
258
+ // In the default HTTP request/response mode, each successful content
259
+ // request consumes one unit immediately after the credential is accepted.
260
+ // This keeps equal-voucher replays bounded by the voucher's remaining
261
+ // balance instead of serving repeated responses for free.
262
+ if (
263
+ !parameters.sse &&
264
+ envelope &&
265
+ isBillableContentRequest(envelope.capturedRequest) &&
266
+ (payload.action === 'open' || payload.action === 'voucher')
267
+ ) {
268
+ const charged = await charge(
269
+ store,
270
+ sessionReceipt.channelId,
271
+ BigInt(resolvedRequest.amount),
272
+ )
273
+ sessionReceipt = {
274
+ ...sessionReceipt,
275
+ spent: charged.spent.toString(),
276
+ units: charged.units,
277
+ }
278
+ }
279
+
254
280
  return sessionReceipt
255
281
  },
256
282
 
@@ -261,28 +287,24 @@ export function session<const parameters extends session.Parameters>(
261
287
  //
262
288
  // close and topUp are always gated (204) — they are pure management.
263
289
  //
264
- // open and voucher are gated only for bodyless POSTs (management
265
- // updates). POSTs with a body are content requests — the client's
266
- // original request piggybacked on the credential so they fall
267
- // through to serve content. GETs always fall through so auto-mode
268
- // clients (whose fetch wrapper bundles open+voucher into a single
269
- // GET retry) receive content as expected.
270
- respond({ credential, input }) {
290
+ // open and voucher share the same captured-request classifier used
291
+ // during verification. Non-billable requests are treated as management
292
+ // updates; billable requests fall through to the application handler.
293
+ respond({ credential, envelope, input }) {
271
294
  const { payload } = credential as Credential.Credential<SessionCredentialPayload>
272
295
 
273
296
  if (payload.action === 'close') return new Response(null, { status: 204 })
274
297
  if (payload.action === 'topUp') return new Response(null, { status: 204 })
275
298
 
276
- // open and voucher: gate only bodyless POSTs (management updates).
277
- // POSTs with a body are content requests — fall through so the
278
- // upstream response is returned to the client.
279
- if (input.method === 'POST') {
280
- const contentLength = input.headers.get('content-length')
281
- if (contentLength !== null && contentLength !== '0') return undefined
282
- if (input.headers.has('transfer-encoding')) return undefined
283
- return new Response(null, { status: 204 })
299
+ const capturedRequest = envelope?.capturedRequest ?? {
300
+ hasBody: input.body !== null,
301
+ headers: input.headers,
302
+ method: input.method,
303
+ url: new URL(input.url),
284
304
  }
285
- return undefined
305
+
306
+ if (isBillableContentRequest(capturedRequest)) return undefined
307
+ return new Response(null, { status: 204 })
286
308
  },
287
309
  })
288
310
  }
@@ -293,9 +315,13 @@ export declare namespace session {
293
315
  'feePayer' | 'recipient'
294
316
  >
295
317
 
318
+ type FeePayerPolicy = Partial<FeePayer.Policy>
319
+
296
320
  type Parameters = {
297
321
  /** 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 */
298
322
  channelStateTtl?: number | undefined
323
+ /** Override the fee-sponsor policy used for sponsored open/topUp transactions. */
324
+ feePayerPolicy?: FeePayerPolicy | undefined
299
325
  /** Minimum voucher delta to accept (numeric string, default: "0"). */
300
326
  minVoucherDelta?: string | undefined
301
327
  /**
@@ -444,6 +470,30 @@ function validateOnChainChannel(
444
470
  }
445
471
  }
446
472
 
473
+ function isBillableContentRequest(input: {
474
+ hasBody?: boolean | undefined
475
+ headers: Headers
476
+ method: string
477
+ }): boolean {
478
+ if (input.method === 'POST') return hasCapturedRequestBody(input)
479
+
480
+ if (input.method === 'HEAD') return false
481
+
482
+ return true
483
+ }
484
+
485
+ function hasCapturedRequestBody(input: {
486
+ hasBody?: boolean | undefined
487
+ headers: Headers
488
+ }): boolean {
489
+ const contentLength = input.headers.get('content-length')
490
+ const headerIndicatesBody =
491
+ (contentLength !== null && contentLength !== '0') || input.headers.has('transfer-encoding')
492
+
493
+ if (input.hasBody === true) return true
494
+ return headerIndicatesBody
495
+ }
496
+
447
497
  /**
448
498
  * Shared logic for verifying an incremental voucher and updating channel state.
449
499
  * Used by both handleVoucher and (indirectly) handleOpen.
@@ -561,13 +611,11 @@ async function handleOpen(
561
611
  payload: SessionCredentialPayload & { action: 'open' },
562
612
  methodDetails: SessionMethodDetails,
563
613
  feePayer: viem_Account | undefined,
614
+ feePayerPolicy: session.FeePayerPolicy | undefined,
564
615
  waitForConfirmation: boolean,
565
616
  ): Promise<SessionReceipt> {
566
- const voucher = parseVoucherFromPayload(
567
- payload.channelId,
568
- payload.cumulativeAmount,
569
- payload.signature,
570
- )
617
+ const channelId = ChannelStore.normalizeChannelId(payload.channelId)
618
+ const voucher = parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature)
571
619
 
572
620
  const recipient = challenge.request.recipient as Address
573
621
  const currency = challenge.request.currency as Address
@@ -577,9 +625,11 @@ async function handleOpen(
577
625
  client,
578
626
  serializedTransaction: payload.transaction,
579
627
  escrowContract: methodDetails.escrowContract,
580
- channelId: payload.channelId,
628
+ channelId,
581
629
  recipient,
582
630
  currency,
631
+ challengeExpires: challenge.expires,
632
+ feePayerPolicy,
583
633
  feePayer,
584
634
  waitForConfirmation,
585
635
  })
@@ -612,7 +662,7 @@ async function handleOpen(
612
662
  throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
613
663
  }
614
664
 
615
- const updated = await store.updateChannel(payload.channelId, (existing) => {
665
+ const updated = await store.updateChannel(channelId, (existing) => {
616
666
  if (existing) {
617
667
  if (voucher.cumulativeAmount <= existing.settledOnChain) {
618
668
  throw new VerificationFailedError({
@@ -644,7 +694,7 @@ async function handleOpen(
644
694
  }
645
695
  }
646
696
  return {
647
- channelId: payload.channelId,
697
+ channelId,
648
698
  chainId: methodDetails.chainId,
649
699
  escrowContract: methodDetails.escrowContract,
650
700
  closeRequestedAt: onChain.closeRequestedAt,
@@ -667,7 +717,7 @@ async function handleOpen(
667
717
 
668
718
  return createSessionReceipt({
669
719
  challengeId: challenge.id,
670
- channelId: payload.channelId,
720
+ channelId: updated.channelId,
671
721
  acceptedCumulative: updated.highestVoucherAmount,
672
722
  spent: updated.spent,
673
723
  units: updated.units,
@@ -689,8 +739,10 @@ async function handleTopUp(
689
739
  payload: SessionCredentialPayload & { action: 'topUp' },
690
740
  methodDetails: SessionMethodDetails,
691
741
  feePayer: viem_Account | undefined,
742
+ feePayerPolicy: session.FeePayerPolicy | undefined,
692
743
  ): Promise<SessionReceipt> {
693
- const channel = await store.getChannel(payload.channelId)
744
+ const channelId = ChannelStore.normalizeChannelId(payload.channelId)
745
+ const channel = await store.getChannel(channelId)
694
746
  if (!channel) {
695
747
  throw new ChannelNotFoundError({ reason: 'channel not found' })
696
748
  }
@@ -701,21 +753,23 @@ async function handleTopUp(
701
753
  client,
702
754
  serializedTransaction: payload.transaction,
703
755
  escrowContract: methodDetails.escrowContract,
704
- channelId: payload.channelId,
756
+ channelId,
705
757
  currency: challenge.request.currency as Address,
706
758
  declaredDeposit,
707
759
  previousDeposit: channel.deposit,
760
+ challengeExpires: challenge.expires,
761
+ feePayerPolicy,
708
762
  feePayer,
709
763
  })
710
764
 
711
- const updated = await store.updateChannel(payload.channelId, (current) => {
765
+ const updated = await store.updateChannel(channelId, (current) => {
712
766
  if (!current) throw new ChannelNotFoundError({ reason: 'channel not found' })
713
767
  return { ...current, deposit: onChainDeposit }
714
768
  })
715
769
 
716
770
  return createSessionReceipt({
717
771
  challengeId: challenge.id,
718
- channelId: payload.channelId,
772
+ channelId: updated?.channelId ?? channel.channelId,
719
773
  acceptedCumulative: updated?.highestVoucherAmount ?? channel.highestVoucherAmount,
720
774
  spent: updated?.spent ?? channel.spent,
721
775
  units: updated?.units ?? channel.units,
@@ -735,7 +789,8 @@ async function handleVoucher(
735
789
  channelStateTtl: number,
736
790
  lastOnChainVerified: Map<Hex, number>,
737
791
  ): Promise<SessionReceipt> {
738
- const channel = await store.getChannel(payload.channelId)
792
+ const channelId = ChannelStore.normalizeChannelId(payload.channelId)
793
+ const channel = await store.getChannel(channelId)
739
794
  if (!channel) {
740
795
  throw new ChannelNotFoundError({ reason: 'channel not found' })
741
796
  }
@@ -743,11 +798,7 @@ async function handleVoucher(
743
798
  throw new ChannelClosedError({ reason: 'channel is finalized' })
744
799
  }
745
800
 
746
- const voucher = parseVoucherFromPayload(
747
- payload.channelId,
748
- payload.cumulativeAmount,
749
- payload.signature,
750
- )
801
+ const voucher = parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature)
751
802
 
752
803
  // Use locally-stored channel state as a trusted cache instead of
753
804
  // reading on-chain for every voucher. The on-chain state is verified
@@ -759,7 +810,7 @@ async function handleVoucher(
759
810
  // To guard against the payer initiating a forced close while vouchers
760
811
  // are still being accepted, re-query on-chain state when the cache
761
812
  // exceeds the configured staleness TTL (default: 5s).
762
- const lastVerified = lastOnChainVerified.get(payload.channelId) ?? 0
813
+ const lastVerified = lastOnChainVerified.get(channelId) ?? 0
763
814
  const isStale = Date.now() - lastVerified > channelStateTtl
764
815
 
765
816
  const onChain = await (async () => {
@@ -767,13 +818,13 @@ async function handleVoucher(
767
818
  const onChainChannel = await getOnChainChannel(
768
819
  client,
769
820
  methodDetails.escrowContract,
770
- payload.channelId,
821
+ channelId,
771
822
  )
772
- lastOnChainVerified.set(payload.channelId, Date.now())
823
+ lastOnChainVerified.set(channelId, Date.now())
773
824
  // Persist closeRequestedAt so the cached path detects force-close
774
825
  // between re-queries.
775
826
  if (onChainChannel.closeRequestedAt !== 0n) {
776
- await store.updateChannel(payload.channelId, (current) =>
827
+ await store.updateChannel(channelId, (current) =>
777
828
  current ? { ...current, closeRequestedAt: onChainChannel.closeRequestedAt } : current,
778
829
  )
779
830
  }
@@ -796,7 +847,7 @@ async function handleVoucher(
796
847
  minVoucherDelta,
797
848
  challenge,
798
849
  channel,
799
- channelId: payload.channelId,
850
+ channelId,
800
851
  voucher,
801
852
  onChain,
802
853
  methodDetails,
@@ -815,7 +866,8 @@ async function handleClose(
815
866
  account?: viem_Account,
816
867
  feePayer?: viem_Account,
817
868
  ): Promise<SessionReceipt> {
818
- const channel = await store.getChannel(payload.channelId)
869
+ const channelId = ChannelStore.normalizeChannelId(payload.channelId)
870
+ const channel = await store.getChannel(channelId)
819
871
  if (!channel) {
820
872
  throw new ChannelNotFoundError({ reason: 'channel not found' })
821
873
  }
@@ -823,13 +875,9 @@ async function handleClose(
823
875
  throw new ChannelClosedError({ reason: 'channel is already finalized' })
824
876
  }
825
877
 
826
- const voucher = parseVoucherFromPayload(
827
- payload.channelId,
828
- payload.cumulativeAmount,
829
- payload.signature,
830
- )
878
+ const voucher = parseVoucherFromPayload(channelId, payload.cumulativeAmount, payload.signature)
831
879
 
832
- const onChain = await getOnChainChannel(client, methodDetails.escrowContract, payload.channelId)
880
+ const onChain = await getOnChainChannel(client, methodDetails.escrowContract, channelId)
833
881
 
834
882
  if (onChain.finalized) {
835
883
  throw new ChannelClosedError({ reason: 'channel is finalized on-chain' })
@@ -867,7 +915,7 @@ async function handleClose(
867
915
  ...(feePayer && account ? { feePayer, account } : { account }),
868
916
  })
869
917
 
870
- const updated = await store.updateChannel(payload.channelId, (current) => {
918
+ const updated = await store.updateChannel(channelId, (current) => {
871
919
  if (!current) return null
872
920
  const updateVoucher = voucher.cumulativeAmount > current.highestVoucherAmount
873
921
  return {
@@ -883,7 +931,7 @@ async function handleClose(
883
931
 
884
932
  return createSessionReceipt({
885
933
  challengeId: challenge.id,
886
- channelId: payload.channelId,
934
+ channelId: updated?.channelId ?? channel.channelId,
887
935
  acceptedCumulative: voucher.cumulativeAmount,
888
936
  spent: updated?.spent ?? channel.spent,
889
937
  units: updated?.units ?? channel.units,