mppx 0.7.0 → 0.8.1

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 (290) hide show
  1. package/CHANGELOG.md +41 -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/cli/internal.d.ts +1 -0
  13. package/dist/cli/internal.d.ts.map +1 -1
  14. package/dist/cli/internal.js +1 -15
  15. package/dist/cli/internal.js.map +1 -1
  16. package/dist/client/Mppx.js +2 -2
  17. package/dist/client/Mppx.js.map +1 -1
  18. package/dist/client/Transport.d.ts +11 -16
  19. package/dist/client/Transport.d.ts.map +1 -1
  20. package/dist/client/Transport.js +55 -75
  21. package/dist/client/Transport.js.map +1 -1
  22. package/dist/client/index.d.ts +3 -0
  23. package/dist/client/index.d.ts.map +1 -1
  24. package/dist/client/index.js +1 -0
  25. package/dist/client/index.js.map +1 -1
  26. package/dist/client/internal/Fetch.d.ts.map +1 -1
  27. package/dist/client/internal/Fetch.js +46 -7
  28. package/dist/client/internal/Fetch.js.map +1 -1
  29. package/dist/client/internal/protocols/Mcp.d.ts +7 -0
  30. package/dist/client/internal/protocols/Mcp.d.ts.map +1 -0
  31. package/dist/client/internal/protocols/Mcp.js +159 -0
  32. package/dist/client/internal/protocols/Mcp.js.map +1 -0
  33. package/dist/client/internal/protocols/Mpp.d.ts +4 -0
  34. package/dist/client/internal/protocols/Mpp.d.ts.map +1 -0
  35. package/dist/client/internal/protocols/Mpp.js +18 -0
  36. package/dist/client/internal/protocols/Mpp.js.map +1 -0
  37. package/dist/client/internal/protocols/Protocol.d.ts +10 -0
  38. package/dist/client/internal/protocols/Protocol.d.ts.map +1 -0
  39. package/dist/client/internal/protocols/Protocol.js +2 -0
  40. package/dist/client/internal/protocols/Protocol.js.map +1 -0
  41. package/dist/client/internal/protocols/Shared.d.ts +5 -0
  42. package/dist/client/internal/protocols/Shared.d.ts.map +1 -0
  43. package/dist/client/internal/protocols/Shared.js +20 -0
  44. package/dist/client/internal/protocols/Shared.js.map +1 -0
  45. package/dist/client/internal/protocols/X402.d.ts +8 -0
  46. package/dist/client/internal/protocols/X402.d.ts.map +1 -0
  47. package/dist/client/internal/protocols/X402.js +39 -0
  48. package/dist/client/internal/protocols/X402.js.map +1 -0
  49. package/dist/evm/client/index.d.ts +1 -0
  50. package/dist/evm/client/index.d.ts.map +1 -1
  51. package/dist/evm/client/index.js +1 -0
  52. package/dist/evm/client/index.js.map +1 -1
  53. package/dist/evm/index.d.ts +2 -0
  54. package/dist/evm/index.d.ts.map +1 -1
  55. package/dist/evm/index.js +2 -0
  56. package/dist/evm/index.js.map +1 -1
  57. package/dist/evm/server/index.d.ts +1 -0
  58. package/dist/evm/server/index.d.ts.map +1 -1
  59. package/dist/evm/server/index.js +1 -0
  60. package/dist/evm/server/index.js.map +1 -1
  61. package/dist/mcp/client/McpClient.d.ts +101 -0
  62. package/dist/mcp/client/McpClient.d.ts.map +1 -0
  63. package/dist/mcp/client/McpClient.js +162 -0
  64. package/dist/mcp/client/McpClient.js.map +1 -0
  65. package/dist/mcp/client/index.d.ts.map +1 -0
  66. package/dist/mcp/client/index.js.map +1 -0
  67. package/dist/mcp/server/Transport.d.ts.map +1 -0
  68. package/dist/mcp/server/Transport.js.map +1 -0
  69. package/dist/mcp/server/index.d.ts.map +1 -0
  70. package/dist/mcp/server/index.js.map +1 -0
  71. package/dist/server/Mppx.d.ts +1 -1
  72. package/dist/server/Mppx.d.ts.map +1 -1
  73. package/dist/server/Mppx.js +9 -0
  74. package/dist/server/Mppx.js.map +1 -1
  75. package/dist/server/Transport.d.ts +1 -1
  76. package/dist/server/Transport.d.ts.map +1 -1
  77. package/dist/server/Transport.js +1 -1
  78. package/dist/server/Transport.js.map +1 -1
  79. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  80. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  81. package/dist/stripe/server/internal/html.gen.js +1 -1
  82. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  83. package/dist/tempo/Proof.d.ts +85 -1
  84. package/dist/tempo/Proof.d.ts.map +1 -1
  85. package/dist/tempo/Proof.js +35 -0
  86. package/dist/tempo/Proof.js.map +1 -1
  87. package/dist/tempo/client/Charge.d.ts +13 -1
  88. package/dist/tempo/client/Charge.d.ts.map +1 -1
  89. package/dist/tempo/client/Charge.js +38 -25
  90. package/dist/tempo/client/Charge.js.map +1 -1
  91. package/dist/tempo/client/Methods.d.ts +5 -3
  92. package/dist/tempo/client/Methods.d.ts.map +1 -1
  93. package/dist/tempo/client/Methods.js +4 -2
  94. package/dist/tempo/client/Methods.js.map +1 -1
  95. package/dist/tempo/client/ResolveAccount.d.ts +40 -0
  96. package/dist/tempo/client/ResolveAccount.d.ts.map +1 -0
  97. package/dist/tempo/client/ResolveAccount.js +2 -0
  98. package/dist/tempo/client/ResolveAccount.js.map +1 -0
  99. package/dist/tempo/internal/fee-payer.d.ts +26 -1
  100. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  101. package/dist/tempo/internal/fee-payer.js +83 -30
  102. package/dist/tempo/internal/fee-payer.js.map +1 -1
  103. package/dist/tempo/internal/proof.d.ts +71 -5
  104. package/dist/tempo/internal/proof.d.ts.map +1 -1
  105. package/dist/tempo/internal/proof.js +42 -6
  106. package/dist/tempo/internal/proof.js.map +1 -1
  107. package/dist/tempo/legacy/client/SessionManager.d.ts.map +1 -1
  108. package/dist/tempo/legacy/client/SessionManager.js +10 -3
  109. package/dist/tempo/legacy/client/SessionManager.js.map +1 -1
  110. package/dist/tempo/server/Charge.d.ts.map +1 -1
  111. package/dist/tempo/server/Charge.js +46 -18
  112. package/dist/tempo/server/Charge.js.map +1 -1
  113. package/dist/tempo/server/Methods.d.ts +4 -2
  114. package/dist/tempo/server/Methods.d.ts.map +1 -1
  115. package/dist/tempo/server/Methods.js +4 -2
  116. package/dist/tempo/server/Methods.js.map +1 -1
  117. package/dist/tempo/server/Subscription.d.ts +10 -0
  118. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  119. package/dist/tempo/server/Subscription.js +135 -23
  120. package/dist/tempo/server/Subscription.js.map +1 -1
  121. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  122. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  123. package/dist/tempo/server/internal/html.gen.js +1 -1
  124. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  125. package/dist/tempo/session/client/ChannelOps.d.ts +2 -3
  126. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  127. package/dist/tempo/session/client/ChannelOps.js +7 -10
  128. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  129. package/dist/tempo/session/client/ChannelStore.d.ts +51 -0
  130. package/dist/tempo/session/client/ChannelStore.d.ts.map +1 -0
  131. package/dist/tempo/session/client/ChannelStore.js +63 -0
  132. package/dist/tempo/session/client/ChannelStore.js.map +1 -0
  133. package/dist/tempo/session/client/CredentialState.d.ts +7 -24
  134. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  135. package/dist/tempo/session/client/CredentialState.js +51 -49
  136. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  137. package/dist/tempo/session/client/Session.d.ts +8 -2
  138. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  139. package/dist/tempo/session/client/Session.js +22 -8
  140. package/dist/tempo/session/client/Session.js.map +1 -1
  141. package/dist/tempo/session/client/SessionManager.d.ts +4 -40
  142. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  143. package/dist/tempo/session/client/SessionManager.js +124 -174
  144. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  145. package/dist/tempo/session/client/index.d.ts +3 -4
  146. package/dist/tempo/session/client/index.d.ts.map +1 -1
  147. package/dist/tempo/session/client/index.js +1 -0
  148. package/dist/tempo/session/client/index.js.map +1 -1
  149. package/dist/tempo/session/precompile/Voucher.d.ts +3 -3
  150. package/dist/tempo/session/precompile/Voucher.d.ts.map +1 -1
  151. package/dist/tempo/session/precompile/Voucher.js +24 -25
  152. package/dist/tempo/session/precompile/Voucher.js.map +1 -1
  153. package/dist/tempo/session/server/CredentialVerification.d.ts +6 -0
  154. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  155. package/dist/tempo/session/server/CredentialVerification.js +13 -0
  156. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  157. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  158. package/dist/tempo/session/server/Settlement.js +4 -2
  159. package/dist/tempo/session/server/Settlement.js.map +1 -1
  160. package/dist/tempo/session/server/Sse.d.ts.map +1 -1
  161. package/dist/tempo/session/server/Sse.js.map +1 -1
  162. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  163. package/dist/tempo/session/server/Ws.js.map +1 -1
  164. package/dist/tempo/subscription/KeyAuthorization.d.ts +712 -1
  165. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  166. package/dist/tempo/subscription/Store.d.ts +2 -0
  167. package/dist/tempo/subscription/Store.d.ts.map +1 -1
  168. package/dist/tempo/subscription/Store.js +16 -1
  169. package/dist/tempo/subscription/Store.js.map +1 -1
  170. package/dist/x402/index.d.ts +1 -0
  171. package/dist/x402/index.d.ts.map +1 -1
  172. package/dist/x402/index.js +1 -0
  173. package/dist/x402/index.js.map +1 -1
  174. package/package.json +21 -10
  175. package/src/Challenge.test.ts +40 -0
  176. package/src/Challenge.ts +19 -6
  177. package/src/Mcp.ts +4 -0
  178. package/src/PaymentRequest.ts +10 -10
  179. package/src/cli/cli.test.ts +15 -15
  180. package/src/cli/config.test.ts +13 -7
  181. package/src/cli/internal.ts +1 -16
  182. package/src/client/Mppx.test-d.ts +21 -1
  183. package/src/client/Mppx.test.ts +1 -1
  184. package/src/client/Mppx.ts +2 -2
  185. package/src/client/Transport.test.ts +225 -178
  186. package/src/client/Transport.ts +77 -83
  187. package/src/client/index.ts +14 -0
  188. package/src/client/internal/Fetch.test.ts +207 -2
  189. package/src/client/internal/Fetch.ts +52 -6
  190. package/src/client/internal/protocols/Mcp.test.ts +220 -0
  191. package/src/client/internal/protocols/Mcp.ts +162 -0
  192. package/src/client/internal/protocols/Mpp.ts +21 -0
  193. package/src/client/internal/protocols/Protocol.ts +10 -0
  194. package/src/client/internal/protocols/Shared.ts +25 -0
  195. package/src/client/internal/protocols/X402.ts +42 -0
  196. package/src/discovery/OpenApi.test.ts +1 -1
  197. package/src/evm/PublicInterface.test-d.ts +1 -1
  198. package/src/evm/client/index.ts +1 -0
  199. package/src/evm/index.ts +2 -0
  200. package/src/evm/server/Charge.test.ts +1 -1
  201. package/src/evm/server/index.ts +1 -0
  202. package/src/{mcp-sdk → mcp}/client/McpClient.integration.test.ts +10 -4
  203. package/src/{mcp-sdk → mcp}/client/McpClient.test-d.ts +45 -18
  204. package/src/{mcp-sdk → mcp}/client/McpClient.test.ts +211 -5
  205. package/src/mcp/client/McpClient.ts +307 -0
  206. package/src/{mcp-sdk → mcp}/client/McpClient.unit.test.ts +9 -5
  207. package/src/middlewares/elysia.test.ts +1 -1
  208. package/src/middlewares/express.test.ts +1 -1
  209. package/src/middlewares/hono.test.ts +1 -1
  210. package/src/middlewares/internal/mppx.test.ts +1 -1
  211. package/src/middlewares/nextjs.test.ts +1 -1
  212. package/src/proxy/Proxy.test.ts +1 -1
  213. package/src/proxy/services/anthropic.test.ts +1 -1
  214. package/src/proxy/services/openai.test.ts +1 -1
  215. package/src/proxy/services/stripe.test.ts +1 -1
  216. package/src/server/Mppx.authorize.test.ts +1 -1
  217. package/src/server/Mppx.test-d.ts +1 -1
  218. package/src/server/Mppx.test.ts +20 -2
  219. package/src/server/Mppx.ts +14 -1
  220. package/src/server/Transport.test.ts +6 -6
  221. package/src/server/Transport.ts +1 -1
  222. package/src/stripe/Charge.integration.test.ts +1 -1
  223. package/src/stripe/client/Charge.test.ts +1 -1
  224. package/src/stripe/server/Charge.test.ts +1 -1
  225. package/src/stripe/server/internal/html/package.json +1 -1
  226. package/src/stripe/server/internal/html.gen.ts +1 -1
  227. package/src/tempo/Proof.conformance.test.ts +146 -0
  228. package/src/tempo/Proof.test-d.ts +15 -0
  229. package/src/tempo/Proof.ts +52 -1
  230. package/src/tempo/Subscription.integration.test.ts +1 -1
  231. package/src/tempo/client/Charge.test.ts +173 -0
  232. package/src/tempo/client/Charge.ts +65 -36
  233. package/src/tempo/client/Methods.ts +4 -2
  234. package/src/tempo/client/ResolveAccount.ts +46 -0
  235. package/src/tempo/internal/fee-payer.test.ts +89 -10
  236. package/src/tempo/internal/fee-payer.ts +128 -41
  237. package/src/tempo/internal/proof.test.ts +12 -4
  238. package/src/tempo/internal/proof.ts +55 -6
  239. package/src/tempo/legacy/client/SessionManager.ts +11 -3
  240. package/src/tempo/legacy/server/Session.test.ts +91 -26
  241. package/src/tempo/server/Charge.test.ts +388 -17
  242. package/src/tempo/server/Charge.ts +50 -24
  243. package/src/tempo/server/Methods.ts +4 -2
  244. package/src/tempo/server/Subscription.test.ts +465 -3
  245. package/src/tempo/server/Subscription.ts +174 -19
  246. package/src/tempo/server/internal/html/package.json +2 -2
  247. package/src/tempo/server/internal/html.gen.ts +1 -1
  248. package/src/tempo/session/client/ChannelOps.ts +5 -19
  249. package/src/tempo/session/client/ChannelStore.ts +111 -0
  250. package/src/tempo/session/client/CredentialState.test.ts +206 -62
  251. package/src/tempo/session/client/CredentialState.ts +58 -73
  252. package/src/tempo/session/client/Session.test.ts +41 -1
  253. package/src/tempo/session/client/Session.ts +36 -10
  254. package/src/tempo/session/client/SessionManager.test.ts +154 -65
  255. package/src/tempo/session/client/SessionManager.ts +141 -235
  256. package/src/tempo/session/client/index.ts +8 -5
  257. package/src/tempo/session/precompile/Voucher.test.ts +45 -7
  258. package/src/tempo/session/precompile/Voucher.ts +27 -25
  259. package/src/tempo/session/server/CredentialVerification.test.ts +36 -0
  260. package/src/tempo/session/server/CredentialVerification.ts +18 -0
  261. package/src/tempo/session/server/Session.test.ts +4 -4
  262. package/src/tempo/session/server/Settlement.test.ts +88 -1
  263. package/src/tempo/session/server/Settlement.ts +2 -1
  264. package/src/tempo/session/server/Sse.ts +0 -2
  265. package/src/tempo/session/server/Ws.ts +0 -4
  266. package/src/tempo/subscription/Store.ts +27 -9
  267. package/src/x402/Exact.e2e.test.ts +1 -1
  268. package/src/x402/PublicInterface.test-d.ts +1 -1
  269. package/src/x402/index.ts +1 -0
  270. package/dist/mcp-sdk/client/McpClient.d.ts +0 -85
  271. package/dist/mcp-sdk/client/McpClient.d.ts.map +0 -1
  272. package/dist/mcp-sdk/client/McpClient.js +0 -118
  273. package/dist/mcp-sdk/client/McpClient.js.map +0 -1
  274. package/dist/mcp-sdk/client/index.d.ts.map +0 -1
  275. package/dist/mcp-sdk/client/index.js.map +0 -1
  276. package/dist/mcp-sdk/server/Transport.d.ts.map +0 -1
  277. package/dist/mcp-sdk/server/Transport.js.map +0 -1
  278. package/dist/mcp-sdk/server/index.d.ts.map +0 -1
  279. package/dist/mcp-sdk/server/index.js.map +0 -1
  280. package/src/mcp-sdk/client/McpClient.ts +0 -228
  281. /package/dist/{mcp-sdk → mcp}/client/index.d.ts +0 -0
  282. /package/dist/{mcp-sdk → mcp}/client/index.js +0 -0
  283. /package/dist/{mcp-sdk → mcp}/server/Transport.d.ts +0 -0
  284. /package/dist/{mcp-sdk → mcp}/server/Transport.js +0 -0
  285. /package/dist/{mcp-sdk → mcp}/server/index.d.ts +0 -0
  286. /package/dist/{mcp-sdk → mcp}/server/index.js +0 -0
  287. /package/src/{mcp-sdk → mcp}/client/index.ts +0 -0
  288. /package/src/{mcp-sdk → mcp}/server/Transport.test.ts +0 -0
  289. /package/src/{mcp-sdk → mcp}/server/Transport.ts +0 -0
  290. /package/src/{mcp-sdk → mcp}/server/index.ts +0 -0
