mppx 0.8.12 → 0.8.14

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 (266) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +16 -1
  3. package/dist/Errors.d.ts +10 -0
  4. package/dist/Errors.d.ts.map +1 -1
  5. package/dist/Errors.js +11 -2
  6. package/dist/Errors.js.map +1 -1
  7. package/dist/Method.d.ts +105 -3
  8. package/dist/Method.d.ts.map +1 -1
  9. package/dist/Method.js +102 -2
  10. package/dist/Method.js.map +1 -1
  11. package/dist/cli/account.d.ts +88 -0
  12. package/dist/cli/account.d.ts.map +1 -1
  13. package/dist/cli/account.js +39 -9
  14. package/dist/cli/account.js.map +1 -1
  15. package/dist/cli/cli.d.ts +2 -1
  16. package/dist/cli/cli.d.ts.map +1 -1
  17. package/dist/cli/cli.js +59 -2
  18. package/dist/cli/cli.js.map +1 -1
  19. package/dist/cli/sessions/Manager.d.ts +31 -0
  20. package/dist/cli/sessions/Manager.d.ts.map +1 -0
  21. package/dist/cli/sessions/Manager.js +56 -0
  22. package/dist/cli/sessions/Manager.js.map +1 -0
  23. package/dist/cli/sessions/commands.d.ts +36 -0
  24. package/dist/cli/sessions/commands.d.ts.map +1 -0
  25. package/dist/cli/sessions/commands.js +407 -0
  26. package/dist/cli/sessions/commands.js.map +1 -0
  27. package/dist/cli/sessions/request.d.ts +35 -0
  28. package/dist/cli/sessions/request.d.ts.map +1 -0
  29. package/dist/cli/sessions/request.js +271 -0
  30. package/dist/cli/sessions/request.js.map +1 -0
  31. package/dist/cli/sessions/store.d.ts +134 -0
  32. package/dist/cli/sessions/store.d.ts.map +1 -0
  33. package/dist/cli/sessions/store.js +723 -0
  34. package/dist/cli/sessions/store.js.map +1 -0
  35. package/dist/cli/utils.d.ts +2 -0
  36. package/dist/cli/utils.d.ts.map +1 -1
  37. package/dist/cli/utils.js +12 -0
  38. package/dist/cli/utils.js.map +1 -1
  39. package/dist/cli/validate/index.d.ts.map +1 -1
  40. package/dist/cli/validate/index.js +12 -6
  41. package/dist/cli/validate/index.js.map +1 -1
  42. package/dist/cli/validate/messages.d.ts +2 -0
  43. package/dist/cli/validate/messages.d.ts.map +1 -0
  44. package/dist/cli/validate/messages.js +8 -0
  45. package/dist/cli/validate/messages.js.map +1 -0
  46. package/dist/client/internal/Fetch.d.ts.map +1 -1
  47. package/dist/client/internal/Fetch.js +12 -2
  48. package/dist/client/internal/Fetch.js.map +1 -1
  49. package/dist/client/internal/MethodChallenge.d.ts +17 -0
  50. package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
  51. package/dist/client/internal/MethodChallenge.js +11 -0
  52. package/dist/client/internal/MethodChallenge.js.map +1 -0
  53. package/dist/client/node.d.ts +29 -0
  54. package/dist/client/node.d.ts.map +1 -0
  55. package/dist/client/node.js +177 -0
  56. package/dist/client/node.js.map +1 -0
  57. package/dist/internal/types.d.ts +24 -0
  58. package/dist/internal/types.d.ts.map +1 -1
  59. package/dist/server/Mppx.d.ts +23 -4
  60. package/dist/server/Mppx.d.ts.map +1 -1
  61. package/dist/server/Mppx.js +91 -82
  62. package/dist/server/Mppx.js.map +1 -1
  63. package/dist/stripe/server/Charge.d.ts +8 -4
  64. package/dist/stripe/server/Charge.d.ts.map +1 -1
  65. package/dist/stripe/server/Charge.js.map +1 -1
  66. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  67. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  68. package/dist/stripe/server/internal/html.gen.js +1 -1
  69. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  70. package/dist/tempo/client/Methods.d.ts +2 -1
  71. package/dist/tempo/client/Methods.d.ts.map +1 -1
  72. package/dist/tempo/internal/fee-payer.d.ts +1 -0
  73. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  74. package/dist/tempo/internal/fee-payer.js +7 -2
  75. package/dist/tempo/internal/fee-payer.js.map +1 -1
  76. package/dist/tempo/internal/types.d.ts +10 -1
  77. package/dist/tempo/internal/types.d.ts.map +1 -1
  78. package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
  79. package/dist/tempo/legacy/client/ChannelOps.js +2 -1
  80. package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
  81. package/dist/tempo/legacy/client/Session.d.ts +2 -2
  82. package/dist/tempo/legacy/server/Session.d.ts +3 -3
  83. package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
  84. package/dist/tempo/legacy/server/Session.js.map +1 -1
  85. package/dist/tempo/server/Charge.d.ts +40 -78
  86. package/dist/tempo/server/Charge.d.ts.map +1 -1
  87. package/dist/tempo/server/Charge.js +289 -209
  88. package/dist/tempo/server/Charge.js.map +1 -1
  89. package/dist/tempo/server/Methods.d.ts +13 -23
  90. package/dist/tempo/server/Methods.d.ts.map +1 -1
  91. package/dist/tempo/server/Methods.js +6 -2
  92. package/dist/tempo/server/Methods.js.map +1 -1
  93. package/dist/tempo/server/Relay.d.ts +48 -0
  94. package/dist/tempo/server/Relay.d.ts.map +1 -0
  95. package/dist/tempo/server/Relay.js +177 -0
  96. package/dist/tempo/server/Relay.js.map +1 -0
  97. package/dist/tempo/server/SponsorBudget.d.ts +59 -0
  98. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
  99. package/dist/tempo/server/SponsorBudget.js +144 -0
  100. package/dist/tempo/server/SponsorBudget.js.map +1 -0
  101. package/dist/tempo/server/Subscription.d.ts +3 -2
  102. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  103. package/dist/tempo/server/Subscription.js.map +1 -1
  104. package/dist/tempo/server/index.d.ts +1 -1
  105. package/dist/tempo/server/index.d.ts.map +1 -1
  106. package/dist/tempo/server/index.js.map +1 -1
  107. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  108. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  109. package/dist/tempo/server/internal/html.gen.js +1 -1
  110. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  111. package/dist/tempo/session/Snapshot.d.ts +9 -0
  112. package/dist/tempo/session/Snapshot.d.ts.map +1 -1
  113. package/dist/tempo/session/Snapshot.js +6 -0
  114. package/dist/tempo/session/Snapshot.js.map +1 -1
  115. package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
  116. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  117. package/dist/tempo/session/client/ChannelOps.js +17 -3
  118. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  119. package/dist/tempo/session/client/CredentialState.d.ts +26 -2
  120. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  121. package/dist/tempo/session/client/CredentialState.js +99 -16
  122. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  123. package/dist/tempo/session/client/Runtime.d.ts +8 -0
  124. package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
  125. package/dist/tempo/session/client/Runtime.js +17 -0
  126. package/dist/tempo/session/client/Runtime.js.map +1 -1
  127. package/dist/tempo/session/client/Session.d.ts +16 -6
  128. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  129. package/dist/tempo/session/client/Session.js +93 -36
  130. package/dist/tempo/session/client/Session.js.map +1 -1
  131. package/dist/tempo/session/client/SessionManager.d.ts +10 -3
  132. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  133. package/dist/tempo/session/client/SessionManager.js +176 -50
  134. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  135. package/dist/tempo/session/client/Transports.d.ts +18 -7
  136. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  137. package/dist/tempo/session/client/Transports.js +81 -33
  138. package/dist/tempo/session/client/Transports.js.map +1 -1
  139. package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
  140. package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
  141. package/dist/tempo/session/client/internal/SessionManager.js +13 -0
  142. package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
  143. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  144. package/dist/tempo/session/precompile/Chain.js +109 -12
  145. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  146. package/dist/tempo/session/precompile/Channel.d.ts +2 -0
  147. package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
  148. package/dist/tempo/session/precompile/Channel.js +4 -0
  149. package/dist/tempo/session/precompile/Channel.js.map +1 -1
  150. package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
  151. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  152. package/dist/tempo/session/server/CredentialVerification.js +14 -0
  153. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  154. package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
  155. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
  156. package/dist/tempo/session/server/MeteredStream.js +5 -4
  157. package/dist/tempo/session/server/MeteredStream.js.map +1 -1
  158. package/dist/tempo/session/server/RequestState.d.ts +5 -1
  159. package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
  160. package/dist/tempo/session/server/RequestState.js +13 -5
  161. package/dist/tempo/session/server/RequestState.js.map +1 -1
  162. package/dist/tempo/session/server/Session.d.ts +21 -6
  163. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  164. package/dist/tempo/session/server/Session.js +4 -1
  165. package/dist/tempo/session/server/Session.js.map +1 -1
  166. package/dist/tempo/session/server/Settlement.d.ts +20 -0
  167. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  168. package/dist/tempo/session/server/Settlement.js +20 -0
  169. package/dist/tempo/session/server/Settlement.js.map +1 -1
  170. package/dist/tempo/session/server/Ws.d.ts +2 -0
  171. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  172. package/dist/tempo/session/server/Ws.js.map +1 -1
  173. package/dist/tempo/session/server/index.d.ts +1 -1
  174. package/dist/tempo/session/server/index.d.ts.map +1 -1
  175. package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
  176. package/dist/validation/core.d.ts.map +1 -1
  177. package/dist/validation/core.js +6 -2
  178. package/dist/validation/core.js.map +1 -1
  179. package/dist/viem/Client.d.ts.map +1 -1
  180. package/dist/viem/Client.js +24 -17
  181. package/dist/viem/Client.js.map +1 -1
  182. package/package.json +6 -1
  183. package/src/Errors.test.ts +23 -0
  184. package/src/Errors.ts +21 -2
  185. package/src/Method.test.ts +102 -1
  186. package/src/Method.ts +241 -5
  187. package/src/cli/account.ts +45 -10
  188. package/src/cli/cli.test.ts +165 -70
  189. package/src/cli/cli.ts +67 -2
  190. package/src/cli/mcp.test.ts +11 -0
  191. package/src/cli/sessions/Manager.test.ts +249 -0
  192. package/src/cli/sessions/Manager.ts +93 -0
  193. package/src/cli/sessions/commands.ts +444 -0
  194. package/src/cli/sessions/request.test.ts +51 -0
  195. package/src/cli/sessions/request.ts +353 -0
  196. package/src/cli/sessions/store.test.ts +581 -0
  197. package/src/cli/sessions/store.ts +940 -0
  198. package/src/cli/utils.test.ts +23 -0
  199. package/src/cli/utils.ts +10 -0
  200. package/src/cli/validate/index.ts +12 -14
  201. package/src/cli/validate/messages.ts +7 -0
  202. package/src/cli/validate.test.ts +38 -5
  203. package/src/client/Mppx.test-d.ts +3 -3
  204. package/src/client/internal/Fetch.ts +16 -7
  205. package/src/client/internal/MethodChallenge.ts +30 -0
  206. package/src/client/node.test.ts +115 -0
  207. package/src/client/node.ts +247 -0
  208. package/src/internal/types.test-d.ts +21 -0
  209. package/src/internal/types.ts +33 -0
  210. package/src/server/Mppx.test-d.ts +44 -0
  211. package/src/server/Mppx.test.ts +205 -0
  212. package/src/server/Mppx.ts +167 -103
  213. package/src/stripe/Methods.test.ts +10 -0
  214. package/src/stripe/server/Charge.test-d.ts +66 -0
  215. package/src/stripe/server/Charge.ts +8 -5
  216. package/src/stripe/server/internal/html.gen.ts +1 -1
  217. package/src/tempo/PublicExports.test-d.ts +20 -0
  218. package/src/tempo/internal/fee-payer.ts +7 -3
  219. package/src/tempo/internal/types.ts +13 -2
  220. package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
  221. package/src/tempo/legacy/client/ChannelOps.ts +2 -1
  222. package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
  223. package/src/tempo/legacy/server/Session.ts +6 -5
  224. package/src/tempo/server/Charge.test.ts +337 -63
  225. package/src/tempo/server/Charge.ts +440 -270
  226. package/src/tempo/server/Methods.ts +16 -4
  227. package/src/tempo/server/Relay.test.ts +523 -0
  228. package/src/tempo/server/Relay.ts +288 -0
  229. package/src/tempo/server/SponsorBudget.test.ts +125 -0
  230. package/src/tempo/server/SponsorBudget.ts +213 -0
  231. package/src/tempo/server/Subscription.ts +6 -4
  232. package/src/tempo/server/index.ts +5 -1
  233. package/src/tempo/server/internal/html.gen.ts +1 -1
  234. package/src/tempo/session/README.md +201 -0
  235. package/src/tempo/session/Snapshot.ts +21 -0
  236. package/src/tempo/session/client/ChannelOps.test.ts +55 -0
  237. package/src/tempo/session/client/ChannelOps.ts +21 -3
  238. package/src/tempo/session/client/CredentialState.test.ts +2 -2
  239. package/src/tempo/session/client/CredentialState.ts +159 -16
  240. package/src/tempo/session/client/Runtime.test.ts +12 -0
  241. package/src/tempo/session/client/Runtime.ts +22 -0
  242. package/src/tempo/session/client/Session.test.ts +238 -5
  243. package/src/tempo/session/client/Session.ts +130 -41
  244. package/src/tempo/session/client/SessionManager.test.ts +239 -8
  245. package/src/tempo/session/client/SessionManager.ts +204 -54
  246. package/src/tempo/session/client/Transports.test.ts +181 -8
  247. package/src/tempo/session/client/Transports.ts +127 -45
  248. package/src/tempo/session/client/internal/SessionManager.ts +36 -0
  249. package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
  250. package/src/tempo/session/precompile/Chain.test.ts +223 -10
  251. package/src/tempo/session/precompile/Chain.ts +119 -12
  252. package/src/tempo/session/precompile/Channel.test.ts +9 -0
  253. package/src/tempo/session/precompile/Channel.ts +5 -0
  254. package/src/tempo/session/server/CredentialVerification.ts +18 -1
  255. package/src/tempo/session/server/MeteredStream.ts +12 -5
  256. package/src/tempo/session/server/RequestState.test.ts +47 -4
  257. package/src/tempo/session/server/RequestState.ts +15 -8
  258. package/src/tempo/session/server/Session.test.ts +461 -0
  259. package/src/tempo/session/server/Session.ts +31 -8
  260. package/src/tempo/session/server/Settlement.ts +45 -0
  261. package/src/tempo/session/server/Ws.test.ts +113 -0
  262. package/src/tempo/session/server/Ws.ts +2 -0
  263. package/src/tempo/session/server/index.ts +2 -0
  264. package/src/validation/core.ts +7 -1
  265. package/src/viem/Client.test.ts +44 -1
  266. package/src/viem/Client.ts +31 -17
