mppx 0.6.28 → 0.6.30

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 (272) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/Challenge.d.ts.map +1 -1
  3. package/dist/Challenge.js +16 -10
  4. package/dist/Challenge.js.map +1 -1
  5. package/dist/Method.d.ts +1 -1
  6. package/dist/Method.d.ts.map +1 -1
  7. package/dist/client/Methods.d.ts +1 -0
  8. package/dist/client/Methods.d.ts.map +1 -1
  9. package/dist/client/Methods.js +1 -0
  10. package/dist/client/Methods.js.map +1 -1
  11. package/dist/client/Mppx.d.ts +3 -3
  12. package/dist/client/Mppx.d.ts.map +1 -1
  13. package/dist/client/Mppx.js +1 -0
  14. package/dist/client/Mppx.js.map +1 -1
  15. package/dist/client/Transport.d.ts +10 -3
  16. package/dist/client/Transport.d.ts.map +1 -1
  17. package/dist/client/Transport.js +60 -7
  18. package/dist/client/Transport.js.map +1 -1
  19. package/dist/client/index.d.ts +1 -1
  20. package/dist/client/index.d.ts.map +1 -1
  21. package/dist/client/index.js +1 -1
  22. package/dist/client/index.js.map +1 -1
  23. package/dist/client/internal/Fetch.d.ts +3 -0
  24. package/dist/client/internal/Fetch.d.ts.map +1 -1
  25. package/dist/client/internal/Fetch.js +12 -20
  26. package/dist/client/internal/Fetch.js.map +1 -1
  27. package/dist/evm/Assets.d.ts +2 -0
  28. package/dist/evm/Assets.d.ts.map +1 -0
  29. package/dist/evm/Assets.js +2 -0
  30. package/dist/evm/Assets.js.map +1 -0
  31. package/dist/evm/Chains.d.ts +5 -0
  32. package/dist/evm/Chains.d.ts.map +1 -0
  33. package/dist/evm/Chains.js +5 -0
  34. package/dist/evm/Chains.js.map +1 -0
  35. package/dist/evm/Methods.d.ts +68 -0
  36. package/dist/evm/Methods.d.ts.map +1 -0
  37. package/dist/evm/Methods.js +28 -0
  38. package/dist/evm/Methods.js.map +1 -0
  39. package/dist/evm/Types.d.ts +143 -0
  40. package/dist/evm/Types.d.ts.map +1 -0
  41. package/dist/evm/Types.js +102 -0
  42. package/dist/evm/Types.js.map +1 -0
  43. package/dist/evm/client/Charge.d.ts +102 -0
  44. package/dist/evm/client/Charge.d.ts.map +1 -0
  45. package/dist/evm/client/Charge.js +141 -0
  46. package/dist/evm/client/Charge.js.map +1 -0
  47. package/dist/evm/client/Methods.d.ts +81 -0
  48. package/dist/evm/client/Methods.d.ts.map +1 -0
  49. package/dist/evm/client/Methods.js +16 -0
  50. package/dist/evm/client/Methods.js.map +1 -0
  51. package/dist/evm/client/index.d.ts +6 -0
  52. package/dist/evm/client/index.d.ts.map +1 -0
  53. package/dist/evm/client/index.js +6 -0
  54. package/dist/evm/client/index.js.map +1 -0
  55. package/dist/evm/index.d.ts +10 -0
  56. package/dist/evm/index.d.ts.map +1 -0
  57. package/dist/evm/index.js +9 -0
  58. package/dist/evm/index.js.map +1 -0
  59. package/dist/evm/server/Charge.d.ts +62 -0
  60. package/dist/evm/server/Charge.d.ts.map +1 -0
  61. package/dist/evm/server/Charge.js +172 -0
  62. package/dist/evm/server/Charge.js.map +1 -0
  63. package/dist/evm/server/Methods.d.ts +80 -0
  64. package/dist/evm/server/Methods.d.ts.map +1 -0
  65. package/dist/evm/server/Methods.js +16 -0
  66. package/dist/evm/server/Methods.js.map +1 -0
  67. package/dist/evm/server/index.d.ts +6 -0
  68. package/dist/evm/server/index.d.ts.map +1 -0
  69. package/dist/evm/server/index.js +6 -0
  70. package/dist/evm/server/index.js.map +1 -0
  71. package/dist/index.d.ts +2 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +2 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/internal/HeaderCodec.d.ts +18 -0
  76. package/dist/internal/HeaderCodec.d.ts.map +1 -0
  77. package/dist/internal/HeaderCodec.js +31 -0
  78. package/dist/internal/HeaderCodec.js.map +1 -0
  79. package/dist/middlewares/elysia.d.ts.map +1 -1
  80. package/dist/middlewares/elysia.js +2 -3
  81. package/dist/middlewares/elysia.js.map +1 -1
  82. package/dist/middlewares/express.js +2 -1
  83. package/dist/middlewares/express.js.map +1 -1
  84. package/dist/proxy/internal/Headers.d.ts.map +1 -1
  85. package/dist/proxy/internal/Headers.js +11 -1
  86. package/dist/proxy/internal/Headers.js.map +1 -1
  87. package/dist/proxy/services/openai.d.ts.map +1 -1
  88. package/dist/proxy/services/openai.js +2 -0
  89. package/dist/proxy/services/openai.js.map +1 -1
  90. package/dist/server/Methods.d.ts +1 -0
  91. package/dist/server/Methods.d.ts.map +1 -1
  92. package/dist/server/Methods.js +1 -0
  93. package/dist/server/Methods.js.map +1 -1
  94. package/dist/server/Mppx.d.ts.map +1 -1
  95. package/dist/server/Mppx.js +90 -12
  96. package/dist/server/Mppx.js.map +1 -1
  97. package/dist/server/Transport.d.ts +10 -0
  98. package/dist/server/Transport.d.ts.map +1 -1
  99. package/dist/server/Transport.js +9 -0
  100. package/dist/server/Transport.js.map +1 -1
  101. package/dist/server/index.d.ts +1 -1
  102. package/dist/server/index.d.ts.map +1 -1
  103. package/dist/server/index.js +1 -1
  104. package/dist/server/index.js.map +1 -1
  105. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  106. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  107. package/dist/stripe/server/internal/html.gen.js +1 -1
  108. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  109. package/dist/tempo/client/ChannelOps.d.ts +3 -3
  110. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  111. package/dist/tempo/client/ChannelOps.js +13 -6
  112. package/dist/tempo/client/ChannelOps.js.map +1 -1
  113. package/dist/tempo/client/Charge.d.ts.map +1 -1
  114. package/dist/tempo/client/Charge.js +8 -5
  115. package/dist/tempo/client/Charge.js.map +1 -1
  116. package/dist/tempo/client/Methods.d.ts +0 -1
  117. package/dist/tempo/client/Methods.d.ts.map +1 -1
  118. package/dist/tempo/client/Session.d.ts +2 -4
  119. package/dist/tempo/client/Session.d.ts.map +1 -1
  120. package/dist/tempo/client/Session.js +10 -12
  121. package/dist/tempo/client/Session.js.map +1 -1
  122. package/dist/tempo/client/SessionManager.d.ts +3 -3
  123. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  124. package/dist/tempo/client/SessionManager.js +1 -1
  125. package/dist/tempo/client/SessionManager.js.map +1 -1
  126. package/dist/tempo/internal/account.d.ts +5 -0
  127. package/dist/tempo/internal/account.d.ts.map +1 -1
  128. package/dist/tempo/internal/account.js +8 -0
  129. package/dist/tempo/internal/account.js.map +1 -1
  130. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  131. package/dist/tempo/internal/fee-payer.js +5 -2
  132. package/dist/tempo/internal/fee-payer.js.map +1 -1
  133. package/dist/tempo/server/Charge.d.ts.map +1 -1
  134. package/dist/tempo/server/Charge.js +23 -1
  135. package/dist/tempo/server/Charge.js.map +1 -1
  136. package/dist/tempo/server/Session.d.ts.map +1 -1
  137. package/dist/tempo/server/Session.js +13 -12
  138. package/dist/tempo/server/Session.js.map +1 -1
  139. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  140. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  141. package/dist/tempo/server/internal/html.gen.js +1 -1
  142. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  143. package/dist/tempo/session/Chain.d.ts +2 -0
  144. package/dist/tempo/session/Chain.d.ts.map +1 -1
  145. package/dist/tempo/session/Chain.js +8 -8
  146. package/dist/tempo/session/Chain.js.map +1 -1
  147. package/dist/tempo/session/Voucher.d.ts +4 -3
  148. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  149. package/dist/tempo/session/Voucher.js +71 -44
  150. package/dist/tempo/session/Voucher.js.map +1 -1
  151. package/dist/tempo/session/Ws.d.ts.map +1 -1
  152. package/dist/tempo/session/Ws.js +15 -0
  153. package/dist/tempo/session/Ws.js.map +1 -1
  154. package/dist/tempo/subscription/KeyAuthorization.d.ts +2 -2
  155. package/dist/x402/Assets.d.ts +29 -0
  156. package/dist/x402/Assets.d.ts.map +1 -0
  157. package/dist/x402/Assets.js +46 -0
  158. package/dist/x402/Assets.js.map +1 -0
  159. package/dist/x402/Header.d.ts +14 -0
  160. package/dist/x402/Header.d.ts.map +1 -0
  161. package/dist/x402/Header.js +18 -0
  162. package/dist/x402/Header.js.map +1 -0
  163. package/dist/x402/Types.d.ts +289 -0
  164. package/dist/x402/Types.d.ts.map +1 -0
  165. package/dist/x402/Types.js +139 -0
  166. package/dist/x402/Types.js.map +1 -0
  167. package/dist/x402/client/Exact.d.ts +38 -0
  168. package/dist/x402/client/Exact.d.ts.map +1 -0
  169. package/dist/x402/client/Exact.js +141 -0
  170. package/dist/x402/client/Exact.js.map +1 -0
  171. package/dist/x402/index.d.ts +6 -0
  172. package/dist/x402/index.d.ts.map +1 -0
  173. package/dist/x402/index.js +6 -0
  174. package/dist/x402/index.js.map +1 -0
  175. package/dist/x402/internal/ChallengeBrand.d.ts +5 -0
  176. package/dist/x402/internal/ChallengeBrand.d.ts.map +1 -0
  177. package/dist/x402/internal/ChallengeBrand.js +13 -0
  178. package/dist/x402/internal/ChallengeBrand.js.map +1 -0
  179. package/dist/x402/internal/RouteBinding.d.ts +8 -0
  180. package/dist/x402/internal/RouteBinding.d.ts.map +1 -0
  181. package/dist/x402/internal/RouteBinding.js +12 -0
  182. package/dist/x402/internal/RouteBinding.js.map +1 -0
  183. package/dist/x402/server/EvmCharge.d.ts +50 -0
  184. package/dist/x402/server/EvmCharge.d.ts.map +1 -0
  185. package/dist/x402/server/EvmCharge.js +301 -0
  186. package/dist/x402/server/EvmCharge.js.map +1 -0
  187. package/dist/x402/server/Facilitator.d.ts +12 -0
  188. package/dist/x402/server/Facilitator.d.ts.map +1 -0
  189. package/dist/x402/server/Facilitator.js +42 -0
  190. package/dist/x402/server/Facilitator.js.map +1 -0
  191. package/package.json +41 -21
  192. package/src/Challenge.test.ts +54 -0
  193. package/src/Challenge.ts +17 -10
  194. package/src/Method.ts +1 -1
  195. package/src/client/Methods.ts +1 -0
  196. package/src/client/Mppx.ts +4 -3
  197. package/src/client/Transport.test.ts +165 -30
  198. package/src/client/Transport.ts +76 -8
  199. package/src/client/index.ts +1 -1
  200. package/src/client/internal/Fetch.test.ts +31 -2
  201. package/src/client/internal/Fetch.ts +26 -19
  202. package/src/evm/Assets.ts +1 -0
  203. package/src/evm/Chains.ts +5 -0
  204. package/src/evm/Methods.ts +44 -0
  205. package/src/evm/PublicInterface.test-d.ts +114 -0
  206. package/src/evm/Types.ts +140 -0
  207. package/src/evm/client/Charge.test.ts +99 -0
  208. package/src/evm/client/Charge.ts +198 -0
  209. package/src/evm/client/Methods.ts +19 -0
  210. package/src/evm/client/index.ts +5 -0
  211. package/src/evm/index.ts +14 -0
  212. package/src/evm/server/Charge.test.ts +199 -0
  213. package/src/evm/server/Charge.ts +283 -0
  214. package/src/evm/server/Methods.ts +22 -0
  215. package/src/evm/server/index.ts +5 -0
  216. package/src/index.ts +2 -0
  217. package/src/internal/HeaderCodec.ts +36 -0
  218. package/src/middlewares/elysia.test.ts +25 -0
  219. package/src/middlewares/elysia.ts +1 -2
  220. package/src/middlewares/express.test.ts +28 -0
  221. package/src/middlewares/express.ts +1 -1
  222. package/src/middlewares/hono.test.ts +138 -2
  223. package/src/middlewares/nextjs.test.ts +22 -0
  224. package/src/proxy/internal/Headers.test.ts +20 -0
  225. package/src/proxy/internal/Headers.ts +12 -1
  226. package/src/proxy/services/openai.test.ts +57 -1
  227. package/src/proxy/services/openai.ts +2 -0
  228. package/src/server/Methods.ts +1 -0
  229. package/src/server/Mppx.test.ts +244 -1
  230. package/src/server/Mppx.ts +124 -11
  231. package/src/server/NodeListener.test.ts +28 -1
  232. package/src/server/Transport.test.ts +19 -0
  233. package/src/server/Transport.ts +20 -0
  234. package/src/server/index.ts +1 -1
  235. package/src/stripe/server/internal/html.gen.ts +1 -1
  236. package/src/tempo/AccessKeyAuthorization.test.ts +231 -0
  237. package/src/tempo/client/ChannelOps.test.ts +61 -7
  238. package/src/tempo/client/ChannelOps.ts +18 -7
  239. package/src/tempo/client/Charge.test.ts +126 -0
  240. package/src/tempo/client/Charge.ts +10 -6
  241. package/src/tempo/client/Session.test.ts +130 -1
  242. package/src/tempo/client/Session.ts +12 -19
  243. package/src/tempo/client/SessionManager.test.ts +69 -2
  244. package/src/tempo/client/SessionManager.ts +4 -4
  245. package/src/tempo/internal/account.ts +13 -0
  246. package/src/tempo/internal/fee-payer.test.ts +32 -2
  247. package/src/tempo/internal/fee-payer.ts +6 -2
  248. package/src/tempo/server/Charge.test.ts +69 -0
  249. package/src/tempo/server/Charge.ts +32 -0
  250. package/src/tempo/server/Session.test.ts +30 -0
  251. package/src/tempo/server/Session.ts +15 -16
  252. package/src/tempo/server/internal/html.gen.ts +1 -1
  253. package/src/tempo/session/Chain.test.ts +4 -4
  254. package/src/tempo/session/Chain.ts +10 -6
  255. package/src/tempo/session/Voucher.test.ts +230 -1
  256. package/src/tempo/session/Voucher.ts +96 -48
  257. package/src/tempo/session/Ws.test.ts +71 -0
  258. package/src/tempo/session/Ws.ts +13 -0
  259. package/src/x402/Assets.ts +65 -0
  260. package/src/x402/Exact.e2e.test.ts +448 -0
  261. package/src/x402/Header.test.ts +73 -0
  262. package/src/x402/Header.ts +34 -0
  263. package/src/x402/PublicInterface.test-d.ts +39 -0
  264. package/src/x402/Types.ts +248 -0
  265. package/src/x402/client/Exact.test.ts +180 -0
  266. package/src/x402/client/Exact.ts +198 -0
  267. package/src/x402/index.ts +5 -0
  268. package/src/x402/internal/ChallengeBrand.ts +14 -0
  269. package/src/x402/internal/RouteBinding.ts +18 -0
  270. package/src/x402/server/EvmCharge.ts +394 -0
  271. package/src/x402/server/Facilitator.test.ts +111 -0
  272. package/src/x402/server/Facilitator.ts +56 -0
