mppx 0.3.2 → 0.3.4

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 (140) hide show
  1. package/dist/Errors.d.ts +7 -7
  2. package/dist/Errors.d.ts.map +1 -1
  3. package/dist/Errors.js +7 -7
  4. package/dist/Errors.js.map +1 -1
  5. package/dist/cli.js +98 -64
  6. package/dist/cli.js.map +1 -1
  7. package/dist/internal/env.d.ts +19 -0
  8. package/dist/internal/env.d.ts.map +1 -0
  9. package/dist/internal/env.js +55 -0
  10. package/dist/internal/env.js.map +1 -0
  11. package/dist/server/Mppx.d.ts +2 -2
  12. package/dist/server/Mppx.d.ts.map +1 -1
  13. package/dist/server/Mppx.js +2 -1
  14. package/dist/server/Mppx.js.map +1 -1
  15. package/dist/tempo/client/ChannelOps.d.ts +5 -5
  16. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  17. package/dist/tempo/client/ChannelOps.js +3 -3
  18. package/dist/tempo/client/ChannelOps.js.map +1 -1
  19. package/dist/tempo/client/Session.d.ts +2 -2
  20. package/dist/tempo/client/Session.d.ts.map +1 -1
  21. package/dist/tempo/client/Session.js +3 -3
  22. package/dist/tempo/client/Session.js.map +1 -1
  23. package/dist/tempo/client/SessionManager.d.ts +4 -4
  24. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  25. package/dist/tempo/client/SessionManager.js +4 -4
  26. package/dist/tempo/client/SessionManager.js.map +1 -1
  27. package/dist/tempo/index.d.ts +1 -1
  28. package/dist/tempo/index.d.ts.map +1 -1
  29. package/dist/tempo/index.js +1 -1
  30. package/dist/tempo/index.js.map +1 -1
  31. package/dist/tempo/server/Charge.js +1 -1
  32. package/dist/tempo/server/Charge.js.map +1 -1
  33. package/dist/tempo/server/Methods.d.ts +1 -1
  34. package/dist/tempo/server/Methods.d.ts.map +1 -1
  35. package/dist/tempo/server/Session.d.ts +8 -8
  36. package/dist/tempo/server/Session.d.ts.map +1 -1
  37. package/dist/tempo/server/Session.js +24 -24
  38. package/dist/tempo/server/Session.js.map +1 -1
  39. package/dist/tempo/server/index.d.ts +2 -2
  40. package/dist/tempo/server/index.d.ts.map +1 -1
  41. package/dist/tempo/server/index.js +2 -2
  42. package/dist/tempo/server/index.js.map +1 -1
  43. package/dist/tempo/server/internal/transport.d.ts +4 -4
  44. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  45. package/dist/tempo/server/internal/transport.js +3 -3
  46. package/dist/tempo/server/internal/transport.js.map +1 -1
  47. package/dist/tempo/session/Chain.d.ts.map +1 -0
  48. package/dist/tempo/session/Chain.js.map +1 -0
  49. package/dist/tempo/session/Channel.d.ts.map +1 -0
  50. package/dist/tempo/session/Channel.js.map +1 -0
  51. package/dist/tempo/session/ChannelStore.d.ts.map +1 -0
  52. package/dist/tempo/session/ChannelStore.js.map +1 -0
  53. package/dist/tempo/session/Receipt.d.ts +22 -0
  54. package/dist/tempo/session/Receipt.d.ts.map +1 -0
  55. package/dist/tempo/{stream → session}/Receipt.js +6 -6
  56. package/dist/tempo/session/Receipt.js.map +1 -0
  57. package/dist/tempo/{stream → session}/Sse.d.ts +7 -7
  58. package/dist/tempo/session/Sse.d.ts.map +1 -0
  59. package/dist/tempo/{stream → session}/Sse.js +4 -4
  60. package/dist/tempo/session/Sse.js.map +1 -0
  61. package/dist/tempo/{stream → session}/Types.d.ts +4 -4
  62. package/dist/tempo/session/Types.d.ts.map +1 -0
  63. package/dist/tempo/{stream → session}/Types.js.map +1 -1
  64. package/dist/tempo/session/Voucher.d.ts.map +1 -0
  65. package/dist/tempo/session/Voucher.js.map +1 -0
  66. package/dist/tempo/{stream → session}/escrow.abi.d.ts.map +1 -1
  67. package/dist/tempo/session/escrow.abi.js.map +1 -0
  68. package/dist/tempo/session/index.d.ts.map +1 -0
  69. package/dist/tempo/session/index.js.map +1 -0
  70. package/package.json +1 -1
  71. package/src/Errors.test.ts +10 -10
  72. package/src/Errors.ts +7 -7
  73. package/src/Expires.test.ts +111 -0
  74. package/src/cli.test.ts +3 -3
  75. package/src/cli.ts +125 -70
  76. package/src/internal/env.ts +54 -0
  77. package/src/mcp-sdk/server/Transport.test.ts +171 -0
  78. package/src/middlewares/express.test.ts +1 -1
  79. package/src/middlewares/hono.test.ts +1 -1
  80. package/src/middlewares/nextjs.test.ts +1 -1
  81. package/src/server/Mppx.ts +5 -4
  82. package/src/server/Transport.test.ts +1 -1
  83. package/src/tempo/client/ChannelOps.test.ts +290 -0
  84. package/src/tempo/client/ChannelOps.ts +8 -8
  85. package/src/tempo/client/Session.test.ts +467 -0
  86. package/src/tempo/client/Session.ts +9 -9
  87. package/src/tempo/client/SessionManager.test.ts +3 -3
  88. package/src/tempo/client/SessionManager.ts +9 -9
  89. package/src/tempo/index.ts +1 -1
  90. package/src/tempo/server/Charge.ts +1 -1
  91. package/src/tempo/server/Session.test.ts +9 -9
  92. package/src/tempo/server/Session.ts +47 -47
  93. package/src/tempo/server/Sse.test.ts +3 -3
  94. package/src/tempo/server/index.ts +2 -2
  95. package/src/tempo/server/internal/transport.ts +6 -6
  96. package/src/tempo/session/Chain.test.ts +511 -0
  97. package/src/tempo/session/Channel.test.ts +108 -0
  98. package/src/tempo/{stream → session}/Receipt.test.ts +16 -12
  99. package/src/tempo/{stream → session}/Receipt.ts +9 -9
  100. package/src/tempo/{stream → session}/Sse.test.ts +5 -5
  101. package/src/tempo/{stream → session}/Sse.ts +11 -11
  102. package/src/tempo/{stream → session}/Types.ts +4 -4
  103. package/dist/tempo/stream/Chain.d.ts.map +0 -1
  104. package/dist/tempo/stream/Chain.js.map +0 -1
  105. package/dist/tempo/stream/Channel.d.ts.map +0 -1
  106. package/dist/tempo/stream/Channel.js.map +0 -1
  107. package/dist/tempo/stream/ChannelStore.d.ts.map +0 -1
  108. package/dist/tempo/stream/ChannelStore.js.map +0 -1
  109. package/dist/tempo/stream/Receipt.d.ts +0 -22
  110. package/dist/tempo/stream/Receipt.d.ts.map +0 -1
  111. package/dist/tempo/stream/Receipt.js.map +0 -1
  112. package/dist/tempo/stream/Sse.d.ts.map +0 -1
  113. package/dist/tempo/stream/Sse.js.map +0 -1
  114. package/dist/tempo/stream/Types.d.ts.map +0 -1
  115. package/dist/tempo/stream/Voucher.d.ts.map +0 -1
  116. package/dist/tempo/stream/Voucher.js.map +0 -1
  117. package/dist/tempo/stream/escrow.abi.js.map +0 -1
  118. package/dist/tempo/stream/index.d.ts.map +0 -1
  119. package/dist/tempo/stream/index.js.map +0 -1
  120. /package/dist/tempo/{stream → session}/Chain.d.ts +0 -0
  121. /package/dist/tempo/{stream → session}/Chain.js +0 -0
  122. /package/dist/tempo/{stream → session}/Channel.d.ts +0 -0
  123. /package/dist/tempo/{stream → session}/Channel.js +0 -0
  124. /package/dist/tempo/{stream → session}/ChannelStore.d.ts +0 -0
  125. /package/dist/tempo/{stream → session}/ChannelStore.js +0 -0
  126. /package/dist/tempo/{stream → session}/Types.js +0 -0
  127. /package/dist/tempo/{stream → session}/Voucher.d.ts +0 -0
  128. /package/dist/tempo/{stream → session}/Voucher.js +0 -0
  129. /package/dist/tempo/{stream → session}/escrow.abi.d.ts +0 -0
  130. /package/dist/tempo/{stream → session}/escrow.abi.js +0 -0
  131. /package/dist/tempo/{stream → session}/index.d.ts +0 -0
  132. /package/dist/tempo/{stream → session}/index.js +0 -0
  133. /package/src/tempo/{stream → session}/Chain.ts +0 -0
  134. /package/src/tempo/{stream → session}/Channel.ts +0 -0
  135. /package/src/tempo/{stream → session}/ChannelStore.test.ts +0 -0
  136. /package/src/tempo/{stream → session}/ChannelStore.ts +0 -0
  137. /package/src/tempo/{stream → session}/Voucher.test.ts +0 -0
  138. /package/src/tempo/{stream → session}/Voucher.ts +0 -0
  139. /package/src/tempo/{stream → session}/escrow.abi.ts +0 -0
  140. /package/src/tempo/{stream → session}/index.ts +0 -0