@@ -1,8 +1,8 @@
1
1
  import { type Address, parseUnits } from 'viem'
2
2
  import { tempo as tempo_chain } from 'viem/chains'
3
3
 
4
+ import * as MethodChallenge from '../../../client/internal/MethodChallenge.js'
4
5
  import * as MethodResponse from '../../../client/internal/MethodResponse.js'
5
- import * as Constants from '../../../Constants.js'
6
6
  import * as Credential from '../../../Credential.js'
7
7
  import * as Method from '../../../Method.js'
8
8
  import * as Account from '../../../viem/Account.js'
@@ -11,26 +11,36 @@ import type {
11
11
  ResolveAccount as ResolveAccount_,
12
12
  ResolveAccountInfo as ResolveAccountInfo_,
13
13
  } from '../../client/ResolveAccount.js'
14
+ import * as AutoSwap from '../../internal/auto-swap.js'
14
15
  import * as defaults from '../../internal/defaults.js'
15
16
  import * as Methods from '../../Methods.js'
16
- import { isEventStream, requireSessionCredentialContext } from '../precompile/Protocol.js'
17
+ import * as Channel from '../precompile/Channel.js'
18
+ import {
19
+ isEventStream,
20
+ readSessionChallengeAmount,
21
+ requireSessionCredentialContext,
22
+ } from '../precompile/Protocol.js'
17
23
  import { serializeCredential, type ChannelEntry } from './ChannelOps.js'