@@ -310,7 +310,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
310
310
  expect(result.onChain.finalized).toBe(false)
311
311
  })
312
312
 
313
- test('fee-payer: rejects unauthorized calls', async () => {
313
+ test('fee-payer relay: rejects unauthorized open calls', async () => {
314
314
  const salt = nextSalt()
315
315
  const deposit = 5_000_000n
316
316
 
@@ -349,7 +349,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
349
349
  channelId,
350
350
  recipient,
351
351
  currency,
352
- feePayer: accounts[0],
352
+ isSponsored: true,
353
353
  }),
354
354
  ).rejects.toThrow('fee-sponsored open transaction contains an unauthorized call')
355
355
  })
@@ -798,7 +798,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
798
798
  expect(result.newDeposit).toBe(deposit + topUpAmount)
799
799
  })
800
800
 
801
- test('fee-payer: rejects unauthorized calls', async () => {
801
+ test('fee-payer relay: rejects unauthorized topUp calls', async () => {
802
802
  const salt = nextSalt()
803
803
  const deposit = 5_000_000n
804
804
  const topUpAmount = 3_000_000n
@@ -847,7 +847,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
847
847
  currency: asset,
848
848
  declaredDeposit: topUpAmount,
849
849
  previousDeposit: deposit,
850
- feePayer: accounts[0],
850
+ isSponsored: true,
851
851
  }),
852
852
  ).rejects.toThrow('fee-sponsored topUp transaction contains an unauthorized call')
853
853
  })
