mppx 0.6.12 → 0.6.14

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 (65) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  3. package/dist/cli/plugins/tempo.js +24 -1
  4. package/dist/cli/plugins/tempo.js.map +1 -1
  5. package/dist/middlewares/express.d.ts.map +1 -1
  6. package/dist/middlewares/express.js +22 -0
  7. package/dist/middlewares/express.js.map +1 -1
  8. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  9. package/dist/tempo/client/SessionManager.js +26 -1
  10. package/dist/tempo/client/SessionManager.js.map +1 -1
  11. package/dist/tempo/internal/fee-payer.d.ts +11 -1
  12. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  13. package/dist/tempo/internal/fee-payer.js +71 -6
  14. package/dist/tempo/internal/fee-payer.js.map +1 -1
  15. package/dist/tempo/server/Charge.d.ts.map +1 -1
  16. package/dist/tempo/server/Charge.js +53 -10
  17. package/dist/tempo/server/Charge.js.map +1 -1
  18. package/dist/tempo/server/Session.d.ts.map +1 -1
  19. package/dist/tempo/server/Session.js +80 -29
  20. package/dist/tempo/server/Session.js.map +1 -1
  21. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  22. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  23. package/dist/tempo/server/internal/html.gen.js +1 -1
  24. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  25. package/dist/tempo/server/internal/request-body.d.ts +1 -1
  26. package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
  27. package/dist/tempo/server/internal/request-body.js +3 -0
  28. package/dist/tempo/server/internal/request-body.js.map +1 -1
  29. package/dist/tempo/server/internal/transport.d.ts +7 -0
  30. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  31. package/dist/tempo/server/internal/transport.js +16 -0
  32. package/dist/tempo/server/internal/transport.js.map +1 -1
  33. package/dist/tempo/session/Chain.d.ts +1 -0
  34. package/dist/tempo/session/Chain.d.ts.map +1 -1
  35. package/dist/tempo/session/Chain.js +28 -11
  36. package/dist/tempo/session/Chain.js.map +1 -1
  37. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  38. package/dist/tempo/session/ChannelStore.js +6 -0
  39. package/dist/tempo/session/ChannelStore.js.map +1 -1
  40. package/dist/tempo/session/Sse.d.ts +1 -0
  41. package/dist/tempo/session/Sse.d.ts.map +1 -1
  42. package/dist/tempo/session/Sse.js +34 -12
  43. package/dist/tempo/session/Sse.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/cli/plugins/tempo.ts +28 -1
  46. package/src/middlewares/express.test.ts +27 -0
  47. package/src/middlewares/express.ts +24 -0
  48. package/src/tempo/client/SessionManager.ts +26 -1
  49. package/src/tempo/internal/fee-payer.test.ts +139 -0
  50. package/src/tempo/internal/fee-payer.ts +85 -6
  51. package/src/tempo/server/Charge.test.ts +119 -0
  52. package/src/tempo/server/Charge.ts +70 -10
  53. package/src/tempo/server/Session.test.ts +327 -0
  54. package/src/tempo/server/Session.ts +91 -39
  55. package/src/tempo/server/internal/html.gen.ts +1 -1
  56. package/src/tempo/server/internal/request-body.test.ts +26 -0
  57. package/src/tempo/server/internal/request-body.ts +4 -1
  58. package/src/tempo/server/internal/transport.test.ts +28 -2
  59. package/src/tempo/server/internal/transport.ts +23 -0
  60. package/src/tempo/session/Chain.test.ts +140 -1
  61. package/src/tempo/session/Chain.ts +34 -10
  62. package/src/tempo/session/ChannelStore.test.ts +21 -0
  63. package/src/tempo/session/ChannelStore.ts +6 -0
  64. package/src/tempo/session/Sse.test.ts +52 -0
  65. package/src/tempo/session/Sse.ts +22 -2