18
24
  import { createChannelStore, type ChannelStore } from './ChannelStore.js'
19
25
  import {
26
+ canSignDescriptor,
20
27
  executeCredentialPlan,
28
+ hasSessionAction,
21
29
  planCredential,
22
30
  resolveChallengeContext,
23
31
  resolveRecoverContext,
24
32
  sessionContextSchema,
33
+ type ChallengeContext,
34
+ type SessionContext as CredentialContext,
25
35
  } from './CredentialState.js'
26
- import { assertWithinMaxDeposit } from './Runtime.js'
36
+ import { assertWithinMaxDeposit, resolveAutomaticTopUp } from './Runtime.js'
27
37
  import {
28
38
  handleSseNeedVoucher,
29
- isTempoSessionChallenge,
30
- managementInput,
39
+ isTip1034SessionChallenge,
31
40
  postTopUp,
32
41
  wrapSseResponse,
33
42
  type SsePaymentDriver,
43
+ type TempoSessionChallenge,
34
44
  } from './Transports.js'
35
45
 
36
46
  export { sessionContextSchema, type SessionContext } from './CredentialState.js'
@@ -45,11 +55,13 @@ export { sessionContextSchema, type SessionContext } from './CredentialState.js'
45
55
  export function session(parameters: session.Parameters = {}) {
46
56
  const {
47
57
  account,
58
+ autoSwap: autoSwapParameter,
48
59
  channelStore,
49
60
  decimals = defaults.decimals,
50
61
  escrow: escrowOverride,
51
62
  getClient: getClientParameter,
52
63
  maxDeposit: maxDepositParameter,
64
+ topUpAmount: topUpAmountParameter,
53
65
  onChannelUpdate,
54
66
  resolveAccount,
55
67
  } = parameters
@@ -61,18 +73,35 @@ export function session(parameters: session.Parameters = {}) {
61
73
  const getAccount = Account.getResolver({ account })
62
74
  const maxDeposit =
63
75
  maxDepositParameter !== undefined ? parseUnits(maxDepositParameter, decimals) : undefined
76
+ const topUpAmount =
77
+ topUpAmountParameter !== undefined ? parseUnits(topUpAmountParameter, decimals) : undefined
64
78
  const store = channelStore ?? createChannelStore()
65
79
  const sink = { store, notifyUpdate: (entry: ChannelEntry) => onChannelUpdate?.(entry) }
66
80
 
81
+ const resolveCredentialAccount = async (
82
+ resolved: ChallengeContext,
83
+ context: CredentialContext | undefined,
84
+ entry: ChannelEntry | undefined,
85
+ ) => {
86
+ const defaultAccount = getAccount(resolved.client, context)
87
+ const descriptor = context?.action
88
+ ? context.descriptor
89
+ : (entry?.descriptor ??
90
+ resolveRecoverContext({ context, snapshot: resolved.snapshot })?.descriptor)
91
+ return (
92
+ (await resolveAccount?.({
93
+ account: defaultAccount,
94
+ chainId: resolved.chainId,
95
+ operation: {
96
+ kind: 'authorizePaymentChannel',
97
+ ...(descriptor ? { authority: Channel.resolveAuthorizedSigner(descriptor) } : {}),
98
+ },
99
+ })) ?? defaultAccount
100
+ )
101
+ }
102
+
67
103
  const method = Method.toClient(Methods.session, {
68
- canHandleChallenge({ challenge }) {
69
- return (
70
- Constants.getMethodDetail(
71
- challenge.request.methodDetails,
72
- Constants.MethodDetailKeys.sessionProtocol,
73
- ) === Constants.SessionProtocols.v2
74
- )
75
- },
104
+ canHandleChallenge: ({ challenge }) => isTip1034SessionChallenge(challenge),
76
105
  context: sessionContextSchema,
77
106
  async createCredential({ challenge, context }) {
78
107
  const resolved = await resolveChallengeContext({
@@ -80,22 +109,8 @@ export function session(parameters: session.Parameters = {}) {
80
109
  escrowOverride,
81
110
  getClient,
82
111
  })
83
- const defaultAccount = getAccount(resolved.client, context)
84
112
  const entry = await store.get(resolved.key)
85
- // Resolve recovery hints early so account selection can satisfy an existing channel authority.
86
- const recoverContext = resolveRecoverContext({ context, snapshot: resolved.snapshot })
87
- const descriptor = context?.action
88
- ? context.descriptor
89
- : (entry?.descriptor ?? recoverContext?.descriptor)
90
- const account =
91
- (await resolveAccount?.({
92
- account: defaultAccount,
93
- chainId: resolved.chainId,
94
- operation: {
95
- kind: 'authorizePaymentChannel',
96
- ...(descriptor ? { authority: descriptor.authorizedSigner } : {}),
97
- },
98
- })) ?? defaultAccount
113
+ const account = await resolveCredentialAccount(resolved, context, entry)
99
114
  const payload = await executeCredentialPlan(
100
115
  planCredential({
101
116
  account,
@@ -106,15 +121,89 @@ export function session(parameters: session.Parameters = {}) {
106
121
  resolved,
107
122
  }),
108
123
  sink,
124
+ AutoSwap.resolve(context?.autoSwap ?? autoSwapParameter, AutoSwap.defaultCurrencies),
109
125
  )
110
126
  return serializeCredential(challenge, payload, resolved.chainId, account)
111
127
  },
112
128
  })
113
129
 
130
+ const topUpChannelIfNeeded = async ({
131
+ challenge,
132
+ channel,
133
+ deposit,
134
+ fetch,
135
+ input,
136
+ requiredCumulative,
137
+ }: {
138
+ challenge: TempoSessionChallenge
139
+ channel: ChannelEntry
140
+ deposit: bigint
141
+ fetch: typeof globalThis.fetch
142
+ input: RequestInfo | URL
143
+ requiredCumulative: bigint
144
+ }) => {
145
+ const knownDeposit = channel.deposit > deposit ? channel.deposit : deposit
146
+ const additionalDeposit = resolveAutomaticTopUp({
147
+ deposit: knownDeposit,
148
+ maxDeposit,
149
+ requiredCumulative,
150
+ suggestedDeposit:
151
+ challenge.request.suggestedDeposit === undefined
152
+ ? undefined
153
+ : BigInt(challenge.request.suggestedDeposit),
154
+ topUpAmount,
155
+ })
156
+ if (additionalDeposit > 0n)
157
+ await postTopUp({
158
+ additionalDeposit,
159
+ challenge,
160
+ channel,
161
+ channelId: channel.channelId,
162
+ createSessionCredential: (challenge, context) =>
163
+ method.createCredential({ challenge, context }),
164
+ fetch,
165
+ input,
166
+ })
167
+ const nextDeposit = knownDeposit + additionalDeposit
168
+ if (nextDeposit === channel.deposit) return
169
+ const updated = { ...channel, deposit: nextDeposit }
170
+ await store.set(updated)
171
+ sink.notifyUpdate(updated)
172
+ }
173
+
174
+ MethodChallenge.register(method, async ({ challenge, context, fetch, input }) => {
175
+ if (!isTip1034SessionChallenge(challenge)) return
176
+ const sessionContext = context === undefined ? undefined : sessionContextSchema.parse(context)
177
+ if (hasSessionAction(sessionContext)) return
178
+ const resolved = await resolveChallengeContext({ challenge, escrowOverride, getClient })
179
+ const channel = await store.get(resolved.key)
180
+ if (!channel?.opened) return
181
+ const snapshot =
182
+ resolved.snapshot?.channelId.toLowerCase() === channel.channelId.toLowerCase()
183
+ ? resolved.snapshot
184
+ : undefined
185
+ const nextCumulative = channel.cumulativeAmount + readSessionChallengeAmount(challenge)
186
+ const snapshotRequired = snapshot ? BigInt(snapshot.requiredCumulative) : nextCumulative
187
+ const requiredCumulative = snapshotRequired > nextCumulative ? snapshotRequired : nextCumulative
188
+ const snapshotDeposit = snapshot ? BigInt(snapshot.deposit) : channel.deposit
189
+ const deposit = snapshotDeposit > channel.deposit ? snapshotDeposit : channel.deposit
190
+ if (requiredCumulative <= deposit && deposit === channel.deposit) return
191
+ const account = await resolveCredentialAccount(resolved, sessionContext, channel)
192
+ if (!canSignDescriptor(account, channel.descriptor)) return
193
+ await topUpChannelIfNeeded({
194
+ challenge,
195
+ channel,
196
+ deposit,
197
+ fetch,
198
+ input,
199
+ requiredCumulative,
200
+ })
201
+ })
202
+
114
203
  return MethodResponse.register(
115
204
  method,
116
205
  async ({ challenge, credential, fetch, headers, input, refetch, response, signal }) => {
117
- if (!isTempoSessionChallenge(challenge)) return response
206
+ if (!isTip1034SessionChallenge(challenge)) return response
118
207
  if (!isEventStream(response)) {
119
208
  const credentialContext = requireSessionCredentialContext(
120
209
  Credential.deserialize(credential).payload,
@@ -142,23 +231,16 @@ export function session(parameters: session.Parameters = {}) {
142
231
  method.createCredential({ challenge, context }),
143
232
  fetch,
144
233
  getChannel: () => channel ?? null,
145
- managementInput,
146
- async topUpIfNeeded({ channelId, deposit, requiredCumulative }) {
147
- if (requiredCumulative <= deposit || !channel) return
148
- const additionalDeposit = requiredCumulative - deposit
149
- await postTopUp({
150
- additionalDeposit,
234
+ async topUpIfNeeded({ deposit, requiredCumulative }) {
235
+ if (!channel) return
236
+ await topUpChannelIfNeeded({
151
237
  challenge,
152
238
  channel,
153
- channelId,
154
- createSessionCredential: (challenge, context) =>
155
- method.createCredential({ challenge, context }),
239
+ deposit: channel.deposit > deposit ? channel.deposit : deposit,
156
240
  fetch,
157
241
  input,
242
+ requiredCumulative,
158
243
  })
159
- channel.deposit += additionalDeposit
160
- await store.set(channel)
161
- sink.notifyUpdate(channel)
162
244
  },
163
245
  } satisfies SsePaymentDriver
164
246
 
@@ -179,6 +261,8 @@ export declare namespace session {
179
261
 
180
262
  type Parameters = Account.getResolver.Parameters &
181
263
  Client.getResolver.Parameters & {
264
+ /** Automatically acquire the session currency from fallback stablecoins before open/top-up. */
265
+ autoSwap?: AutoSwap.resolve.Value | undefined
182
266
  /** Pluggable persistence for reusable channels. Defaults to an in-memory store. */
183
267
  channelStore?: ChannelStore | undefined
184
268
  /** Token decimals for parsing human-readable amounts (default: 6). */
@@ -187,6 +271,11 @@ export declare namespace session {
187
271
  escrow?: Address | undefined
188
272
  /** Maximum channel deposit in human-readable units. Caps server-suggested opens and automatic top-ups. */
189
273
  maxDeposit?: string | undefined
274
+ /**
275
+ * Preferred automatic top-up size in human-readable units. When omitted,
276
+ * a bounded server `suggestedDeposit` is preferred, then the exact shortfall.
277
+ */
278
+ topUpAmount?: string | undefined
190
279
  /** Called whenever channel state changes. */
191
280
  onChannelUpdate?: ((entry: ChannelEntry) => void) | undefined
192
281
  /** Selects the account that signs this session credential after the challenge is known. */
@@ -14,6 +14,7 @@ import { createSessionReceipt, serializeSessionReceipt } from '../precompile/Pro
14
14
  import type { NeedVoucherEvent, SessionReceipt } from '../precompile/Protocol.js'
15
15
  import { formatNeedVoucherEvent, parseEvent } from '../precompile/Protocol.js'
16
16
  import type { SessionCredentialPayload } from '../precompile/Protocol.js'
17
+ import * as Voucher from '../precompile/Voucher.js'
17
18
  import { computeFallbackCloseAmount, sessionManager } from './SessionManager.js'
18
19
 
19
20
  const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
@@ -44,6 +45,24 @@ const client = createClient({
44
45
  }),
45
46
  })
46
47
 
48
+ function channelStateClient(state: { closeRequestedAt: number; deposit: bigint; settled: bigint }) {
49
+ return createClient({
50
+ account,
51
+ chain: { id: 4217 } as never,
52
+ transport: custom({
53
+ async request(args) {
54
+ if (args.method === 'eth_call')
55
+ return encodeFunctionResult({
56
+ abi: escrowAbi,
57
+ functionName: 'getChannelState',
58
+ result: state,
59
+ })
60
+ throw new Error(`unexpected rpc request: ${args.method}`)
61
+ },
62
+ }),
63
+ })
64
+ }
65
+
47
66
  const storedDescriptor = {
48
67
  authorizedSigner: account.address,
49
68
  expiringNonceHash: `0x${'11'.repeat(32)}` as Hex,
@@ -332,6 +351,17 @@ describe('Session', () => {
332
351
  test('seeds a same-route HEAD snapshot and resolves the account when resuming it', async () => {
333
352
  const { store, set } = makeChannelStore()
334
353
  const posted: SessionCredentialPayload[] = []
354
+ const highestVoucher = {
355
+ channelId: storedChannelId,
356
+ cumulativeAmount: '1000000',
357
+ signature: await Voucher.signVoucher(
358
+ client,
359
+ account,
360
+ { channelId: storedChannelId, cumulativeAmount: 1_000_000n },
361
+ tip20ChannelEscrow,
362
+ 4217,
363
+ ),
364
+ }
335
365
  const resolveAccount = vi.fn(
336
366
  (info: Parameters<NonNullable<sessionManager.Parameters['resolveAccount']>>[0]) =>
337
367
  info.account,
@@ -356,6 +386,7 @@ describe('Session', () => {
356
386
  deposit: '10000000',
357
387
  descriptor: storedDescriptor,
358
388
  escrow: tip20ChannelEscrow,
389
+ highestVoucher,
359
390
  requiredCumulative: '1000000',
360
391
  settled: '0',
361
392
  spent: '0',
@@ -372,7 +403,11 @@ describe('Session', () => {
372
403
  ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
373
404
  : undefined
374
405
  if (payload) posted.push(payload)
375
- if (!payload) return Promise.resolve(make402Response())
406
+ if (!payload) {
407
+ expect(s.channelId).toBe(storedChannelId)
408
+ expect(s.cumulative).toBe(1_000_000n)
409
+ return Promise.resolve(make402Response())
410
+ }
376
411
  return Promise.resolve(makeOkResponse())
377
412
  })
378
413
  const s = sessionManager({
@@ -389,8 +424,8 @@ describe('Session', () => {
389
424
  expect(response.status).toBe(200)
390
425
  expect(set).toHaveBeenCalledWith(expect.objectContaining({ channelId: storedChannelId }))
391
426
  expect(posted[0]).toMatchObject({ action: 'voucher', channelId: storedChannelId })
392
- expect(resolveAccount).toHaveBeenCalledOnce()
393
- expect(resolveAccount).toHaveBeenCalledWith({
427
+ expect(resolveAccount).toHaveBeenCalledTimes(2)
428
+ expect(resolveAccount).toHaveBeenNthCalledWith(1, {
394
429
  account,
395
430
  chainId: 4217,
396
431
  operation: { authority: account.address, kind: 'authorizePaymentChannel' },
@@ -399,6 +434,103 @@ describe('Session', () => {
399
434
  expect(new Headers(contentCall?.[1]?.headers).get('Payment-Session')).toBeNull()
400
435
  })
401
436
 
437
+ test('does not cache a server snapshot for a channel that is closed on-chain', async () => {
438
+ const { store, set } = makeChannelStore()
439
+ const highestVoucher = {
440
+ channelId: storedChannelId,
441
+ cumulativeAmount: '1000000',
442
+ signature: await Voucher.signVoucher(
443
+ client,
444
+ account,
445
+ { channelId: storedChannelId, cumulativeAmount: 1_000_000n },
446
+ tip20ChannelEscrow,
447
+ 4217,
448
+ ),
449
+ }
450
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
451
+ const headers = new Headers(init?.headers)
452
+ if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization))
453
+ return Promise.resolve(make402Response(makeChargeChallenge()))
454
+ if (init?.method === 'HEAD')
455
+ return Promise.resolve(
456
+ new Response(null, {
457
+ status: 204,
458
+ headers: {
459
+ [Constants.Headers.paymentSessionSnapshot]: sessionManager.serializeSnapshot({
460
+ acceptedCumulative: '1000000',
461
+ chainId: 4217,
462
+ channelId: storedChannelId,
463
+ deposit: '10000000',
464
+ descriptor: storedDescriptor,
465
+ escrow: tip20ChannelEscrow,
466
+ highestVoucher,
467
+ requiredCumulative: '1000000',
468
+ settled: '0',
469
+ spent: '0',
470
+ units: 0,
471
+ }),
472
+ },
473
+ }),
474
+ )
475
+ return Promise.resolve(makeOkResponse())
476
+ })
477
+ const s = sessionManager({
478
+ account,
479
+ bootstrap: true,
480
+ client: channelStateClient({ closeRequestedAt: 0, deposit: 0n, settled: 0n }),
481
+ fetch: mockFetch as typeof globalThis.fetch,
482
+ channelStore: store,
483
+ })
484
+
485
+ expect((await s.fetch('https://api.example.com/data')).status).toBe(200)
486
+ expect(set).not.toHaveBeenCalled()
487
+ })
488
+
489
+ test('does not cache a server snapshot with an invalid highest voucher signature', async () => {
490
+ const { store, set } = makeChannelStore()
491
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
492
+ const headers = new Headers(init?.headers)
493
+ if (init?.method === 'HEAD' && !headers.get(Constants.Headers.authorization))
494
+ return Promise.resolve(make402Response(makeChargeChallenge()))
495
+ if (init?.method === 'HEAD')
496
+ return Promise.resolve(
497
+ new Response(null, {
498
+ status: 204,
499
+ headers: {
500
+ [Constants.Headers.paymentSessionSnapshot]: sessionManager.serializeSnapshot({
501
+ acceptedCumulative: '1000000',
502
+ chainId: 4217,
503
+ channelId: storedChannelId,
504
+ deposit: '10000000',
505
+ descriptor: storedDescriptor,
506
+ escrow: tip20ChannelEscrow,
507
+ highestVoucher: {
508
+ channelId: storedChannelId,
509
+ cumulativeAmount: '1000000',
510
+ signature: `0x${'00'.repeat(64)}`,
511
+ },
512
+ requiredCumulative: '1000000',
513
+ settled: '0',
514
+ spent: '0',
515
+ units: 0,
516
+ }),
517
+ },
518
+ }),
519
+ )
520
+ return Promise.resolve(makeOkResponse())
521
+ })
522
+ const s = sessionManager({
523
+ account,
524
+ bootstrap: true,
525
+ client,
526
+ fetch: mockFetch as typeof globalThis.fetch,
527
+ channelStore: store,
528
+ })
529
+
530
+ expect((await s.fetch('https://api.example.com/data')).status).toBe(200)
531
+ expect(set).not.toHaveBeenCalled()
532
+ })
533
+
402
534
  test('does not answer non-zero bootstrap charge challenges', async () => {
403
535
  const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
404
536
  const headers = new Headers(init?.headers)
@@ -477,6 +609,54 @@ describe('Session', () => {
477
609
  expect(s.channelId).not.toBe(storedChannelId)
478
610
  })
479
611
 
612
+ test('keeps a persisted channel after its committed top-up when the paid retry fails', async () => {
613
+ const seeded = channelEntry({ cumulativeAmount: 10_000_000n, deposit: 10_000_000n })
614
+ const { store, delete: remove, map } = makeChannelStore([seeded])
615
+ const postedPayloads: SessionCredentialPayload[] = []
616
+ let probes = 0
617
+ const mockFetch = vi.fn().mockImplementation((_input, init?: RequestInit) => {
618
+ const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
619
+ const payload = authorization
620
+ ? Credential.deserialize<SessionCredentialPayload>(authorization).payload
621
+ : undefined
622
+ if (!payload) {
623
+ probes++
624
+ if (probes > 1) throw new Error('unexpected fresh-channel retry')
625
+ return Promise.resolve(make402Response())
626
+ }
627
+
628
+ postedPayloads.push(payload)
629
+ if (payload.action === 'topUp') return Promise.resolve(new Response(null, { status: 204 }))
630
+ if (payload.action === 'voucher')
631
+ return Promise.resolve(new Response('upstream failed', { status: 500 }))
632
+ throw new Error(`unexpected ${payload.action} credential`)
633
+ })
634
+ const s = sessionManager({
635
+ account,
636
+ client,
637
+ fetch: mockFetch as typeof globalThis.fetch,
638
+ maxDeposit: '20',
639
+ channelStore: store,
640
+ })
641
+
642
+ const response = await s.fetch('https://api.example.com/data')
643
+
644
+ expect(response.status).toBe(500)
645
+ expect(postedPayloads.map((payload) => payload.action)).toEqual(['topUp', 'voucher'])
646
+ expect(remove).not.toHaveBeenCalled()
647
+ expect(map.get(entryKey(seeded))).toMatchObject({
648
+ channelId: storedChannelId,
649
+ cumulativeAmount: 10_000_000n,
650
+ deposit: 11_000_000n,
651
+ })
652
+ expect(s.channelId).toBe(storedChannelId)
653
+ expect(s.state).toMatchObject({
654
+ status: 'active',
655
+ channelId: storedChannelId,
656
+ deposit: '11000000',
657
+ })
658
+ })
659
+
480
660
  test('rolls back an optimistic open when a replacement challenge does not reference it', async () => {
481
661
  const { store, delete: remove } = makeChannelStore()
482
662
  const postedPayloads: SessionCredentialPayload[] = []
@@ -621,10 +801,11 @@ describe('Session', () => {
621
801
  await restarted.fetch('https://api.example.com/data')
622
802
 
623
803
  // The first request carries no hint header; after the 402 the restarted
624
- // manager resumes the persisted channel from the entry index with a
625
- // voucher rather than opening a new one.
804
+ // manager tops up and resumes the full persisted channel rather than
805
+ // opening a new one.
626
806
  expect(new Headers(resumeFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
627
- expect(posted[0]).toMatchObject({ action: 'voucher', channelId })
807
+ expect(posted.map((payload) => payload.action)).toEqual(['topUp', 'voucher'])
808
+ expect(posted[1]).toMatchObject({ channelId })
628
809
  })
629
810
 
630
811
  test('persists opened channels and deletes closed channels when supported', async () => {
@@ -855,6 +1036,7 @@ describe('Session', () => {
855
1036
  client,
856
1037
  fetch: mockFetch as typeof globalThis.fetch,
857
1038
  maxDeposit: '10',
1039
+ topUpAmount: '5',
858
1040
  })
859
1041
 
860
1042
  await s.fetch('https://api.example.com/data')
@@ -862,10 +1044,13 @@ describe('Session', () => {
862
1044
 
863
1045
  expect(response.status).toBe(200)
864
1046
  expect(postedPayloads.map((payload) => payload.action)).toEqual(['open', 'topUp', 'voucher'])
1047
+ const topUp = postedPayloads[1]
1048
+ if (topUp?.action !== 'topUp') throw new Error('expected top-up payload')
1049
+ expect(topUp.additionalDeposit).toBe('5000000')
865
1050
  expect(s.state).toMatchObject({
866
1051
  status: 'active',
867
1052
  acceptedCumulative: '2000000',
868
- deposit: '2000000',
1053
+ deposit: '6000000',
869
1054
  spent: '2000000',
870
1055
  units: 2,
871
1056
  })
@@ -1082,7 +1267,7 @@ describe('Session', () => {
1082
1267
  expect(voucherPayload.channelId).toBe(openPayload.channelId)
1083
1268
  expect(voucherPayload.descriptor).toEqual(openPayload.descriptor)
1084
1269
  expect(voucherPayload.cumulativeAmount).toBe('2000000')
1085
- expect(requestedUrls[2]).toBe('https://api.example.com/stream')
1270
+ expect(requestedUrls[2]).toBe('https://api.example.com/stream?prompt=paid')
1086
1271
  })
1087
1272
 
1088
1273
  test('ignores precompile SSE voucher requests for a different channel', async () => {
@@ -1266,6 +1451,51 @@ describe('Session', () => {
1266
1451
  })
1267
1452
  })
1268
1453
 
1454
+ describe('.ws()', () => {
1455
+ test('tops up a full persisted channel before constructing the socket', async () => {
1456
+ const events: string[] = []
1457
+ const { map, set, store } = makeChannelStore([
1458
+ channelEntry({ cumulativeAmount: 1_000_000n, deposit: 1_000_000n }),
1459
+ ])
1460
+ set.mockImplementation((entry) => {
1461
+ map.set(entryKey(entry), entry)
1462
+ events.push(`set:${entry.deposit}:${entry.cumulativeAmount}`)
1463
+ })
1464
+ const fetch = vi.fn(async (_input, init?: RequestInit) => {
1465
+ const authorization = new Headers(init?.headers).get(Constants.Headers.authorization)
1466
+ if (!authorization) {
1467
+ events.push('probe')
1468
+ return make402Response(makeChallenge({ suggestedDeposit: '1000000' }))
1469
+ }
1470
+ const payload = Credential.deserialize<SessionCredentialPayload>(authorization).payload
1471
+ if (payload.action !== 'topUp') throw new Error(`unexpected ${payload.action} fetch`)
1472
+ events.push('topUp')
1473
+ return new Response(null, { status: 204 })
1474
+ })
1475
+ function WebSocket() {
1476
+ events.push('socket')
1477
+ throw new Error('stop after preparation')
1478
+ }
1479
+ const s = sessionManager({
1480
+ account,
1481
+ client,
1482
+ fetch: fetch as typeof globalThis.fetch,
1483
+ maxDeposit: '3',
1484
+ channelStore: store,
1485
+ webSocket: WebSocket as never,
1486
+ })
1487
+
1488
+ await expect(s.ws('wss://api.example.com/socket')).rejects.toThrow('stop after preparation')
1489
+ expect(events).toEqual([
1490
+ 'probe',
1491
+ 'topUp',
1492
+ 'set:2000000:1000000',
1493
+ 'set:2000000:2000000',
1494
+ 'socket',
1495
+ ])
1496
+ })
1497
+ })
1498
+
1269
1499
  describe('.close()', () => {
1270
1500
  test('is no-op when not opened', async () => {
1271
1501
  const mockFetch = vi.fn()
@@ -1321,6 +1551,7 @@ describe('Session', () => {
1321
1551
  challengeId,
1322
1552
  channelId: payload.channelId,
1323
1553
  spent: BigInt(payload.cumulativeAmount),
1554
+ txHash: `0x${'aa'.repeat(32)}` as Hex,
1324
1555
  units: 1,
1325
1556
  }),
1326
1557
  ),