@@ -5,6 +5,8 @@ import { describe, expect, test, vi } from 'vp/test'
5
5
  import * as Challenge from '../../../Challenge.js'
6
6
  import * as Constants from '../../../Constants.js'
7
7
  import * as Credential from '../../../Credential.js'
8
+ import type { ChannelEntry } from '../client/ChannelOps.js'
9
+ import { createJsonChannelStore, entryKey, type ChannelStore } from '../client/ChannelStore.js'
8
10
  import * as Channel from '../precompile/Channel.js'
9
11
  import { escrowAbi } from '../precompile/escrow.abi.js'
10
12
  import { tip20ChannelEscrow } from '../precompile/Protocol.js'
@@ -13,7 +15,6 @@ import type { NeedVoucherEvent, SessionReceipt } from '../precompile/Protocol.js
13
15
  import { formatNeedVoucherEvent, parseEvent } from '../precompile/Protocol.js'
14
16
  import type { SessionCredentialPayload } from '../precompile/Protocol.js'
15
17
  import { computeFallbackCloseAmount, sessionManager } from './SessionManager.js'
16
- import type { StoredSessionChannel } from './SessionManager.js'
17
18
 
18
19
  const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
19
20
  const challengeId = 'test-challenge-1'
@@ -59,20 +60,40 @@ const storedChannelId = Channel.computeId({
59
60
  escrow: tip20ChannelEscrow,
60
61
  })
