mppx 0.7.0 → 0.8.0

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 (278) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +20 -11
  3. package/dist/Challenge.d.ts.map +1 -1
  4. package/dist/Challenge.js +18 -6
  5. package/dist/Challenge.js.map +1 -1
  6. package/dist/Mcp.d.ts +3 -0
  7. package/dist/Mcp.d.ts.map +1 -1
  8. package/dist/Mcp.js +2 -0
  9. package/dist/Mcp.js.map +1 -1
  10. package/dist/PaymentRequest.d.ts +10 -10
  11. package/dist/PaymentRequest.js +8 -8
  12. package/dist/client/Mppx.js +2 -2
  13. package/dist/client/Mppx.js.map +1 -1
  14. package/dist/client/Transport.d.ts +11 -16
  15. package/dist/client/Transport.d.ts.map +1 -1
  16. package/dist/client/Transport.js +55 -75
  17. package/dist/client/Transport.js.map +1 -1
  18. package/dist/client/index.d.ts +3 -0
  19. package/dist/client/index.d.ts.map +1 -1
  20. package/dist/client/index.js +1 -0
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/internal/Fetch.d.ts.map +1 -1
  23. package/dist/client/internal/Fetch.js +46 -7
  24. package/dist/client/internal/Fetch.js.map +1 -1
  25. package/dist/client/internal/protocols/Mcp.d.ts +7 -0
  26. package/dist/client/internal/protocols/Mcp.d.ts.map +1 -0
  27. package/dist/client/internal/protocols/Mcp.js +159 -0
  28. package/dist/client/internal/protocols/Mcp.js.map +1 -0
  29. package/dist/client/internal/protocols/Mpp.d.ts +4 -0
  30. package/dist/client/internal/protocols/Mpp.d.ts.map +1 -0
  31. package/dist/client/internal/protocols/Mpp.js +18 -0
  32. package/dist/client/internal/protocols/Mpp.js.map +1 -0
  33. package/dist/client/internal/protocols/Protocol.d.ts +10 -0
  34. package/dist/client/internal/protocols/Protocol.d.ts.map +1 -0
  35. package/dist/client/internal/protocols/Protocol.js +2 -0
  36. package/dist/client/internal/protocols/Protocol.js.map +1 -0
  37. package/dist/client/internal/protocols/Shared.d.ts +5 -0
  38. package/dist/client/internal/protocols/Shared.d.ts.map +1 -0
  39. package/dist/client/internal/protocols/Shared.js +20 -0
  40. package/dist/client/internal/protocols/Shared.js.map +1 -0
  41. package/dist/client/internal/protocols/X402.d.ts +8 -0
  42. package/dist/client/internal/protocols/X402.d.ts.map +1 -0
  43. package/dist/client/internal/protocols/X402.js +39 -0
  44. package/dist/client/internal/protocols/X402.js.map +1 -0
  45. package/dist/evm/client/index.d.ts +1 -0
  46. package/dist/evm/client/index.d.ts.map +1 -1
  47. package/dist/evm/client/index.js +1 -0
  48. package/dist/evm/client/index.js.map +1 -1
  49. package/dist/evm/index.d.ts +2 -0
  50. package/dist/evm/index.d.ts.map +1 -1
  51. package/dist/evm/index.js +2 -0
  52. package/dist/evm/index.js.map +1 -1
  53. package/dist/evm/server/index.d.ts +1 -0
  54. package/dist/evm/server/index.d.ts.map +1 -1
  55. package/dist/evm/server/index.js +1 -0
  56. package/dist/evm/server/index.js.map +1 -1
  57. package/dist/mcp/client/McpClient.d.ts +101 -0
  58. package/dist/mcp/client/McpClient.d.ts.map +1 -0
  59. package/dist/mcp/client/McpClient.js +162 -0
  60. package/dist/mcp/client/McpClient.js.map +1 -0
  61. package/dist/mcp/client/index.d.ts.map +1 -0
  62. package/dist/mcp/client/index.js.map +1 -0
  63. package/dist/mcp/server/Transport.d.ts.map +1 -0
  64. package/dist/mcp/server/Transport.js.map +1 -0
  65. package/dist/mcp/server/index.d.ts.map +1 -0
  66. package/dist/mcp/server/index.js.map +1 -0
  67. package/dist/server/Mppx.d.ts +1 -1
  68. package/dist/server/Mppx.d.ts.map +1 -1
  69. package/dist/server/Mppx.js +9 -0
  70. package/dist/server/Mppx.js.map +1 -1
  71. package/dist/server/Transport.d.ts +1 -1
  72. package/dist/server/Transport.d.ts.map +1 -1
  73. package/dist/server/Transport.js +1 -1
  74. package/dist/server/Transport.js.map +1 -1
  75. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  76. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  77. package/dist/stripe/server/internal/html.gen.js +1 -1
  78. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  79. package/dist/tempo/Proof.d.ts +85 -1
  80. package/dist/tempo/Proof.d.ts.map +1 -1
  81. package/dist/tempo/Proof.js +35 -0
  82. package/dist/tempo/Proof.js.map +1 -1
  83. package/dist/tempo/client/Charge.d.ts +13 -1
  84. package/dist/tempo/client/Charge.d.ts.map +1 -1
  85. package/dist/tempo/client/Charge.js +38 -25
  86. package/dist/tempo/client/Charge.js.map +1 -1
  87. package/dist/tempo/client/Methods.d.ts +5 -3
  88. package/dist/tempo/client/Methods.d.ts.map +1 -1
  89. package/dist/tempo/client/Methods.js +4 -2
  90. package/dist/tempo/client/Methods.js.map +1 -1
  91. package/dist/tempo/client/ResolveAccount.d.ts +40 -0
  92. package/dist/tempo/client/ResolveAccount.d.ts.map +1 -0
  93. package/dist/tempo/client/ResolveAccount.js +2 -0
  94. package/dist/tempo/client/ResolveAccount.js.map +1 -0
  95. package/dist/tempo/internal/fee-payer.d.ts +9 -1
  96. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  97. package/dist/tempo/internal/fee-payer.js +35 -6
  98. package/dist/tempo/internal/fee-payer.js.map +1 -1
  99. package/dist/tempo/internal/proof.d.ts +71 -5
  100. package/dist/tempo/internal/proof.d.ts.map +1 -1
  101. package/dist/tempo/internal/proof.js +42 -6
  102. package/dist/tempo/internal/proof.js.map +1 -1
  103. package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -1
  104. package/dist/tempo/legacy/client/SessionManager.js +10 -3
  105. package/dist/tempo/legacy/client/SessionManager.js.map +1 -1
  106. package/dist/tempo/server/Charge.d.ts.map +1 -1
  107. package/dist/tempo/server/Charge.js +42 -18
  108. package/dist/tempo/server/Charge.js.map +1 -1
  109. package/dist/tempo/server/Methods.d.ts +4 -2
  110. package/dist/tempo/server/Methods.d.ts.map +1 -1
  111. package/dist/tempo/server/Methods.js +4 -2
  112. package/dist/tempo/server/Methods.js.map +1 -1
  113. package/dist/tempo/server/Subscription.d.ts +10 -0
  114. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  115. package/dist/tempo/server/Subscription.js +135 -23
  116. package/dist/tempo/server/Subscription.js.map +1 -1
  117. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  118. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  119. package/dist/tempo/server/internal/html.gen.js +1 -1
  120. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  121. package/dist/tempo/session/client/ChannelOps.d.ts +2 -3
  122. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  123. package/dist/tempo/session/client/ChannelOps.js +7 -10
  124. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  125. package/dist/tempo/session/client/ChannelStore.d.ts +51 -0
  126. package/dist/tempo/session/client/ChannelStore.d.ts.map +1 -0
  127. package/dist/tempo/session/client/ChannelStore.js +63 -0
  128. package/dist/tempo/session/client/ChannelStore.js.map +1 -0
  129. package/dist/tempo/session/client/CredentialState.d.ts +7 -24
  130. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  131. package/dist/tempo/session/client/CredentialState.js +51 -49
  132. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  133. package/dist/tempo/session/client/Session.d.ts +8 -2
  134. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  135. package/dist/tempo/session/client/Session.js +22 -8
  136. package/dist/tempo/session/client/Session.js.map +1 -1
  137. package/dist/tempo/session/client/SessionManager.d.ts +4 -40
  138. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  139. package/dist/tempo/session/client/SessionManager.js +124 -174
  140. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  141. package/dist/tempo/session/client/index.d.ts +3 -4
  142. package/dist/tempo/session/client/index.d.ts.map +1 -1
  143. package/dist/tempo/session/client/index.js +1 -0
  144. package/dist/tempo/session/client/index.js.map +1 -1
  145. package/dist/tempo/session/precompile/Voucher.d.ts +3 -3
  146. package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -1
  147. package/dist/tempo/session/precompile/Voucher.js +24 -25
  148. package/dist/tempo/session/precompile/Voucher.js.map +1 -1
  149. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  150. package/dist/tempo/session/server/Settlement.js +4 -2
  151. package/dist/tempo/session/server/Settlement.js.map +1 -1
  152. package/dist/tempo/session/server/Sse.d.ts.map +1 -1
  153. package/dist/tempo/session/server/Sse.js.map +1 -1
  154. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  155. package/dist/tempo/session/server/Ws.js.map +1 -1
  156. package/dist/tempo/subscription/KeyAuthorization.d.ts +712 -1
  157. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  158. package/dist/tempo/subscription/Store.d.ts +2 -0
  159. package/dist/tempo/subscription/Store.d.ts.map +1 -1
  160. package/dist/tempo/subscription/Store.js +16 -1
  161. package/dist/tempo/subscription/Store.js.map +1 -1
  162. package/dist/x402/index.d.ts +1 -0
  163. package/dist/x402/index.d.ts.map +1 -1
  164. package/dist/x402/index.js +1 -0
  165. package/dist/x402/index.js.map +1 -1
  166. package/package.json +21 -10
  167. package/src/Challenge.test.ts +40 -0
  168. package/src/Challenge.ts +19 -6
  169. package/src/Mcp.ts +4 -0
  170. package/src/PaymentRequest.ts +10 -10
  171. package/src/cli/cli.test.ts +15 -15
  172. package/src/client/Mppx.test-d.ts +21 -1
  173. package/src/client/Mppx.test.ts +1 -1
  174. package/src/client/Mppx.ts +2 -2
  175. package/src/client/Transport.test.ts +225 -178
  176. package/src/client/Transport.ts +77 -83
  177. package/src/client/index.ts +14 -0
  178. package/src/client/internal/Fetch.test.ts +207 -2
  179. package/src/client/internal/Fetch.ts +52 -6
  180. package/src/client/internal/protocols/Mcp.test.ts +220 -0
  181. package/src/client/internal/protocols/Mcp.ts +162 -0
  182. package/src/client/internal/protocols/Mpp.ts +21 -0
  183. package/src/client/internal/protocols/Protocol.ts +10 -0
  184. package/src/client/internal/protocols/Shared.ts +25 -0
  185. package/src/client/internal/protocols/X402.ts +42 -0
  186. package/src/discovery/OpenApi.test.ts +1 -1
  187. package/src/evm/PublicInterface.test-d.ts +1 -1
  188. package/src/evm/client/index.ts +1 -0
  189. package/src/evm/index.ts +2 -0
  190. package/src/evm/server/Charge.test.ts +1 -1
  191. package/src/evm/server/index.ts +1 -0
  192. package/src/{mcp-sdk → mcp}/client/McpClient.integration.test.ts +10 -4
  193. package/src/{mcp-sdk → mcp}/client/McpClient.test-d.ts +45 -18
  194. package/src/{mcp-sdk → mcp}/client/McpClient.test.ts +211 -5
  195. package/src/mcp/client/McpClient.ts +307 -0
  196. package/src/{mcp-sdk → mcp}/client/McpClient.unit.test.ts +9 -5
  197. package/src/middlewares/elysia.test.ts +1 -1
  198. package/src/middlewares/express.test.ts +1 -1
  199. package/src/middlewares/hono.test.ts +1 -1
  200. package/src/middlewares/internal/mppx.test.ts +1 -1
  201. package/src/middlewares/nextjs.test.ts +1 -1
  202. package/src/proxy/Proxy.test.ts +1 -1
  203. package/src/proxy/services/anthropic.test.ts +1 -1
  204. package/src/proxy/services/openai.test.ts +1 -1
  205. package/src/proxy/services/stripe.test.ts +1 -1
  206. package/src/server/Mppx.authorize.test.ts +1 -1
  207. package/src/server/Mppx.test-d.ts +1 -1
  208. package/src/server/Mppx.test.ts +20 -2
  209. package/src/server/Mppx.ts +14 -1
  210. package/src/server/Transport.test.ts +6 -6
  211. package/src/server/Transport.ts +1 -1
  212. package/src/stripe/Charge.integration.test.ts +1 -1
  213. package/src/stripe/client/Charge.test.ts +1 -1
  214. package/src/stripe/server/Charge.test.ts +1 -1
  215. package/src/stripe/server/internal/html/package.json +1 -1
  216. package/src/stripe/server/internal/html.gen.ts +1 -1
  217. package/src/tempo/Proof.conformance.test.ts +146 -0
  218. package/src/tempo/Proof.test-d.ts +15 -0
  219. package/src/tempo/Proof.ts +52 -1
  220. package/src/tempo/Subscription.integration.test.ts +1 -1
  221. package/src/tempo/client/Charge.test.ts +173 -0
  222. package/src/tempo/client/Charge.ts +65 -36
  223. package/src/tempo/client/Methods.ts +4 -2
  224. package/src/tempo/client/ResolveAccount.ts +46 -0
  225. package/src/tempo/internal/fee-payer.test.ts +65 -10
  226. package/src/tempo/internal/fee-payer.ts +42 -6
  227. package/src/tempo/internal/proof.test.ts +12 -4
  228. package/src/tempo/internal/proof.ts +55 -6
  229. package/src/tempo/legacy/client/SessionManager.ts +11 -3
  230. package/src/tempo/legacy/server/Session.test.ts +91 -26
  231. package/src/tempo/server/Charge.test.ts +388 -17
  232. package/src/tempo/server/Charge.ts +46 -24
  233. package/src/tempo/server/Methods.ts +4 -2
  234. package/src/tempo/server/Subscription.test.ts +465 -3
  235. package/src/tempo/server/Subscription.ts +174 -19
  236. package/src/tempo/server/internal/html/package.json +2 -2
  237. package/src/tempo/server/internal/html.gen.ts +1 -1
  238. package/src/tempo/session/client/ChannelOps.ts +5 -19
  239. package/src/tempo/session/client/ChannelStore.ts +111 -0
  240. package/src/tempo/session/client/CredentialState.test.ts +206 -62
  241. package/src/tempo/session/client/CredentialState.ts +58 -73
  242. package/src/tempo/session/client/Session.test.ts +41 -1
  243. package/src/tempo/session/client/Session.ts +36 -10
  244. package/src/tempo/session/client/SessionManager.test.ts +154 -65
  245. package/src/tempo/session/client/SessionManager.ts +141 -235
  246. package/src/tempo/session/client/index.ts +8 -5
  247. package/src/tempo/session/precompile/Voucher.test.ts +45 -7
  248. package/src/tempo/session/precompile/Voucher.ts +27 -25
  249. package/src/tempo/session/server/Session.test.ts +4 -4
  250. package/src/tempo/session/server/Settlement.test.ts +88 -1
  251. package/src/tempo/session/server/Settlement.ts +2 -1
  252. package/src/tempo/session/server/Sse.ts +0 -2
  253. package/src/tempo/session/server/Ws.ts +0 -4
  254. package/src/tempo/subscription/Store.ts +27 -9
  255. package/src/x402/Exact.e2e.test.ts +1 -1
  256. package/src/x402/PublicInterface.test-d.ts +1 -1
  257. package/src/x402/index.ts +1 -0
  258. package/dist/mcp-sdk/client/McpClient.d.ts +0 -85
  259. package/dist/mcp-sdk/client/McpClient.d.ts.map +0 -1
  260. package/dist/mcp-sdk/client/McpClient.js +0 -118
  261. package/dist/mcp-sdk/client/McpClient.js.map +0 -1
  262. package/dist/mcp-sdk/client/index.d.ts.map +0 -1
  263. package/dist/mcp-sdk/client/index.js.map +0 -1
  264. package/dist/mcp-sdk/server/Transport.d.ts.map +0 -1
  265. package/dist/mcp-sdk/server/Transport.js.map +0 -1
  266. package/dist/mcp-sdk/server/index.d.ts.map +0 -1
  267. package/dist/mcp-sdk/server/index.js.map +0 -1
  268. package/src/mcp-sdk/client/McpClient.ts +0 -228
  269. /package/dist/{mcp-sdk → mcp}/client/index.d.ts +0 -0
  270. /package/dist/{mcp-sdk → mcp}/client/index.js +0 -0
  271. /package/dist/{mcp-sdk → mcp}/server/Transport.d.ts +0 -0
  272. /package/dist/{mcp-sdk → mcp}/server/Transport.js +0 -0
  273. /package/dist/{mcp-sdk → mcp}/server/index.d.ts +0 -0
  274. /package/dist/{mcp-sdk → mcp}/server/index.js +0 -0
  275. /package/src/{mcp-sdk → mcp}/client/index.ts +0 -0
  276. /package/src/{mcp-sdk → mcp}/server/Transport.test.ts +0 -0
  277. /package/src/{mcp-sdk → mcp}/server/Transport.ts +0 -0
  278. /package/src/{mcp-sdk → mcp}/server/index.ts +0 -0