@@ -466,6 +466,7 @@ export async function broadcastOpenTransaction(parameters: {
466
466
  challengeExpires?: string | undefined
467
467
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
468
468
  feePayer?: Account | undefined
469
+ isSponsored?: boolean | undefined
469
470
  beforeBroadcast?: ((onChain: OnChainChannel) => Promise<void> | void) | undefined
470
471
  /** When false, simulates instead of waiting for confirmation and returns derived on-chain state. @default true */
471
472
  waitForConfirmation?: boolean | undefined
@@ -480,11 +481,12 @@ export async function broadcastOpenTransaction(parameters: {
480
481
  challengeExpires,
481
482
  feePayerPolicy,
482
483
  feePayer,
484
+ isSponsored = Boolean(feePayer),
483
485
  beforeBroadcast,
484
486
  waitForConfirmation = true,
485
487
  } = parameters
486
488
 
487
- if (feePayer && !FeePayer.isTempoTransaction(serializedTransaction))
489
+ if (isSponsored && !FeePayer.isTempoTransaction(serializedTransaction))
488
490
  throw new BadRequestError({
489
491
  reason: 'Only Tempo (0x76/0x78) transactions are supported',
490
492
  })
@@ -493,11 +495,11 @@ export async function broadcastOpenTransaction(parameters: {
493
495
  serializedTransaction as Transaction.TransactionSerializedTempo,
494
496
  )
495
497
 
496
- if (feePayer) assertSenderSigned(transaction)
498
+ if (isSponsored) assertSenderSigned(transaction)
497
499
 
498
500
  const calls = transaction.calls ?? []
499
501
 
500
- const sponsoredOpenCall = feePayer
502
+ const sponsoredOpenCall = isSponsored
501
503
  ? validateSponsoredOpenCalls({
502
504
  calls,
503
505
  currency,
@@ -669,6 +671,7 @@ export async function broadcastTopUpTransaction(parameters: {
669
671
  challengeExpires?: string | undefined
670
672
  feePayerPolicy?: Partial<FeePayer.Policy> | undefined
671
673
  feePayer?: Account | undefined
674
+ isSponsored?: boolean | undefined
672
675
  }): Promise<{ txHash: Hex; newDeposit: bigint }> {
673
676
  const {
674
677
  client,
@@ -681,9 +684,10 @@ export async function broadcastTopUpTransaction(parameters: {
681
684
  challengeExpires,
682
685
  feePayerPolicy,
683
686
  feePayer,
687
+ isSponsored = Boolean(feePayer),
684
688
  } = parameters
685
689
 
686
- if (feePayer && !FeePayer.isTempoTransaction(serializedTransaction))
690
+ if (isSponsored && !FeePayer.isTempoTransaction(serializedTransaction))
687
691
  throw new BadRequestError({
688
692
  reason: 'Only Tempo (0x76/0x78) transactions are supported',
689
693
  })
@@ -692,11 +696,11 @@ export async function broadcastTopUpTransaction(parameters: {
692
696
  serializedTransaction as Transaction.TransactionSerializedTempo,
693
697
  )
694
698
 
695
- if (feePayer) assertSenderSigned(transaction)
699
+ if (isSponsored) assertSenderSigned(transaction)
696
700
 
697
701
  const calls = transaction.calls ?? []
698
702
 
699
- const sponsoredTopUpCall = feePayer
703
+ const sponsoredTopUpCall = isSponsored
700
704
  ? validateSponsoredTopUpCalls({
701
705
  calls,
702
706
  currency,
@@ -1,5 +1,8 @@
1
- import { createClient, http } from 'viem'
1
+ import { P256, Secp256k1, Signature } from 'ox'
2
+ import { SignatureEnvelope } from 'ox/tempo'
3
+ import { createClient, http, type Account, type Hex } from 'viem'
2
4
  import { privateKeyToAccount } from 'viem/accounts'
5
+ import { Account as TempoAccount, WebCryptoP256 } from 'viem/tempo'
3
6
  import { describe, expect, test } from 'vp/test'
4
7
 
5
8
  import { parseVoucherFromPayload, signVoucher, verifyVoucher } from './Voucher.js'
@@ -39,6 +42,232 @@ describe('Voucher', () => {
39
42
  expect(isValid).toBe(true)
40
43
  })
41
44
 
45
+ test('signVoucher rejects direct WebCrypto P256 voucher signatures', async () => {
46
+ const keyPair = await WebCryptoP256.createKeyPair()
47
+ const p256Account = TempoAccount.fromWebCryptoP256(keyPair)
48
+ const p256Client = createClient({
49
+ account: p256Account,
50
+ transport: http('http://127.0.0.1'),
51
+ })
52
+
53
+ await expect(
54
+ signVoucher(
55
+ p256Client,
56
+ p256Account,
57
+ { channelId, cumulativeAmount },
58
+ escrowContract,
59
+ chainId,
60
+ ),
61
+ ).rejects.toThrow('Session vouchers only support secp256k1 signatures')
62
+ })
63
+
64
+ test('signVoucher rejects direct WebAuthn voucher signatures', async () => {
65
+ const webAuthnAccount = TempoAccount.fromHeadlessWebAuthn(P256.randomPrivateKey(), {
66
+ origin: 'https://example.com',
67
+ rpId: 'example.com',
68
+ })
69
+ const webAuthnClient = createClient({
70
+ account: webAuthnAccount,
71
+ transport: http('http://127.0.0.1'),
72
+ })
73
+
74
+ await expect(
75
+ signVoucher(
76
+ webAuthnClient,
77
+ webAuthnAccount,
78
+ { channelId, cumulativeAmount },
79
+ escrowContract,
80
+ chainId,
81
+ ),
82
+ ).rejects.toThrow('Session vouchers only support secp256k1 signatures')
83
+ })
84
+
85
+ test('signVoucher signs v1 secp256k1 access keys with raw signatures', async () => {
86
+ const accessKey = TempoAccount.fromSecp256k1(
87
+ '0x59c6995e998f97a5a0044966f09453863d462d2b3f1446a99f0a3d7b5d0f5a0d',
88
+ { access: account, internal_version: 'v1' },
89
+ )
90
+ const accessKeyClient = createClient({
91
+ account: accessKey,
92
+ transport: http('http://127.0.0.1'),
93
+ })
94
+
95
+ const signature = await signVoucher(
96
+ accessKeyClient,
97
+ accessKey,
98
+ { channelId, cumulativeAmount },
99
+ escrowContract,
100
+ chainId,
101
+ )
102
+ const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
103
+
104
+ expect(envelope.type).toBe('secp256k1')
105
+ expect(signature.length).toBe(132)
106
+
107
+ const isValid = await verifyVoucher(
108
+ escrowContract,
109
+ chainId,
110
+ { channelId, cumulativeAmount, signature },
111
+ accessKey.accessKeyAddress,
112
+ )
113
+ expect(isValid).toBe(true)
114
+ })
115
+
116
+ test('signVoucher unwraps legacy secp256k1 keychain signatures', async () => {
117
+ const privateKey = '0x59c6995e998f97a5a0044966f09453863d462d2b3f1446a99f0a3d7b5d0f5a0d'
118
+ const rawAccessKey = TempoAccount.fromSecp256k1(privateKey)
119
+ const keychainSigner = {
120
+ accessKeyAddress: rawAccessKey.address,
121
+ address: account.address,
122
+ async sign({ hash }: { hash: Hex }) {
123
+ const inner = SignatureEnvelope.from(
124
+ Signature.toHex(Secp256k1.sign({ payload: hash, privateKey })),
125
+ )
126
+ return SignatureEnvelope.serialize({
127
+ type: 'keychain',
128
+ version: 'v1',
129
+ userAddress: account.address,
130
+ inner,
131
+ })
132
+ },
133
+ } as unknown as Account
134
+
135
+ const signature = await signVoucher(
136
+ client,
137
+ keychainSigner,
138
+ { channelId, cumulativeAmount },
139
+ escrowContract,
140
+ chainId,
141
+ )
142
+ const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
143
+
144
+ expect(envelope.type).toBe('secp256k1')
145
+ expect(signature.length).toBe(132)
146
+
147
+ const isValid = await verifyVoucher(
148
+ escrowContract,
149
+ chainId,
150
+ { channelId, cumulativeAmount, signature },
151
+ rawAccessKey.address,
152
+ )
153
+ expect(isValid).toBe(true)
154
+ })
155
+
156
+ test('signVoucher signs v2 secp256k1 access keys with raw signatures', async () => {
157
+ const accessKey = TempoAccount.fromSecp256k1(
158
+ '0x59c6995e998f97a5a0044966f09453863d462d2b3f1446a99f0a3d7b5d0f5a0d',
159
+ { access: account },
160
+ )
161
+ const accessKeyClient = createClient({
162
+ account: accessKey,
163
+ transport: http('http://127.0.0.1'),
164
+ })
165
+
166
+ const signature = await signVoucher(
167
+ accessKeyClient,
168
+ accessKey,
169
+ { channelId, cumulativeAmount },
170
+ escrowContract,
171
+ chainId,
172
+ )
173
+ const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
174
+
175
+ expect(envelope.type).toBe('secp256k1')
176
+ expect(signature.length).toBe(132)
177
+
178
+ const isValid = await verifyVoucher(
179
+ escrowContract,
180
+ chainId,
181
+ { channelId, cumulativeAmount, signature },
182
+ accessKey.accessKeyAddress,
183
+ )
184
+ expect(isValid).toBe(true)
185
+ })
186
+
187
+ test('verifyVoucher rejects keychain envelopes', async () => {
188
+ const privateKey = '0x59c6995e998f97a5a0044966f09453863d462d2b3f1446a99f0a3d7b5d0f5a0d'
189
+ const inner = SignatureEnvelope.from(
190
+ Signature.toHex(Secp256k1.sign({ payload: channelId, privateKey })),
191
+ )
192
+ const keychainSignature = SignatureEnvelope.serialize({
193
+ type: 'keychain',
194
+ version: 'v2',
195
+ userAddress: account.address,
196
+ inner,
197
+ })
198
+
199
+ const isValid = await verifyVoucher(
200
+ escrowContract,
201
+ chainId,
202
+ { channelId, cumulativeAmount, signature: keychainSignature },
203
+ account.address,
204
+ )
205
+ expect(isValid).toBe(false)
206
+ })
207
+
208
+ test('signVoucher signs non-secp256k1 access keys without keychain envelopes', async () => {
209
+ const accessKey = TempoAccount.fromP256(P256.randomPrivateKey(), {
210
+ access: account,
211
+ internal_version: 'v1',
212
+ })
213
+ const accessKeyClient = createClient({
214
+ account: accessKey,
215
+ transport: http('http://127.0.0.1'),
216
+ })
217
+
218
+ await expect(
219
+ signVoucher(
220
+ accessKeyClient,
221
+ accessKey,
222
+ { channelId, cumulativeAmount },
223
+ escrowContract,
224
+ chainId,
225
+ ),
226
+ ).rejects.toThrow('Session vouchers only support secp256k1 signatures')
227
+ })
228
+
229
+ test('verifyVoucher rejects magic-suffixed secp256k1 signatures', async () => {
230
+ const signature = await signVoucher(
231
+ client,
232
+ account,
233
+ { channelId, cumulativeAmount },
234
+ escrowContract,
235
+ chainId,
236
+ )
237
+ const signatureWithMagic = SignatureEnvelope.serialize(SignatureEnvelope.from(signature), {
238
+ magic: true,
239
+ })
240
+
241
+ const isValid = await verifyVoucher(
242
+ escrowContract,
243
+ chainId,
244
+ { channelId, cumulativeAmount, signature: signatureWithMagic },
245
+ account.address,
246
+ )
247
+ expect(isValid).toBe(false)
248
+ })
249
+
250
+ test('verifyVoucher rejects EIP-155-style secp256k1 v values', async () => {
251
+ const signature = await signVoucher(
252
+ client,
253
+ account,
254
+ { channelId, cumulativeAmount },
255
+ escrowContract,
256
+ chainId,
257
+ )
258
+ const signatureWithEip155V = `${signature.slice(0, -2)}${
259
+ signature.endsWith('1b') ? '23' : '24'
260
+ }` as `0x${string}`
261
+
262
+ const isValid = await verifyVoucher(
263
+ escrowContract,
264
+ chainId,
265
+ { channelId, cumulativeAmount, signature: signatureWithEip155V },
266
+ account.address,
267
+ )
268
+ expect(isValid).toBe(false)
269
+ })
270
+
42
271
  test('verifyVoucher rejects wrong signer', async () => {
43
272
  const signature = await signVoucher(
44
273
  client,
@@ -1,10 +1,10 @@
1
- import { type Address, Signature } from 'ox'
1
+ import { Signature, type Address } from 'ox'
2
2
  import { SignatureEnvelope } from 'ox/tempo'
3
3
  import type { Account, Client, Hex } from 'viem'
4
- import { recoverTypedDataAddress } from 'viem'
4
+ import { hashTypedData } from 'viem'
5
5
  import { signTypedData } from 'viem/actions'
6
6
 
7
- import * as TempoAddress from '../internal/address.js'
7
+ import { getAccountSignerAddress, isAccessKeyAccount } from '../internal/account.js'
8
8
  import type { SignedVoucher, Voucher } from './Types.js'
9
9
 
10
10
  /** Must match the on-chain TempoStreamChannel DOMAIN_SEPARATOR name. */
@@ -35,48 +35,110 @@ const voucherTypes = {
35
35
  ],
36
36
  } as const
37
37
 
38
- /**
39
- * Sign a voucher with an account.
40
- */
41
- export async function signVoucher(
38
+ const acceptTip1020VoucherSignatures = false
39
+
40
+ function getVoucherMessage(message: Voucher) {
41
+ return {
42
+ channelId: message.channelId,
43
+ cumulativeAmount: message.cumulativeAmount,
44
+ }
45
+ }
46
+
47
+ function getVoucherDigest(escrowContract: Address.Address, chainId: number, message: Voucher) {
48
+ return hashTypedData({
49
+ domain: getVoucherDomain(escrowContract, chainId),
50
+ types: voucherTypes,
51
+ primaryType: 'Voucher',
52
+ message: getVoucherMessage(message),
53
+ })
54
+ }
55
+
56
+ async function signVoucherTypedData(
42
57
  client: Client,
43
58
  account: Account,
44
59
  message: Voucher,
45
60
  escrowContract: Address.Address,
46
61
  chainId: number,
47
- authorizedSigner?: Address.Address | undefined,
48
62
  ): Promise<Hex> {
49
- const signature = await signTypedData(client, {
63
+ return signTypedData(client, {
50
64
  account,
51
65
  domain: getVoucherDomain(escrowContract, chainId),
52
66
  types: voucherTypes,
53
67
  primaryType: 'Voucher',
54
- message: {
55
- channelId: message.channelId,
56
- cumulativeAmount: message.cumulativeAmount,
57
- },
68
+ message: getVoucherMessage(message),
58
69
  })
70
+ }
71
+
72
+ function normalizeVoucherSignature(signature: Hex): Hex {
73
+ const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
59
74
 
60
- // When a separate authorizedSigner is used (e.g. access key), unwrap the
61
- // keychain envelope the escrow contract verifies raw ECDSA signatures
62
- // against authorizedSigner, not keychain-wrapped ones.
63
- // TODO: when TIP-1020 is implemented, we can remove this.
64
- if (authorizedSigner) {
65
- try {
66
- const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
67
- if (envelope.type === 'keychain' && envelope.inner.type === 'secp256k1')
68
- return Signature.toHex(envelope.inner.signature)
69
- } catch {}
75
+ if (envelope.type === 'keychain') {
76
+ if (envelope.inner.type !== 'secp256k1')
77
+ throw new Error(
78
+ 'Session vouchers only unwrap secp256k1 keychain signatures; pass a direct voucherSigner for other key types.',
79
+ )
80
+
81
+ return Signature.toHex(envelope.inner.signature)
70
82
  }
71
83
 
72
- return signature
84
+ // Tempo local accounts may append signature-envelope magic bytes for RPC
85
+ // routing. Voucher signatures are direct envelopes without magic bytes.
86
+ return SignatureEnvelope.serialize(envelope)
87
+ }
88
+
89
+ function acceptsVoucherEnvelope(envelope: SignatureEnvelope.SignatureEnvelope): boolean {
90
+ if (envelope.type === 'keychain') return false
91
+ if (envelope.type === 'secp256k1') return true
92
+ return acceptTip1020VoucherSignatures
93
+ }
94
+
95
+ function assertSupportedVoucherEnvelope(envelope: SignatureEnvelope.SignatureEnvelope) {
96
+ if (acceptsVoucherEnvelope(envelope)) return
97
+
98
+ throw new Error(
99
+ 'Session vouchers only support secp256k1 signatures until TIP-1020 voucher verification is enabled.',
100
+ )
101
+ }
102
+
103
+ /**
104
+ * Sign a voucher with an account.
105
+ */
106
+ export async function signVoucher(
107
+ client: Client,
108
+ account: Account,
109
+ message: Voucher,
110
+ escrowContract: Address.Address,
111
+ chainId: number,
112
+ voucherSigner?: Account | undefined,
113
+ ): Promise<Hex> {
114
+ const signer = voucherSigner ?? account
115
+ const expectedSigner = getAccountSignerAddress(signer)
116
+
117
+ const digest = getVoucherDigest(escrowContract, chainId, message)
118
+ const signature = isAccessKeyAccount(signer)
119
+ ? await signer.sign({ hash: digest, raw: true })
120
+ : await signVoucherTypedData(client, signer, message, escrowContract, chainId)
121
+ const normalized = normalizeVoucherSignature(signature)
122
+ const envelope = SignatureEnvelope.from(normalized as SignatureEnvelope.Serialized)
123
+ assertSupportedVoucherEnvelope(envelope)
124
+
125
+ if (
126
+ !SignatureEnvelope.verify(envelope, {
127
+ address: expectedSigner,
128
+ payload: digest,
129
+ })
130
+ )
131
+ throw new Error('voucher signature does not match voucher signer')
132
+
133
+ return normalized
73
134
  }
74
135
 
75
136
  /**
76
137
  * Verify a voucher signature matches the expected signer.
77
138
  *
78
- * Only accepts raw secp256k1 signatures — the escrow contract verifies
79
- * via ecrecover. Keychain, p256, and webAuthn signatures are rejected.
139
+ * Accepts canonical raw secp256k1 voucher signatures.
140
+ *
141
+ * TIP-1020 voucher signatures will be enabled when onchain escrow verification ships.
80
142
  */
81
143
  export async function verifyVoucher(
82
144
  escrowContract: Address.Address,
@@ -85,31 +147,17 @@ export async function verifyVoucher(
85
147
  expectedSigner: Address.Address,
86
148
  ): Promise<boolean> {
87
149
  try {
88
- const domain = getVoucherDomain(escrowContract, chainId)
89
- const message = {
90
- channelId: voucher.channelId,
91
- cumulativeAmount: voucher.cumulativeAmount,
92
- }
93
-
94
150
  const envelope = SignatureEnvelope.from(voucher.signature)
95
151
 
96
- // Reject keychain signatures — the escrow contract verifies raw ECDSA
97
- // signatures against authorizedSigner, not keychain-wrapped ones.
98
- if (envelope.type === 'keychain') return false
99
-
100
- // Reject non-secp256k1 signatures (p256, webAuthn) — the escrow contract
101
- // only supports ecrecover-based verification.
102
- // TODO: remove this once TIP-1020 is implemented
103
- if (envelope.type !== 'secp256k1') return false
104
-
105
- const signer = await recoverTypedDataAddress({
106
- domain,
107
- types: voucherTypes,
108
- primaryType: 'Voucher',
109
- message,
110
- signature: voucher.signature,
152
+ if (!acceptsVoucherEnvelope(envelope)) return false
153
+
154
+ const canonical = SignatureEnvelope.serialize(envelope)
155
+ if (canonical.toLowerCase() !== voucher.signature.toLowerCase()) return false
156
+
157
+ return SignatureEnvelope.verify(envelope, {
158
+ address: expectedSigner,
159
+ payload: getVoucherDigest(escrowContract, chainId, voucher),
111
160
  })
112
- return TempoAddress.isEqual(signer, expectedSigner)
113
161
  } catch {
114
162
  return false
115
163
  }
@@ -407,4 +407,75 @@ describe('isows', () => {
407
407
  expect(channel?.spent).toBe(0n)
408
408
  expect(channel?.units).toBe(0)
409
409
  })
410
+
411
+ test('does not meter or emit application messages after close is requested on-chain', async () => {
412
+ const socket = new MockSocket()
413
+ const store = memoryChannelStore()
414
+ await store.updateChannel(channelId, () => ({
415
+ channelId,
416
+ payer: '0x0000000000000000000000000000000000000001' as Address,
417
+ payee: '0x0000000000000000000000000000000000000002' as Address,
418
+ token: '0x0000000000000000000000000000000000000003' as Address,
419
+ authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
420
+ chainId: 42431,
421
+ escrowContract: '0x0000000000000000000000000000000000000005' as Address,
422
+ deposit: 1n,
423
+ settledOnChain: 0n,
424
+ highestVoucherAmount: 1n,
425
+ highestVoucher: null,
426
+ spent: 0n,
427
+ units: 0,
428
+ closeRequestedAt: 1n,
429
+ finalized: false,
430
+ createdAt: new Date().toISOString(),
431
+ }))
432
+
433
+ await Ws.serve({
434
+ socket,
435
+ store,
436
+ url: 'ws://example.test/stream',
437
+ route: async () => ({
438
+ status: 200,
439
+ withReceipt(response = new Response(null, { status: 204 })) {
440
+ response.headers.set(
441
+ 'Payment-Receipt',
442
+ serializeSessionReceipt(
443
+ createSessionReceipt({
444
+ challengeId: challenge.id,
445
+ channelId,
446
+ acceptedCumulative: 1n,
447
+ spent: 0n,
448
+ units: 0,
449
+ }),
450
+ ),
451
+ )
452
+ return response
453
+ },
454
+ }),
455
+ generate: (async function* () {
456
+ yield 'should-not-reach'
457
+ })(),
458
+ })
459
+
460
+ socket.receive(
461
+ Ws.formatAuthorizationMessage(
462
+ makeCredential({
463
+ action: 'open',
464
+ channelId,
465
+ cumulativeAmount: '1',
466
+ signature: `0x${'77'.repeat(65)}`,
467
+ transaction: '0x01',
468
+ type: 'transaction',
469
+ }),
470
+ ),
471
+ )
472
+
473
+ await sleep(100)
474
+
475
+ expect(socket.closed).toBe(true)
476
+ expect(socket.sent.some((message) => message.includes('should-not-reach'))).toBe(false)
477
+ const channel = await store.getChannel(channelId)
478
+ expect(channel?.spent).toBe(0n)
479
+ expect(channel?.units).toBe(0)
480
+ })
410
481
  })
@@ -1,4 +1,5 @@
1
1
  import * as Credential from '../../Credential.js'
2
+ import { ChannelClosedError } from '../../Errors.js'
2
3
  import * as ChannelStore from './ChannelStore.js'
3
4
  import { deserializeSessionReceipt } from './Receipt.js'
4
5
  import { createSessionReceipt } from './Receipt.js'
@@ -405,6 +406,7 @@ async function reserveChargeOrWait(options: {
405
406
 
406
407
  let channel = await store.getChannel(channelId)
407
408
  if (!channel) throw new Error('channel not found')
409
+ throwIfChannelClosed(channel)
408
410
 
409
411
  const hasHeadroom = (state: ChannelStore.State) =>
410
412
  state.highestVoucherAmount - state.spent - reservedAmount >= amount
@@ -424,6 +426,7 @@ async function reserveChargeOrWait(options: {
424
426
  await waitForUpdate(store, channelId, pollIntervalMs, signal)
425
427
  channel = await store.getChannel(channelId)
426
428
  if (!channel) throw new Error('channel not found')
429
+ throwIfChannelClosed(channel)
427
430
  }
428
431
  }
429
432
 
@@ -440,6 +443,7 @@ async function commitReservedCharges(options: {
440
443
  const channel = await store.updateChannel(channelId, (current) => {
441
444
  if (!current) return null
442
445
  if (current.finalized) return current
446
+ if (current.closeRequestedAt !== 0n) return current
443
447
  if (current.highestVoucherAmount - current.spent < amount) return current
444
448
  committed = true
445
449
  return {
@@ -450,9 +454,18 @@ async function commitReservedCharges(options: {
450
454
  })
451
455
 
452
456
  if (!channel) throw new Error('channel not found')
457
+ if (channel.finalized) throw new ChannelClosedError({ reason: 'channel is finalized' })
458
+ if (channel.closeRequestedAt !== 0n)
459
+ throw new ChannelClosedError({ reason: 'channel has a pending close request' })
453
460
  if (!committed) throw new Error('reserved voucher coverage is no longer available')
454
461
  }
455
462
 
463
+ function throwIfChannelClosed(channel: ChannelStore.State): void {
464
+ if (channel.finalized) throw new ChannelClosedError({ reason: 'channel is finalized' })
465
+ if (channel.closeRequestedAt !== 0n)
466
+ throw new ChannelClosedError({ reason: 'channel has a pending close request' })
467
+ }
468
+
456
469
  async function waitForUpdate(
457
470
  store: ChannelStore.ChannelStore,
458
471
  channelId: SessionCredentialPayload['channelId'],