mppx 0.8.11 → 0.8.13

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 (218) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +16 -1
  3. package/dist/cli/account.d.ts +88 -0
  4. package/dist/cli/account.d.ts.map +1 -1
  5. package/dist/cli/account.js +39 -9
  6. package/dist/cli/account.js.map +1 -1
  7. package/dist/cli/cli.d.ts +2 -1
  8. package/dist/cli/cli.d.ts.map +1 -1
  9. package/dist/cli/cli.js +59 -2
  10. package/dist/cli/cli.js.map +1 -1
  11. package/dist/cli/sessions/Manager.d.ts +31 -0
  12. package/dist/cli/sessions/Manager.d.ts.map +1 -0
  13. package/dist/cli/sessions/Manager.js +56 -0
  14. package/dist/cli/sessions/Manager.js.map +1 -0
  15. package/dist/cli/sessions/commands.d.ts +36 -0
  16. package/dist/cli/sessions/commands.d.ts.map +1 -0
  17. package/dist/cli/sessions/commands.js +407 -0
  18. package/dist/cli/sessions/commands.js.map +1 -0
  19. package/dist/cli/sessions/request.d.ts +35 -0
  20. package/dist/cli/sessions/request.d.ts.map +1 -0
  21. package/dist/cli/sessions/request.js +271 -0
  22. package/dist/cli/sessions/request.js.map +1 -0
  23. package/dist/cli/sessions/store.d.ts +134 -0
  24. package/dist/cli/sessions/store.d.ts.map +1 -0
  25. package/dist/cli/sessions/store.js +723 -0
  26. package/dist/cli/sessions/store.js.map +1 -0
  27. package/dist/cli/utils.d.ts +2 -0
  28. package/dist/cli/utils.d.ts.map +1 -1
  29. package/dist/cli/utils.js +12 -0
  30. package/dist/cli/utils.js.map +1 -1
  31. package/dist/cli/validate/index.d.ts.map +1 -1
  32. package/dist/cli/validate/index.js +12 -6
  33. package/dist/cli/validate/index.js.map +1 -1
  34. package/dist/cli/validate/messages.d.ts +2 -0
  35. package/dist/cli/validate/messages.d.ts.map +1 -0
  36. package/dist/cli/validate/messages.js +8 -0
  37. package/dist/cli/validate/messages.js.map +1 -0
  38. package/dist/client/internal/Fetch.d.ts.map +1 -1
  39. package/dist/client/internal/Fetch.js +35 -6
  40. package/dist/client/internal/Fetch.js.map +1 -1
  41. package/dist/client/internal/MethodResponse.d.ts +23 -0
  42. package/dist/client/internal/MethodResponse.d.ts.map +1 -0
  43. package/dist/client/internal/MethodResponse.js +15 -0
  44. package/dist/client/internal/MethodResponse.js.map +1 -0
  45. package/dist/client/node.d.ts +29 -0
  46. package/dist/client/node.d.ts.map +1 -0
  47. package/dist/client/node.js +177 -0
  48. package/dist/client/node.js.map +1 -0
  49. package/dist/internal/types.d.ts +24 -0
  50. package/dist/internal/types.d.ts.map +1 -1
  51. package/dist/stripe/server/Charge.d.ts +8 -4
  52. package/dist/stripe/server/Charge.d.ts.map +1 -1
  53. package/dist/stripe/server/Charge.js.map +1 -1
  54. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  55. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  56. package/dist/stripe/server/internal/html.gen.js +1 -1
  57. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  58. package/dist/tempo/client/Methods.d.ts +1 -1
  59. package/dist/tempo/internal/types.d.ts +10 -1
  60. package/dist/tempo/internal/types.d.ts.map +1 -1
  61. package/dist/tempo/legacy/client/ChannelOps.d.ts +2 -0
  62. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  63. package/dist/tempo/legacy/client/ChannelOps.js +4 -0
  64. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  65. package/dist/tempo/legacy/client/Session.d.ts +2 -2
  66. package/dist/tempo/legacy/client/Session.d.ts.map +1 -1
  67. package/dist/tempo/legacy/client/Session.js +7 -0
  68. package/dist/tempo/legacy/client/Session.js.map +1 -1
  69. package/dist/tempo/legacy/server/Session.d.ts +3 -3
  70. package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
  71. package/dist/tempo/legacy/server/Session.js.map +1 -1
  72. package/dist/tempo/server/Charge.d.ts +3 -2
  73. package/dist/tempo/server/Charge.d.ts.map +1 -1
  74. package/dist/tempo/server/Charge.js.map +1 -1
  75. package/dist/tempo/server/Methods.d.ts +2 -23
  76. package/dist/tempo/server/Methods.d.ts.map +1 -1
  77. package/dist/tempo/server/Methods.js +2 -2
  78. package/dist/tempo/server/Methods.js.map +1 -1
  79. package/dist/tempo/server/Subscription.d.ts +3 -2
  80. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  81. package/dist/tempo/server/Subscription.js.map +1 -1
  82. package/dist/tempo/server/index.d.ts +1 -1
  83. package/dist/tempo/server/index.d.ts.map +1 -1
  84. package/dist/tempo/server/index.js.map +1 -1
  85. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  86. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  87. package/dist/tempo/server/internal/html.gen.js +1 -1
  88. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  89. package/dist/tempo/session/Snapshot.d.ts +9 -0
  90. package/dist/tempo/session/Snapshot.d.ts.map +1 -1
  91. package/dist/tempo/session/Snapshot.js +6 -0
  92. package/dist/tempo/session/Snapshot.js.map +1 -1
  93. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  94. package/dist/tempo/session/client/ChannelOps.js +11 -2
  95. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  96. package/dist/tempo/session/client/CredentialState.d.ts +21 -1
  97. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  98. package/dist/tempo/session/client/CredentialState.js +66 -1
  99. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  100. package/dist/tempo/session/client/Session.d.ts +1 -1
  101. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  102. package/dist/tempo/session/client/Session.js +53 -1
  103. package/dist/tempo/session/client/Session.js.map +1 -1
  104. package/dist/tempo/session/client/SessionManager.d.ts +2 -2
  105. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  106. package/dist/tempo/session/client/SessionManager.js +130 -30
  107. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  108. package/dist/tempo/session/client/Transports.d.ts +21 -2
  109. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  110. package/dist/tempo/session/client/Transports.js +129 -18
  111. package/dist/tempo/session/client/Transports.js.map +1 -1
  112. package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
  113. package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
  114. package/dist/tempo/session/client/internal/SessionManager.js +13 -0
  115. package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
  116. package/dist/tempo/session/precompile/Chain.js +13 -4
  117. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  118. package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
  119. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  120. package/dist/tempo/session/server/CredentialVerification.js +14 -0
  121. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  122. package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
  123. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
  124. package/dist/tempo/session/server/MeteredStream.js +5 -4
  125. package/dist/tempo/session/server/MeteredStream.js.map +1 -1
  126. package/dist/tempo/session/server/RequestState.d.ts +5 -1
  127. package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
  128. package/dist/tempo/session/server/RequestState.js +10 -4
  129. package/dist/tempo/session/server/RequestState.js.map +1 -1
  130. package/dist/tempo/session/server/Session.d.ts +21 -6
  131. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  132. package/dist/tempo/session/server/Session.js +4 -1
  133. package/dist/tempo/session/server/Session.js.map +1 -1
  134. package/dist/tempo/session/server/Settlement.d.ts +20 -0
  135. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  136. package/dist/tempo/session/server/Settlement.js +20 -0
  137. package/dist/tempo/session/server/Settlement.js.map +1 -1
  138. package/dist/tempo/session/server/Ws.d.ts +2 -0
  139. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  140. package/dist/tempo/session/server/Ws.js.map +1 -1
  141. package/dist/tempo/session/server/index.d.ts +1 -1
  142. package/dist/tempo/session/server/index.d.ts.map +1 -1
  143. package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
  144. package/dist/validation/core.d.ts.map +1 -1
  145. package/dist/validation/core.js +6 -2
  146. package/dist/validation/core.js.map +1 -1
  147. package/dist/viem/Client.d.ts.map +1 -1
  148. package/dist/viem/Client.js +24 -17
  149. package/dist/viem/Client.js.map +1 -1
  150. package/package.json +6 -1
  151. package/src/cli/account.ts +45 -10
  152. package/src/cli/cli.test.ts +165 -70
  153. package/src/cli/cli.ts +67 -2
  154. package/src/cli/mcp.test.ts +11 -0
  155. package/src/cli/sessions/Manager.test.ts +249 -0
  156. package/src/cli/sessions/Manager.ts +93 -0
  157. package/src/cli/sessions/commands.ts +444 -0
  158. package/src/cli/sessions/request.test.ts +51 -0
  159. package/src/cli/sessions/request.ts +353 -0
  160. package/src/cli/sessions/store.test.ts +581 -0
  161. package/src/cli/sessions/store.ts +940 -0
  162. package/src/cli/utils.test.ts +23 -0
  163. package/src/cli/utils.ts +10 -0
  164. package/src/cli/validate/index.ts +12 -14
  165. package/src/cli/validate/messages.ts +7 -0
  166. package/src/cli/validate.test.ts +38 -5
  167. package/src/client/internal/Fetch.test.ts +29 -0
  168. package/src/client/internal/Fetch.ts +51 -6
  169. package/src/client/internal/MethodResponse.ts +39 -0
  170. package/src/client/node.test.ts +115 -0
  171. package/src/client/node.ts +247 -0
  172. package/src/internal/types.test-d.ts +21 -0
  173. package/src/internal/types.ts +33 -0
  174. package/src/server/Mppx.test-d.ts +42 -0
  175. package/src/stripe/Methods.test.ts +10 -0
  176. package/src/stripe/server/Charge.test-d.ts +66 -0
  177. package/src/stripe/server/Charge.ts +8 -5
  178. package/src/stripe/server/internal/html.gen.ts +1 -1
  179. package/src/tempo/internal/types.ts +13 -2
  180. package/src/tempo/legacy/client/ChannelOps.test.ts +21 -0
  181. package/src/tempo/legacy/client/ChannelOps.ts +7 -0
  182. package/src/tempo/legacy/client/Session.test.ts +102 -1
  183. package/src/tempo/legacy/client/Session.ts +10 -0
  184. package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
  185. package/src/tempo/legacy/server/Session.ts +6 -5
  186. package/src/tempo/server/Charge.ts +6 -4
  187. package/src/tempo/server/Methods.ts +5 -4
  188. package/src/tempo/server/Subscription.ts +6 -4
  189. package/src/tempo/server/index.ts +5 -1
  190. package/src/tempo/server/internal/html.gen.ts +1 -1
  191. package/src/tempo/session/README.md +201 -0
  192. package/src/tempo/session/Snapshot.ts +21 -0
  193. package/src/tempo/session/client/ChannelOps.test.ts +40 -0
  194. package/src/tempo/session/client/ChannelOps.ts +12 -2
  195. package/src/tempo/session/client/CredentialState.test.ts +2 -2
  196. package/src/tempo/session/client/CredentialState.ts +103 -1
  197. package/src/tempo/session/client/Session.test.ts +86 -4
  198. package/src/tempo/session/client/Session.ts +73 -1
  199. package/src/tempo/session/client/SessionManager.test.ts +137 -4
  200. package/src/tempo/session/client/SessionManager.ts +141 -33
  201. package/src/tempo/session/client/Transports.test.ts +225 -8
  202. package/src/tempo/session/client/Transports.ts +156 -21
  203. package/src/tempo/session/client/internal/SessionManager.ts +36 -0
  204. package/src/tempo/session/precompile/Chain.test.ts +26 -0
  205. package/src/tempo/session/precompile/Chain.ts +17 -4
  206. package/src/tempo/session/server/CredentialVerification.ts +18 -1
  207. package/src/tempo/session/server/MeteredStream.ts +12 -5
  208. package/src/tempo/session/server/RequestState.test.ts +39 -3
  209. package/src/tempo/session/server/RequestState.ts +13 -6
  210. package/src/tempo/session/server/Session.test.ts +461 -0
  211. package/src/tempo/session/server/Session.ts +31 -8
  212. package/src/tempo/session/server/Settlement.ts +45 -0
  213. package/src/tempo/session/server/Ws.test.ts +113 -0
  214. package/src/tempo/session/server/Ws.ts +2 -0
  215. package/src/tempo/session/server/index.ts +2 -0
  216. package/src/validation/core.ts +7 -1
  217. package/src/viem/Client.test.ts +44 -1
  218. package/src/viem/Client.ts +31 -17