61
62
 
62
- function storedChannel(overrides: Partial<StoredSessionChannel> = {}): StoredSessionChannel {
63
+ function channelEntry(overrides: Partial<ChannelEntry> = {}): ChannelEntry {
63
64
  return {
64
65
  channelId: storedChannelId,
65
- cumulativeAmount: '1000000',
66
- deposit: '10000000',
66
+ cumulativeAmount: 1_000_000n,
67
+ deposit: 10_000_000n,
67
68
  descriptor: storedDescriptor,
68
69
  escrow: tip20ChannelEscrow,
69
70
  chainId: 4217,
70
71
  opened: true,
71
- updatedAt: 0,
72
72
  ...overrides,
73
73
  }
74
74
  }
75
75
 
76
+ /**
77
+ * In-memory {@link ChannelStore} with spied `set`/`delete`, optionally seeded.
78
+ * Seeded entries live in the entry index, so the plugin resumes them from
79
+ * `store.get(resolved.key)` after a 402 (there is no first-request hint).
80
+ */
81
+ function makeChannelStore(seed: readonly ChannelEntry[] = []) {
82
+ const map = new Map<string, ChannelEntry>(seed.map((entry) => [entryKey(entry), entry]))
83
+ const set = vi.fn((entry: ChannelEntry) => {
84
+ map.set(entryKey(entry), entry)
85
+ })
86
+ const remove = vi.fn((key: string) => {
87
+ map.delete(key)
88
+ })
89
+ const store: ChannelStore = {
90
+ get: (key) => map.get(key),
91
+ set,
92
+ delete: remove,
93
+ }
94
+ return { store, set, delete: remove, map }
95
+ }
96
+
76
97
  function makeChallenge(overrides: Record<string, unknown> = {}): Challenge.Challenge {
77
98
  return Challenge.from({
78
99
  id: challengeId,
@@ -234,6 +255,33 @@ describe('Session', () => {
234
255
  expect(mockFetch).toHaveBeenCalledOnce()
235
256
  })
236
257
 
258
+ test('rejects a concurrent request while one is in flight', async () => {
259
+ let release!: () => void
260
+ const gate = new Promise<void>((resolve) => {
261
+ release = resolve
262
+ })
263
+ const mockFetch = vi.fn().mockImplementation(async () => {
264
+ await gate
265
+ return makeOkResponse('hello')
266
+ })
267
+
268
+ const s = sessionManager({
269
+ account: '0x0000000000000000000000000000000000000001',
270
+ fetch: mockFetch as typeof globalThis.fetch,
271
+ })
272
+
273
+ const first = s.fetch('https://api.example.com/data')
274
+ await expect(s.fetch('https://api.example.com/data')).rejects.toThrow(
275
+ 'concurrent requests on one manager are not supported',
276
+ )
277
+
278
+ release()
279
+ expect((await first).status).toBe(200)
280
+
281
+ // The guard clears after the in-flight request settles.
282
+ expect((await s.fetch('https://api.example.com/data')).status).toBe(200)
283
+ })
284
+
237
285
  test('binds the default global fetch for browser runtimes', async () => {
238
286
  const originalFetch = globalThis.fetch
239
287
  const mockFetch = vi.fn(function (this: unknown) {
@@ -255,28 +303,35 @@ describe('Session', () => {
255
303
  }
256
304
  })
257
305
 
258
- test('adds a stored channel hint to the first request', async () => {
306
+ test('resumes a seeded channel after a 402 without a first-request hint', async () => {
307
+ const posted: SessionCredentialPayload[] = []
259
308
  const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
260
- expect(new Headers(init?.headers).get('Payment-Session')).toBe(storedChannelId)
309
+ const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
310
+ const payload = authorization
311
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
312
+ : undefined
313
+ if (payload) posted.push(payload)
314
+ if (!payload) return Promise.resolve(make402Response())
261
315
  return Promise.resolve(makeOkResponse())
262
316
  })
263
317
  const s = sessionManager({
264
318
  account,
265
319
  client,
266
320
  fetch: mockFetch as typeof globalThis.fetch,
267
- sessionStore: {
268
- get: () => storedChannel(),
269
- set: vi.fn(),
270
- },
321
+ channelStore: makeChannelStore([channelEntry()]).store,
271
322
  })
272
323
 
273
324
  await s.fetch('https://api.example.com/data')
274
325
 
275
- expect(mockFetch).toHaveBeenCalledOnce()
326
+ // No persisted hint: the first request carries no Payment-Session header,
327
+ // and the channel is resumed from the entry index with a voucher.
328
+ expect(new Headers(mockFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
329
+ expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
276
330
  })
277
331
 
278
- test('stores same-route HEAD snapshot as a first-request channel hint', async () => {
279
- const set = vi.fn()
332
+ test('seeds a same-route HEAD snapshot into the entry index and resumes it', async () => {
333
+ const { store, set } = makeChannelStore()
334
+ const posted: SessionCredentialPayload[] = []
280
335
  const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
281
336
  const headers = new Headers(init?.headers)
282
337
  if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization)) {
@@ -306,7 +361,14 @@ describe('Session', () => {
306
361
  }),
307
362
  )
308
363
  }
309
- expect(headers.get(Constants.Headers.paymentSession)).toBe(storedChannelId)
364
+ // The seeded snapshot is not sent as a first-request hint; the content
365
+ // request gets a 402 and resumes from the entry index with a voucher.
366
+ const authorization = headers.get(Constants.Headers.authorization)
367
+ const payload = authorization
368
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
369
+ : undefined
370
+ if (payload) posted.push(payload)
371
+ if (!payload) return Promise.resolve(make402Response())
310
372
  return Promise.resolve(makeOkResponse())
311
373
  })
312
374
  const s = sessionManager({
@@ -314,20 +376,16 @@ describe('Session', () => {
314
376
  bootstrap: true,
315
377
  client,
316
378
  fetch: mockFetch as typeof globalThis.fetch,
317
- sessionStore: {
318
- get: () => null,
319
- set,
320
- },
379
+ channelStore: store,
321
380
  })
322
381
 
323
382
  const response = await s.fetch('https://api.example.com/data')
324
383
 
325
384
  expect(response.status).toBe(200)
326
- expect(response.channelId).toBeNull()
327
- expect(s.channelId).toBeUndefined()
328
- expect(s.cumulative).toBe(0n)
329
385
  expect(set).toHaveBeenCalledWith(expect.objectContaining({ channelId: storedChannelId }))
330
- expect(mockFetch).toHaveBeenCalledTimes(3)
386
+ expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
387
+ const contentCall = mockFetch.mock.calls.find((call) => call[1]?.method !== 'HEAD')
388
+ expect(new Headers(contentCall?.[1]?.headers).get('Payment-Session')).toBeNull()
331
389
  })
332
390
 
333
391
  test('does not answer non-zero bootstrap charge challenges', async () => {
@@ -374,28 +432,8 @@ describe('Session', () => {
374
432
  expect(new Headers(mockFetch.mock.calls[1]?.[1]?.headers).get('Payment-Session')).toBeNull()
375
433
  })
376
434
 
377
- test('clears stale stored channel hints and retries with a fresh channel', async () => {
378
- const remove = vi.fn()
379
- const staleClient = createClient({
380
- account,
381
- chain: { id: 4217 } as never,
382
- transport: custom({
383
- async request(args) {
384
- if (args.method === 'eth_chainId') return '0x1079'
385
- if (args.method === 'eth_getTransactionCount') return '0x0'
386
- if (args.method === 'eth_estimateGas') return '0x5208'
387
- if (args.method === 'eth_maxPriorityFeePerGas') return '0x1'
388
- if (args.method === 'eth_getBlockByNumber') return { baseFeePerGas: '0x1' }
389
- if (args.method === 'eth_call')
390
- return encodeFunctionResult({
391
- abi: escrowAbi,
392
- functionName: 'getChannelState',
393
- result: { settled: 0n, deposit: 0n, closeRequestedAt: 0 },
394
- })
395
- throw new Error(`unexpected rpc request: ${args.method}`)
396
- },
397
- }),
398
- })
435
+ test('drops a stale stored channel the server rejects and retries with a fresh one', async () => {
436
+ const { store, delete: remove } = makeChannelStore([channelEntry()])
399
437
  const postedPayloads: SessionCredentialPayload[] = []
400
438
  const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
401
439
  const headers = new Headers(init?.headers)
@@ -405,28 +443,25 @@ describe('Session', () => {
405
443
  : undefined
406
444
  if (payload) postedPayloads.push(payload)
407
445
 
408
- if (init?.method === 'HEAD') return Promise.resolve(new Response(null, { status: 204 }))
409
446
  if (!payload) return Promise.resolve(make402Response())
447
+ // Reject any reuse of the stale stored channel; accept a freshly opened one.
448
+ if (payload.channelId === storedChannelId)
449
+ return Promise.resolve(new Response('gone', { status: 500 }))
410
450
  return Promise.resolve(makeOkResponse())
411
451
  })
412
452
  const s = sessionManager({
413
453
  account,
414
- bootstrap: true,
415
- client: staleClient,
454
+ client,
416
455
  fetch: mockFetch as typeof globalThis.fetch,
417
456
  maxDeposit: '10',
418
- sessionStore: {
419
- get: () => storedChannel(),
420
- set: vi.fn(),
421
- delete: remove,
422
- },
457
+ channelStore: store,
423
458
  })
424
459
 
425
460
  const response = await s.fetch('https://api.example.com/data')
426
461
 
427
462
  expect(response.status).toBe(200)
428
463
  expect(remove).toHaveBeenCalledOnce()
429
- expect(postedPayloads.map((payload) => payload.action)).toEqual(['open'])
464
+ expect(postedPayloads.map((payload) => payload.action)).toEqual(['voucher', 'open'])
430
465
  expect(s.opened).toBe(true)
431
466
  expect(s.channelId).not.toBe(storedChannelId)
432
467
  })
@@ -461,10 +496,7 @@ describe('Session', () => {
461
496
  account,
462
497
  client,
463
498
  fetch: mockFetch as typeof globalThis.fetch,
464
- sessionStore: {
465
- get: () => storedChannel(),
466
- set: vi.fn(),
467
- },
499
+ channelStore: makeChannelStore([channelEntry()]).store,
468
500
  })
469
501
 
470
502
  await s.fetch('https://api.example.com/data')
@@ -475,9 +507,70 @@ describe('Session', () => {
475
507
  })
476
508
  })
477
509
 
510
+ test('resumes a persisted channel across a restart via the entry index', async () => {
511
+ // A shared durable KV backing two manager instances simulates a restart:
512
+ // the second manager shares only what survived to disk.
513
+ const backend = new Map<string, string>()
514
+ const durableStore = () =>
515
+ createJsonChannelStore({
516
+ get: (key) => backend.get(key),
517
+ set: (key, value) => {
518
+ backend.set(key, value)
519
+ },
520
+ delete: (key) => {
521
+ backend.delete(key)
522
+ },
523
+ })
524
+
525
+ const openFetch = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
526
+ const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
527
+ if (!authorization) return Promise.resolve(make402Response())
528
+ return Promise.resolve(makeOkResponse())
529
+ })
530
+ const first = sessionManager({
531
+ account,
532
+ client,
533
+ fetch: openFetch as typeof globalThis.fetch,
534
+ maxDeposit: '10',
535
+ channelStore: durableStore(),
536
+ })
537
+ await first.fetch('https://api.example.com/data')
538
+ const channelId = first.channelId
539
+ expect(channelId).toBeDefined()
540
+
541
+ // The durable channel entry must have reached the KV.
542
+ expect([...backend.keys()].some((key) => key.startsWith('chan:'))).toBe(true)
543
+ // No persistent hints are written; only the durable channel entry survives.
544
+ expect([...backend.keys()].some((key) => key.startsWith('hint:'))).toBe(false)
545
+
546
+ const posted: SessionCredentialPayload[] = []
547
+ const resumeFetch = vi.fn((_input: RequestInfo | URL, init?: RequestInit) => {
548
+ const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
549
+ const payload = authorization
550
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
551
+ : undefined
552
+ if (payload) posted.push(payload)
553
+ if (!payload) return Promise.resolve(make402Response())
554
+ return Promise.resolve(makeOkResponse())
555
+ })
556
+ const restarted = sessionManager({
557
+ account,
558
+ client,
559
+ fetch: resumeFetch as typeof globalThis.fetch,
560
+ maxDeposit: '10',
561
+ channelStore: durableStore(),
562
+ })
563
+ await restarted.fetch('https://api.example.com/data')
564
+
565
+ // The first request carries no hint header; after the 402 the restarted
566
+ // manager resumes the persisted channel from the entry index with a
567
+ // voucher rather than opening a new one.
568
+ expect(new Headers(resumeFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
569
+ expect(posted[0]).toMatchObject({ action: 'voucher', channelId })
570
+ })
571
+
478
572
  test('persists opened channels and deletes closed channels when supported', async () => {
479
- const set = vi.fn()
480
- const remove = vi.fn()
573
+ const { store, set, delete: remove } = makeChannelStore()
481
574
  let callCount = 0
482
575
  const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
483
576
  const authorization = new Headers(init?.headers).get('Authorization')
@@ -527,11 +620,7 @@ describe('Session', () => {
527
620
  client,
528
621
  fetch: mockFetch as typeof globalThis.fetch,
529
622
  maxDeposit: '10',
530
- sessionStore: {
531
- get: () => null,
532
- set,
533
- delete: remove,
534
- },
623
+ channelStore: store,
535
624
  })
536
625
 
537
626
  await s.fetch('https://api.example.com/data')