@@ -168,9 +168,10 @@ export function session<const parameters extends session.Parameters>(
168
168
 
169
169
  // Extract feePayer.
170
170
  const resolvedFeePayer = (() => {
171
+ if (request.feePayer === false) return credential ? false : undefined
171
172
  const account = typeof request.feePayer === 'object' ? request.feePayer : feePayer
172
- const requested = request.feePayer !== false && (account ?? feePayer ?? feePayerUrl)
173
- if (credential) return account
173
+ const requested = account ?? feePayer ?? feePayerUrl
174
+ if (credential) return account ?? (feePayerUrl ? true : undefined)
174
175
  if (requested) return true
175
176
  return undefined
176
177
  })()
@@ -196,7 +197,17 @@ export function session<const parameters extends session.Parameters>(
196
197
  const methodDetails = resolvedRequest.methodDetails as SessionMethodDetails
197
198
  const client = await getClient({ chainId: methodDetails.chainId })
198
199
 
199
- const resolvedFeePayer = methodDetails.feePayer === true ? feePayer : undefined
200
+ const requestAllowsFeePayer =
201
+ request.feePayer !== false &&
202
+ (request.feePayer === undefined ||
203
+ request.feePayer === true ||
204
+ typeof request.feePayer === 'object')
205
+ const resolvedFeePayer =
206
+ methodDetails.feePayer === true && requestAllowsFeePayer
207
+ ? typeof request.feePayer === 'object'
208
+ ? request.feePayer
209
+ : feePayer
210
+ : undefined
200
211
  const minVoucherDelta = parseUnits(parameters.minVoucherDelta ?? '0', decimals)
201
212
  const effectiveMinVoucherDelta = methodDetails.minVoucherDelta
202
213
  ? BigInt(methodDetails.minVoucherDelta)
@@ -268,7 +279,6 @@ export function session<const parameters extends session.Parameters>(
268
279
  // This keeps equal-voucher replays bounded by the voucher's remaining
269
280
  // balance instead of serving repeated responses for free.
270
281
  if (
271
- !parameters.sse &&
272
282
  envelope &&
273
283
  isSessionContentRequest(envelope.capturedRequest) &&
274
284
  (payload.action === 'open' || payload.action === 'voucher')
@@ -283,6 +293,7 @@ export function session<const parameters extends session.Parameters>(
283
293
  spent: charged.spent.toString(),
284
294
  units: charged.units,
285
295
  }
296
+ if (parameters.sse) sessionReceipt = Transport.markPrepaidSessionTick(sessionReceipt)
286
297
  }
287
298
 
288
299
  return sessionReceipt
@@ -454,6 +465,9 @@ export async function charge(
454
465
  throw new ChannelClosedError({ reason: 'channel not found' })
455
466
  }
456
467
  if (!result.ok) {
468
+ if (result.channel.finalized) throw new ChannelClosedError({ reason: 'channel is finalized' })
469
+ if (result.channel.closeRequestedAt !== 0n)
470
+ throw new ChannelClosedError({ reason: 'channel has a pending close request' })
457
471
  const available = result.channel.highestVoucherAmount - result.channel.spent
458
472
  throw new InsufficientBalanceError({
459
473
  reason: `requested ${amount}, available ${available}`,
@@ -622,6 +636,38 @@ async function handleOpen(
622
636
  const currency = challenge.request.currency as Address
623
637
  const amount = challenge.request.amount ? BigInt(challenge.request.amount as string) : undefined
624
638
 
639
+ const validateOpenVoucher = async (onChain: OnChainChannel) => {
640
+ validateOnChainChannel(onChain, recipient, currency, amount)
641
+
642
+ const authorizedSigner =
643
+ onChain.authorizedSigner === '0x0000000000000000000000000000000000000000'
644
+ ? onChain.payer
645
+ : onChain.authorizedSigner
646
+
647
+ if (voucher.cumulativeAmount > onChain.deposit) {
648
+ throw new AmountExceedsDepositError({ reason: 'voucher amount exceeds on-chain deposit' })
649
+ }
650
+
651
+ if (voucher.cumulativeAmount <= onChain.settled) {
652
+ throw new VerificationFailedError({
653
+ reason: 'voucher cumulativeAmount is below on-chain settled amount',
654
+ })
655
+ }
656
+
657
+ const isValid = await verifyVoucher(
658
+ methodDetails.escrowContract,
659
+ methodDetails.chainId,
660
+ voucher,
661
+ authorizedSigner,
662
+ )
663
+
664
+ if (!isValid) {
665
+ throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
666
+ }
667
+
668
+ return authorizedSigner
669
+ }
670
+
625
671
  const { onChain, txHash } = await broadcastOpenTransaction({
626
672
  client,
627
673
  serializedTransaction: payload.transaction,
@@ -632,36 +678,13 @@ async function handleOpen(
632
678
  challengeExpires: challenge.expires,
633
679
  feePayerPolicy,
634
680
  feePayer,
681
+ beforeBroadcast: async (pendingOnChain) => {
682
+ await validateOpenVoucher(pendingOnChain)
683
+ },
635
684
  waitForConfirmation,
636
685
  })
637
686
 
638
- validateOnChainChannel(onChain, recipient, currency, amount)
639
-
640
- const authorizedSigner =
641
- onChain.authorizedSigner === '0x0000000000000000000000000000000000000000'
642
- ? onChain.payer
643
- : onChain.authorizedSigner
644
-
645
- if (voucher.cumulativeAmount > onChain.deposit) {
646
- throw new AmountExceedsDepositError({ reason: 'voucher amount exceeds on-chain deposit' })
647
- }
648
-
649
- if (voucher.cumulativeAmount <= onChain.settled) {
650
- throw new VerificationFailedError({
651
- reason: 'voucher cumulativeAmount is below on-chain settled amount',
652
- })
653
- }
654
-
655
- const isValid = await verifyVoucher(
656
- methodDetails.escrowContract,
657
- methodDetails.chainId,
658
- voucher,
659
- authorizedSigner,
660
- )
661
-
662
- if (!isValid) {
663
- throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
664
- }
687
+ const authorizedSigner = await validateOpenVoucher(onChain)
665
688
 
666
689
  const updated = await store.updateChannel(channelId, (existing) => {
667
690
  if (existing) {
@@ -914,16 +937,45 @@ async function handleClose(
914
937
  throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
915
938
  }
916
939
 
917
- assertSettlementSender({
918
- operation: 'close',
919
- channelId: payload.channelId,
920
- payee: onChain.payee,
921
- sender: account?.address ?? client.account?.address,
940
+ const pendingCloseStartedAt = BigInt(Math.floor(Date.now() / 1000) || 1)
941
+ const previousCloseRequestedAt = channel.closeRequestedAt
942
+ let pendingCloseMarked = false
943
+ await store.updateChannel(channelId, (current) => {
944
+ if (!current) return null
945
+ if (current.finalized) throw new ChannelClosedError({ reason: 'channel is already finalized' })
946
+ if (current.closeRequestedAt !== 0n)
947
+ throw new ChannelClosedError({ reason: 'channel has a pending close request' })
948
+ if (voucher.cumulativeAmount < current.spent) {
949
+ throw new VerificationFailedError({
950
+ reason: `close voucher amount must be >= ${current.spent} (spent)`,
951
+ })
952
+ }
953
+ pendingCloseMarked = true
954
+ return { ...current, closeRequestedAt: pendingCloseStartedAt }
922
955
  })
923
956
 
924
- const txHash = await closeOnChain(client, methodDetails.escrowContract, voucher, {
925
- ...(feePayer && account ? { feePayer, account } : { account }),
926
- })
957
+ let txHash: Hex | undefined
958
+ try {
959
+ assertSettlementSender({
960
+ operation: 'close',
961
+ channelId: payload.channelId,
962
+ payee: onChain.payee,
963
+ sender: account?.address ?? client.account?.address,
964
+ })
965
+
966
+ txHash = await closeOnChain(client, methodDetails.escrowContract, voucher, {
967
+ ...(feePayer && account ? { feePayer, account } : { account }),
968
+ })
969
+ } catch (error) {
970
+ if (pendingCloseMarked) {
971
+ await store.updateChannel(channelId, (current) =>
972
+ current && current.closeRequestedAt === pendingCloseStartedAt
973
+ ? { ...current, closeRequestedAt: previousCloseRequestedAt }
974
+ : current,
975
+ )
976
+ }
977
+ throw error
978
+ }
927
979
 
928
980
  const updated = await store.updateChannel(channelId, (current) => {
929
981
  if (!current) return null