@@ -9,14 +9,7 @@
9
9
  * @see https://tips.sh/1034-1
10
10
  */
11
11
  import { Hex } from 'ox'
12
- import {
13
- encodeFunctionData,
14
- isAddress,
15
- zeroAddress,
16
- type Account,
17
- type Address,
18
- type Client,
19
- } from 'viem'
12
+ import { encodeFunctionData, isAddress, type Account, type Address, type Client } from 'viem'
20
13
  import { prepareTransactionRequest, signTransaction } from 'viem/actions'
21
14
  import { Transaction } from 'viem/tempo'
22
15
 
@@ -62,10 +55,6 @@ export type ChannelEntry = {
62
55
  opened: boolean
63
56
  }
64
57
 
65
- function voucherAuthorizedSigner(address: Address): Address | undefined {
66
- return address.toLowerCase() === zeroAddress ? undefined : address
67
- }
68
-
69
58
  function isObject(value: unknown): value is Record<string, unknown> {
70
59
  return typeof value === 'object' && value !== null
71
60
  }
@@ -78,9 +67,9 @@ function readAccessKeyAddress(account: Account): Address | undefined {
78
67
  return readOptionalAddress((account as AccountWithAccessKey).accessKeyAddress)
79
68
  }
80
69
 
81
- /** Resolves the voucher signer address for a client account and optional override. */
82
- export function resolveAuthorizedSigner(account: Account, override?: Address | undefined): Address {
83
- return override ?? readAccessKeyAddress(account) ?? account.address
70
+ /** Resolves the voucher authority address for a client account. */
71
+ export function resolveAuthorizedSigner(account: Account): Address {
72
+ return readAccessKeyAddress(account) ?? account.address
84
73
  }
85
74
 
86
75
  async function prepareTempoChannelTransaction(
@@ -169,7 +158,6 @@ export async function createVoucherPayload(
169
158
  { channelId, cumulativeAmount: amount },
170
159
  escrow,
171
160
  chainId,
172
- voucherAuthorizedSigner(descriptor.authorizedSigner),
173
161
  )
174
162
 
175
163
  return {
@@ -223,7 +211,6 @@ export async function createOpenPayload(
223
211
  client: Client,
224
212
  account: Account,
225
213
  parameters: {
226
- authorizedSigner?: Address | undefined
227
214
  chainId: number
228
215
  deposit: bigint
229
216
  escrow?: Address | undefined
@@ -234,7 +221,7 @@ export async function createOpenPayload(
234
221
  token: Address
235
222
  },
236
223
  ): Promise<OpenCredentialPayload> {
237
- const authorizedSigner = resolveAuthorizedSigner(account, parameters.authorizedSigner)
224
+ const authorizedSigner = resolveAuthorizedSigner(account)
238
225
  const escrow = parameters.escrow ?? tip20ChannelEscrow
239
226
  const operator = parameters.operator ?? '0x0000000000000000000000000000000000000000'
240
227
  const salt = Hex.random(32)
@@ -282,7 +269,6 @@ export async function createOpenPayload(
282
269
  { channelId, cumulativeAmount: initialAmount },
283
270
  escrow,
284
271
  parameters.chainId,
285
- voucherAuthorizedSigner(authorizedSigner),
286
272
  )
287
273
  return {
288
274
  action: 'open',
@@ -0,0 +1,111 @@
1
+ import type { Address } from 'viem'
2
+
3
+ import type { MaybePromise } from '../../../internal/types.js'
4
+ import type { ChannelEntry } from './ChannelOps.js'
5
+
6
+ /** Store of reusable payer session channels keyed by payment scope. */
7
+ export type ChannelStore = {
8
+ /** Returns the channel cached for `key`, when present. */
9
+ get(key: string): MaybePromise<ChannelEntry | undefined>
10
+ /** Inserts or replaces a channel entry. */
11
+ set(entry: ChannelEntry): MaybePromise<void>
12
+ /** Removes the channel cached for `key`. */
13
+ delete(key: string): MaybePromise<void>
14
+ }
15
+
16
+ /** Channel persistence and update notification for credential results. */
17
+ export type ChannelSink = {
18
+ store: ChannelStore
19
+ notifyUpdate: (entry: ChannelEntry) => void
20
+ }
21
+
22
+ /** Returns the scope key for a reusable payer session channel. */
23
+ export function channelKey(scope: {
24
+ payee: Address
25
+ token: Address
26
+ escrow: Address
27
+ chainId: number
28
+ }): string {
29
+ const { payee, token, escrow, chainId } = scope
30
+ return `${payee.toLowerCase()}:${token.toLowerCase()}:${escrow.toLowerCase()}:${chainId}`
31
+ }
32
+
33
+ /** Returns the scope key for a stored channel entry. */
34
+ export function entryKey(entry: ChannelEntry): string {
35
+ return channelKey({
36
+ payee: entry.descriptor.payee,
37
+ token: entry.descriptor.token,
38
+ escrow: entry.escrow,
39
+ chainId: entry.chainId,
40
+ })
41
+ }
42
+
43
+ /** Creates the default in-memory {@link ChannelStore}. */
44
+ export function createChannelStore(): ChannelStore {
45
+ const channels = new Map<string, ChannelEntry>()
46
+ return {
47
+ get: (key) => channels.get(key),
48
+ set(entry) {
49
+ channels.set(entryKey(entry), entry)
50
+ },
51
+ delete(key) {
52
+ channels.delete(key)
53
+ },
54
+ } satisfies ChannelStore
55
+ }
56
+
57
+ /** JSON-safe projection of a {@link ChannelEntry}, with bigint amounts as decimal strings. */
58
+ export type StoredChannel = Omit<ChannelEntry, 'cumulativeAmount' | 'deposit'> & {
59
+ /** Cumulative voucher authorization in raw token units, as a decimal string. */
60
+ cumulativeAmount: string
61
+ /** Channel deposit in raw token units, as a decimal string. */
62
+ deposit: string
63
+ }
64
+
65
+ /** Converts a channel entry into its JSON-safe stored form. */
66
+ export function serializeEntry(entry: ChannelEntry): StoredChannel {
67
+ return {
68
+ ...entry,
69
+ cumulativeAmount: entry.cumulativeAmount.toString(),
70
+ deposit: entry.deposit.toString(),
71
+ }
72
+ }
73
+
74
+ /** Restores a channel entry from its JSON-safe stored form. */
75
+ export function deserializeEntry(stored: StoredChannel): ChannelEntry {
76
+ return {
77
+ ...stored,
78
+ cumulativeAmount: BigInt(stored.cumulativeAmount),
79
+ deposit: BigInt(stored.deposit),
80
+ }
81
+ }
82
+
83
+ /** Prefix for serialized channel entries persisted by {@link createJsonChannelStore}. */
84
+ const channelPrefix = 'chan:'
85
+
86
+ /** Plain string key-value backend a {@link createJsonChannelStore} persists into. */
87
+ export type JsonChannelKv = {
88
+ /** Returns the value stored at `key`, when present. */
89
+ get(key: string): MaybePromise<string | undefined>
90
+ /** Persists a `value` at `key`. */
91
+ set(key: string, value: string): MaybePromise<void>
92
+ /** Removes the value stored at `key`. */
93
+ delete(key: string): MaybePromise<void>
94
+ }
95
+
96
+ /** Wraps a string KV backend as a bigint-safe channel store. */
97
+ export function createJsonChannelStore(kv: JsonChannelKv): ChannelStore {
98
+ return {
99
+ async get(key) {
100
+ const value = await kv.get(channelPrefix + key)
101
+ if (value === undefined) return undefined
102
+ return deserializeEntry(JSON.parse(value) as StoredChannel)
103
+ },
104
+ async set(entry) {
105
+ await kv.set(channelPrefix + entryKey(entry), JSON.stringify(serializeEntry(entry)))
106
+ },
107
+ async delete(key) {
108
+ await kv.delete(channelPrefix + key)
109
+ },
110
+ } satisfies ChannelStore
111
+ }
@@ -11,7 +11,15 @@ import type { SessionSnapshot } from '../Snapshot.js'
11
11
  import type { ChannelEntry } from './ChannelOps.js'
12
12
  import {
13
13
  channelKey,
14
- createChannelCache,
14
+ createChannelStore,
15
+ createJsonChannelStore,
16
+ deserializeEntry,
17
+ entryKey,
18
+ serializeEntry,
19
+ type ChannelSink,
20
+ } from './ChannelStore.js'
21
+ import {
22
+ canSignDescriptor,
15
23
  executeCredentialPlan,
16
24
  hasCredentialCumulativeAmount,
17
25
  hasManualSessionDescriptor,
@@ -26,12 +34,15 @@ import {
26
34
  resolveRecoverContext,
27
35
  resolveReusableChannel,
28
36
  sessionContextSchema,
29
- storeChannelEntry,
30
- updateCachedCumulative,
31
37
  type ChallengeContext,
32
38
  type SessionContext,
33
39
  } from './CredentialState.js'
34
40
 
41
+ /** Builds a credential sink backed by a fresh in-memory store. */
42
+ function sink(): ChannelSink {
43
+ return { store: createChannelStore(), notifyUpdate: () => {} }
44
+ }
45
+
35
46
  describe('ChannelCache', () => {
36
47
  const channelId = `0x${'11'.repeat(32)}` as Hex
37
48
 
@@ -66,16 +77,6 @@ describe('ChannelCache', () => {
66
77
  }
67
78
  }
68
79
 
69
- function close(cumulativeAmount: string): SessionCredentialPayload {
70
- return {
71
- action: 'close',
72
- channelId,
73
- descriptor: channel().descriptor,
74
- cumulativeAmount,
75
- signature: '0x1234',
76
- }
77
- }
78
-
79
80
  function topUp(additionalDeposit: string): SessionCredentialPayload {
80
81
  return {
81
82
  action: 'topUp',
@@ -87,41 +88,51 @@ describe('ChannelCache', () => {
87
88
  }
88
89
  }
89
90
 
90
- describe('precompile client ChannelCache', () => {
91
- test('creates stable case-insensitive reusable channel keys', () => {
91
+ describe('precompile client ChannelStore', () => {
92
+ test('creates stable case-insensitive reusable channel keys scoped by chain', () => {
92
93
  expect(
93
- channelKey(
94
- '0x00000000000000000000000000000000000000AA' as Address,
95
- '0x20C0000000000000000000000000000000000001' as Address,
96
- '0x4D50500000000000000000000000000000000000' as Address,
97
- ),
94
+ channelKey({
95
+ payee: '0x00000000000000000000000000000000000000AA' as Address,
96
+ token: '0x20C0000000000000000000000000000000000001' as Address,
97
+ escrow: '0x4D50500000000000000000000000000000000000' as Address,
98
+ chainId: 4217,
99
+ }),
98
100
  ).toBe(
99
- '0x00000000000000000000000000000000000000aa:0x20c0000000000000000000000000000000000001:0x4d50500000000000000000000000000000000000',
101
+ '0x00000000000000000000000000000000000000aa:0x20c0000000000000000000000000000000000001:0x4d50500000000000000000000000000000000000:4217',
102
+ )
103
+ })
104
+
105
+ test('derives a stored entry key from its descriptor, escrow, and chain', () => {
106
+ const entry = channel()
107
+ expect(entryKey(entry)).toBe(
108
+ channelKey({
109
+ payee: entry.descriptor.payee,
110
+ token: entry.descriptor.token,
111
+ escrow: entry.escrow,
112
+ chainId: entry.chainId,
113
+ }),
100
114
  )
101
115
  })
102
116
 
103
- test('stores entries by key and channel ID and notifies observers', () => {
104
- const updates: ChannelEntry[] = []
105
- const cache = createChannelCache((entry) => updates.push(entry))
117
+ test('stores, gets, and deletes entries by derived key', () => {
118
+ const store = createChannelStore()
106
119
  const entry = channel()
120
+ store.set(entry)
107
121
 
108
- storeChannelEntry(cache, 'payee:token:escrow', entry)
122
+ expect(store.get(entryKey(entry))).toBe(entry)
109
123
 
110
- expect(cache.channels.get('payee:token:escrow')).toBe(entry)
111
- expect(cache.channelIdToKey.get(channelId)).toBe('payee:token:escrow')
112
- expect(updates).toEqual([entry])
124
+ store.delete(entryKey(entry))
125
+ expect(store.get(entryKey(entry))).toBeUndefined()
113
126
  })
114
127
 
115
- test('updates cached cumulative amounts monotonically', () => {
116
- const cache = createChannelCache()
117
- const entry = channel({ cumulativeAmount: 10n })
118
- storeChannelEntry(cache, 'payee:token:escrow', entry)
119
-
120
- updateCachedCumulative(cache, channelId, voucher('8'))
121
- expect(entry.cumulativeAmount).toBe(10n)
128
+ test('replaces entries that share a scope key', () => {
129
+ const store = createChannelStore()
130
+ const first = channel({ cumulativeAmount: 10n })
131
+ const second = channel({ cumulativeAmount: 12n })
132
+ store.set(first)
133
+ store.set(second)
122
134
 
123
- updateCachedCumulative(cache, channelId, voucher('12'))
124
- expect(entry.cumulativeAmount).toBe(12n)
135
+ expect(store.get(entryKey(second))).toBe(second)
125
136
  })
126
137
 
127
138
  test('reads cumulative amounts only from cumulative credential payloads', () => {
@@ -130,26 +141,50 @@ describe('ChannelCache', () => {
130
141
  expect(hasCredentialCumulativeAmount(topUp('12'))).toBe(false)
131
142
  expect(readCredentialCumulativeAmount(topUp('12'))).toBeUndefined()
132
143
  })
144
+ })
133
145
 
134
- test('ignores non-cumulative top-up credentials when updating cached cumulative amount', () => {
135
- const cache = createChannelCache()
136
- const entry = channel({ cumulativeAmount: 10n })
137
- storeChannelEntry(cache, 'payee:token:escrow', entry)
138
-
139
- updateCachedCumulative(cache, channelId, topUp('12'))
146
+ describe('serialization', () => {
147
+ test('serializes bigint amounts to decimal strings', () => {
148
+ const entry = channel({ cumulativeAmount: 2n ** 70n, deposit: 0n })
149
+ const stored = serializeEntry(entry)
150
+ expect(stored.cumulativeAmount).toBe((2n ** 70n).toString())
151
+ expect(stored.deposit).toBe('0')
152
+ })
140
153
 
141
- expect(entry.cumulativeAmount).toBe(10n)
154
+ test('round-trips a channel entry through JSON', () => {
155
+ const entry = channel({ cumulativeAmount: 2n ** 100n + 7n, deposit: 999n })
156
+ const roundtrip = deserializeEntry(
157
+ JSON.parse(JSON.stringify(serializeEntry(entry))) as ReturnType<typeof serializeEntry>,
158
+ )
159
+ expect(roundtrip).toEqual(entry)
142
160
  })
161
+ })
143
162
 
144
- test('marks cached channels closed from close credentials', () => {
145
- const cache = createChannelCache()
146
- const entry = channel({ opened: true })
147
- storeChannelEntry(cache, 'payee:token:escrow', entry)
163
+ describe('createJsonChannelStore', () => {
164
+ function jsonStore() {
165
+ const backend = new Map<string, string>()
166
+ const store = createJsonChannelStore({
167
+ get: (key) => backend.get(key),
168
+ set: (key, value) => {
169
+ backend.set(key, value)
170
+ },
171
+ delete: (key) => {
172
+ backend.delete(key)
173
+ },
174
+ })
175
+ return { backend, store }
176
+ }
148
177
 
149
- updateCachedCumulative(cache, channelId, close('12'))
178
+ test('persists, gets, and deletes via a string KV backend', async () => {
179
+ const { backend, store } = jsonStore()
180
+ const entry = channel({ cumulativeAmount: 2n ** 64n, deposit: 5n })
181
+ await store.set(entry)
150
182
 
151
- expect(entry.cumulativeAmount).toBe(12n)
152
- expect(entry.opened).toBe(false)
183
+ expect(backend.size).toBe(1)
184
+ expect(await store.get(entryKey(entry))).toEqual(entry)
185
+
186
+ await store.delete(entryKey(entry))
187
+ expect(await store.get(entryKey(entry))).toBeUndefined()
153
188
  })
154
189
  })
155
190
  })
@@ -346,7 +381,7 @@ describe('CredentialPlan', () => {
346
381
  token,
347
382
  })
348
383
  expect(resolved.key).toBe(
349
- `${payee.toLowerCase()}:${token.toLowerCase()}:${escrow.toLowerCase()}`,
384
+ `${payee.toLowerCase()}:${token.toLowerCase()}:${escrow.toLowerCase()}:42431`,
350
385
  )
351
386
  })
352
387
 
@@ -425,10 +460,9 @@ describe('CredentialPlan', () => {
425
460
  })
426
461
 
427
462
  test('plans manual credentials only when an explicit action includes descriptor', () => {
428
- const cache = createChannelCache()
429
463
  const plan = planCredential({
430
464
  account,
431
- cache,
465
+ entry: undefined,
432
466
  context: { action: 'voucher', descriptor, cumulativeAmountRaw: '10' },
433
467
  decimals: 6,
434
468
  resolved: challengeContext(),
@@ -443,7 +477,7 @@ describe('CredentialPlan', () => {
443
477
  expect(() =>
444
478
  planCredential({
445
479
  account,
446
- cache: createChannelCache(),
480
+ entry: undefined,
447
481
  context: { action: 'voucher', cumulativeAmountRaw: '10' },
448
482
  decimals: 6,
449
483
  resolved: challengeContext(),
@@ -454,7 +488,7 @@ describe('CredentialPlan', () => {
454
488
  test('rejects manual descriptors that do not match the active challenge', async () => {
455
489
  const plan = planCredential({
456
490
  account,
457
- cache: createChannelCache(),
491
+ entry: undefined,
458
492
  context: {
459
493
  action: 'voucher',
460
494
  cumulativeAmountRaw: '10',
@@ -467,15 +501,44 @@ describe('CredentialPlan', () => {
467
501
  resolved: challengeContext(),
468
502
  })
469
503
 
470
- await expect(executeCredentialPlan(plan, createChannelCache())).rejects.toThrow(
504
+ await expect(executeCredentialPlan(plan, sink())).rejects.toThrow(
471
505
  'context descriptor payee does not match challenge',
472
506
  )
473
507
  })
474
508
 
509
+ test('leaves stored scope entry unchanged when a manual credential targets another channel', async () => {
510
+ const entry = channel()
511
+ const originalCumulative = entry.cumulativeAmount
512
+ const notifications: ChannelEntry[] = []
513
+ const store = createChannelStore()
514
+ const manualDescriptor = { ...descriptor, salt: `0x${'55'.repeat(32)}` as Hex }
515
+ await store.set(entry)
516
+
517
+ await executeCredentialPlan(
518
+ planCredential({
519
+ account,
520
+ entry,
521
+ context: {
522
+ action: 'voucher',
523
+ descriptor: manualDescriptor,
524
+ cumulativeAmountRaw: '25',
525
+ },
526
+ decimals: 6,
527
+ resolved: challengeContext(),
528
+ }),
529
+ { store, notifyUpdate: (updated) => notifications.push(updated) },
530
+ )
531
+
532
+ const stored = await store.get(entryKey(entry))
533
+ expect(stored?.channelId).toBe(entry.channelId)
534
+ expect(stored?.cumulativeAmount).toBe(originalCumulative)
535
+ expect(notifications).toEqual([])
536
+ })
537
+
475
538
  test('plans recovery from server snapshot when no reusable cache entry exists', () => {
476
539
  const plan = planCredential({
477
540
  account,
478
- cache: createChannelCache(),
541
+ entry: undefined,
479
542
  decimals: 6,
480
543
  resolved: challengeContext({ snapshot: snapshot() }),
481
544
  })
@@ -487,13 +550,11 @@ describe('CredentialPlan', () => {
487
550
  })
488
551
 
489
552
  test('plans voucher reuse before snapshot recovery when cache entry is open', () => {
490
- const cache = createChannelCache()
491
553
  const entry = channel()
492
- storeChannelEntry(cache, 'payee:token:escrow', entry)
493
554
 
494
555
  const plan = planCredential({
495
556
  account,
496
- cache,
557
+ entry,
497
558
  decimals: 6,
498
559
  resolved: challengeContext({ snapshot: snapshot() }),
499
560
  })
@@ -501,11 +562,94 @@ describe('CredentialPlan', () => {
501
562
  expect(plan).toMatchObject({ type: 'voucher', entry })
502
563
  })
503
564
 
565
+ test('opens fresh instead of vouchering when the account cannot sign the cached entry', () => {
566
+ const entry = channel({
567
+ descriptor: {
568
+ ...descriptor,
569
+ authorizedSigner: '0x00000000000000000000000000000000000000aa' as Address,
570
+ },
571
+ })
572
+
573
+ const plan = planCredential({
574
+ account,
575
+ entry,
576
+ decimals: 6,
577
+ resolved: challengeContext(),
578
+ })
579
+
580
+ expect(plan.type).toBe('open')
581
+ })
582
+
583
+ test('opens fresh instead of recovering when the account cannot sign the snapshot', () => {
584
+ const plan = planCredential({
585
+ account,
586
+ entry: undefined,
587
+ decimals: 6,
588
+ resolved: challengeContext({
589
+ snapshot: snapshot({
590
+ descriptor: {
591
+ ...snapshotDescriptor,
592
+ authorizedSigner: '0x00000000000000000000000000000000000000aa' as Address,
593
+ },
594
+ }),
595
+ }),
596
+ })
597
+
598
+ expect(plan.type).toBe('open')
599
+ })
600
+
601
+ test('vouchers when the account can satisfy the cached voucher authority', () => {
602
+ const delegatedAccount = privateKeyToAccount(
603
+ '0x2000000000000000000000000000000000000000000000000000000000000000',
604
+ )
605
+ const entry = channel({
606
+ descriptor: { ...descriptor, authorizedSigner: delegatedAccount.address },
607
+ })
608
+
609
+ const plan = planCredential({
610
+ account: Object.assign({}, account, { accessKeyAddress: delegatedAccount.address }),
611
+ entry,
612
+ decimals: 6,
613
+ resolved: challengeContext(),
614
+ })
615
+
616
+ expect(plan).toMatchObject({ type: 'voucher', entry })
617
+ })
618
+
619
+ test('canSignDescriptor matches root, zero, and delegated authorities', () => {
620
+ const delegatedAuthority = '0x00000000000000000000000000000000000000aa' as Address
621
+ expect(canSignDescriptor(account, descriptor)).toBe(true)
622
+ expect(
623
+ canSignDescriptor(account, {
624
+ ...descriptor,
625
+ authorizedSigner: '0x0000000000000000000000000000000000000000' as Address,
626
+ }),
627
+ ).toBe(true)
628
+ expect(
629
+ canSignDescriptor(account, { ...descriptor, authorizedSigner: delegatedAuthority }),
630
+ ).toBe(false)
631
+ expect(
632
+ canSignDescriptor(Object.assign({}, account, { accessKeyAddress: delegatedAuthority }), {
633
+ ...descriptor,
634
+ authorizedSigner: delegatedAuthority,
635
+ }),
636
+ ).toBe(true)
637
+ const otherPayer = '0x00000000000000000000000000000000000000bb' as Address
638
+ expect(canSignDescriptor(account, { ...descriptor, payer: otherPayer })).toBe(false)
639
+ expect(
640
+ canSignDescriptor(Object.assign({}, account, { accessKeyAddress: delegatedAuthority }), {
641
+ ...descriptor,
642
+ payer: otherPayer,
643
+ authorizedSigner: delegatedAuthority,
644
+ }),
645
+ ).toBe(false)
646
+ })
647
+
504
648
  test('rejects channel ID reuse without descriptor or cache entry', () => {
505
649
  expect(() =>
506
650
  planCredential({
507
651
  account,
508
- cache: createChannelCache(),
652
+ entry: undefined,
509
653
  context: { channelId },
510
654
  decimals: 6,
511
655
  resolved: challengeContext(),