mppx 0.4.8 → 0.4.10

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 (267) hide show
  1. package/CHANGELOG.md +26 -3
  2. package/README.md +13 -13
  3. package/dist/BodyDigest.d.ts.map +1 -1
  4. package/dist/BodyDigest.js.map +1 -1
  5. package/dist/Challenge.d.ts.map +1 -1
  6. package/dist/Challenge.js.map +1 -1
  7. package/dist/Credential.d.ts.map +1 -1
  8. package/dist/Credential.js.map +1 -1
  9. package/dist/Errors.js +64 -67
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/PaymentRequest.d.ts.map +1 -1
  12. package/dist/PaymentRequest.js.map +1 -1
  13. package/dist/Receipt.d.ts.map +1 -1
  14. package/dist/Receipt.js.map +1 -1
  15. package/dist/Store.d.ts +9 -0
  16. package/dist/Store.d.ts.map +1 -1
  17. package/dist/Store.js +17 -0
  18. package/dist/Store.js.map +1 -1
  19. package/dist/cli/account.d.ts.map +1 -1
  20. package/dist/cli/account.js +40 -5
  21. package/dist/cli/account.js.map +1 -1
  22. package/dist/cli/cli.d.ts.map +1 -1
  23. package/dist/cli/cli.js +157 -1
  24. package/dist/cli/cli.js.map +1 -1
  25. package/dist/cli/internal.d.ts.map +1 -1
  26. package/dist/cli/internal.js.map +1 -1
  27. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  28. package/dist/cli/plugins/stripe.js.map +1 -1
  29. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  30. package/dist/cli/plugins/tempo.js +2 -1
  31. package/dist/cli/plugins/tempo.js.map +1 -1
  32. package/dist/cli/utils.d.ts.map +1 -1
  33. package/dist/cli/utils.js.map +1 -1
  34. package/dist/client/internal/Fetch.d.ts +2 -0
  35. package/dist/client/internal/Fetch.d.ts.map +1 -1
  36. package/dist/client/internal/Fetch.js +1 -1
  37. package/dist/client/internal/Fetch.js.map +1 -1
  38. package/dist/discovery/Discovery.d.ts +146 -0
  39. package/dist/discovery/Discovery.d.ts.map +1 -0
  40. package/dist/discovery/Discovery.js +60 -0
  41. package/dist/discovery/Discovery.js.map +1 -0
  42. package/dist/discovery/OpenApi.d.ts +61 -0
  43. package/dist/discovery/OpenApi.d.ts.map +1 -0
  44. package/dist/discovery/OpenApi.js +139 -0
  45. package/dist/discovery/OpenApi.js.map +1 -0
  46. package/dist/discovery/Validate.d.ts +10 -0
  47. package/dist/discovery/Validate.d.ts.map +1 -0
  48. package/dist/discovery/Validate.js +63 -0
  49. package/dist/discovery/Validate.js.map +1 -0
  50. package/dist/discovery/index.d.ts +4 -0
  51. package/dist/discovery/index.d.ts.map +1 -0
  52. package/dist/discovery/index.js +4 -0
  53. package/dist/discovery/index.js.map +1 -0
  54. package/dist/internal/types.d.ts.map +1 -1
  55. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  56. package/dist/mcp-sdk/client/McpClient.js +1 -1
  57. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  58. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  59. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  60. package/dist/middlewares/elysia.d.ts +52 -1
  61. package/dist/middlewares/elysia.d.ts.map +1 -1
  62. package/dist/middlewares/elysia.js +17 -0
  63. package/dist/middlewares/elysia.js.map +1 -1
  64. package/dist/middlewares/express.d.ts +13 -1
  65. package/dist/middlewares/express.d.ts.map +1 -1
  66. package/dist/middlewares/express.js +23 -2
  67. package/dist/middlewares/express.js.map +1 -1
  68. package/dist/middlewares/hono.d.ts +19 -1
  69. package/dist/middlewares/hono.d.ts.map +1 -1
  70. package/dist/middlewares/hono.js +51 -0
  71. package/dist/middlewares/hono.js.map +1 -1
  72. package/dist/middlewares/internal/mppx.d.ts +4 -2
  73. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  74. package/dist/middlewares/internal/mppx.js +10 -3
  75. package/dist/middlewares/internal/mppx.js.map +1 -1
  76. package/dist/middlewares/nextjs.d.ts +11 -0
  77. package/dist/middlewares/nextjs.d.ts.map +1 -1
  78. package/dist/middlewares/nextjs.js +15 -0
  79. package/dist/middlewares/nextjs.js.map +1 -1
  80. package/dist/proxy/Proxy.d.ts +6 -0
  81. package/dist/proxy/Proxy.d.ts.map +1 -1
  82. package/dist/proxy/Proxy.js +56 -80
  83. package/dist/proxy/Proxy.js.map +1 -1
  84. package/dist/proxy/Service.d.ts +16 -23
  85. package/dist/proxy/Service.d.ts.map +1 -1
  86. package/dist/proxy/Service.js +20 -84
  87. package/dist/proxy/Service.js.map +1 -1
  88. package/dist/proxy/internal/Route.js +1 -1
  89. package/dist/proxy/internal/Route.js.map +1 -1
  90. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  91. package/dist/proxy/services/anthropic.js +5 -0
  92. package/dist/proxy/services/anthropic.js.map +1 -1
  93. package/dist/proxy/services/openai.d.ts.map +1 -1
  94. package/dist/proxy/services/openai.js +6 -3
  95. package/dist/proxy/services/openai.js.map +1 -1
  96. package/dist/proxy/services/stripe.d.ts.map +1 -1
  97. package/dist/proxy/services/stripe.js +6 -3
  98. package/dist/proxy/services/stripe.js.map +1 -1
  99. package/dist/server/Mppx.d.ts.map +1 -1
  100. package/dist/server/Mppx.js +35 -17
  101. package/dist/server/Mppx.js.map +1 -1
  102. package/dist/server/Request.d.ts.map +1 -1
  103. package/dist/server/Request.js.map +1 -1
  104. package/dist/stripe/Methods.d.ts.map +1 -1
  105. package/dist/stripe/Methods.js.map +1 -1
  106. package/dist/tempo/Methods.d.ts.map +1 -1
  107. package/dist/tempo/Methods.js.map +1 -1
  108. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  109. package/dist/tempo/client/ChannelOps.js.map +1 -1
  110. package/dist/tempo/client/Charge.d.ts.map +1 -1
  111. package/dist/tempo/client/Charge.js.map +1 -1
  112. package/dist/tempo/client/Session.d.ts.map +1 -1
  113. package/dist/tempo/client/Session.js.map +1 -1
  114. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  115. package/dist/tempo/client/SessionManager.js +1 -1
  116. package/dist/tempo/client/SessionManager.js.map +1 -1
  117. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  118. package/dist/tempo/internal/auto-swap.js +1 -1
  119. package/dist/tempo/internal/auto-swap.js.map +1 -1
  120. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  121. package/dist/tempo/internal/fee-payer.js +1 -1
  122. package/dist/tempo/internal/fee-payer.js.map +1 -1
  123. package/dist/tempo/server/Charge.d.ts.map +1 -1
  124. package/dist/tempo/server/Charge.js +1 -1
  125. package/dist/tempo/server/Charge.js.map +1 -1
  126. package/dist/tempo/server/Session.d.ts.map +1 -1
  127. package/dist/tempo/server/Session.js +18 -5
  128. package/dist/tempo/server/Session.js.map +1 -1
  129. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  130. package/dist/tempo/server/internal/transport.js +8 -0
  131. package/dist/tempo/server/internal/transport.js.map +1 -1
  132. package/dist/tempo/session/Chain.d.ts.map +1 -1
  133. package/dist/tempo/session/Chain.js +1 -1
  134. package/dist/tempo/session/Chain.js.map +1 -1
  135. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  136. package/dist/tempo/session/ChannelStore.js.map +1 -1
  137. package/dist/tempo/session/Receipt.d.ts.map +1 -1
  138. package/dist/tempo/session/Receipt.js.map +1 -1
  139. package/dist/tempo/session/Sse.d.ts.map +1 -1
  140. package/dist/tempo/session/Sse.js.map +1 -1
  141. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  142. package/dist/tempo/session/Voucher.js.map +1 -1
  143. package/dist/viem/Client.d.ts.map +1 -1
  144. package/dist/viem/Client.js.map +1 -1
  145. package/package.json +6 -1
  146. package/src/BodyDigest.test.ts +1 -1
  147. package/src/BodyDigest.ts +1 -0
  148. package/src/Challenge.fuzz.test.ts +121 -0
  149. package/src/Challenge.test-d.ts +2 -1
  150. package/src/Challenge.test.ts +1 -1
  151. package/src/Challenge.ts +1 -0
  152. package/src/Credential.fuzz.test.ts +62 -0
  153. package/src/Credential.test.ts +1 -1
  154. package/src/Credential.ts +1 -0
  155. package/src/Errors.test.ts +28 -40
  156. package/src/Expires.test.ts +2 -1
  157. package/src/Method.test.ts +1 -1
  158. package/src/PaymentRequest.test.ts +1 -1
  159. package/src/PaymentRequest.ts +1 -0
  160. package/src/Receipt.test.ts +1 -1
  161. package/src/Receipt.ts +1 -0
  162. package/src/Store.test-d.ts +2 -1
  163. package/src/Store.test.ts +57 -7
  164. package/src/Store.ts +25 -0
  165. package/src/cli/account.ts +65 -30
  166. package/src/cli/cli.test.ts +215 -2
  167. package/src/cli/cli.ts +166 -1
  168. package/src/cli/config.test.ts +1 -0
  169. package/src/cli/internal.ts +1 -0
  170. package/src/cli/plugins/stripe.ts +1 -0
  171. package/src/cli/plugins/tempo.ts +4 -1
  172. package/src/cli/utils.ts +1 -0
  173. package/src/client/Mppx.test-d.ts +2 -1
  174. package/src/client/Mppx.test.ts +1 -1
  175. package/src/client/Transport.test.ts +1 -1
  176. package/src/client/internal/Fetch.browser.test.ts +2 -1
  177. package/src/client/internal/Fetch.test-d.ts +2 -1
  178. package/src/client/internal/Fetch.test.ts +3 -1
  179. package/src/client/internal/Fetch.ts +1 -1
  180. package/src/discovery/Discovery.test.ts +152 -0
  181. package/src/discovery/Discovery.ts +72 -0
  182. package/src/discovery/OpenApi.test.ts +425 -0
  183. package/src/discovery/OpenApi.ts +224 -0
  184. package/src/discovery/Validate.test.ts +188 -0
  185. package/src/discovery/Validate.ts +76 -0
  186. package/src/discovery/index.ts +3 -0
  187. package/src/internal/constantTimeEqual.test.ts +2 -1
  188. package/src/internal/types.ts +1 -3
  189. package/src/mcp-sdk/client/McpClient.test-d.ts +2 -1
  190. package/src/mcp-sdk/client/McpClient.test.ts +2 -1
  191. package/src/mcp-sdk/client/McpClient.ts +2 -0
  192. package/src/mcp-sdk/server/Transport.test.ts +2 -1
  193. package/src/mcp-sdk/server/Transport.ts +1 -0
  194. package/src/middlewares/elysia.test.ts +28 -2
  195. package/src/middlewares/elysia.ts +36 -1
  196. package/src/middlewares/express.test.ts +95 -7
  197. package/src/middlewares/express.ts +40 -2
  198. package/src/middlewares/hono.test.ts +28 -6
  199. package/src/middlewares/hono.ts +74 -1
  200. package/src/middlewares/internal/mppx.test.ts +2 -1
  201. package/src/middlewares/internal/mppx.ts +14 -6
  202. package/src/middlewares/nextjs.test.ts +32 -6
  203. package/src/middlewares/nextjs.ts +28 -0
  204. package/src/proxy/Proxy.test.ts +55 -270
  205. package/src/proxy/Proxy.ts +73 -93
  206. package/src/proxy/Service.test.ts +24 -1
  207. package/src/proxy/Service.ts +48 -88
  208. package/src/proxy/internal/Headers.test.ts +2 -1
  209. package/src/proxy/internal/Route.test.ts +9 -1
  210. package/src/proxy/internal/Route.ts +1 -1
  211. package/src/proxy/services/anthropic.test.ts +132 -0
  212. package/src/proxy/services/anthropic.ts +5 -0
  213. package/src/proxy/services/openai.test.ts +2 -1
  214. package/src/proxy/services/openai.ts +6 -4
  215. package/src/proxy/services/stripe.test.ts +132 -0
  216. package/src/proxy/services/stripe.ts +6 -4
  217. package/src/server/Mppx.test-d.ts +1 -1
  218. package/src/server/Mppx.test.ts +194 -1
  219. package/src/server/Mppx.ts +38 -19
  220. package/src/server/NodeListener.test.ts +1 -1
  221. package/src/server/Request.test.ts +2 -1
  222. package/src/server/Request.ts +1 -0
  223. package/src/server/Response.test.ts +2 -1
  224. package/src/server/Transport.test.ts +2 -1
  225. package/src/stripe/Charge.integration.test.ts +1 -1
  226. package/src/stripe/Methods.test.ts +1 -1
  227. package/src/stripe/Methods.ts +1 -0
  228. package/src/stripe/client/Charge.test.ts +2 -1
  229. package/src/stripe/server/Charge.test.ts +2 -1
  230. package/src/tempo/Attribution.test.ts +2 -1
  231. package/src/tempo/Methods.test.ts +1 -1
  232. package/src/tempo/Methods.ts +1 -0
  233. package/src/tempo/client/ChannelOps.test.ts +7 -3
  234. package/src/tempo/client/ChannelOps.ts +1 -0
  235. package/src/tempo/client/Charge.ts +1 -0
  236. package/src/tempo/client/Session.test.ts +6 -2
  237. package/src/tempo/client/Session.ts +1 -0
  238. package/src/tempo/client/SessionManager.test.ts +29 -1
  239. package/src/tempo/client/SessionManager.ts +2 -1
  240. package/src/tempo/internal/auto-swap.test.ts +2 -1
  241. package/src/tempo/internal/auto-swap.ts +1 -0
  242. package/src/tempo/internal/defaults.test.ts +2 -1
  243. package/src/tempo/internal/fee-payer.test.ts +2 -1
  244. package/src/tempo/internal/fee-payer.ts +1 -0
  245. package/src/tempo/server/Charge.test.ts +2 -1
  246. package/src/tempo/server/Charge.ts +1 -0
  247. package/src/tempo/server/Session.test.ts +88 -37
  248. package/src/tempo/server/Session.ts +26 -8
  249. package/src/tempo/server/Sse.test.ts +2 -1
  250. package/src/tempo/server/internal/transport.test.ts +25 -1
  251. package/src/tempo/server/internal/transport.ts +11 -0
  252. package/src/tempo/session/Chain.test.ts +6 -2
  253. package/src/tempo/session/Chain.ts +2 -1
  254. package/src/tempo/session/Channel.test.ts +2 -1
  255. package/src/tempo/session/ChannelStore.test.ts +2 -1
  256. package/src/tempo/session/ChannelStore.ts +1 -0
  257. package/src/tempo/session/Receipt.test.ts +2 -1
  258. package/src/tempo/session/Receipt.ts +1 -0
  259. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  260. package/src/tempo/session/Sse.test.ts +2 -1
  261. package/src/tempo/session/Sse.ts +1 -0
  262. package/src/tempo/session/Voucher.test.ts +2 -1
  263. package/src/tempo/session/Voucher.ts +1 -0
  264. package/src/viem/Account.test.ts +2 -1
  265. package/src/viem/Client.test.ts +2 -1
  266. package/src/viem/Client.ts +1 -0
  267. package/src/zod.test.ts +147 -0
