mppx 0.8.13 → 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.
- package/CHANGELOG.md +16 -0
- package/dist/Errors.d.ts +10 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +11 -2
- package/dist/Errors.js.map +1 -1
- package/dist/Method.d.ts +105 -3
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +102 -2
- package/dist/Method.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +12 -2
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +17 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +11 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- package/dist/server/Mppx.d.ts +23 -4
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +91 -82
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +7 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +2 -1
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +38 -77
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +289 -209
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +12 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Relay.d.ts +48 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +177 -0
- package/dist/tempo/server/Relay.js.map +1 -0
- package/dist/tempo/server/SponsorBudget.d.ts +59 -0
- package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
- package/dist/tempo/server/SponsorBudget.js +144 -0
- package/dist/tempo/server/SponsorBudget.js.map +1 -0
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/client/ChannelOps.d.ts +7 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +9 -4
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +5 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +37 -19
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Runtime.d.ts +8 -0
- package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
- package/dist/tempo/session/client/Runtime.js +17 -0
- package/dist/tempo/session/client/Runtime.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +15 -5
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +93 -36
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +8 -1
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +54 -26
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +7 -5
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +48 -24
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +96 -8
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/precompile/Channel.d.ts +2 -0
- package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Channel.js +4 -0
- package/dist/tempo/session/precompile/Channel.js.map +1 -1
- package/dist/tempo/session/server/RequestState.js +3 -1
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/package.json +1 -1
- package/src/Errors.test.ts +23 -0
- package/src/Errors.ts +21 -2
- package/src/Method.test.ts +102 -1
- package/src/Method.ts +241 -5
- package/src/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.ts +16 -7
- package/src/client/internal/MethodChallenge.ts +30 -0
- package/src/server/Mppx.test-d.ts +2 -0
- package/src/server/Mppx.test.ts +205 -0
- package/src/server/Mppx.ts +167 -103
- package/src/tempo/PublicExports.test-d.ts +20 -0
- package/src/tempo/internal/fee-payer.ts +7 -3
- package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
- package/src/tempo/legacy/client/ChannelOps.ts +2 -1
- package/src/tempo/server/Charge.test.ts +337 -63
- package/src/tempo/server/Charge.ts +435 -267
- package/src/tempo/server/Methods.ts +11 -0
- package/src/tempo/server/Relay.test.ts +523 -0
- package/src/tempo/server/Relay.ts +288 -0
- package/src/tempo/server/SponsorBudget.test.ts +125 -0
- package/src/tempo/server/SponsorBudget.ts +213 -0
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/ChannelOps.test.ts +17 -2
- package/src/tempo/session/client/ChannelOps.ts +12 -4
- package/src/tempo/session/client/CredentialState.ts +61 -20
- package/src/tempo/session/client/Runtime.test.ts +12 -0
- package/src/tempo/session/client/Runtime.ts +22 -0
- package/src/tempo/session/client/Session.test.ts +235 -2
- package/src/tempo/session/client/Session.ts +130 -41
- package/src/tempo/session/client/SessionManager.test.ts +102 -4
- package/src/tempo/session/client/SessionManager.ts +71 -27
- package/src/tempo/session/client/Transports.test.ts +32 -1
- package/src/tempo/session/client/Transports.ts +79 -35
- package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
- package/src/tempo/session/precompile/Chain.test.ts +197 -10
- package/src/tempo/session/precompile/Chain.ts +102 -8
- package/src/tempo/session/precompile/Channel.test.ts +9 -0
- package/src/tempo/session/precompile/Channel.ts +5 -0
- package/src/tempo/session/server/RequestState.test.ts +8 -1
- package/src/tempo/session/server/RequestState.ts +2 -2
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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. */
|
|
@@ -609,6 +609,54 @@ describe('Session', () => {
|
|
|
609
609
|
expect(s.channelId).not.toBe(storedChannelId)
|
|
610
610
|
})
|
|
611
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
|
+
|
|
612
660
|
test('rolls back an optimistic open when a replacement challenge does not reference it', async () => {
|
|
613
661
|
const { store, delete: remove } = makeChannelStore()
|
|
614
662
|
const postedPayloads: SessionCredentialPayload[] = []
|
|
@@ -753,10 +801,11 @@ describe('Session', () => {
|
|
|
753
801
|
await restarted.fetch('https://api.example.com/data')
|
|
754
802
|
|
|
755
803
|
// The first request carries no hint header; after the 402 the restarted
|
|
756
|
-
// manager resumes the persisted channel
|
|
757
|
-
//
|
|
804
|
+
// manager tops up and resumes the full persisted channel rather than
|
|
805
|
+
// opening a new one.
|
|
758
806
|
expect(new Headers(resumeFetch.mock.calls[0]?.[1]?.headers).get('Payment-Session')).toBeNull()
|
|
759
|
-
expect(posted
|
|
807
|
+
expect(posted.map((payload) => payload.action)).toEqual(['topUp', 'voucher'])
|
|
808
|
+
expect(posted[1]).toMatchObject({ channelId })
|
|
760
809
|
})
|
|
761
810
|
|
|
762
811
|
test('persists opened channels and deletes closed channels when supported', async () => {
|
|
@@ -987,6 +1036,7 @@ describe('Session', () => {
|
|
|
987
1036
|
client,
|
|
988
1037
|
fetch: mockFetch as typeof globalThis.fetch,
|
|
989
1038
|
maxDeposit: '10',
|
|
1039
|
+
topUpAmount: '5',
|
|
990
1040
|
})
|
|
991
1041
|
|
|
992
1042
|
await s.fetch('https://api.example.com/data')
|
|
@@ -994,10 +1044,13 @@ describe('Session', () => {
|
|
|
994
1044
|
|
|
995
1045
|
expect(response.status).toBe(200)
|
|
996
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')
|
|
997
1050
|
expect(s.state).toMatchObject({
|
|
998
1051
|
status: 'active',
|
|
999
1052
|
acceptedCumulative: '2000000',
|
|
1000
|
-
deposit: '
|
|
1053
|
+
deposit: '6000000',
|
|
1001
1054
|
spent: '2000000',
|
|
1002
1055
|
units: 2,
|
|
1003
1056
|
})
|
|
@@ -1398,6 +1451,51 @@ describe('Session', () => {
|
|
|
1398
1451
|
})
|
|
1399
1452
|
})
|
|
1400
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
|
+
|
|
1401
1499
|
describe('.close()', () => {
|
|
1402
1500
|
test('is no-op when not opened', async () => {
|
|
1403
1501
|
const mockFetch = vi.fn()
|
|
@@ -4,6 +4,7 @@ import { tempo as tempo_chain } from 'viem/chains'
|
|
|
4
4
|
|
|
5
5
|
import * as Challenge from '../../../Challenge.js'
|
|
6
6
|
import * as Fetch from '../../../client/internal/Fetch.js'
|
|
7
|
+
import * as MethodChallenge from '../../../client/internal/MethodChallenge.js'
|
|
7
8
|
import * as MethodResponse from '../../../client/internal/MethodResponse.js'
|
|
8
9
|
import * as Constants from '../../../Constants.js'
|
|
9
10
|
import * as Account from '../../../viem/Account.js'
|
|
@@ -16,7 +17,7 @@ import { hydrateSessionSnapshot, type SessionContext } from '../client/Credentia
|
|
|
16
17
|
import { session as sessionPlugin } from '../client/Session.js'
|
|
17
18
|
import * as Channel from '../precompile/Channel.js'
|
|
18
19
|
import { deserializeSessionReceipt } from '../precompile/Protocol.js'
|
|
19
|
-
import {
|
|
20
|
+
import type { SessionReceipt } from '../precompile/Protocol.js'
|
|
20
21
|
import {
|
|
21
22
|
deserializeSnapshot as deserializeSessionSnapshot,
|
|
22
23
|
serializeSnapshot as serializeSessionSnapshot,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
dispatchSessionEvent,
|
|
35
36
|
restoreCumulativeAuthorization,
|
|
36
37
|
restoreRuntimeSnapshot as restoreRuntimeStateSnapshot,
|
|
38
|
+
resolveAutomaticTopUp,
|
|
37
39
|
type RuntimeSnapshot,
|
|
38
40
|
} from './Runtime.js'
|
|
39
41
|
import { closeSocketSession } from './Runtime.js'
|
|
@@ -41,7 +43,6 @@ import {
|
|
|
41
43
|
closeHttpSession,
|
|
42
44
|
getSessionSnapshot,
|
|
43
45
|
isTempoSessionChallenge,
|
|
44
|
-
managementInput,
|
|
45
46
|
postTopUp,
|
|
46
47
|
retryHttpPaymentRequired,
|
|
47
48
|
type TempoSessionChallenge,
|
|
@@ -111,6 +112,8 @@ type SessionManagerConfig = {
|
|
|
111
112
|
fetch: typeof globalThis.fetch
|
|
112
113
|
/** Local maximum cumulative voucher authorization, or null when uncapped. */
|
|
113
114
|
maxVoucherCumulative: bigint | null
|
|
115
|
+
/** Preferred top-up, or null to use a bounded server suggestion and then the exact shortfall. */
|
|
116
|
+
topUpAmount: bigint | null
|
|
114
117
|
/** WebSocket constructor available in the current runtime, when configured. */
|
|
115
118
|
WebSocket: WebSocketConstructor | undefined
|
|
116
119
|
}
|
|
@@ -168,6 +171,8 @@ function resolveSessionManagerConfig(parameters: sessionManager.Parameters): Ses
|
|
|
168
171
|
fetch: parameters.fetch ?? globalThis.fetch.bind(globalThis),
|
|
169
172
|
maxVoucherCumulative:
|
|
170
173
|
parameters.maxDeposit !== undefined ? parseUnits(parameters.maxDeposit, decimals) : null,
|
|
174
|
+
topUpAmount:
|
|
175
|
+
parameters.topUpAmount !== undefined ? parseUnits(parameters.topUpAmount, decimals) : null,
|
|
171
176
|
WebSocket,
|
|
172
177
|
}
|
|
173
178
|
}
|
|
@@ -201,6 +206,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
201
206
|
// Tracks one fetch's channel reuse so stale stored entries can be evicted once.
|
|
202
207
|
type ChannelUse = {
|
|
203
208
|
challengesReceived: number
|
|
209
|
+
committed: RuntimeSnapshot | undefined
|
|
204
210
|
created: Map<string, ChannelEntry>
|
|
205
211
|
seenExisting: Set<string>
|
|
206
212
|
previous: RuntimeSnapshot
|
|
@@ -221,7 +227,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
221
227
|
const entry = await getReusable(key)
|
|
222
228
|
if (entry && channelUse) {
|
|
223
229
|
channelUse.seenExisting.add(key)
|
|
224
|
-
if (!channelUse.created.has(key)) channelUse.resumed ??= entry
|
|
230
|
+
if (!channelUse.committed && !channelUse.created.has(key)) channelUse.resumed ??= entry
|
|
225
231
|
}
|
|
226
232
|
return entry
|
|
227
233
|
},
|
|
@@ -270,13 +276,32 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
270
276
|
return dispatchSessionEvent(runtime, event)
|
|
271
277
|
}
|
|
272
278
|
|
|
279
|
+
function commitDurableTopUp(entry: ChannelEntry) {
|
|
280
|
+
const use = channelUse
|
|
281
|
+
const baseline = use?.committed?.channel?.entry ?? use?.resumed
|
|
282
|
+
if (
|
|
283
|
+
!use ||
|
|
284
|
+
baseline?.channelId.toLowerCase() !== entry.channelId.toLowerCase() ||
|
|
285
|
+
entry.deposit <= baseline.deposit
|
|
286
|
+
)
|
|
287
|
+
return
|
|
288
|
+
use.resumed = undefined
|
|
289
|
+
use.committed = captureRuntimeStateSnapshot({
|
|
290
|
+
channel: runtime.channel,
|
|
291
|
+
spent: runtime.spent,
|
|
292
|
+
state: runtime.state,
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
273
296
|
const method = sessionPlugin({
|
|
274
297
|
account: parameters.account,
|
|
298
|
+
autoSwap: parameters.autoSwap,
|
|
275
299
|
getClient: parameters.client ? () => parameters.client! : parameters.getClient,
|
|
276
300
|
resolveAccount: parameters.resolveAccount,
|
|
277
301
|
escrow: parameters.escrow,
|
|
278
302
|
decimals: config.decimals,
|
|
279
303
|
maxDeposit: parameters.maxDeposit,
|
|
304
|
+
topUpAmount: parameters.topUpAmount,
|
|
280
305
|
channelStore: store,
|
|
281
306
|
onChannelUpdate(entry) {
|
|
282
307
|
if (entry.channelId !== runtime.channel?.channelId) runtime.spent = 0n
|
|
@@ -290,6 +315,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
290
315
|
units: 0,
|
|
291
316
|
})
|
|
292
317
|
}
|
|
318
|
+
commitDurableTopUp(entry)
|
|
293
319
|
},
|
|
294
320
|
})
|
|
295
321
|
MethodResponse.unregister(method)
|
|
@@ -313,17 +339,6 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
313
339
|
if (use) use.challengesReceived++
|
|
314
340
|
runtime.lastChallenge = challenge
|
|
315
341
|
dispatch({ type: 'challengeReceived', challengeId: challenge.id })
|
|
316
|
-
if (runtime.channel?.opened && runtime.lastUrl) {
|
|
317
|
-
const requiredCumulative =
|
|
318
|
-
runtime.channel.cumulativeAmount + readSessionChallengeAmount(challenge)
|
|
319
|
-
await topUpIfNeeded({
|
|
320
|
-
challenge,
|
|
321
|
-
input: runtime.lastUrl,
|
|
322
|
-
channelId: runtime.channel.channelId,
|
|
323
|
-
deposit: runtime.channel.deposit,
|
|
324
|
-
requiredCumulative,
|
|
325
|
-
})
|
|
326
|
-
}
|
|
327
342
|
return undefined
|
|
328
343
|
},
|
|
329
344
|
})
|
|
@@ -366,16 +381,12 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
366
381
|
const snapshot = deserializeSessionSnapshot(header)
|
|
367
382
|
const client = await getClient({ chainId: snapshot.chainId })
|
|
368
383
|
const defaultAccount = getAccount(client)
|
|
369
|
-
const authority =
|
|
370
|
-
BigInt(snapshot.descriptor.authorizedSigner) === 0n
|
|
371
|
-
? snapshot.descriptor.payer
|
|
372
|
-
: snapshot.descriptor.authorizedSigner
|
|
373
384
|
const account =
|
|
374
385
|
(await parameters.resolveAccount?.({
|
|
375
386
|
account: defaultAccount,
|
|
376
387
|
chainId: snapshot.chainId,
|
|
377
388
|
operation: {
|
|
378
|
-
authority,
|
|
389
|
+
authority: Channel.resolveAuthorizedSigner(snapshot.descriptor),
|
|
379
390
|
kind: 'authorizePaymentChannel',
|
|
380
391
|
},
|
|
381
392
|
})) ?? defaultAccount
|
|
@@ -520,9 +531,11 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
520
531
|
challenge: TempoSessionChallenge
|
|
521
532
|
channelId: Hex.Hex
|
|
522
533
|
input: RequestInfo | URL
|
|
534
|
+
knownDeposit?: bigint | undefined
|
|
523
535
|
}) {
|
|
536
|
+
const { knownDeposit, ...topUp } = parameters
|
|
524
537
|
const receipt = await postTopUp({
|
|
525
|
-
...
|
|
538
|
+
...topUp,
|
|
526
539
|
channel: runtime.channel,
|
|
527
540
|
createSessionCredential,
|
|
528
541
|
fetch: config.fetch,
|
|
@@ -534,6 +547,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
534
547
|
channelId: parameters.channelId,
|
|
535
548
|
challengeId: runtime.lastChallenge?.id,
|
|
536
549
|
currentState: runtime.state,
|
|
550
|
+
knownDeposit,
|
|
537
551
|
receipt,
|
|
538
552
|
spent: runtime.spent,
|
|
539
553
|
})
|
|
@@ -546,18 +560,32 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
546
560
|
spent: runtime.spent.toString(),
|
|
547
561
|
units: runtime.state.status === 'active' ? runtime.state.units : 0,
|
|
548
562
|
})
|
|
563
|
+
commitDurableTopUp(applied.channel)
|
|
549
564
|
}
|
|
550
565
|
return receipt
|
|
551
566
|
}
|
|
552
567
|
|
|
553
568
|
async function topUpIfNeeded(parameters: TopUpRequirement) {
|
|
554
|
-
|
|
555
|
-
|
|
569
|
+
const channelDeposit =
|
|
570
|
+
runtime.channel?.channelId === parameters.channelId ? runtime.channel.deposit : 0n
|
|
571
|
+
const deposit = channelDeposit > parameters.deposit ? channelDeposit : parameters.deposit
|
|
572
|
+
const additionalDeposit = resolveAutomaticTopUp({
|
|
573
|
+
deposit,
|
|
574
|
+
maxDeposit: config.maxVoucherCumulative,
|
|
575
|
+
requiredCumulative: parameters.requiredCumulative,
|
|
576
|
+
suggestedDeposit:
|
|
577
|
+
parameters.challenge.request.suggestedDeposit === undefined
|
|
578
|
+
? undefined
|
|
579
|
+
: BigInt(parameters.challenge.request.suggestedDeposit),
|
|
580
|
+
topUpAmount: config.topUpAmount,
|
|
581
|
+
})
|
|
582
|
+
if (additionalDeposit === 0n) return
|
|
556
583
|
await postTopUpAndApply({
|
|
557
584
|
challenge: parameters.challenge,
|
|
558
585
|
input: parameters.input,
|
|
559
586
|
channelId: parameters.channelId,
|
|
560
|
-
additionalDeposit
|
|
587
|
+
additionalDeposit,
|
|
588
|
+
knownDeposit: deposit,
|
|
561
589
|
})
|
|
562
590
|
}
|
|
563
591
|
|
|
@@ -617,6 +645,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
617
645
|
})
|
|
618
646
|
const use: ChannelUse = {
|
|
619
647
|
challengesReceived: 0,
|
|
648
|
+
committed: undefined,
|
|
620
649
|
created: new Map(),
|
|
621
650
|
previous,
|
|
622
651
|
seenExisting: new Set(),
|
|
@@ -650,7 +679,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
650
679
|
try {
|
|
651
680
|
response = await wrappedFetch(input, effectiveInit)
|
|
652
681
|
} catch (error) {
|
|
653
|
-
restoreRuntime(previous)
|
|
682
|
+
restoreRuntime(use.committed ?? previous)
|
|
654
683
|
if (await retryWithoutResumed()) continue
|
|
655
684
|
throw error
|
|
656
685
|
}
|
|
@@ -677,7 +706,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
677
706
|
}
|
|
678
707
|
}
|
|
679
708
|
if (!attemptedHttpManagement && !paymentResponse.ok && !paymentResponse.receipt) {
|
|
680
|
-
restoreRuntime(previous)
|
|
709
|
+
restoreRuntime(use.committed ?? previous)
|
|
681
710
|
if (await retryWithoutResumed()) continue
|
|
682
711
|
return paymentResponse
|
|
683
712
|
}
|
|
@@ -719,7 +748,6 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
719
748
|
getChannel: () => runtime.channel,
|
|
720
749
|
getChallenge: () => runtime.lastChallenge,
|
|
721
750
|
assertVoucherWithinLocalLimit,
|
|
722
|
-
managementInput,
|
|
723
751
|
acceptReceipt(receipt: SessionReceipt) {
|
|
724
752
|
updateSpentFromReceipt(receipt)
|
|
725
753
|
},
|
|
@@ -777,7 +805,16 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
777
805
|
const liveHint = runtime.channel?.opened ? runtime.channel.channelId : undefined
|
|
778
806
|
|
|
779
807
|
const prepared = await prepareWebSocketSession({
|
|
780
|
-
createSessionCredential,
|
|
808
|
+
async createSessionCredential(challenge, context) {
|
|
809
|
+
runtime.lastChallenge = challenge
|
|
810
|
+
await MethodChallenge.handle(method, {
|
|
811
|
+
challenge,
|
|
812
|
+
context,
|
|
813
|
+
fetch: config.fetch,
|
|
814
|
+
input: probeUrl,
|
|
815
|
+
})
|
|
816
|
+
return createSessionCredential(challenge, context)
|
|
817
|
+
},
|
|
781
818
|
fetch: config.fetch,
|
|
782
819
|
input,
|
|
783
820
|
onProbeUrl(httpUrl) {
|
|
@@ -884,6 +921,8 @@ export namespace sessionManager {
|
|
|
884
921
|
|
|
885
922
|
export type Parameters = Account.getResolver.Parameters &
|
|
886
923
|
Client.getResolver.Parameters & {
|
|
924
|
+
/** Automatically acquire the session currency from fallback stablecoins before open/top-up. */
|
|
925
|
+
autoSwap?: sessionPlugin.Parameters['autoSwap']
|
|
887
926
|
/** Enables same-route HEAD bootstrap from a server session snapshot before opening a new channel. */
|
|
888
927
|
bootstrap?: boolean | undefined
|
|
889
928
|
/** Viem client instance. Shorthand for `getClient: () => client`. */
|
|
@@ -896,6 +935,11 @@ export namespace sessionManager {
|
|
|
896
935
|
fetch?: typeof globalThis.fetch | undefined
|
|
897
936
|
/** Maximum deposit in human-readable units (e.g. `'10'` for 10 tokens). Converted to raw units via `decimals`. */
|
|
898
937
|
maxDeposit?: string | undefined
|
|
938
|
+
/**
|
|
939
|
+
* Preferred automatic top-up size in human-readable units. When omitted,
|
|
940
|
+
* a bounded server `suggestedDeposit` is preferred, then the exact shortfall.
|
|
941
|
+
*/
|
|
942
|
+
topUpAmount?: string | undefined
|
|
899
943
|
/** Selects the account that signs session credentials. */
|
|
900
944
|
resolveAccount?: sessionPlugin.ResolveAccount | undefined
|
|
901
945
|
/** Store for reusable session channels. Defaults to in-memory. */
|