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
@@ -8,9 +8,9 @@ import type * as Account from '../../../viem/Account.js'
8
8
  import type * as Client from '../../../viem/Client.js'
9
9
  import { charge as chargePlugin } from '../../client/Charge.js'
10
10
  import type { ChannelEntry } from '../client/ChannelOps.js'
11
+ import { createChannelStore, entryKey, type ChannelStore } from '../client/ChannelStore.js'
11
12
  import type { SessionContext } from '../client/CredentialState.js'
12
13
  import { session as sessionPlugin } from '../client/Session.js'
13
- import type { ChannelDescriptor } from '../precompile/Protocol.js'
14
14
  import { deserializeSessionReceipt } from '../precompile/Protocol.js'
15
15
  import { readSessionChallengeAmount, type SessionReceipt } from '../precompile/Protocol.js'
16
16
  import {
@@ -34,7 +34,6 @@ import {
34
34
  import { closeSocketSession } from './Runtime.js'
35
35
  import {
36
36
  closeHttpSession,
37
- getSessionSnapshot,
38
37
  isTempoSessionChallenge,
39
38
  managementInput,
40
39
  postTopUp,
@@ -98,36 +97,6 @@ export type PaymentResponse = Response & {
98
97
  cumulative: bigint
99
98
  }
100
99
 
101
- /** Serializable client-side channel snapshot stored between manager instances. */
102
- export type StoredSessionChannel = {
103
- /** Latest known channel ID. Used as the next request's channel hint. */
104
- channelId: Hex.Hex
105
- /** Latest local cumulative voucher authorization in raw token units. */
106
- cumulativeAmount: string
107
- /** Latest known deposit in raw token units. */
108
- deposit: string
109
- /** TIP-1034 channel descriptor, used as a fallback when the server cannot snapshot. */
110
- descriptor: ChannelDescriptor
111
- /** Escrow address used to derive the channel ID. */
112
- escrow: Address
113
- /** Chain ID used to derive the channel ID. */
114
- chainId: number
115
- /** Whether the channel was open when stored. Closed channels are not used as hints. */
116
- opened: boolean
117
- /** Client timestamp for debugging or app-level eviction. */
118
- updatedAt: number
119
- }
120
-
121
- /** Optional per-manager store for channel hints and restart recovery. */
122
- export type SessionStore = {
123
- /** Reads the latest stored channel snapshot for this manager scope. */
124
- get(): Promise<StoredSessionChannel | null | undefined> | StoredSessionChannel | null | undefined
125
- /** Persists the latest channel snapshot. */
126
- set(channel: StoredSessionChannel): Promise<void> | void
127
- /** Deletes the stored snapshot when a channel is closed, when supported. */
128
- delete?(): Promise<void> | void
129
- }
130
-
131
100
  /** Normalized runtime dependencies derived from `sessionManager()` parameters. */
132
101
  type SessionManagerConfig = {
133
102
  /** Decimal precision used when parsing human-readable manager amounts. */
@@ -156,65 +125,16 @@ function isZeroAmountChargeChallenge(challenge: Challenge.Challenge) {
156
125
  }
157
126
  }
158
127
 
159
- function storedChannelFromEntry(entry: ChannelEntry): StoredSessionChannel {
160
- return {
161
- channelId: entry.channelId,
162
- cumulativeAmount: entry.cumulativeAmount.toString(),
163
- deposit: entry.deposit.toString(),
164
- descriptor: entry.descriptor,
165
- escrow: entry.escrow,
166
- chainId: entry.chainId,
167
- opened: entry.opened,
168
- updatedAt: Date.now(),
169
- }
170
- }
171
-
172
- function storedChannelContext(channel: StoredSessionChannel): SessionContext {
173
- return {
174
- channelId: channel.channelId,
175
- cumulativeAmountRaw: channel.cumulativeAmount,
176
- descriptor: channel.descriptor,
177
- }
178
- }
179
-
180
- function storedChannelFromSnapshot(
181
- snapshot: ReturnType<typeof deserializeSessionSnapshot>,
182
- ): StoredSessionChannel {
128
+ /** Builds a reusable channel entry from a server session snapshot header. */
129
+ function entryFromSnapshot(snapshot: ReturnType<typeof deserializeSessionSnapshot>): ChannelEntry {
183
130
  return {
184
131
  channelId: snapshot.channelId,
185
- cumulativeAmount: snapshot.acceptedCumulative,
186
- deposit: snapshot.deposit,
132
+ cumulativeAmount: BigInt(snapshot.acceptedCumulative),
133
+ deposit: BigInt(snapshot.deposit),
187
134
  descriptor: snapshot.descriptor,
188
135
  escrow: snapshot.escrow,
189
136
  chainId: snapshot.chainId,
190
137
  opened: true,
191
- updatedAt: Date.now(),
192
- }
193
- }
194
-
195
- type CredentialContextResolution = {
196
- context: SessionContext
197
- usedStoredChannel: boolean
198
- }
199
-
200
- function resolveCredentialContext(parameters: {
201
- channel: ChannelEntry | null
202
- challenge: TempoSessionChallenge
203
- context: SessionContext
204
- storedChannel: StoredSessionChannel | null
205
- }): CredentialContextResolution {
206
- const { channel, challenge, context, storedChannel } = parameters
207
- if (
208
- context.action ||
209
- channel?.opened ||
210
- !storedChannel?.opened ||
211
- getSessionSnapshot(challenge)
212
- ) {
213
- return { context, usedStoredChannel: false }
214
- }
215
- return {
216
- context: { ...context, ...storedChannelContext(storedChannel) },
217
- usedStoredChannel: true,
218
138
  }
219
139
  }
220
140
 
@@ -267,40 +187,71 @@ function resolveSessionManagerConfig(parameters: sessionManager.Parameters): Ses
267
187
  * channel state management and credential creation, and to `Fetch.from`
268
188
  * for the 402 challenge/retry flow.
269
189
  *
270
- * ## Session resumption
271
- *
272
- * The manager only keeps active client transport state in memory. Channel
273
- * descriptors and cumulative voucher state are hydrated from server snapshots
274
- * when challenges include them; otherwise the next request opens a fresh
275
- * TIP-1034 channel. `maxDeposit` remains the local cap for all automatic
276
- * voucher and top-up decisions.
190
+ * `channelStore` can persist reusable channels between manager instances.
277
191
  */
278
192
  export function sessionManager(parameters: sessionManager.Parameters): SessionManager {
279
193
  const config = resolveSessionManagerConfig(parameters)
280
- const sessionStore = parameters.sessionStore
281
- let storedChannel: StoredSessionChannel | null = null
282
- let bootstrapChannelId: Hex.Hex | null = null
283
- const ignoredStoredChannelIds = new Set<Hex.Hex>()
284
194
  const runtime = createSessionManagerRuntime()
285
195
  const receipts = createSessionReceiptCoordinator({
286
196
  getSocketSession: () => runtime.socketSession,
287
197
  })
288
198
 
199
+ const backing = parameters.channelStore ?? createChannelStore()
200
+ const ignoredChannelIds = new Set<Hex.Hex>()
201
+
202
+ // Tracks one fetch's channel reuse so stale stored entries can be evicted once.
203
+ type ChannelUse = {
204
+ seenExisting: Set<string>
205
+ createdKeys: Set<string>
206
+ resumed: ChannelEntry | undefined
207
+ }
208
+ let channelUse: ChannelUse | undefined
209
+
210
+ /** Returns the backing entry for `key` only when it is open and not ignored. */
211
+ async function getReusable(key: string): Promise<ChannelEntry | undefined> {
212
+ const entry = await backing.get(key)
213
+ if (entry?.opened && !ignoredChannelIds.has(entry.channelId)) return entry
214
+ return undefined
215
+ }
216
+
217
+ const store: ChannelStore = {
218
+ async get(key) {
219
+ const entry = await getReusable(key)
220
+ if (entry && channelUse) {
221
+ channelUse.seenExisting.add(key)
222
+ if (!channelUse.createdKeys.has(key)) channelUse.resumed ??= entry
223
+ }
224
+ return entry
225
+ },
226
+ async set(entry) {
227
+ const key = entryKey(entry)
228
+ if (entry.opened) ignoredChannelIds.delete(entry.channelId)
229
+ if (channelUse && !channelUse.seenExisting.has(key)) channelUse.createdKeys.add(key)
230
+ await backing.set(entry)
231
+ },
232
+ delete: (key) => backing.delete(key),
233
+ }
234
+
235
+ /** Removes a failed channel from candidacy for the rest of this manager's life. */
236
+ async function ignoreChannel(entry: ChannelEntry) {
237
+ ignoredChannelIds.add(entry.channelId)
238
+ await Promise.resolve(backing.delete(entryKey(entry))).catch(() => undefined)
239
+ }
240
+
289
241
  function dispatch(event: Parameters<typeof dispatchSessionEvent>[1]) {
290
242
  return dispatchSessionEvent(runtime, event)
291
243
  }
292
244
 
293
245
  const method = sessionPlugin({
294
246
  account: parameters.account,
295
- authorizedSigner: parameters.authorizedSigner,
296
247
  getClient: parameters.client ? () => parameters.client! : parameters.getClient,
297
248
  escrow: parameters.escrow,
298
249
  decimals: config.decimals,
299
250
  maxDeposit: parameters.maxDeposit,
251
+ channelStore: store,
300
252
  onChannelUpdate(entry) {
301
253
  if (entry.channelId !== runtime.channel?.channelId) runtime.spent = 0n
302
254
  runtime.channel = entry
303
- persistStoredChannel(entry)
304
255
  if (runtime.lastChallenge) {
305
256
  dispatch({
306
257
  type: 'activated',
@@ -335,28 +286,12 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
335
286
  requiredCumulative,
336
287
  })
337
288
  }
338
- const resolved = resolveCredentialContext({
339
- challenge,
340
- channel: runtime.channel,
341
- context: {},
342
- storedChannel,
343
- })
344
- if (resolved.usedStoredChannel) return _helpers.createCredential(resolved.context)
345
289
  return undefined
346
290
  },
347
291
  })
348
292
 
349
293
  function createSessionCredential(challenge: TempoSessionChallenge, context: SessionContext) {
350
- const resolved = resolveCredentialContext({
351
- challenge,
352
- channel: runtime.channel,
353
- context,
354
- storedChannel,
355
- })
356
- return method.createCredential({
357
- challenge,
358
- context: resolved.context,
359
- })
294
+ return method.createCredential({ challenge, context })
360
295
  }
361
296
 
362
297
  function updateSpentFromReceipt(receipt: SessionReceipt | null | undefined) {
@@ -386,50 +321,21 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
386
321
  })
387
322
  }
388
323
 
389
- async function getStoredChannel() {
390
- if (!sessionStore) return null
391
- if (storedChannel) return storedChannel
392
- const channel = await sessionStore.get()
393
- storedChannel =
394
- channel?.opened && !ignoredStoredChannelIds.has(channel.channelId) ? channel : null
395
- return storedChannel
396
- }
397
-
398
- async function clearStoredChannel() {
399
- if (!sessionStore) {
400
- storedChannel = null
401
- bootstrapChannelId = null
402
- return
403
- }
404
- const channel = storedChannel
405
- if (channel) ignoredStoredChannelIds.add(channel.channelId)
406
- storedChannel = null
407
- bootstrapChannelId = null
408
- if (sessionStore.delete) {
409
- await Promise.resolve(sessionStore.delete()).catch(() => undefined)
410
- return
411
- }
412
- if (channel) {
413
- await Promise.resolve(
414
- sessionStore.set({ ...channel, opened: false, updatedAt: Date.now() }),
415
- ).catch(() => undefined)
416
- }
417
- }
418
-
419
- async function storeSnapshotHeader(response: Response) {
324
+ /** Persists a server snapshot into the channel store and returns the entry. */
325
+ async function storeSnapshotHeader(response: Response): Promise<ChannelEntry | undefined> {
420
326
  const header = response.headers.get(Constants.Headers.paymentSessionSnapshot)
421
- if (!header) return
422
- const snapshot = deserializeSessionSnapshot(header)
423
- bootstrapChannelId = snapshot.channelId
424
- const channel = storedChannelFromSnapshot(snapshot)
425
- storedChannel = channel
426
- if (sessionStore) await Promise.resolve(sessionStore.set(channel)).catch(() => undefined)
327
+ if (!header) return undefined
328
+ const entry = entryFromSnapshot(deserializeSessionSnapshot(header))
329
+ await Promise.resolve(store.set(entry)).catch(() => undefined)
330
+ return entry
427
331
  }
428
332
 
429
- async function bootstrapSession(input: RequestInfo | URL, init?: RequestInit | undefined) {
430
- if (!parameters.bootstrap) return
431
- if (runtime.channel?.opened) return
432
- if (await getStoredChannel()) return
333
+ async function bootstrapSession(
334
+ input: RequestInfo | URL,
335
+ init?: RequestInit | undefined,
336
+ ): Promise<ChannelEntry | undefined> {
337
+ if (!parameters.bootstrap) return undefined
338
+ if (runtime.channel?.opened) return undefined
433
339
 
434
340
  const requestHeaders = input instanceof Request ? input.headers : undefined
435
341
  const { body: _body, method: _method, ...bootstrapInit } = init ?? {}
@@ -446,13 +352,10 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
446
352
 
447
353
  try {
448
354
  const challengeResponse = await config.fetch(bootstrapInput, headInit)
449
- if (challengeResponse.status !== 402) {
450
- await storeSnapshotHeader(challengeResponse)
451
- return
452
- }
355
+ if (challengeResponse.status !== 402) return await storeSnapshotHeader(challengeResponse)
453
356
  const challenge = Challenge.fromResponseList(challengeResponse).find(isTempoChargeChallenge)
454
- if (!challenge) return
455
- if (!isZeroAmountChargeChallenge(challenge)) return
357
+ if (!challenge) return undefined
358
+ if (!isZeroAmountChargeChallenge(challenge)) return undefined
456
359
  const credential = await chargeMethod.createCredential({
457
360
  challenge: challenge as never,
458
361
  context: {},
@@ -464,22 +367,13 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
464
367
  [Constants.Headers.authorization]: credential,
465
368
  },
466
369
  })
467
- if (response.ok) await storeSnapshotHeader(response)
370
+ if (response.ok) return await storeSnapshotHeader(response)
371
+ return undefined
468
372
  } catch {
469
- return
373
+ return undefined
470
374
  }
471
375
  }
472
376
 
473
- function persistStoredChannel(entry: ChannelEntry) {
474
- if (!sessionStore) return
475
- const channel = storedChannelFromEntry(entry)
476
- ignoredStoredChannelIds.delete(channel.channelId)
477
- const operation =
478
- entry.opened || !sessionStore.delete ? sessionStore.set(channel) : sessionStore.delete()
479
- void Promise.resolve(operation).catch(() => undefined)
480
- storedChannel = entry.opened ? channel : null
481
- }
482
-
483
377
  function getFallbackCloseAmount(challenge: TempoSessionChallenge, channelId: Hex.Hex): bigint {
484
378
  const currentSocket = runtime.socketSession
485
379
  return computeFallbackCloseAmount({
@@ -597,68 +491,83 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
597
491
  }
598
492
 
599
493
  async function doFetch(input: RequestInfo | URL, init?: RequestInit): Promise<PaymentResponse> {
494
+ // The manager drives one shared `runtime` state machine, so requests are
495
+ // single-flight. Reject overlap loudly instead of letting concurrent calls
496
+ // corrupt each other's runtime and channel tracking.
497
+ if (channelUse)
498
+ throw new Error(
499
+ 'SessionManager: a request is already in flight; concurrent requests on one manager are not supported',
500
+ )
501
+ const use: ChannelUse = { seenExisting: new Set(), createdKeys: new Set(), resumed: undefined }
502
+ channelUse = use
503
+
600
504
  runtime.lastUrl = input
601
- const stored = await getStoredChannel()
602
- await bootstrapSession(input, init)
603
- const hintedInit = requestInitWithSessionHint(
604
- input,
605
- init,
606
- (await getStoredChannel())?.channelId ?? bootstrapChannelId ?? stored?.channelId,
607
- )
505
+
608
506
  const previous = captureRuntimeStateSnapshot({
609
507
  channel: runtime.channel,
610
508
  spent: runtime.spent,
611
509
  state: runtime.state,
612
510
  })
613
511
 
614
- let effectiveInit = hintedInit
615
- let canRetryWithoutStoredHint = Boolean(stored?.opened && !previous.channel?.opened)
616
-
617
- for (;;) {
618
- let response: Response
619
- try {
620
- response = await wrappedFetch(input, effectiveInit)
621
- } catch (error) {
622
- restoreRuntime(previous)
623
- if (!canRetryWithoutStoredHint) throw error
624
- canRetryWithoutStoredHint = false
625
- await clearStoredChannel()
626
- await bootstrapSession(input, init)
627
- effectiveInit = requestInitWithSessionHint(input, init, bootstrapChannelId ?? undefined)
628
- continue
512
+ // Cold starts resume from `channelStore` after the 402 reveals the scope.
513
+ const liveHint = runtime.channel?.opened ? runtime.channel.channelId : undefined
514
+
515
+ try {
516
+ await bootstrapSession(input, init)
517
+
518
+ let effectiveInit = requestInitWithSessionHint(input, init, liveHint)
519
+ // Stored channels may be stale, so retry once after evicting the resumed entry.
520
+ let canRetryResumed = !previous.channel?.opened
521
+
522
+ async function retryWithoutResumed(): Promise<boolean> {
523
+ const resumed = use.resumed
524
+ if (!canRetryResumed || !resumed) return false
525
+ canRetryResumed = false
526
+ await ignoreChannel(resumed)
527
+ effectiveInit = requestInitWithSessionHint(input, init, undefined)
528
+ return true
629
529
  }
630
530
 
631
- let paymentResponse = toPaymentResponse(response)
632
- let attemptedHttpManagement = false
633
- if (paymentResponse.status === 402) {
634
- const retry = await retryHttpPaymentRequired({
635
- input,
636
- init: effectiveInit,
637
- response: paymentResponse,
638
- createSessionCredential,
639
- fetch: config.fetch,
640
- getChannel: () => runtime.channel,
641
- restoreCumulative,
642
- setChallenge(challenge) {
643
- runtime.lastChallenge = challenge
644
- },
645
- topUpIfNeeded,
646
- })
647
- if (retry) {
648
- attemptedHttpManagement = true
649
- paymentResponse = toPaymentResponse(retry)
531
+ for (;;) {
532
+ let response: Response
533
+ try {
534
+ response = await wrappedFetch(input, effectiveInit)
535
+ } catch (error) {
536
+ restoreRuntime(previous)
537
+ if (await retryWithoutResumed()) continue
538
+ throw error
650
539
  }
540
+
541
+ let paymentResponse = toPaymentResponse(response)
542
+ let attemptedHttpManagement = false
543
+ if (paymentResponse.status === 402) {
544
+ const retry = await retryHttpPaymentRequired({
545
+ input,
546
+ init: effectiveInit,
547
+ response: paymentResponse,
548
+ createSessionCredential,
549
+ fetch: config.fetch,
550
+ getChannel: () => runtime.channel,
551
+ restoreCumulative,
552
+ setChallenge(challenge) {
553
+ runtime.lastChallenge = challenge
554
+ },
555
+ topUpIfNeeded,
556
+ })
557
+ if (retry) {
558
+ attemptedHttpManagement = true
559
+ paymentResponse = toPaymentResponse(retry)
560
+ }
561
+ }
562
+ if (!attemptedHttpManagement && !paymentResponse.ok && !paymentResponse.receipt) {
563
+ restoreRuntime(previous)
564
+ if (await retryWithoutResumed()) continue
565
+ return paymentResponse
566
+ }
567
+ return paymentResponse
651
568
  }
652
- if (!attemptedHttpManagement && !paymentResponse.ok && !paymentResponse.receipt) {
653
- restoreRuntime(previous)
654
- if (!canRetryWithoutStoredHint) return paymentResponse
655
- canRetryWithoutStoredHint = false
656
- await clearStoredChannel()
657
- await bootstrapSession(input, init)
658
- effectiveInit = requestInitWithSessionHint(input, init, bootstrapChannelId ?? undefined)
659
- continue
660
- }
661
- return paymentResponse
569
+ } finally {
570
+ channelUse = undefined
662
571
  }
663
572
  }
664
573
 
@@ -741,7 +650,10 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
741
650
  )
742
651
  }
743
652
  const probeUrl = webSocketProbeUrl(input)
744
- await bootstrapSession(probeUrl, init?.signal ? { signal: init.signal } : undefined)
653
+ const signalInit = init?.signal ? { signal: init.signal } : undefined
654
+ await bootstrapSession(probeUrl, signalInit)
655
+ // Cold starts resume from `channelStore` after the probe's 402.
656
+ const liveHint = runtime.channel?.opened ? runtime.channel.channelId : undefined
745
657
 
746
658
  const prepared = await prepareWebSocketSession({
747
659
  createSessionCredential,
@@ -750,11 +662,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
750
662
  onProbeUrl(httpUrl) {
751
663
  runtime.lastUrl = httpUrl.toString()
752
664
  },
753
- probeInit: requestInitWithSessionHint(
754
- probeUrl,
755
- init?.signal ? { signal: init.signal } : undefined,
756
- (await getStoredChannel())?.channelId ?? bootstrapChannelId ?? undefined,
757
- ),
665
+ probeInit: requestInitWithSessionHint(probeUrl, signalInit, liveHint),
758
666
  signal: init?.signal,
759
667
  })
760
668
  const { challenge, credential, httpUrl, wsUrl } = prepared
@@ -823,8 +731,6 @@ export namespace sessionManager {
823
731
 
824
732
  export type Parameters = Account.getResolver.Parameters &
825
733
  Client.getResolver.Parameters & {
826
- /** Address authorized to sign vouchers. Defaults to the account access key address when available, otherwise the account address. */
827
- authorizedSigner?: Address | undefined
828
734
  /** Enables same-route HEAD bootstrap from a server session snapshot before opening a new channel. */
829
735
  bootstrap?: boolean | undefined
830
736
  /** Viem client instance. Shorthand for `getClient: () => client`. */
@@ -837,8 +743,8 @@ export namespace sessionManager {
837
743
  fetch?: typeof globalThis.fetch | undefined
838
744
  /** Maximum deposit in human-readable units (e.g. `'10'` for 10 tokens). Converted to raw units via `decimals`. */
839
745
  maxDeposit?: string | undefined
840
- /** Optional per-manager store for persisted channel hints and restart recovery. */
841
- sessionStore?: SessionStore | undefined
746
+ /** Store for reusable session channels. Defaults to in-memory. */
747
+ channelStore?: ChannelStore | undefined
842
748
  /** Optional websocket constructor for runtimes without a global WebSocket. */
843
749
  webSocket?: WebSocketConstructor | undefined
844
750
  }
@@ -2,18 +2,20 @@ export { session } from './Session.js'
2
2
  export { sessionManager } from './SessionManager.js'
3
3
  export * as Machine from './Runtime.js'
4
4
  export { deserializeSnapshot, serializeSnapshot } from '../Snapshot.js'
5
- /** Public client session manager types. */
5
+ export {
6
+ createChannelStore,
7
+ createJsonChannelStore,
8
+ entryKey,
9
+ type ChannelStore,
10
+ type JsonChannelKv,
11
+ } from './ChannelStore.js'
6
12
  export type {
7
13
  PaymentResponse,
8
- SessionStore,
9
14
  SessionManager,
10
15
  SessionManagerSseOptions,
11
16
  SessionManagerWebSocketOptions,
12
- StoredSessionChannel,
13
17
  } from './SessionManager.js'
14
- /** Public managed WebSocket facade returned by `sessionManager().ws()`. */
15
18
  export type { SessionManagedWebSocket } from './Transports.js'
16
- /** Public pure state-machine types. */
17
19
  export type {
18
20
  ActiveSessionState,
19
21
  ChallengedSessionState,
@@ -35,3 +37,4 @@ export type {
35
37
  WithdrawableSessionState,
36
38
  } from './Runtime.js'
37
39
  export type { SessionSnapshot } from '../Snapshot.js'
40
+ export type { ResolveAccount, ResolveAccountInfo } from '../../client/ResolveAccount.js'
@@ -271,7 +271,7 @@ describe('Precompile Voucher', () => {
271
271
  ).toBe(false)
272
272
  })
273
273
 
274
- test('sign rejects p256 keychain access-key voucher delegation explicitly', async () => {
274
+ test('signs and verifies p256 access-key vouchers as primitives', async () => {
275
275
  const rootAccount = TempoAccount.fromSecp256k1(Secp256k1.randomPrivateKey())
276
276
  const accessKey = TempoAccount.fromP256(P256.randomPrivateKey(), {
277
277
  access: rootAccount,
@@ -281,16 +281,54 @@ describe('Precompile Voucher', () => {
281
281
  transport: http('http://127.0.0.1'),
282
282
  })
283
283
 
284
- await expect(
285
- signVoucher(
286
- accessKeyClient,
287
- accessKey,
288
- { channelId, cumulativeAmount },
284
+ const signature = await signVoucher(
285
+ accessKeyClient,
286
+ accessKey,
287
+ { channelId, cumulativeAmount },
288
+ escrowContract,
289
+ chainId,
290
+ )
291
+
292
+ const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
293
+ expect(envelope.type).toBe('p256')
294
+ expect(
295
+ verifyVoucher(
289
296
  escrowContract,
290
297
  chainId,
298
+ { channelId, cumulativeAmount, signature },
291
299
  accessKey.accessKeyAddress,
292
300
  ),
293
- ).rejects.toThrow('TIP-1034 voucher signing only supports secp256k1 voucher signatures.')
301
+ ).toBe(true)
302
+ })
303
+
304
+ test('signs and verifies webAuthn root vouchers as primitives', async () => {
305
+ const webAuthnAccount = TempoAccount.fromHeadlessWebAuthn(P256.randomPrivateKey(), {
306
+ origin: 'https://example.com',
307
+ rpId: 'example.com',
308
+ })
309
+ const webAuthnClient = createClient({
310
+ account: webAuthnAccount,
311
+ transport: http('http://127.0.0.1'),
312
+ })
313
+
314
+ const signature = await signVoucher(
315
+ webAuthnClient,
316
+ webAuthnAccount,
317
+ { channelId, cumulativeAmount },
318
+ escrowContract,
319
+ chainId,
320
+ )
321
+
322
+ const envelope = SignatureEnvelope.from(signature as SignatureEnvelope.Serialized)
323
+ expect(envelope.type).toBe('webAuthn')
324
+ expect(
325
+ verifyVoucher(
326
+ escrowContract,
327
+ chainId,
328
+ { channelId, cumulativeAmount, signature },
329
+ webAuthnAccount.address,
330
+ ),
331
+ ).toBe(true)
294
332
  })
295
333
 
296
334
  test('domain and type match TIP-1034', () => {