@@ -4,7 +4,10 @@ import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
4
  import { type Address, createClient, type Hex } from 'viem'
5
5
  import { waitForTransactionReceipt } from 'viem/actions'
6
6
  import { Addresses } from 'viem/tempo'
7
- import { beforeAll, beforeEach, describe, expect, test } from 'vitest'
7
+ import { beforeAll, beforeEach, describe, expect, expectTypeOf, test } from 'vp/test'
8
+ import { nodeEnv } from '~test/config.js'
9
+
10
+ const isLocalnet = nodeEnv === 'localnet'
8
11
  import {
9
12
  deployEscrow,
10
13
  requestCloseChannel,
@@ -13,12 +16,12 @@ import {
13
16
  topUpChannel,
14
17
  } from '~test/tempo/session.js'
15
18
  import { accounts, asset, chain, client, fundAccount, http } from '~test/tempo/viem.js'
19
+
16
20
  import {
17
21
  ChannelClosedError,
18
22
  ChannelNotFoundError,
19
23
  InsufficientBalanceError,
20
24
  InvalidSignatureError,
21
- VerificationFailedError,
22
25
  } from '../../Errors.js'
23
26
  import * as Store from '../../Store.js'
24
27
  import {
@@ -32,6 +35,7 @@ import { signVoucher } from '../session/Voucher.js'
32
35
  import { charge, session, settle } from './Session.js'
33
36
 
34
37
  const payer = accounts[2]
38
+ const recipientAccount = accounts[0]
35
39
  const recipient = accounts[0].address
36
40
  const currency = asset
37
41
 
@@ -39,12 +43,13 @@ let escrowContract: Address
39
43
  let saltCounter = 0
40
44
 
41
45
  beforeAll(async () => {
46
+ if (!isLocalnet) return
42
47
  escrowContract = await deployEscrow()
43
48
  await fundAccount({ address: payer.address, token: Addresses.pathUsd })
44
49
  await fundAccount({ address: payer.address, token: currency })
45
50
  })
46
51
 
47
- describe('session', () => {
52
+ describe.runIf(isLocalnet)('session', () => {
48
53
  let rawStore: Store.Store
49
54
  let store: ChannelStore.ChannelStore
50
55
 
@@ -57,7 +62,7 @@ describe('session', () => {
57
62
  return session({
58
63
  store: rawStore,
59
64
  getClient: () => client,
60
- account: recipient,
65
+ account: recipientAccount,
61
66
  currency,
62
67
  escrowContract,
63
68
  chainId: chain.id,
@@ -617,7 +622,47 @@ describe('session', () => {
617
622
  ).rejects.toThrow(InvalidSignatureError)
618
623
  })
619
624
 
620
- test('rejects exact replay of already-verified voucher (non-increasing)', async () => {
625
+ test('accepts exact replay of already-verified voucher as idempotent', async () => {
626
+ const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
627
+ const server = createServer()
628
+ await openServerChannel(server, channelId, serializedTransaction)
629
+
630
+ const payload = {
631
+ action: 'voucher' as const,
632
+ channelId,
633
+ cumulativeAmount: '2000000',
634
+ signature: await signTestVoucher(channelId, 2000000n),
635
+ }
636
+
637
+ await server.verify({
638
+ credential: {
639
+ challenge: makeChallenge({ id: 'challenge-2', channelId }),
640
+ payload,
641
+ },
642
+ request: makeRequest(),
643
+ })
644
+
645
+ const channelAfterFirstAccept = await store.getChannel(channelId)
646
+
647
+ const replayReceipt = (await server.verify({
648
+ credential: {
649
+ challenge: makeChallenge({ id: 'challenge-3', channelId }),
650
+ payload,
651
+ },
652
+ request: makeRequest(),
653
+ })) as SessionReceipt
654
+
655
+ expect(replayReceipt.status).toBe('success')
656
+ expect(replayReceipt.acceptedCumulative).toBe('2000000')
657
+ expect(replayReceipt.spent).toBe(channelAfterFirstAccept!.spent.toString())
658
+ expect(replayReceipt.units).toBe(channelAfterFirstAccept!.units)
659
+
660
+ const channelAfterReplay = await store.getChannel(channelId)
661
+ expect(channelAfterReplay).toEqual(channelAfterFirstAccept)
662
+ expect(channelAfterReplay!.highestVoucherAmount).toBe(2000000n)
663
+ })
664
+
665
+ test('rejects exact replay with invalid signature', async () => {
621
666
  const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
622
667
  const server = createServer()
623
668
  await openServerChannel(server, channelId, serializedTransaction)
@@ -641,11 +686,14 @@ describe('session', () => {
641
686
  server.verify({
642
687
  credential: {
643
688
  challenge: makeChallenge({ id: 'challenge-3', channelId }),
644
- payload,
689
+ payload: {
690
+ ...payload,
691
+ signature: `0x${'ab'.repeat(65)}` as Hex,
692
+ },
645
693
  },
646
694
  request: makeRequest(),
647
695
  }),
648
- ).rejects.toThrow(VerificationFailedError)
696
+ ).rejects.toThrow(InvalidSignatureError)
649
697
  })
650
698
 
651
699
  test('rejects replayed voucher at settled amount after on-chain settlement', async () => {
@@ -1190,27 +1238,29 @@ describe('session', () => {
1190
1238
  expect(ch!.finalized).toBe(true)
1191
1239
  })
1192
1240
 
1193
- test('close throws when client has no account', async () => {
1194
- const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
1195
- const server = createServer({
1196
- getClient: () => createClient({ chain, transport: http() }),
1197
- })
1198
- await openServerChannel(server, channelId, serializedTransaction)
1199
-
1200
- await expect(
1201
- server.verify({
1202
- credential: {
1203
- challenge: makeChallenge({ id: 'challenge-2', channelId }),
1204
- payload: {
1205
- action: 'close' as const,
1206
- channelId,
1207
- cumulativeAmount: '1000000',
1208
- signature: await signTestVoucher(channelId, 1000000n),
1209
- },
1210
- },
1211
- request: makeRequest(),
1212
- }),
1213
- ).rejects.toThrow('Cannot close channel: no account available')
1241
+ test('session() throws at initialization when no account provided', () => {
1242
+ expect(() =>
1243
+ session({
1244
+ store: rawStore,
1245
+ getClient: () => client,
1246
+ account: recipient as Address,
1247
+ currency,
1248
+ escrowContract,
1249
+ chainId: chain.id,
1250
+ } as session.Parameters),
1251
+ ).toThrow('tempo.session() requires an `account`')
1252
+ })
1253
+
1254
+ test('session() throws at initialization with no account at all', () => {
1255
+ expect(() =>
1256
+ session({
1257
+ store: rawStore,
1258
+ getClient: () => client,
1259
+ currency,
1260
+ escrowContract,
1261
+ chainId: chain.id,
1262
+ } as session.Parameters),
1263
+ ).toThrow('tempo.session() requires an `account`')
1214
1264
  })
1215
1265
  })
1216
1266
 
@@ -2207,6 +2257,7 @@ describe('monotonicity and TOCTOU (unit tests)', () => {
2207
2257
  })
2208
2258
 
2209
2259
  describe('session default currency resolution', () => {
2260
+ const mockAccount = accounts[0]
2210
2261
  const mockClient = createClient({ transport: http('http://localhost:1') })
2211
2262
  const mockMainnetClient = createClient({
2212
2263
  chain: {
@@ -2231,7 +2282,7 @@ describe('session default currency resolution', () => {
2231
2282
  const server = session({
2232
2283
  store: Store.memory(),
2233
2284
  getClient: () => mockClient,
2234
- account: '0x0000000000000000000000000000000000000001',
2285
+ account: mockAccount,
2235
2286
  escrowContract: '0x0000000000000000000000000000000000000002',
2236
2287
  } as session.Parameters)
2237
2288
  expect(server.defaults?.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
@@ -2241,7 +2292,7 @@ describe('session default currency resolution', () => {
2241
2292
  const server = session({
2242
2293
  store: Store.memory(),
2243
2294
  getClient: () => mockClient,
2244
- account: '0x0000000000000000000000000000000000000001',
2295
+ account: mockAccount,
2245
2296
  escrowContract: '0x0000000000000000000000000000000000000002',
2246
2297
  testnet: true,
2247
2298
  } as session.Parameters)
@@ -2252,7 +2303,7 @@ describe('session default currency resolution', () => {
2252
2303
  const server = session({
2253
2304
  store: Store.memory(),
2254
2305
  getClient: () => mockClient,
2255
- account: '0x0000000000000000000000000000000000000001',
2306
+ account: mockAccount,
2256
2307
  escrowContract: '0x0000000000000000000000000000000000000002',
2257
2308
  chainId: 69420,
2258
2309
  } as session.Parameters)
@@ -2263,7 +2314,7 @@ describe('session default currency resolution', () => {
2263
2314
  const server = session({
2264
2315
  store: Store.memory(),
2265
2316
  getClient: () => mockClient,
2266
- account: '0x0000000000000000000000000000000000000001',
2317
+ account: mockAccount,
2267
2318
  currency: '0xcustom',
2268
2319
  escrowContract: '0x0000000000000000000000000000000000000002',
2269
2320
  chainId: 4217,
@@ -2276,7 +2327,7 @@ describe('session default currency resolution', () => {
2276
2327
  const server = session({
2277
2328
  store: Store.memory(),
2278
2329
  getClient: () => mockClient,
2279
- account: '0x0000000000000000000000000000000000000001',
2330
+ account: mockAccount,
2280
2331
  escrowContract: '0x0000000000000000000000000000000000000002',
2281
2332
  chainId: 42431,
2282
2333
  } as session.Parameters)
@@ -2289,7 +2340,7 @@ describe('session default currency resolution', () => {
2289
2340
  tempo_server.session({
2290
2341
  store: Store.memory(),
2291
2342
  getClient: () => mockMainnetClient,
2292
- account: '0x0000000000000000000000000000000000000001',
2343
+ account: mockAccount,
2293
2344
  escrowContract: '0x0000000000000000000000000000000000000002',
2294
2345
  chainId: 4217,
2295
2346
  testnet: false,
@@ -2316,7 +2367,7 @@ describe('session default currency resolution', () => {
2316
2367
  tempo_server.session({
2317
2368
  store: Store.memory(),
2318
2369
  getClient: () => mockTestnetClient,
2319
- account: '0x0000000000000000000000000000000000000001',
2370
+ account: mockAccount,
2320
2371
  escrowContract: '0x0000000000000000000000000000000000000002',
2321
2372
  testnet: true,
2322
2373
  }),
@@ -2343,7 +2394,7 @@ describe('session default currency resolution', () => {
2343
2394
  tempo_server.session({
2344
2395
  store: Store.memory(),
2345
2396
  getClient: () => mockTestnetClient,
2346
- account: '0x0000000000000000000000000000000000000001',
2397
+ account: mockAccount,
2347
2398
  escrowContract: '0x0000000000000000000000000000000000000002',
2348
2399
  chainId: 69420,
2349
2400
  }),
@@ -2369,7 +2420,7 @@ describe('session default currency resolution', () => {
2369
2420
  tempo_server.session({
2370
2421
  store: Store.memory(),
2371
2422
  getClient: () => mockClient,
2372
- account: '0x0000000000000000000000000000000000000001',
2423
+ account: mockAccount,
2373
2424
  currency: '0xcustom',
2374
2425
  escrowContract: '0x0000000000000000000000000000000000000002',
2375
2426
  chainId: 4217,
@@ -18,6 +18,7 @@ import {
18
18
  type Client as viem_Client,
19
19
  } from 'viem'
20
20
  import { tempo as tempo_chain } from 'viem/chains'
21
+
21
22
  import {
22
23
  AmountExceedsDepositError,
23
24
  BadRequestError,
@@ -100,6 +101,11 @@ export function session<const parameters extends session.Parameters>(p?: paramet
100
101
 
101
102
  const { account, recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
102
103
 
104
+ if (!account)
105
+ throw new Error(
106
+ 'tempo.session() requires an `account` (viem Account, e.g. privateKeyToAccount("0x...")). An address string is not sufficient — the server needs a signing account for on-chain channel close and settlement.',
107
+ )
108
+
103
109
  const getClient = Client.getResolver({
104
110
  chain: tempo_chain,
105
111
  feePayerUrl,
@@ -461,19 +467,12 @@ async function verifyAndAcceptVoucher(parameters: {
461
467
  throw new AmountExceedsDepositError({ reason: 'voucher amount exceeds on-chain deposit' })
462
468
  }
463
469
 
464
- if (voucher.cumulativeAmount <= channel.highestVoucherAmount) {
470
+ if (voucher.cumulativeAmount < channel.highestVoucherAmount) {
465
471
  throw new VerificationFailedError({
466
472
  reason: 'voucher cumulativeAmount must be strictly greater than highest accepted voucher',
467
473
  })
468
474
  }
469
475
 
470
- const delta = voucher.cumulativeAmount - channel.highestVoucherAmount
471
- if (delta < minVoucherDelta) {
472
- throw new DeltaTooSmallError({
473
- reason: `voucher delta ${delta} below minimum ${minVoucherDelta}`,
474
- })
475
- }
476
-
477
476
  const isValid = await verifyVoucher(
478
477
  methodDetails.escrowContract,
479
478
  methodDetails.chainId,
@@ -485,6 +484,25 @@ async function verifyAndAcceptVoucher(parameters: {
485
484
  throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
486
485
  }
487
486
 
487
+ // Idempotent replay: equal cumulative voucher is accepted without
488
+ // advancing channel state or charging additional value.
489
+ if (voucher.cumulativeAmount === channel.highestVoucherAmount) {
490
+ return createSessionReceipt({
491
+ challengeId: challenge.id,
492
+ channelId,
493
+ acceptedCumulative: channel.highestVoucherAmount,
494
+ spent: channel.spent,
495
+ units: channel.units,
496
+ })
497
+ }
498
+
499
+ const delta = voucher.cumulativeAmount - channel.highestVoucherAmount
500
+ if (delta < minVoucherDelta) {
501
+ throw new DeltaTooSmallError({
502
+ reason: `voucher delta ${delta} below minimum ${minVoucherDelta}`,
503
+ })
504
+ }
505
+
488
506
  const updated = await store.updateChannel(channelId, (current) => {
489
507
  if (!current) throw new ChannelNotFoundError({ reason: 'channel not found' })
490
508
  if (voucher.cumulativeAmount > current.highestVoucherAmount) {
@@ -1,5 +1,6 @@
1
1
  import type { Address, Hex } from 'viem'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test } from 'vp/test'
3
+
3
4
  import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
4
5
  import type * as ChannelStore from '../session/ChannelStore.js'
5
6
  import { serve, toResponse } from '../session/Sse.js'
@@ -1,6 +1,7 @@
1
1
  import { Challenge, Credential } from 'mppx'
2
2
  import type { Address, Hex } from 'viem'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
+
4
5
  import * as Store from '../../../Store.js'
5
6
  import { chainId, escrowContract as escrowContractDefaults } from '../../internal/defaults.js'
6
7
  import * as ChannelStore from '../../session/ChannelStore.js'
@@ -291,6 +292,29 @@ describe('sse transport', () => {
291
292
  expect(response.headers.get('Payment-Receipt')).toBeTruthy()
292
293
  })
293
294
 
295
+ test('respondReceipt with 204 management response keeps null body and receipt', async () => {
296
+ const store = memoryStore()
297
+ await seedChannel(store, 10000000n)
298
+ const transport = sse({ store })
299
+
300
+ transport.getCredential(makeAuthorizedRequest())
301
+
302
+ const managementResponse = new Response(null, { status: 204 })
303
+ const response = transport.respondReceipt({
304
+ receipt: makeReceipt(),
305
+ response: managementResponse,
306
+ challengeId,
307
+ })
308
+
309
+ expect(response.status).toBe(204)
310
+ expect(await response.text()).toBe('')
311
+ expect(response.headers.get('Payment-Receipt')).toBeTruthy()
312
+
313
+ const channel = await store.getChannel(channelId)
314
+ expect(channel!.spent).toBe(0n)
315
+ expect(channel!.units).toBe(0)
316
+ })
317
+
294
318
  test('poll: true strips waitForUpdate from store', async () => {
295
319
  const store = memoryStore()
296
320
  ;(store as any).waitForUpdate = async () => {}
@@ -100,6 +100,13 @@ export function sse(options: sse.Options & { store: ChannelStore.ChannelStore })
100
100
  const ctx = contextMap.get(challengeId)
101
101
  if (ctx) {
102
102
  contextMap.delete(challengeId)
103
+
104
+ // Null-body statuses (e.g. 204 from management actions) cannot carry a
105
+ // response body per Fetch/HTTP semantics.
106
+ if (isNullBodyStatus(baseResponse.status)) {
107
+ return baseResponse
108
+ }
109
+
103
110
  const stream = new ReadableStream<Uint8Array>({
104
111
  async start(controller) {
105
112
  // deduction completes before consumer reads
@@ -191,3 +198,7 @@ function isAsyncGeneratorFunction(
191
198
  function isAsyncIterable(value: unknown): value is AsyncIterable<string> {
192
199
  return value !== null && typeof value === 'object' && Symbol.asyncIterator in (value as object)
193
200
  }
201
+
202
+ function isNullBodyStatus(status: number): boolean {
203
+ return [101, 204, 205, 304].includes(status)
204
+ }
@@ -1,7 +1,8 @@
1
1
  import { type Address, encodeFunctionData, erc20Abi, type Hex } from 'viem'
2
2
  import { waitForTransactionReceipt } from 'viem/actions'
3
3
  import { Addresses, Transaction } from 'viem/tempo'
4
- import { beforeAll, describe, expect, test } from 'vitest'
4
+ import { beforeAll, describe, expect, test } from 'vp/test'
5
+ import { nodeEnv } from '~test/config.js'
5
6
  import {
6
7
  closeChannelOnChain,
7
8
  deployEscrow,
@@ -11,6 +12,7 @@ import {
11
12
  topUpChannel,
12
13
  } from '~test/tempo/session.js'
13
14
  import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
15
+
14
16
  import {
15
17
  broadcastOpenTransaction,
16
18
  broadcastTopUpTransaction,
@@ -21,6 +23,8 @@ import {
21
23
  } from './Chain.js'
22
24
  import { signVoucher } from './Voucher.js'
23
25
 
26
+ const isLocalnet = nodeEnv === 'localnet'
27
+
24
28
  const UINT128_MAX = 2n ** 128n - 1n
25
29
 
26
30
  describe('assertUint128 (via settleOnChain / closeOnChain)', () => {
@@ -69,7 +73,7 @@ describe('assertUint128 (via settleOnChain / closeOnChain)', () => {
69
73
  })
70
74
  })
71
75
 
72
- describe('on-chain', () => {
76
+ describe.runIf(isLocalnet)('on-chain', () => {
73
77
  const payer = accounts[2]
74
78
  const recipient = accounts[0].address
75
79
  const currency = asset
@@ -19,6 +19,7 @@ import {
19
19
  writeContract,
20
20
  } from 'viem/actions'
21
21
  import { Transaction } from 'viem/tempo'
22
+
22
23
  import { BadRequestError, ChannelClosedError, VerificationFailedError } from '../../Errors.js'
23
24
  import * as TempoAddress from '../internal/address.js'
24
25
  import * as defaults from '../internal/defaults.js'
@@ -131,7 +132,7 @@ export async function closeOnChain(
131
132
  const resolved = account ?? client.account
132
133
  if (!resolved)
133
134
  throw new Error(
134
- 'Cannot close channel: no account available. Provide an `account` in the session config or a `getClient` that returns an account-bearing client.',
135
+ 'Cannot close channel: no account available. Pass an `account` (viem Account, e.g. privateKeyToAccount("0x...")) to tempo.session(), or provide a `getClient` that returns an account-bearing client.',
135
136
  )
136
137
  const args = [voucher.channelId, voucher.cumulativeAmount, voucher.signature] as const
137
138
  if (feePayer) {
@@ -1,5 +1,6 @@
1
1
  import { AbiParameters, Hash } from 'ox'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test } from 'vp/test'
3
+
3
4
  import * as Channel from './Channel.js'
4
5
 
5
6
  describe('computeId', () => {
@@ -1,5 +1,6 @@
1
1
  import type { Address, Hex } from 'viem'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test } from 'vp/test'
3
+
3
4
  import * as Store from '../../Store.js'
4
5
  import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
5
6
  import * as ChannelStore from './ChannelStore.js'
@@ -1,4 +1,5 @@
1
1
  import type { Address, Hex } from 'viem'
2
+
2
3
  import type * as Store from '../../Store.js'
3
4
  import type { SignedVoucher } from './Types.js'
4
5
 
@@ -1,5 +1,6 @@
1
1
  import type { Hex } from 'viem'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test } from 'vp/test'
3
+
3
4
  import {
4
5
  createSessionReceipt,
5
6
  deserializeSessionReceipt,
@@ -1,5 +1,6 @@
1
1
  import { Base64 } from 'ox'
2
2
  import type { Hex } from 'viem'
3
+
3
4
  import type { SessionReceipt } from './Types.js'
4
5
 
5
6
  /**
@@ -0,0 +1,138 @@
1
+ import * as fc from 'fast-check'
2
+ import { describe, expect, test } from 'vp/test'
3
+
4
+ import * as Sse from './Sse.js'
5
+
6
+ function createChunkedResponse(chunks: Uint8Array[]): Response {
7
+ let index = 0
8
+ const stream = new ReadableStream<Uint8Array>({
9
+ pull(controller) {
10
+ if (index < chunks.length) {
11
+ controller.enqueue(chunks[index]!)
12
+ index++
13
+ } else {
14
+ controller.close()
15
+ }
16
+ },
17
+ })
18
+ return new Response(stream, {
19
+ headers: { 'Content-Type': 'text/event-stream' },
20
+ })
21
+ }
22
+
23
+ function splitAtPositions(str: string, positions: number[]): Uint8Array[] {
24
+ const encoder = new TextEncoder()
25
+ const sorted = [...new Set([0, ...positions, str.length])]
26
+ .filter((p) => p >= 0 && p <= str.length)
27
+ .sort((a, b) => a - b)
28
+ const chunks: Uint8Array[] = []
29
+ for (let i = 0; i < sorted.length - 1; i++) {
30
+ const chunk = str.slice(sorted[i], sorted[i + 1])
31
+ if (chunk.length > 0) chunks.push(encoder.encode(chunk))
32
+ }
33
+ return chunks
34
+ }
35
+
36
+ async function collectData(response: Response): Promise<string[]> {
37
+ const results: string[] = []
38
+ for await (const data of Sse.iterateData(response)) {
39
+ results.push(data)
40
+ }
41
+ return results
42
+ }
43
+
44
+ describe('parseEvent', () => {
45
+ test('never throws on arbitrary message-type input', () => {
46
+ fc.assert(
47
+ fc.property(fc.string(), (input) => {
48
+ const result = Sse.parseEvent(input)
49
+ if (result !== null) {
50
+ expect(result.type).toBe('message')
51
+ expect(typeof result.data).toBe('string')
52
+ }
53
+ }),
54
+ { numRuns: 10_000 },
55
+ )
56
+ })
57
+
58
+ test('parseEvent with valid SSE format', () => {
59
+ const sseMessageArb = fc
60
+ .array(
61
+ fc.string().filter((s) => !s.includes('\n')),
62
+ { minLength: 1, maxLength: 5 },
63
+ )
64
+ .map((lines) => lines.map((l) => `data: ${l}`).join('\n'))
65
+
66
+ fc.assert(
67
+ fc.property(sseMessageArb, (raw) => {
68
+ const result = Sse.parseEvent(raw)
69
+ expect(result).not.toBeNull()
70
+ expect(result!.type).toBe('message')
71
+ const expectedData = raw
72
+ .split('\n')
73
+ .map((l) => l.slice(6))
74
+ .join('\n')
75
+ expect(result!.data).toBe(expectedData)
76
+ }),
77
+ { numRuns: 5_000 },
78
+ )
79
+ })
80
+ })
81
+
82
+ describe('iterateData', () => {
83
+ const sseEventArb = fc
84
+ .array(
85
+ fc.string().filter((s) => !s.includes('\n\n') && !s.includes('\n')),
86
+ { minLength: 1, maxLength: 3 },
87
+ )
88
+ .map((lines) => lines.map((l) => `data: ${l}`).join('\n'))
89
+
90
+ const sseStreamArb = fc
91
+ .array(sseEventArb, { minLength: 1, maxLength: 5 })
92
+ .map((events) => events.join('\n\n') + '\n\n')
93
+
94
+ test('chunk boundary invariance', async () => {
95
+ await fc.assert(
96
+ fc.asyncProperty(
97
+ sseStreamArb,
98
+ fc.array(fc.nat(), { minLength: 1, maxLength: 10 }),
99
+ async (stream, positions) => {
100
+ const encoder = new TextEncoder()
101
+
102
+ const singleChunk = createChunkedResponse([encoder.encode(stream)])
103
+ const singleResult = await collectData(singleChunk)
104
+
105
+ const boundedPositions = positions.map((p) => p % (stream.length + 1))
106
+ const chunks = splitAtPositions(stream, boundedPositions)
107
+ const multiChunk = createChunkedResponse(chunks)
108
+ const multiResult = await collectData(multiChunk)
109
+
110
+ expect(multiResult).toEqual(singleResult)
111
+ },
112
+ ),
113
+ { numRuns: 1_000 },
114
+ )
115
+ })
116
+
117
+ test('iterateData never throws on arbitrary chunked input', async () => {
118
+ await fc.assert(
119
+ fc.asyncProperty(
120
+ fc.array(fc.uint8Array({ minLength: 1, maxLength: 100 }), {
121
+ minLength: 1,
122
+ maxLength: 5,
123
+ }),
124
+ async (chunks) => {
125
+ const response = createChunkedResponse(chunks)
126
+ const results: string[] = []
127
+ for await (const data of Sse.iterateData(response)) {
128
+ results.push(data)
129
+ }
130
+ for (const item of results) {
131
+ expect(typeof item).toBe('string')
132
+ }
133
+ },
134
+ ),
135
+ { numRuns: 5_000 },
136
+ )
137
+ })
138
+ })
@@ -1,5 +1,6 @@
1
1
  import type { Address, Hex } from 'viem'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test } from 'vp/test'
3
+
3
4
  import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
4
5
  import type * as ChannelStore from './ChannelStore.js'
5
6
  import { formatNeedVoucherEvent, formatReceiptEvent, parseEvent, serve } from './Sse.js'
@@ -7,6 +7,7 @@
7
7
  * building HTTP responses from the stream.
8
8
  */
9
9
  import type { Hex } from 'viem'
10
+
10
11
  import * as Credential from '../../Credential.js'
11
12
  import * as ChannelStore from './ChannelStore.js'
12
13
  import { createSessionReceipt } from './Receipt.js'
@@ -1,6 +1,7 @@
1
1
  import { createClient, http } from 'viem'
2
2
  import { privateKeyToAccount } from 'viem/accounts'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
+
4
5
  import { parseVoucherFromPayload, signVoucher, verifyVoucher } from './Voucher.js'
5
6
 
6
7
  const account = privateKeyToAccount(
@@ -3,6 +3,7 @@ import { SignatureEnvelope } from 'ox/tempo'
3
3
  import type { Account, Client, Hex } from 'viem'
4
4
  import { recoverTypedDataAddress } from 'viem'
5
5
  import { signTypedData } from 'viem/actions'
6
+
6
7
  import * as TempoAddress from '../internal/address.js'
7
8
  import type { SignedVoucher, Voucher } from './Types.js'
8
9