@@ -0,0 +1,467 @@
1
+ import { type Address, createClient, type Hex, http } from 'viem'
2
+ import { privateKeyToAccount } from 'viem/accounts'
3
+ import { Addresses } from 'viem/tempo'
4
+ import { beforeAll, describe, expect, test } from 'vitest'
5
+ import { deployEscrow, openChannel } from '~test/tempo/session.js'
6
+ import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
7
+ import * as Challenge from '../../Challenge.js'
8
+ import * as Credential from '../../Credential.js'
9
+ import type { SessionCredentialPayload } from '../session/Types.js'
10
+ import { session } from './Session.js'
11
+
12
+ function deserializePayload(result: string) {
13
+ const cred = Credential.deserialize<SessionCredentialPayload>(result)
14
+ return cred
15
+ }
16
+
17
+ const pureAccount = privateKeyToAccount(
18
+ '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
19
+ )
20
+ const pureClient = createClient({
21
+ account: pureAccount,
22
+ transport: http('http://127.0.0.1'),
23
+ })
24
+
25
+ const escrowAddress = '0x542831e3E4Ace07559b7C8787395f4Fb99F70787' as Address
26
+ const recipient = '0x2222222222222222222222222222222222222222' as Address
27
+ const currency = '0x3333333333333333333333333333333333333333' as Address
28
+
29
+ function makeChallenge(overrides?: Record<string, unknown>) {
30
+ return Challenge.from({
31
+ id: 'test-challenge-id',
32
+ realm: 'test.com',
33
+ method: 'tempo',
34
+ intent: 'session',
35
+ request: {
36
+ amount: '1000000',
37
+ recipient,
38
+ currency,
39
+ unitType: 'token',
40
+ methodDetails: {
41
+ chainId: 42431,
42
+ escrowContract: escrowAddress,
43
+ },
44
+ ...overrides,
45
+ },
46
+ }) as any
47
+ }
48
+
49
+ describe('session (pure)', () => {
50
+ describe('error: no action and no deposit/maxDeposit', () => {
51
+ test('throws when neither configured', async () => {
52
+ const method = session({
53
+ getClient: () => pureClient,
54
+ account: pureAccount,
55
+ })
56
+
57
+ await expect(
58
+ method.createCredential({ challenge: makeChallenge(), context: {} }),
59
+ ).rejects.toThrow('No `action` in context and no `deposit` or `maxDeposit` configured')
60
+ })
61
+ })
62
+
63
+ describe('manual action validation', () => {
64
+ const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
65
+
66
+ test('open requires transaction', async () => {
67
+ const method = session({ getClient: () => pureClient, account: pureAccount })
68
+
69
+ await expect(
70
+ method.createCredential({
71
+ challenge: makeChallenge(),
72
+ context: { action: 'open', channelId, cumulativeAmount: '1' },
73
+ }),
74
+ ).rejects.toThrow('transaction required for open action')
75
+ })
76
+
77
+ test('open requires cumulativeAmount', async () => {
78
+ const method = session({ getClient: () => pureClient, account: pureAccount })
79
+
80
+ await expect(
81
+ method.createCredential({
82
+ challenge: makeChallenge(),
83
+ context: { action: 'open', channelId, transaction: '0xabc' },
84
+ }),
85
+ ).rejects.toThrow('cumulativeAmount required for open action')
86
+ })
87
+
88
+ test('topUp requires transaction', async () => {
89
+ const method = session({ getClient: () => pureClient, account: pureAccount })
90
+
91
+ await expect(
92
+ method.createCredential({
93
+ challenge: makeChallenge(),
94
+ context: { action: 'topUp', channelId, additionalDeposit: '5' },
95
+ }),
96
+ ).rejects.toThrow('transaction required for topUp action')
97
+ })
98
+
99
+ test('topUp requires additionalDeposit', async () => {
100
+ const method = session({ getClient: () => pureClient, account: pureAccount })
101
+
102
+ await expect(
103
+ method.createCredential({
104
+ challenge: makeChallenge(),
105
+ context: { action: 'topUp', channelId, transaction: '0xabc' },
106
+ }),
107
+ ).rejects.toThrow('additionalDeposit required for topUp action')
108
+ })
109
+
110
+ test('voucher requires cumulativeAmount', async () => {
111
+ const method = session({ getClient: () => pureClient, account: pureAccount })
112
+
113
+ await expect(
114
+ method.createCredential({
115
+ challenge: makeChallenge(),
116
+ context: { action: 'voucher', channelId },
117
+ }),
118
+ ).rejects.toThrow('cumulativeAmount required for voucher action')
119
+ })
120
+
121
+ test('close requires cumulativeAmount', async () => {
122
+ const method = session({ getClient: () => pureClient, account: pureAccount })
123
+
124
+ await expect(
125
+ method.createCredential({
126
+ challenge: makeChallenge(),
127
+ context: { action: 'close', channelId },
128
+ }),
129
+ ).rejects.toThrow('cumulativeAmount required for close action')
130
+ })
131
+
132
+ test('manual voucher produces valid credential', async () => {
133
+ const method = session({ getClient: () => pureClient, account: pureAccount })
134
+
135
+ const result = await method.createCredential({
136
+ challenge: makeChallenge(),
137
+ context: { action: 'voucher', channelId, cumulativeAmount: '5' },
138
+ })
139
+
140
+ const cred = deserializePayload(result)
141
+ expect(cred.challenge.id).toBe('test-challenge-id')
142
+ expect(cred.challenge.realm).toBe('test.com')
143
+ expect(cred.challenge.method).toBe('tempo')
144
+ expect(cred.challenge.intent).toBe('session')
145
+ expect(cred.payload.action).toBe('voucher')
146
+ expect(cred.payload.channelId).toBe(channelId)
147
+ if (cred.payload.action === 'voucher') {
148
+ expect(cred.payload.cumulativeAmount).toBe('5000000')
149
+ expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
150
+ }
151
+ expect(cred.source).toBe(`did:pkh:eip155:42431:${pureAccount.address}`)
152
+ })
153
+
154
+ test('manual open produces valid credential', async () => {
155
+ const method = session({ getClient: () => pureClient, account: pureAccount })
156
+
157
+ const result = await method.createCredential({
158
+ challenge: makeChallenge(),
159
+ context: {
160
+ action: 'open',
161
+ channelId,
162
+ cumulativeAmount: '5',
163
+ transaction: '0xdeadbeef',
164
+ },
165
+ })
166
+
167
+ const cred = deserializePayload(result)
168
+ expect(cred.challenge.id).toBe('test-challenge-id')
169
+ expect(cred.payload.action).toBe('open')
170
+ expect(cred.payload.channelId).toBe(channelId)
171
+ if (cred.payload.action === 'open') {
172
+ expect(cred.payload.type).toBe('transaction')
173
+ expect(cred.payload.transaction).toBe('0xdeadbeef')
174
+ expect(cred.payload.cumulativeAmount).toBe('5000000')
175
+ expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
176
+ expect(cred.payload.authorizedSigner).toBe(pureAccount.address)
177
+ }
178
+ expect(cred.source).toBe(`did:pkh:eip155:42431:${pureAccount.address}`)
179
+ })
180
+
181
+ test('manual close produces valid credential', async () => {
182
+ const method = session({ getClient: () => pureClient, account: pureAccount })
183
+
184
+ const result = await method.createCredential({
185
+ challenge: makeChallenge(),
186
+ context: { action: 'close', channelId, cumulativeAmount: '5' },
187
+ })
188
+
189
+ const cred = deserializePayload(result)
190
+ expect(cred.challenge.id).toBe('test-challenge-id')
191
+ expect(cred.payload.action).toBe('close')
192
+ expect(cred.payload.channelId).toBe(channelId)
193
+ if (cred.payload.action === 'close') {
194
+ expect(cred.payload.cumulativeAmount).toBe('5000000')
195
+ expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
196
+ }
197
+ expect(cred.source).toBe(`did:pkh:eip155:42431:${pureAccount.address}`)
198
+ })
199
+ })
200
+ })
201
+
202
+ describe('session (on-chain)', () => {
203
+ const payer = accounts[2]
204
+ const payee = accounts[1].address
205
+ let escrowContract: Address
206
+ let saltCounter = 0
207
+
208
+ function nextSalt(): Hex {
209
+ saltCounter++
210
+ return `0x${saltCounter.toString(16).padStart(64, '0')}` as Hex
211
+ }
212
+
213
+ function makeLiveChallenge(overrides?: Record<string, unknown>) {
214
+ return Challenge.from({
215
+ id: 'live-challenge',
216
+ realm: 'test.com',
217
+ method: 'tempo',
218
+ intent: 'session',
219
+ request: {
220
+ amount: '1000000',
221
+ recipient: payee,
222
+ currency: asset,
223
+ unitType: 'token',
224
+ methodDetails: {
225
+ chainId: chain.id,
226
+ escrowContract,
227
+ },
228
+ ...overrides,
229
+ },
230
+ }) as any
231
+ }
232
+
233
+ beforeAll(async () => {
234
+ escrowContract = await deployEscrow()
235
+ await fundAccount({ address: payer.address, token: Addresses.pathUsd })
236
+ await fundAccount({ address: payer.address, token: asset })
237
+ })
238
+
239
+ describe('auto deposit selection', () => {
240
+ test('context.depositRaw wins over everything', async () => {
241
+ const method = session({
242
+ getClient: () => client,
243
+ account: payer,
244
+ deposit: '99',
245
+ escrowContract,
246
+ })
247
+
248
+ const result = await method.createCredential({
249
+ challenge: makeLiveChallenge(),
250
+ context: { depositRaw: '5000000' },
251
+ })
252
+
253
+ const cred = deserializePayload(result)
254
+ expect(cred.payload.action).toBe('open')
255
+ expect(cred.payload.channelId).toMatch(/^0x[0-9a-f]{64}$/)
256
+ if (cred.payload.action === 'open') {
257
+ expect(cred.payload.type).toBe('transaction')
258
+ expect(cred.payload.cumulativeAmount).toBe('1000000')
259
+ expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
260
+ }
261
+ expect(cred.source).toContain(`did:pkh:eip155:${chain.id}:`)
262
+ })
263
+
264
+ test('suggestedDeposit capped by maxDeposit', async () => {
265
+ const method = session({
266
+ getClient: () => client,
267
+ account: payer,
268
+ maxDeposit: '5',
269
+ escrowContract,
270
+ })
271
+
272
+ const challenge = makeLiveChallenge({ suggestedDeposit: '10000000' })
273
+
274
+ const result = await method.createCredential({ challenge, context: {} })
275
+
276
+ const cred = deserializePayload(result)
277
+ expect(cred.payload.action).toBe('open')
278
+ if (cred.payload.action === 'open') {
279
+ expect(cred.payload.type).toBe('transaction')
280
+ expect(cred.payload.cumulativeAmount).toBe('1000000')
281
+ }
282
+ expect(cred.source).toContain(`did:pkh:eip155:${chain.id}:`)
283
+ })
284
+
285
+ test('suggestedDeposit alone', async () => {
286
+ const method = session({
287
+ getClient: () => client,
288
+ account: payer,
289
+ deposit: '99',
290
+ escrowContract,
291
+ })
292
+
293
+ const challenge = makeLiveChallenge({ suggestedDeposit: '7000000' })
294
+
295
+ const result = await method.createCredential({ challenge, context: {} })
296
+
297
+ const cred = deserializePayload(result)
298
+ expect(cred.payload.action).toBe('open')
299
+ if (cred.payload.action === 'open') {
300
+ expect(cred.payload.cumulativeAmount).toBe('1000000')
301
+ }
302
+ })
303
+
304
+ test('maxDeposit alone', async () => {
305
+ const method = session({
306
+ getClient: () => client,
307
+ account: payer,
308
+ maxDeposit: '10',
309
+ escrowContract,
310
+ })
311
+
312
+ const result = await method.createCredential({
313
+ challenge: makeLiveChallenge(),
314
+ context: {},
315
+ })
316
+
317
+ const cred = deserializePayload(result)
318
+ expect(cred.payload.action).toBe('open')
319
+ if (cred.payload.action === 'open') {
320
+ expect(cred.payload.cumulativeAmount).toBe('1000000')
321
+ }
322
+ })
323
+
324
+ test('parameters.deposit as last resort', async () => {
325
+ const method = session({
326
+ getClient: () => client,
327
+ account: payer,
328
+ deposit: '10',
329
+ escrowContract,
330
+ })
331
+
332
+ const result = await method.createCredential({
333
+ challenge: makeLiveChallenge(),
334
+ context: {},
335
+ })
336
+
337
+ const cred = deserializePayload(result)
338
+ expect(cred.payload.action).toBe('open')
339
+ if (cred.payload.action === 'open') {
340
+ expect(cred.payload.cumulativeAmount).toBe('1000000')
341
+ }
342
+ })
343
+
344
+ test('throws when no deposit source available', async () => {
345
+ const method = session({
346
+ getClient: () => client,
347
+ account: payer,
348
+ escrowContract,
349
+ })
350
+
351
+ await expect(
352
+ method.createCredential({ challenge: makeLiveChallenge(), context: {} }),
353
+ ).rejects.toThrow()
354
+ })
355
+ })
356
+
357
+ describe('channel recovery', () => {
358
+ test('recovers channel via suggestedChannelId', async () => {
359
+ const salt = nextSalt()
360
+ const { channelId } = await openChannel({
361
+ escrow: escrowContract,
362
+ payer,
363
+ payee,
364
+ token: asset,
365
+ deposit: 10_000_000n,
366
+ salt,
367
+ })
368
+
369
+ const method = session({
370
+ getClient: () => client,
371
+ account: payer,
372
+ deposit: '10',
373
+ escrowContract,
374
+ })
375
+
376
+ const challenge = makeLiveChallenge({
377
+ methodDetails: {
378
+ chainId: chain.id,
379
+ escrowContract,
380
+ channelId,
381
+ },
382
+ })
383
+
384
+ const result = await method.createCredential({ challenge, context: {} })
385
+
386
+ const cred = deserializePayload(result)
387
+ expect(cred.payload.action).toBe('voucher')
388
+ if (cred.payload.action === 'voucher') {
389
+ expect(cred.payload.channelId).toBe(channelId)
390
+ expect(cred.payload.cumulativeAmount).toBe('1000000')
391
+ expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
392
+ }
393
+ expect(cred.source).toContain(`did:pkh:eip155:${chain.id}:`)
394
+ })
395
+
396
+ test('throws when explicit channelId cannot be recovered', async () => {
397
+ const method = session({
398
+ getClient: () => client,
399
+ account: payer,
400
+ deposit: '10',
401
+ escrowContract,
402
+ })
403
+
404
+ await expect(
405
+ method.createCredential({
406
+ challenge: makeLiveChallenge(),
407
+ context: {
408
+ channelId: '0x0000000000000000000000000000000000000000000000000000000000000bad',
409
+ },
410
+ }),
411
+ ).rejects.toThrow('cannot be reused')
412
+ })
413
+ })
414
+
415
+ describe('cumulative tracking in auto mode', () => {
416
+ test('first call opens channel, second issues voucher with increased cumulative', async () => {
417
+ const updates: { cumulativeAmount: bigint }[] = []
418
+ const method = session({
419
+ getClient: () => client,
420
+ account: payer,
421
+ deposit: '10',
422
+ escrowContract,
423
+ onChannelUpdate: (entry) => updates.push({ cumulativeAmount: entry.cumulativeAmount }),
424
+ })
425
+
426
+ const challenge = makeLiveChallenge()
427
+
428
+ const first = await method.createCredential({ challenge, context: {} })
429
+ const firstCred = deserializePayload(first)
430
+ expect(firstCred.payload.action).toBe('open')
431
+ if (firstCred.payload.action === 'open') {
432
+ expect(firstCred.payload.type).toBe('transaction')
433
+ expect(firstCred.payload.cumulativeAmount).toBe('1000000')
434
+ }
435
+
436
+ const second = await method.createCredential({ challenge, context: {} })
437
+ const secondCred = deserializePayload(second)
438
+ expect(secondCred.payload.action).toBe('voucher')
439
+ if (secondCred.payload.action === 'voucher') {
440
+ expect(secondCred.payload.channelId).toBe(firstCred.payload.channelId)
441
+ expect(secondCred.payload.cumulativeAmount).toBe('2000000')
442
+ expect(secondCred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
443
+ }
444
+
445
+ expect(updates.length).toBe(2)
446
+ expect(updates[0]!.cumulativeAmount).toBe(1_000_000n)
447
+ expect(updates[1]!.cumulativeAmount).toBe(2_000_000n)
448
+ })
449
+ })
450
+
451
+ describe('onChannelUpdate callback', () => {
452
+ test('fires on auto-manage open', async () => {
453
+ const updates: unknown[] = []
454
+ const method = session({
455
+ getClient: () => client,
456
+ account: payer,
457
+ deposit: '10',
458
+ escrowContract,
459
+ onChannelUpdate: (entry) => updates.push(entry),
460
+ })
461
+
462
+ await method.createCredential({ challenge: makeLiveChallenge(), context: {} })
463
+
464
+ expect(updates.length).toBeGreaterThan(0)
465
+ })
466
+ })
467
+ })
@@ -8,8 +8,8 @@ import * as Client from '../../viem/Client.js'
8
8
  import * as z from '../../zod.js'
9
9
  import * as defaults from '../internal/defaults.js'
10
10
  import * as Methods from '../Methods.js'
11
- import type { StreamCredentialPayload } from '../stream/Types.js'
12
- import { signVoucher } from '../stream/Voucher.js'
11
+ import type { SessionCredentialPayload } from '../session/Types.js'
12
+ import { signVoucher } from '../session/Voucher.js'
13
13
  import {
14
14
  type ChannelEntry,
15
15
  createOpenPayload,
@@ -19,7 +19,7 @@ import {
19
19
  tryRecoverChannel,
20
20
  } from './ChannelOps.js'
21
21
 
22
- export const streamContextSchema = z.object({
22
+ export const sessionContextSchema = z.object({
23
23
  account: z.optional(z.custom<Account.getResolver.Parameters['account']>()),
24
24
  action: z.optional(z.enum(['open', 'topUp', 'voucher', 'close'])),
25
25
  channelId: z.optional(z.string()),
@@ -32,7 +32,7 @@ export const streamContextSchema = z.object({
32
32
  depositRaw: z.optional(z.string()),
33
33
  })
34
34
 
35
- export type StreamContext = z.infer<typeof streamContextSchema>
35
+ export type SessionContext = z.infer<typeof sessionContextSchema>
36
36
 
37
37
  /**
38
38
  * Creates a session payment method for use with `Mppx.create()`.
@@ -112,7 +112,7 @@ export function session(parameters: session.Parameters = {}) {
112
112
  async function autoManageCredential(
113
113
  challenge: Challenge.Challenge,
114
114
  account: viem_Account,
115
- context?: StreamContext,
115
+ context?: SessionContext,
116
116
  ): Promise<string> {
117
117
  const md = challenge.request.methodDetails as
118
118
  | { chainId?: number; escrowContract?: string; channelId?: string; feePayer?: boolean }
@@ -174,7 +174,7 @@ export function session(parameters: session.Parameters = {}) {
174
174
  }
175
175
  }
176
176
 
177
- let payload: StreamCredentialPayload
177
+ let payload: SessionCredentialPayload
178
178
 
179
179
  if (entry?.opened) {
180
180
  entry.cumulativeAmount += amount
@@ -212,7 +212,7 @@ export function session(parameters: session.Parameters = {}) {
212
212
  async function manualCredential(
213
213
  challenge: Challenge.Challenge,
214
214
  account: viem_Account,
215
- context: StreamContext,
215
+ context: SessionContext,
216
216
  ): Promise<string> {
217
217
  const md = challenge.request.methodDetails as
218
218
  | { chainId?: number; escrowContract?: string; channelId?: string }
@@ -242,7 +242,7 @@ export function session(parameters: session.Parameters = {}) {
242
242
  const escrowContract = resolveEscrowCached(challenge, chainId, channelId)
243
243
  escrowContractMap.set(channelId, escrowContract)
244
244
 
245
- let payload: StreamCredentialPayload
245
+ let payload: SessionCredentialPayload
246
246
 
247
247
  switch (action) {
248
248
  case 'open': {
@@ -341,7 +341,7 @@ export function session(parameters: session.Parameters = {}) {
341
341
  }
342
342
 
343
343
  return Method.toClient(Methods.session, {
344
- context: streamContextSchema,
344
+ context: sessionContextSchema,
345
345
 
346
346
  async createCredential({ challenge, context }) {
347
347
  const chainId = challenge.request.methodDetails?.chainId ?? 0
@@ -1,8 +1,8 @@
1
1
  import type { Hex } from 'viem'
2
2
  import { describe, expect, test, vi } from 'vitest'
3
3
  import * as Challenge from '../../Challenge.js'
4
- import { formatNeedVoucherEvent, parseEvent } from '../stream/Sse.js'
5
- import type { NeedVoucherEvent, StreamReceipt } from '../stream/Types.js'
4
+ import { formatNeedVoucherEvent, parseEvent } from '../session/Sse.js'
5
+ import type { NeedVoucherEvent, SessionReceipt } from '../session/Types.js'
6
6
  import { sessionManager } from './SessionManager.js'
7
7
 
8
8
  const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
@@ -139,7 +139,7 @@ describe('Session', () => {
139
139
  acceptedCumulative: '2000000',
140
140
  spent: '2000000',
141
141
  units: 2,
142
- } satisfies StreamReceipt)}\n\n`,
142
+ } satisfies SessionReceipt)}\n\n`,
143
143
  ]
144
144
 
145
145
  let callCount = 0
@@ -4,9 +4,9 @@ import type * as Challenge from '../../Challenge.js'
4
4
  import * as Fetch from '../../client/internal/Fetch.js'
5
5
  import type * as Account from '../../viem/Account.js'
6
6
  import type * as Client from '../../viem/Client.js'
7
- import { deserializeStreamReceipt } from '../stream/Receipt.js'
8
- import { parseEvent } from '../stream/Sse.js'
9
- import type { StreamReceipt } from '../stream/Types.js'
7
+ import { deserializeSessionReceipt } from '../session/Receipt.js'
8
+ import { parseEvent } from '../session/Sse.js'
9
+ import type { SessionReceipt } from '../session/Types.js'
10
10
  import type { ChannelEntry } from './ChannelOps.js'
11
11
  import { session as sessionPlugin } from './Session.js'
12
12
 
@@ -20,15 +20,15 @@ export type SessionManager = {
20
20
  sse(
21
21
  input: RequestInfo | URL,
22
22
  init?: RequestInit & {
23
- onReceipt?: ((receipt: StreamReceipt) => void) | undefined
23
+ onReceipt?: ((receipt: SessionReceipt) => void) | undefined
24
24
  signal?: AbortSignal | undefined
25
25
  },
26
26
  ): Promise<AsyncIterable<string>>
27
- close(): Promise<StreamReceipt | undefined>
27
+ close(): Promise<SessionReceipt | undefined>
28
28
  }
29
29
 
30
30
  export type PaymentResponse = Response & {
31
- receipt: StreamReceipt | null
31
+ receipt: SessionReceipt | null
32
32
  challenge: Challenge.Challenge | null
33
33
  channelId: Hex.Hex | null
34
34
  cumulative: bigint
@@ -83,7 +83,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
83
83
 
84
84
  function toPaymentResponse(response: Response): PaymentResponse {
85
85
  const receiptHeader = response.headers.get('Payment-Receipt')
86
- const receipt = receiptHeader ? deserializeStreamReceipt(receiptHeader) : null
86
+ const receipt = receiptHeader ? deserializeSessionReceipt(receiptHeader) : null
87
87
  return Object.assign(response, {
88
88
  receipt,
89
89
  challenge: lastChallenge,
@@ -241,14 +241,14 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
241
241
  },
242
242
  })
243
243
 
244
- let receipt: StreamReceipt | undefined
244
+ let receipt: SessionReceipt | undefined
245
245
  if (lastUrl) {
246
246
  const response = await fetchFn(lastUrl, {
247
247
  method: 'POST',
248
248
  headers: { Authorization: credential },
249
249
  })
250
250
  const receiptHeader = response.headers.get('Payment-Receipt')
251
- if (receiptHeader) receipt = deserializeStreamReceipt(receiptHeader)
251
+ if (receiptHeader) receipt = deserializeSessionReceipt(receiptHeader)
252
252
  }
253
253
 
254
254
  return receipt
@@ -1,2 +1,2 @@
1
1
  export * as Methods from './Methods.js'
2
- export * as Stream from './stream/index.js'
2
+ export * as Session from './session/index.js'
@@ -67,7 +67,7 @@ export function charge<const parameters extends charge.Parameters>(
67
67
  recipient,
68
68
  } as unknown as Defaults,
69
69
 
70
- // TODO: dedupe `{charge,stream}.request`
70
+ // TODO: dedupe `{charge,session}.request`
71
71
  async request({ credential, request }) {
72
72
  const chainId = await (async () => {
73
73
  if (request.chainId) return request.chainId