@@ -1,7 +1,9 @@
1
1
  import { type Address, parseUnits } from 'viem'
2
2
  import { tempo as tempo_chain } from 'viem/chains'
3
3
 
4
+ import * as MethodResponse from '../../../client/internal/MethodResponse.js'
4
5
  import * as Constants from '../../../Constants.js'
6
+ import * as Credential from '../../../Credential.js'
5
7
  import * as Method from '../../../Method.js'
6
8
  import * as Account from '../../../viem/Account.js'
7
9
  import * as Client from '../../../viem/Client.js'
@@ -11,6 +13,7 @@ import type {
11
13
  } from '../../client/ResolveAccount.js'
12
14
  import * as defaults from '../../internal/defaults.js'
13
15
  import * as Methods from '../../Methods.js'
16
+ import { isEventStream, requireSessionCredentialContext } from '../precompile/Protocol.js'
14
17
  import { serializeCredential, type ChannelEntry } from './ChannelOps.js'
15
18
  import { createChannelStore, type ChannelStore } from './ChannelStore.js'
16
19
  import {
@@ -20,6 +23,15 @@ import {
20
23
  resolveRecoverContext,
21
24
  sessionContextSchema,
22
25
  } from './CredentialState.js'
26
+ import { assertWithinMaxDeposit } from './Runtime.js'
27
+ import {
28
+ handleSseNeedVoucher,
29
+ isTempoSessionChallenge,
30
+ managementInput,
31
+ postTopUp,
32
+ wrapSseResponse,
33
+ type SsePaymentDriver,
34
+ } from './Transports.js'
23
35
 
24
36
  export { sessionContextSchema, type SessionContext } from './CredentialState.js'
25
37
 
@@ -52,7 +64,7 @@ export function session(parameters: session.Parameters = {}) {
52
64
  const store = channelStore ?? createChannelStore()
53
65
  const sink = { store, notifyUpdate: (entry: ChannelEntry) => onChannelUpdate?.(entry) }
54
66
 
55
- return Method.toClient(Methods.session, {
67
+ const method = Method.toClient(Methods.session, {
56
68
  canHandleChallenge({ challenge }) {
57
69
  return (
58
70
  Constants.getMethodDetail(
@@ -98,6 +110,66 @@ export function session(parameters: session.Parameters = {}) {
98
110
  return serializeCredential(challenge, payload, resolved.chainId, account)
99
111
  },
100
112
  })
113
+
114
+ return MethodResponse.register(
115
+ method,
116
+ async ({ challenge, credential, fetch, headers, input, refetch, response, signal }) => {
117
+ if (!isTempoSessionChallenge(challenge)) return response
118
+ if (!isEventStream(response)) {
119
+ const credentialContext = requireSessionCredentialContext(
120
+ Credential.deserialize(credential).payload,
121
+ )
122
+ if (
123
+ credentialContext.action === 'open' &&
124
+ headers.get('accept')?.toLowerCase().includes('text/event-stream')
125
+ )
126
+ return (await refetch?.()) ?? response
127
+ return response
128
+ }
129
+
130
+ const channelKey = (
131
+ await resolveChallengeContext({
132
+ challenge,
133
+ escrowOverride,
134
+ getClient,
135
+ })
136
+ ).key
137
+ let channel = await store.get(channelKey)
138
+ const driver = {
139
+ assertVoucherWithinLocalLimit: (cumulativeAmount) =>
140
+ assertWithinMaxDeposit(cumulativeAmount, maxDeposit),
141
+ createSessionCredential: (challenge, context) =>
142
+ method.createCredential({ challenge, context }),
143
+ fetch,
144
+ getChannel: () => channel ?? null,
145
+ managementInput,
146
+ async topUpIfNeeded({ channelId, deposit, requiredCumulative }) {
147
+ if (requiredCumulative <= deposit || !channel) return
148
+ const additionalDeposit = requiredCumulative - deposit
149
+ await postTopUp({
150
+ additionalDeposit,
151
+ challenge,
152
+ channel,
153
+ channelId,
154
+ createSessionCredential: (challenge, context) =>
155
+ method.createCredential({ challenge, context }),
156
+ fetch,
157
+ input,
158
+ })
159
+ channel.deposit += additionalDeposit
160
+ await store.set(channel)
161
+ sink.notifyUpdate(channel)
162
+ },
163
+ } satisfies SsePaymentDriver
164
+
165
+ return wrapSseResponse({
166
+ onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
167
+ onReceipt() {},
168
+ response,
169
+ signal,
170
+ })
171
+ },
172
+ )
101
173
  }
102
174
 
103
175
  /** Type helpers for the low-level TIP-1034 session client method. */
@@ -14,6 +14,7 @@ import { createSessionReceipt, serializeSessionReceipt } from '../precompile/Pro
14
14
  import type { NeedVoucherEvent, SessionReceipt } from '../precompile/Protocol.js'
15
15
  import { formatNeedVoucherEvent, parseEvent } from '../precompile/Protocol.js'
16
16
  import type { SessionCredentialPayload } from '../precompile/Protocol.js'
17
+ import * as Voucher from '../precompile/Voucher.js'
17
18
  import { computeFallbackCloseAmount, sessionManager } from './SessionManager.js'
18
19
 
19
20
  const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
@@ -44,6 +45,24 @@ const client = createClient({
44
45
  }),
45
46
  })
46
47
 
48
+ function channelStateClient(state: { closeRequestedAt: number; deposit: bigint; settled: bigint }) {
49
+ return createClient({
50
+ account,
51
+ chain: { id: 4217 } as never,
52
+ transport: custom({
53
+ async request(args) {
54
+ if (args.method === 'eth_call')
55
+ return encodeFunctionResult({
56
+ abi: escrowAbi,
57
+ functionName: 'getChannelState',
58
+ result: state,
59
+ })
60
+ throw new Error(`unexpected rpc request: ${args.method}`)
61
+ },
62
+ }),
63
+ })
64
+ }
65
+
47
66
  const storedDescriptor = {
48
67
  authorizedSigner: account.address,
49
68
  expiringNonceHash: `0x${'11'.repeat(32)}` as Hex,
@@ -332,6 +351,17 @@ describe('Session', () => {
332
351
  test('seeds a same-route HEAD snapshot and resolves the account when resuming it', async () => {
333
352
  const { store, set } = makeChannelStore()
334
353
  const posted: SessionCredentialPayload[] = []
354
+ const highestVoucher = {
355
+ channelId: storedChannelId,
356
+ cumulativeAmount: '1000000',
357
+ signature: await Voucher.signVoucher(
358
+ client,
359
+ account,
360
+ { channelId: storedChannelId, cumulativeAmount: 1_000_000n },
361
+ tip20ChannelEscrow,
362
+ 4217,
363
+ ),
364
+ }
335
365
  const resolveAccount = vi.fn(
336
366
  (info: Parameters<NonNullable<sessionManager.Parameters['resolveAccount']>>[0]) =>
337
367
  info.account,
@@ -356,6 +386,7 @@ describe('Session', () => {
356
386
  deposit: '10000000',
357
387
  descriptor: storedDescriptor,
358
388
  escrow: tip20ChannelEscrow,
389
+ highestVoucher,
359
390
  requiredCumulative: '1000000',
360
391
  settled: '0',
361
392
  spent: '0',
@@ -372,7 +403,11 @@ describe('Session', () => {
372
403
  ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
373
404
  : undefined
374
405
  if (payload) posted.push(payload)
375
- if (!payload) return Promise.resolve(make402Response())
406
+ if (!payload) {
407
+ expect(s.channelId).toBe(storedChannelId)
408
+ expect(s.cumulative).toBe(1_000_000n)
409
+ return Promise.resolve(make402Response())
410
+ }
376
411
  return Promise.resolve(makeOkResponse())
377
412
  })
378
413
  const s = sessionManager({
@@ -389,8 +424,8 @@ describe('Session', () => {
389
424
  expect(response.status).toBe(200)
390
425
  expect(set).toHaveBeenCalledWith(expect.objectContaining({ channelId: storedChannelId }))
391
426
  expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
392
- expect(resolveAccount).toHaveBeenCalledOnce()
393
- expect(resolveAccount).toHaveBeenCalledWith({
427
+ expect(resolveAccount).toHaveBeenCalledTimes(2)
428
+ expect(resolveAccount).toHaveBeenNthCalledWith(1, {
394
429
  account,
395
430
  chainId: 4217,
396
431
  operation: { authority: account.address, kind: 'authorizePaymentChannel' },
@@ -399,6 +434,103 @@ describe('Session', () => {
399
434
  expect(new Headers(contentCall?.[1]?.headers).get('Payment-Session')).toBeNull()
400
435
  })
401
436
 
437
+ test('does not cache a server snapshot for a channel that is closed on-chain', async () => {
438
+ const { store, set } = makeChannelStore()
439
+ const highestVoucher = {
440
+ channelId: storedChannelId,
441
+ cumulativeAmount: '1000000',
442
+ signature: await Voucher.signVoucher(
443
+ client,
444
+ account,
445
+ { channelId: storedChannelId, cumulativeAmount: 1_000_000n },
446
+ tip20ChannelEscrow,
447
+ 4217,
448
+ ),
449
+ }
450
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
451
+ const headers = new Headers(init?.headers)
452
+ if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization))
453
+ return Promise.resolve(make402Response(makeChargeChallenge()))
454
+ if (init?.method === 'HEAD')
455
+ return Promise.resolve(
456
+ new Response(null, {
457
+ status: 204,
458
+ headers: {
459
+ [Constants.Headers.paymentSessionSnapshot]: sessionManager.serializeSnapshot({
460
+ acceptedCumulative: '1000000',
461
+ chainId: 4217,
462
+ channelId: storedChannelId,
463
+ deposit: '10000000',
464
+ descriptor: storedDescriptor,
465
+ escrow: tip20ChannelEscrow,
466
+ highestVoucher,
467
+ requiredCumulative: '1000000',
468
+ settled: '0',
469
+ spent: '0',
470
+ units: 0,
471
+ }),
472
+ },
473
+ }),
474
+ )
475
+ return Promise.resolve(makeOkResponse())
476
+ })
477
+ const s = sessionManager({
478
+ account,
479
+ bootstrap: true,
480
+ client: channelStateClient({ closeRequestedAt: 0, deposit: 0n, settled: 0n }),
481
+ fetch: mockFetch as typeof globalThis.fetch,
482
+ channelStore: store,
483
+ })
484
+
485
+ expect((await s.fetch('https://api.example.com/data')).status).toBe(200)
486
+ expect(set).not.toHaveBeenCalled()
487
+ })
488
+
489
+ test('does not cache a server snapshot with an invalid highest voucher signature', async () => {
490
+ const { store, set } = makeChannelStore()
491
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
492
+ const headers = new Headers(init?.headers)
493
+ if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization))
494
+ return Promise.resolve(make402Response(makeChargeChallenge()))
495
+ if (init?.method === 'HEAD')
496
+ return Promise.resolve(
497
+ new Response(null, {
498
+ status: 204,
499
+ headers: {
500
+ [Constants.Headers.paymentSessionSnapshot]: sessionManager.serializeSnapshot({
501
+ acceptedCumulative: '1000000',
502
+ chainId: 4217,
503
+ channelId: storedChannelId,
504
+ deposit: '10000000',
505
+ descriptor: storedDescriptor,
506
+ escrow: tip20ChannelEscrow,
507
+ highestVoucher: {
508
+ channelId: storedChannelId,
509
+ cumulativeAmount: '1000000',
510
+ signature: `0x${'00'.repeat(64)}`,
511
+ },
512
+ requiredCumulative: '1000000',
513
+ settled: '0',
514
+ spent: '0',
515
+ units: 0,
516
+ }),
517
+ },
518
+ }),
519
+ )
520
+ return Promise.resolve(makeOkResponse())
521
+ })
522
+ const s = sessionManager({
523
+ account,
524
+ bootstrap: true,
525
+ client,
526
+ fetch: mockFetch as typeof globalThis.fetch,
527
+ channelStore: store,
528
+ })
529
+
530
+ expect((await s.fetch('https://api.example.com/data')).status).toBe(200)
531
+ expect(set).not.toHaveBeenCalled()
532
+ })
533
+
402
534
  test('does not answer non-zero bootstrap charge challenges', async () => {
403
535
  const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
404
536
  const headers = new Headers(init?.headers)
@@ -1082,7 +1214,7 @@ describe('Session', () => {
1082
1214
  expect(voucherPayload.channelId).toBe(openPayload.channelId)
1083
1215
  expect(voucherPayload.descriptor).toEqual(openPayload.descriptor)
1084
1216
  expect(voucherPayload.cumulativeAmount).toBe('2000000')
1085
- expect(requestedUrls[2]).toBe('https://api.example.com/stream')
1217
+ expect(requestedUrls[2]).toBe('https://api.example.com/stream?prompt=paid')
1086
1218
  })
1087
1219
 
1088
1220
  test('ignores precompile SSE voucher requests for a different channel', async () => {
@@ -1321,6 +1453,7 @@ describe('Session', () => {
1321
1453
  challengeId,
1322
1454
  channelId: payload.channelId,
1323
1455
  spent: BigInt(payload.cumulativeAmount),
1456
+ txHash: `0x${'aa'.repeat(32)}` as Hex,
1324
1457
  units: 1,
1325
1458
  }),
1326
1459
  ),
@@ -1,22 +1,27 @@
1
1
  import type { Hex } from 'ox'
2
2
  import { parseUnits, type Address } from 'viem'
3
+ import { tempo as tempo_chain } from 'viem/chains'
3
4
 
4
5
  import * as Challenge from '../../../Challenge.js'
5
6
  import * as Fetch from '../../../client/internal/Fetch.js'
7
+ import * as MethodResponse from '../../../client/internal/MethodResponse.js'
6
8
  import * as Constants from '../../../Constants.js'
7
- import type * as Account from '../../../viem/Account.js'
8
- import type * as Client from '../../../viem/Client.js'
9
+ import * as Account from '../../../viem/Account.js'
10
+ import * as Client from '../../../viem/Client.js'
9
11
  import { charge as chargePlugin } from '../../client/Charge.js'
12
+ import * as defaults from '../../internal/defaults.js'
10
13
  import type { ChannelEntry } from '../client/ChannelOps.js'
11
14
  import { createChannelStore, entryKey, type ChannelStore } from '../client/ChannelStore.js'
12
- import type { SessionContext } from '../client/CredentialState.js'
15
+ import { hydrateSessionSnapshot, type SessionContext } from '../client/CredentialState.js'
13
16
  import { session as sessionPlugin } from '../client/Session.js'
17
+ import * as Channel from '../precompile/Channel.js'
14
18
  import { deserializeSessionReceipt } from '../precompile/Protocol.js'
15
19
  import { readSessionChallengeAmount, type SessionReceipt } from '../precompile/Protocol.js'
16
20
  import {
17
21
  deserializeSnapshot as deserializeSessionSnapshot,
18
22
  serializeSnapshot as serializeSessionSnapshot,
19
23
  } from '../Snapshot.js'
24
+ import { registerSessionManagerInternals } from './internal/SessionManager.js'
20
25
  import { createSessionReceiptCoordinator } from './ReceiptCoordinator.js'
21
26
  import { resolveCloseTarget, type CloseTarget } from './Runtime.js'
22
27
  import { assertVoucherWithinLocalLimit as assertVoucherWithinLocalAuthorization } from './Runtime.js'
@@ -34,6 +39,7 @@ import {
34
39
  import { closeSocketSession } from './Runtime.js'
35
40
  import {
36
41
  closeHttpSession,
42
+ getSessionSnapshot,
37
43
  isTempoSessionChallenge,
38
44
  managementInput,
39
45
  postTopUp,
@@ -46,7 +52,7 @@ import {
46
52
  type WebSocketConstructor,
47
53
  WebSocketReadyState,
48
54
  } from './Transports.js'
49
- import { openSseSession, type SseDriverOptions } from './Transports.js'
55
+ import { consumeSseSessionResponse, openSseSession, type SseDriverOptions } from './Transports.js'
50
56
  import { applyTopUpResult, resolveManualTopUp, type TopUpRequirement } from './Transports.js'
51
57
  import {
52
58
  openWebSocketSession,
@@ -125,19 +131,6 @@ function isZeroAmountChargeChallenge(challenge: Challenge.Challenge) {
125
131
  }
126
132
  }
127
133
 
128
- /** Builds a reusable channel entry from a server session snapshot header. */
129
- function entryFromSnapshot(snapshot: ReturnType<typeof deserializeSessionSnapshot>): ChannelEntry {
130
- return {
131
- channelId: snapshot.channelId,
132
- cumulativeAmount: BigInt(snapshot.acceptedCumulative),
133
- deposit: BigInt(snapshot.deposit),
134
- descriptor: snapshot.descriptor,
135
- escrow: snapshot.escrow,
136
- chainId: snapshot.chainId,
137
- opened: true,
138
- }
139
- }
140
-
141
134
  function requestInitWithSessionHint(
142
135
  input: RequestInfo | URL,
143
136
  init: RequestInit | undefined,
@@ -191,6 +184,12 @@ function resolveSessionManagerConfig(parameters: sessionManager.Parameters): Ses
191
184
  */
192
185
  export function sessionManager(parameters: sessionManager.Parameters): SessionManager {
193
186
  const config = resolveSessionManagerConfig(parameters)
187
+ const getClient = Client.getResolver({
188
+ chain: tempo_chain,
189
+ getClient: parameters.client ? () => parameters.client! : parameters.getClient,
190
+ rpcUrl: defaults.rpcUrl,
191
+ })
192
+ const getAccount = Account.getResolver({ account: parameters.account })
194
193
  const runtime = createSessionManagerRuntime()
195
194
  const receipts = createSessionReceiptCoordinator({
196
195
  getSocketSession: () => runtime.socketSession,
@@ -293,6 +292,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
293
292
  }
294
293
  },
295
294
  })
295
+ MethodResponse.unregister(method)
296
296
  const chargeMethod = chargePlugin({
297
297
  account: parameters.account,
298
298
  getClient: parameters.client ? () => parameters.client! : parameters.getClient,
@@ -363,8 +363,27 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
363
363
  async function storeSnapshotHeader(response: Response): Promise<ChannelEntry | undefined> {
364
364
  const header = response.headers.get(Constants.Headers.paymentSessionSnapshot)
365
365
  if (!header) return undefined
366
- const entry = entryFromSnapshot(deserializeSessionSnapshot(header))
366
+ const snapshot = deserializeSessionSnapshot(header)
367
+ const client = await getClient({ chainId: snapshot.chainId })
368
+ const defaultAccount = getAccount(client)
369
+ const authority =
370
+ BigInt(snapshot.descriptor.authorizedSigner) === 0n
371
+ ? snapshot.descriptor.payer
372
+ : snapshot.descriptor.authorizedSigner
373
+ const account =
374
+ (await parameters.resolveAccount?.({
375
+ account: defaultAccount,
376
+ chainId: snapshot.chainId,
377
+ operation: {
378
+ authority,
379
+ kind: 'authorizePaymentChannel',
380
+ },
381
+ })) ?? defaultAccount
382
+ const { entry, spent } = await hydrateSessionSnapshot({ account, client, snapshot })
383
+ assertVoucherWithinLocalLimit(entry.cumulativeAmount)
367
384
  await Promise.resolve(store.set(entry)).catch(() => undefined)
385
+ runtime.channel = entry
386
+ runtime.spent = spent
368
387
  return entry
369
388
  }
370
389
 
@@ -428,8 +447,60 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
428
447
  })
429
448
  }
430
449
 
431
- function getValidatedFallbackCloseAmount(target: CloseTarget) {
432
- const closeAmount = getFallbackCloseAmount(target.challenge, target.channelId)
450
+ function validateCloseSnapshot(channel: ChannelEntry, challenge: TempoSessionChallenge) {
451
+ const snapshot = getSessionSnapshot(challenge)
452
+ if (!snapshot) return undefined
453
+
454
+ const computedSnapshotId = Channel.computeId({
455
+ ...snapshot.descriptor,
456
+ chainId: snapshot.chainId,
457
+ escrow: snapshot.escrow,
458
+ })
459
+ if (computedSnapshotId.toLowerCase() !== snapshot.channelId.toLowerCase()) {
460
+ throw new Error('close snapshot descriptor does not match its channel ID')
461
+ }
462
+ if (snapshot.channelId.toLowerCase() !== channel.channelId.toLowerCase()) {
463
+ throw new Error('close snapshot channel ID does not match local session')
464
+ }
465
+ if (
466
+ snapshot.chainId !== channel.chainId ||
467
+ snapshot.escrow.toLowerCase() !== channel.escrow.toLowerCase()
468
+ ) {
469
+ throw new Error('close snapshot payment scope does not match local session')
470
+ }
471
+
472
+ const acceptedCumulative = BigInt(snapshot.acceptedCumulative)
473
+ const snapshotSpent = BigInt(snapshot.spent)
474
+ if (acceptedCumulative < 0n || snapshotSpent < 0n) {
475
+ throw new Error('close snapshot amounts must not be negative')
476
+ }
477
+ if (snapshotSpent > acceptedCumulative) {
478
+ throw new Error('close snapshot spent exceeds accepted cumulative amount')
479
+ }
480
+ if (acceptedCumulative > channel.cumulativeAmount) {
481
+ throw new Error('close snapshot accepted cumulative exceeds local voucher state')
482
+ }
483
+ return { acceptedCumulative, spent: snapshotSpent }
484
+ }
485
+
486
+ function applyCloseSnapshot(target: CloseTarget, challenge: TempoSessionChallenge) {
487
+ const snapshot = validateCloseSnapshot(target.channel, challenge)
488
+ if (!snapshot) return
489
+
490
+ const { acceptedCumulative, spent } = snapshot
491
+ if (runtime.spent > acceptedCumulative) {
492
+ throw new Error('close snapshot accepted cumulative is below locally confirmed spend')
493
+ }
494
+ if (spent > runtime.spent) runtime.spent = spent
495
+ }
496
+
497
+ function getValidatedFallbackCloseAmount(
498
+ target: CloseTarget,
499
+ challenge: TempoSessionChallenge = target.challenge,
500
+ applySnapshot = false,
501
+ ) {
502
+ if (applySnapshot) applyCloseSnapshot(target, challenge)
503
+ const closeAmount = getFallbackCloseAmount(challenge, target.channelId)
433
504
  if (closeAmount > target.channel.cumulativeAmount) {
434
505
  throw new Error('fallback close amount exceeds local voucher state')
435
506
  }
@@ -467,6 +538,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
467
538
  spent: runtime.spent,
468
539
  })
469
540
  if (applied?.channel && runtime.lastChallenge) {
541
+ await store.set(applied.channel)
470
542
  dispatch({
471
543
  type: 'activated',
472
544
  challengeId: runtime.lastChallenge.id,
@@ -623,6 +695,8 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
623
695
  createSessionCredential,
624
696
  fetch: config.fetch,
625
697
  lastUrl: runtime.lastUrl,
698
+ resolveSignedCloseAmount: (challenge) =>
699
+ getValidatedFallbackCloseAmount(target, challenge, true),
626
700
  signedCloseAmount: getValidatedFallbackCloseAmount(target),
627
701
  setChallenge(challenge) {
628
702
  runtime.lastChallenge = challenge
@@ -638,6 +712,20 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
638
712
  return receipt
639
713
  }
640
714
 
715
+ const sseDriver = {
716
+ createSessionCredential,
717
+ doFetch,
718
+ fetch: config.fetch,
719
+ getChannel: () => runtime.channel,
720
+ getChallenge: () => runtime.lastChallenge,
721
+ assertVoucherWithinLocalLimit,
722
+ managementInput,
723
+ acceptReceipt(receipt: SessionReceipt) {
724
+ updateSpentFromReceipt(receipt)
725
+ },
726
+ topUpIfNeeded,
727
+ }
728
+
641
729
  const self: SessionManager = {
642
730
  get channelId() {
643
731
  return runtime.channel?.channelId
@@ -673,19 +761,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
673
761
  },
674
762
 
675
763
  async sse(input, init) {
676
- return openSseSession(input, init, {
677
- createSessionCredential,
678
- doFetch,
679
- fetch: config.fetch,
680
- getChannel: () => runtime.channel,
681
- getChallenge: () => runtime.lastChallenge,
682
- assertVoucherWithinLocalLimit,
683
- managementInput,
684
- acceptReceipt(receipt) {
685
- updateSpentFromReceipt(receipt)
686
- },
687
- topUpIfNeeded,
688
- })
764
+ return openSseSession(input, init, sseDriver)
689
765
  },
690
766
 
691
767
  async ws(input, init) {
@@ -766,6 +842,38 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
766
842
  },
767
843
  }
768
844
 
845
+ registerSessionManagerInternals(self, {
846
+ consumeSseResponse(input, response, options) {
847
+ return consumeSseSessionResponse(input, response, options, sseDriver)
848
+ },
849
+ rehydrate({ channel, challenge, input, spent }) {
850
+ if (!channel.opened) throw new Error('Cannot restore a closed session channel.')
851
+ if (!isTempoSessionChallenge(challenge)) {
852
+ throw new Error('Cannot restore session: challenge is not tempo/session.')
853
+ }
854
+ if (spent < 0n) throw new Error('Cannot restore session: spent must not be negative.')
855
+ if (spent > channel.cumulativeAmount) {
856
+ throw new Error('Cannot restore session: spent exceeds local voucher state.')
857
+ }
858
+ assertVoucherWithinLocalLimit(channel.cumulativeAmount)
859
+ const snapshot = validateCloseSnapshot(channel, challenge)
860
+ const restoredSpent = snapshot && snapshot.spent > spent ? snapshot.spent : spent
861
+
862
+ runtime.lastUrl = input
863
+ runtime.lastChallenge = challenge
864
+ runtime.channel = channel
865
+ runtime.spent = restoredSpent
866
+ dispatch({ type: 'challengeReceived', challengeId: challenge.id })
867
+ dispatch({
868
+ type: 'activated',
869
+ challengeId: challenge.id,
870
+ entry: channel,
871
+ spent: restoredSpent.toString(),
872
+ units: 0,
873
+ })
874
+ },
875
+ })
876
+
769
877
  return self
770
878
  }
771
879