mppx 0.8.11 → 0.8.13
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 +25 -0
- package/README.md +16 -1
- package/dist/cli/account.d.ts +88 -0
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +39 -9
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +59 -2
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/sessions/Manager.d.ts +31 -0
- package/dist/cli/sessions/Manager.d.ts.map +1 -0
- package/dist/cli/sessions/Manager.js +56 -0
- package/dist/cli/sessions/Manager.js.map +1 -0
- package/dist/cli/sessions/commands.d.ts +36 -0
- package/dist/cli/sessions/commands.d.ts.map +1 -0
- package/dist/cli/sessions/commands.js +407 -0
- package/dist/cli/sessions/commands.js.map +1 -0
- package/dist/cli/sessions/request.d.ts +35 -0
- package/dist/cli/sessions/request.d.ts.map +1 -0
- package/dist/cli/sessions/request.js +271 -0
- package/dist/cli/sessions/request.js.map +1 -0
- package/dist/cli/sessions/store.d.ts +134 -0
- package/dist/cli/sessions/store.d.ts.map +1 -0
- package/dist/cli/sessions/store.js +723 -0
- package/dist/cli/sessions/store.js.map +1 -0
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +12 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +12 -6
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/messages.d.ts +2 -0
- package/dist/cli/validate/messages.d.ts.map +1 -0
- package/dist/cli/validate/messages.js +8 -0
- package/dist/cli/validate/messages.js.map +1 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +35 -6
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodResponse.d.ts +23 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -0
- package/dist/client/internal/MethodResponse.js +15 -0
- package/dist/client/internal/MethodResponse.js.map +1 -0
- package/dist/client/node.d.ts +29 -0
- package/dist/client/node.d.ts.map +1 -0
- package/dist/client/node.js +177 -0
- package/dist/client/node.js.map +1 -0
- package/dist/internal/types.d.ts +24 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts +8 -4
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -1
- package/dist/tempo/internal/types.d.ts +10 -1
- package/dist/tempo/internal/types.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts +2 -0
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +4 -0
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/legacy/client/Session.d.ts +2 -2
- package/dist/tempo/legacy/client/Session.d.ts.map +1 -1
- package/dist/tempo/legacy/client/Session.js +7 -0
- package/dist/tempo/legacy/client/Session.js.map +1 -1
- package/dist/tempo/legacy/server/Session.d.ts +3 -3
- package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
- package/dist/tempo/legacy/server/Session.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +3 -2
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +2 -23
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +2 -2
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +3 -2
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/index.d.ts +1 -1
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js.map +1 -1
- 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/Snapshot.d.ts +9 -0
- package/dist/tempo/session/Snapshot.d.ts.map +1 -1
- package/dist/tempo/session/Snapshot.js +6 -0
- package/dist/tempo/session/Snapshot.js.map +1 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +11 -2
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +21 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +66 -1
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +1 -1
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +53 -1
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +2 -2
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +130 -30
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +21 -2
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +129 -18
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
- package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
- package/dist/tempo/session/client/internal/SessionManager.js +13 -0
- package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
- package/dist/tempo/session/precompile/Chain.js +13 -4
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +14 -0
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
- package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
- package/dist/tempo/session/server/MeteredStream.js +5 -4
- package/dist/tempo/session/server/MeteredStream.js.map +1 -1
- package/dist/tempo/session/server/RequestState.d.ts +5 -1
- package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
- package/dist/tempo/session/server/RequestState.js +10 -4
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts +21 -6
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +4 -1
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/tempo/session/server/Settlement.d.ts +20 -0
- package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
- package/dist/tempo/session/server/Settlement.js +20 -0
- package/dist/tempo/session/server/Settlement.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts +2 -0
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/session/server/index.d.ts +1 -1
- package/dist/tempo/session/server/index.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +6 -2
- package/dist/validation/core.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +24 -17
- package/dist/viem/Client.js.map +1 -1
- package/package.json +6 -1
- package/src/cli/account.ts +45 -10
- package/src/cli/cli.test.ts +165 -70
- package/src/cli/cli.ts +67 -2
- package/src/cli/mcp.test.ts +11 -0
- package/src/cli/sessions/Manager.test.ts +249 -0
- package/src/cli/sessions/Manager.ts +93 -0
- package/src/cli/sessions/commands.ts +444 -0
- package/src/cli/sessions/request.test.ts +51 -0
- package/src/cli/sessions/request.ts +353 -0
- package/src/cli/sessions/store.test.ts +581 -0
- package/src/cli/sessions/store.ts +940 -0
- package/src/cli/utils.test.ts +23 -0
- package/src/cli/utils.ts +10 -0
- package/src/cli/validate/index.ts +12 -14
- package/src/cli/validate/messages.ts +7 -0
- package/src/cli/validate.test.ts +38 -5
- package/src/client/internal/Fetch.test.ts +29 -0
- package/src/client/internal/Fetch.ts +51 -6
- package/src/client/internal/MethodResponse.ts +39 -0
- package/src/client/node.test.ts +115 -0
- package/src/client/node.ts +247 -0
- package/src/internal/types.test-d.ts +21 -0
- package/src/internal/types.ts +33 -0
- package/src/server/Mppx.test-d.ts +42 -0
- package/src/stripe/Methods.test.ts +10 -0
- package/src/stripe/server/Charge.test-d.ts +66 -0
- package/src/stripe/server/Charge.ts +8 -5
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/internal/types.ts +13 -2
- package/src/tempo/legacy/client/ChannelOps.test.ts +21 -0
- package/src/tempo/legacy/client/ChannelOps.ts +7 -0
- package/src/tempo/legacy/client/Session.test.ts +102 -1
- package/src/tempo/legacy/client/Session.ts +10 -0
- package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
- package/src/tempo/legacy/server/Session.ts +6 -5
- package/src/tempo/server/Charge.ts +6 -4
- package/src/tempo/server/Methods.ts +5 -4
- package/src/tempo/server/Subscription.ts +6 -4
- package/src/tempo/server/index.ts +5 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +201 -0
- package/src/tempo/session/Snapshot.ts +21 -0
- package/src/tempo/session/client/ChannelOps.test.ts +40 -0
- package/src/tempo/session/client/ChannelOps.ts +12 -2
- package/src/tempo/session/client/CredentialState.test.ts +2 -2
- package/src/tempo/session/client/CredentialState.ts +103 -1
- package/src/tempo/session/client/Session.test.ts +86 -4
- package/src/tempo/session/client/Session.ts +73 -1
- package/src/tempo/session/client/SessionManager.test.ts +137 -4
- package/src/tempo/session/client/SessionManager.ts +141 -33
- package/src/tempo/session/client/Transports.test.ts +225 -8
- package/src/tempo/session/client/Transports.ts +156 -21
- package/src/tempo/session/client/internal/SessionManager.ts +36 -0
- package/src/tempo/session/precompile/Chain.test.ts +26 -0
- package/src/tempo/session/precompile/Chain.ts +17 -4
- package/src/tempo/session/server/CredentialVerification.ts +18 -1
- package/src/tempo/session/server/MeteredStream.ts +12 -5
- package/src/tempo/session/server/RequestState.test.ts +39 -3
- package/src/tempo/session/server/RequestState.ts +13 -6
- package/src/tempo/session/server/Session.test.ts +461 -0
- package/src/tempo/session/server/Session.ts +31 -8
- package/src/tempo/session/server/Settlement.ts +45 -0
- package/src/tempo/session/server/Ws.test.ts +113 -0
- package/src/tempo/session/server/Ws.ts +2 -0
- package/src/tempo/session/server/index.ts +2 -0
- package/src/validation/core.ts +7 -1
- package/src/viem/Client.test.ts +44 -1
- package/src/viem/Client.ts +31 -17
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type Address, parseUnits } from 'viem'
|
|
2
2
|
import { tempo as tempo_chain } from 'viem/chains'
|
|
3
3
|
|
|
4
|
+
import * as MethodResponse from '../../../client/internal/MethodResponse.js'
|
|
4
5
|
import * as Constants from '../../../Constants.js'
|
|
6
|
+
import * as Credential from '../../../Credential.js'
|
|
5
7
|
import * as Method from '../../../Method.js'
|
|
6
8
|
import * as Account from '../../../viem/Account.js'
|
|
7
9
|
import * as Client from '../../../viem/Client.js'
|
|
@@ -11,6 +13,7 @@ import type {
|
|
|
11
13
|
} from '../../client/ResolveAccount.js'
|
|
12
14
|
import * as defaults from '../../internal/defaults.js'
|
|
13
15
|
import * as Methods from '../../Methods.js'
|
|
16
|
+
import { isEventStream, requireSessionCredentialContext } from '../precompile/Protocol.js'
|
|
14
17
|
import { serializeCredential, type ChannelEntry } from './ChannelOps.js'
|
|
15
18
|
import { createChannelStore, type ChannelStore } from './ChannelStore.js'
|
|
16
19
|
import {
|
|
@@ -20,6 +23,15 @@ import {
|
|
|
20
23
|
resolveRecoverContext,
|
|
21
24
|
sessionContextSchema,
|
|
22
25
|
} from './CredentialState.js'
|
|
26
|
+
import { assertWithinMaxDeposit } from './Runtime.js'
|
|
27
|
+
import {
|
|
28
|
+
handleSseNeedVoucher,
|
|
29
|
+
isTempoSessionChallenge,
|
|
30
|
+
managementInput,
|
|
31
|
+
postTopUp,
|
|
32
|
+
wrapSseResponse,
|
|
33
|
+
type SsePaymentDriver,
|
|
34
|
+
} from './Transports.js'
|
|
23
35
|
|
|
24
36
|
export { sessionContextSchema, type SessionContext } from './CredentialState.js'
|
|
25
37
|
|
|
@@ -52,7 +64,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
52
64
|
const store = channelStore ?? createChannelStore()
|
|
53
65
|
const sink = { store, notifyUpdate: (entry: ChannelEntry) => onChannelUpdate?.(entry) }
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
const method = Method.toClient(Methods.session, {
|
|
56
68
|
canHandleChallenge({ challenge }) {
|
|
57
69
|
return (
|
|
58
70
|
Constants.getMethodDetail(
|
|
@@ -98,6 +110,66 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
98
110
|
return serializeCredential(challenge, payload, resolved.chainId, account)
|
|
99
111
|
},
|
|
100
112
|
})
|
|
113
|
+
|
|
114
|
+
return MethodResponse.register(
|
|
115
|
+
method,
|
|
116
|
+
async ({ challenge, credential, fetch, headers, input, refetch, response, signal }) => {
|
|
117
|
+
if (!isTempoSessionChallenge(challenge)) return response
|
|
118
|
+
if (!isEventStream(response)) {
|
|
119
|
+
const credentialContext = requireSessionCredentialContext(
|
|
120
|
+
Credential.deserialize(credential).payload,
|
|
121
|
+
)
|
|
122
|
+
if (
|
|
123
|
+
credentialContext.action === 'open' &&
|
|
124
|
+
headers.get('accept')?.toLowerCase().includes('text/event-stream')
|
|
125
|
+
)
|
|
126
|
+
return (await refetch?.()) ?? response
|
|
127
|
+
return response
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const channelKey = (
|
|
131
|
+
await resolveChallengeContext({
|
|
132
|
+
challenge,
|
|
133
|
+
escrowOverride,
|
|
134
|
+
getClient,
|
|
135
|
+
})
|
|
136
|
+
).key
|
|
137
|
+
let channel = await store.get(channelKey)
|
|
138
|
+
const driver = {
|
|
139
|
+
assertVoucherWithinLocalLimit: (cumulativeAmount) =>
|
|
140
|
+
assertWithinMaxDeposit(cumulativeAmount, maxDeposit),
|
|
141
|
+
createSessionCredential: (challenge, context) =>
|
|
142
|
+
method.createCredential({ challenge, context }),
|
|
143
|
+
fetch,
|
|
144
|
+
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,
|
|
151
|
+
challenge,
|
|
152
|
+
channel,
|
|
153
|
+
channelId,
|
|
154
|
+
createSessionCredential: (challenge, context) =>
|
|
155
|
+
method.createCredential({ challenge, context }),
|
|
156
|
+
fetch,
|
|
157
|
+
input,
|
|
158
|
+
})
|
|
159
|
+
channel.deposit += additionalDeposit
|
|
160
|
+
await store.set(channel)
|
|
161
|
+
sink.notifyUpdate(channel)
|
|
162
|
+
},
|
|
163
|
+
} satisfies SsePaymentDriver
|
|
164
|
+
|
|
165
|
+
return wrapSseResponse({
|
|
166
|
+
onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
|
|
167
|
+
onReceipt() {},
|
|
168
|
+
response,
|
|
169
|
+
signal,
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
)
|
|
101
173
|
}
|
|
102
174
|
|
|
103
175
|
/** Type helpers for the low-level TIP-1034 session client method. */
|
|
@@ -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)
|
|
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).
|
|
393
|
-
expect(resolveAccount).
|
|
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)
|
|
@@ -1082,7 +1214,7 @@ describe('Session', () => {
|
|
|
1082
1214
|
expect(voucherPayload.channelId).toBe(openPayload.channelId)
|
|
1083
1215
|
expect(voucherPayload.descriptor).toEqual(openPayload.descriptor)
|
|
1084
1216
|
expect(voucherPayload.cumulativeAmount).toBe('2000000')
|
|
1085
|
-
expect(requestedUrls[2]).toBe('https://api.example.com/stream')
|
|
1217
|
+
expect(requestedUrls[2]).toBe('https://api.example.com/stream?prompt=paid')
|
|
1086
1218
|
})
|
|
1087
1219
|
|
|
1088
1220
|
test('ignores precompile SSE voucher requests for a different channel', async () => {
|
|
@@ -1321,6 +1453,7 @@ describe('Session', () => {
|
|
|
1321
1453
|
challengeId,
|
|
1322
1454
|
channelId: payload.channelId,
|
|
1323
1455
|
spent: BigInt(payload.cumulativeAmount),
|
|
1456
|
+
txHash: `0x${'aa'.repeat(32)}` as Hex,
|
|
1324
1457
|
units: 1,
|
|
1325
1458
|
}),
|
|
1326
1459
|
),
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import type { Hex } from 'ox'
|
|
2
2
|
import { parseUnits, type Address } from 'viem'
|
|
3
|
+
import { tempo as tempo_chain } from 'viem/chains'
|
|
3
4
|
|
|
4
5
|
import * as Challenge from '../../../Challenge.js'
|
|
5
6
|
import * as Fetch from '../../../client/internal/Fetch.js'
|
|
7
|
+
import * as MethodResponse from '../../../client/internal/MethodResponse.js'
|
|
6
8
|
import * as Constants from '../../../Constants.js'
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
+
import * as Account from '../../../viem/Account.js'
|
|
10
|
+
import * as Client from '../../../viem/Client.js'
|
|
9
11
|
import { charge as chargePlugin } from '../../client/Charge.js'
|
|
12
|
+
import * as defaults from '../../internal/defaults.js'
|
|
10
13
|
import type { ChannelEntry } from '../client/ChannelOps.js'
|
|
11
14
|
import { createChannelStore, entryKey, type ChannelStore } from '../client/ChannelStore.js'
|
|
12
|
-
import type
|
|
15
|
+
import { hydrateSessionSnapshot, type SessionContext } from '../client/CredentialState.js'
|
|
13
16
|
import { session as sessionPlugin } from '../client/Session.js'
|
|
17
|
+
import * as Channel from '../precompile/Channel.js'
|
|
14
18
|
import { deserializeSessionReceipt } from '../precompile/Protocol.js'
|
|
15
19
|
import { readSessionChallengeAmount, type SessionReceipt } from '../precompile/Protocol.js'
|
|
16
20
|
import {
|
|
17
21
|
deserializeSnapshot as deserializeSessionSnapshot,
|
|
18
22
|
serializeSnapshot as serializeSessionSnapshot,
|
|
19
23
|
} from '../Snapshot.js'
|
|
24
|
+
import { registerSessionManagerInternals } from './internal/SessionManager.js'
|
|
20
25
|
import { createSessionReceiptCoordinator } from './ReceiptCoordinator.js'
|
|
21
26
|
import { resolveCloseTarget, type CloseTarget } from './Runtime.js'
|
|
22
27
|
import { assertVoucherWithinLocalLimit as assertVoucherWithinLocalAuthorization } from './Runtime.js'
|
|
@@ -34,6 +39,7 @@ import {
|
|
|
34
39
|
import { closeSocketSession } from './Runtime.js'
|
|
35
40
|
import {
|
|
36
41
|
closeHttpSession,
|
|
42
|
+
getSessionSnapshot,
|
|
37
43
|
isTempoSessionChallenge,
|
|
38
44
|
managementInput,
|
|
39
45
|
postTopUp,
|
|
@@ -46,7 +52,7 @@ import {
|
|
|
46
52
|
type WebSocketConstructor,
|
|
47
53
|
WebSocketReadyState,
|
|
48
54
|
} from './Transports.js'
|
|
49
|
-
import { openSseSession, type SseDriverOptions } from './Transports.js'
|
|
55
|
+
import { consumeSseSessionResponse, openSseSession, type SseDriverOptions } from './Transports.js'
|
|
50
56
|
import { applyTopUpResult, resolveManualTopUp, type TopUpRequirement } from './Transports.js'
|
|
51
57
|
import {
|
|
52
58
|
openWebSocketSession,
|
|
@@ -125,19 +131,6 @@ function isZeroAmountChargeChallenge(challenge: Challenge.Challenge) {
|
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
/** Builds a reusable channel entry from a server session snapshot header. */
|
|
129
|
-
function entryFromSnapshot(snapshot: ReturnType<typeof deserializeSessionSnapshot>): ChannelEntry {
|
|
130
|
-
return {
|
|
131
|
-
channelId: snapshot.channelId,
|
|
132
|
-
cumulativeAmount: BigInt(snapshot.acceptedCumulative),
|
|
133
|
-
deposit: BigInt(snapshot.deposit),
|
|
134
|
-
descriptor: snapshot.descriptor,
|
|
135
|
-
escrow: snapshot.escrow,
|
|
136
|
-
chainId: snapshot.chainId,
|
|
137
|
-
opened: true,
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
134
|
function requestInitWithSessionHint(
|
|
142
135
|
input: RequestInfo | URL,
|
|
143
136
|
init: RequestInit | undefined,
|
|
@@ -191,6 +184,12 @@ function resolveSessionManagerConfig(parameters: sessionManager.Parameters): Ses
|
|
|
191
184
|
*/
|
|
192
185
|
export function sessionManager(parameters: sessionManager.Parameters): SessionManager {
|
|
193
186
|
const config = resolveSessionManagerConfig(parameters)
|
|
187
|
+
const getClient = Client.getResolver({
|
|
188
|
+
chain: tempo_chain,
|
|
189
|
+
getClient: parameters.client ? () => parameters.client! : parameters.getClient,
|
|
190
|
+
rpcUrl: defaults.rpcUrl,
|
|
191
|
+
})
|
|
192
|
+
const getAccount = Account.getResolver({ account: parameters.account })
|
|
194
193
|
const runtime = createSessionManagerRuntime()
|
|
195
194
|
const receipts = createSessionReceiptCoordinator({
|
|
196
195
|
getSocketSession: () => runtime.socketSession,
|
|
@@ -293,6 +292,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
293
292
|
}
|
|
294
293
|
},
|
|
295
294
|
})
|
|
295
|
+
MethodResponse.unregister(method)
|
|
296
296
|
const chargeMethod = chargePlugin({
|
|
297
297
|
account: parameters.account,
|
|
298
298
|
getClient: parameters.client ? () => parameters.client! : parameters.getClient,
|
|
@@ -363,8 +363,27 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
363
363
|
async function storeSnapshotHeader(response: Response): Promise<ChannelEntry | undefined> {
|
|
364
364
|
const header = response.headers.get(Constants.Headers.paymentSessionSnapshot)
|
|
365
365
|
if (!header) return undefined
|
|
366
|
-
const
|
|
366
|
+
const snapshot = deserializeSessionSnapshot(header)
|
|
367
|
+
const client = await getClient({ chainId: snapshot.chainId })
|
|
368
|
+
const defaultAccount = getAccount(client)
|
|
369
|
+
const authority =
|
|
370
|
+
BigInt(snapshot.descriptor.authorizedSigner) === 0n
|
|
371
|
+
? snapshot.descriptor.payer
|
|
372
|
+
: snapshot.descriptor.authorizedSigner
|
|
373
|
+
const account =
|
|
374
|
+
(await parameters.resolveAccount?.({
|
|
375
|
+
account: defaultAccount,
|
|
376
|
+
chainId: snapshot.chainId,
|
|
377
|
+
operation: {
|
|
378
|
+
authority,
|
|
379
|
+
kind: 'authorizePaymentChannel',
|
|
380
|
+
},
|
|
381
|
+
})) ?? defaultAccount
|
|
382
|
+
const { entry, spent } = await hydrateSessionSnapshot({ account, client, snapshot })
|
|
383
|
+
assertVoucherWithinLocalLimit(entry.cumulativeAmount)
|
|
367
384
|
await Promise.resolve(store.set(entry)).catch(() => undefined)
|
|
385
|
+
runtime.channel = entry
|
|
386
|
+
runtime.spent = spent
|
|
368
387
|
return entry
|
|
369
388
|
}
|
|
370
389
|
|
|
@@ -428,8 +447,60 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
428
447
|
})
|
|
429
448
|
}
|
|
430
449
|
|
|
431
|
-
function
|
|
432
|
-
const
|
|
450
|
+
function validateCloseSnapshot(channel: ChannelEntry, challenge: TempoSessionChallenge) {
|
|
451
|
+
const snapshot = getSessionSnapshot(challenge)
|
|
452
|
+
if (!snapshot) return undefined
|
|
453
|
+
|
|
454
|
+
const computedSnapshotId = Channel.computeId({
|
|
455
|
+
...snapshot.descriptor,
|
|
456
|
+
chainId: snapshot.chainId,
|
|
457
|
+
escrow: snapshot.escrow,
|
|
458
|
+
})
|
|
459
|
+
if (computedSnapshotId.toLowerCase() !== snapshot.channelId.toLowerCase()) {
|
|
460
|
+
throw new Error('close snapshot descriptor does not match its channel ID')
|
|
461
|
+
}
|
|
462
|
+
if (snapshot.channelId.toLowerCase() !== channel.channelId.toLowerCase()) {
|
|
463
|
+
throw new Error('close snapshot channel ID does not match local session')
|
|
464
|
+
}
|
|
465
|
+
if (
|
|
466
|
+
snapshot.chainId !== channel.chainId ||
|
|
467
|
+
snapshot.escrow.toLowerCase() !== channel.escrow.toLowerCase()
|
|
468
|
+
) {
|
|
469
|
+
throw new Error('close snapshot payment scope does not match local session')
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const acceptedCumulative = BigInt(snapshot.acceptedCumulative)
|
|
473
|
+
const snapshotSpent = BigInt(snapshot.spent)
|
|
474
|
+
if (acceptedCumulative < 0n || snapshotSpent < 0n) {
|
|
475
|
+
throw new Error('close snapshot amounts must not be negative')
|
|
476
|
+
}
|
|
477
|
+
if (snapshotSpent > acceptedCumulative) {
|
|
478
|
+
throw new Error('close snapshot spent exceeds accepted cumulative amount')
|
|
479
|
+
}
|
|
480
|
+
if (acceptedCumulative > channel.cumulativeAmount) {
|
|
481
|
+
throw new Error('close snapshot accepted cumulative exceeds local voucher state')
|
|
482
|
+
}
|
|
483
|
+
return { acceptedCumulative, spent: snapshotSpent }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function applyCloseSnapshot(target: CloseTarget, challenge: TempoSessionChallenge) {
|
|
487
|
+
const snapshot = validateCloseSnapshot(target.channel, challenge)
|
|
488
|
+
if (!snapshot) return
|
|
489
|
+
|
|
490
|
+
const { acceptedCumulative, spent } = snapshot
|
|
491
|
+
if (runtime.spent > acceptedCumulative) {
|
|
492
|
+
throw new Error('close snapshot accepted cumulative is below locally confirmed spend')
|
|
493
|
+
}
|
|
494
|
+
if (spent > runtime.spent) runtime.spent = spent
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function getValidatedFallbackCloseAmount(
|
|
498
|
+
target: CloseTarget,
|
|
499
|
+
challenge: TempoSessionChallenge = target.challenge,
|
|
500
|
+
applySnapshot = false,
|
|
501
|
+
) {
|
|
502
|
+
if (applySnapshot) applyCloseSnapshot(target, challenge)
|
|
503
|
+
const closeAmount = getFallbackCloseAmount(challenge, target.channelId)
|
|
433
504
|
if (closeAmount > target.channel.cumulativeAmount) {
|
|
434
505
|
throw new Error('fallback close amount exceeds local voucher state')
|
|
435
506
|
}
|
|
@@ -467,6 +538,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
467
538
|
spent: runtime.spent,
|
|
468
539
|
})
|
|
469
540
|
if (applied?.channel && runtime.lastChallenge) {
|
|
541
|
+
await store.set(applied.channel)
|
|
470
542
|
dispatch({
|
|
471
543
|
type: 'activated',
|
|
472
544
|
challengeId: runtime.lastChallenge.id,
|
|
@@ -623,6 +695,8 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
623
695
|
createSessionCredential,
|
|
624
696
|
fetch: config.fetch,
|
|
625
697
|
lastUrl: runtime.lastUrl,
|
|
698
|
+
resolveSignedCloseAmount: (challenge) =>
|
|
699
|
+
getValidatedFallbackCloseAmount(target, challenge, true),
|
|
626
700
|
signedCloseAmount: getValidatedFallbackCloseAmount(target),
|
|
627
701
|
setChallenge(challenge) {
|
|
628
702
|
runtime.lastChallenge = challenge
|
|
@@ -638,6 +712,20 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
638
712
|
return receipt
|
|
639
713
|
}
|
|
640
714
|
|
|
715
|
+
const sseDriver = {
|
|
716
|
+
createSessionCredential,
|
|
717
|
+
doFetch,
|
|
718
|
+
fetch: config.fetch,
|
|
719
|
+
getChannel: () => runtime.channel,
|
|
720
|
+
getChallenge: () => runtime.lastChallenge,
|
|
721
|
+
assertVoucherWithinLocalLimit,
|
|
722
|
+
managementInput,
|
|
723
|
+
acceptReceipt(receipt: SessionReceipt) {
|
|
724
|
+
updateSpentFromReceipt(receipt)
|
|
725
|
+
},
|
|
726
|
+
topUpIfNeeded,
|
|
727
|
+
}
|
|
728
|
+
|
|
641
729
|
const self: SessionManager = {
|
|
642
730
|
get channelId() {
|
|
643
731
|
return runtime.channel?.channelId
|
|
@@ -673,19 +761,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
673
761
|
},
|
|
674
762
|
|
|
675
763
|
async sse(input, init) {
|
|
676
|
-
return openSseSession(input, init,
|
|
677
|
-
createSessionCredential,
|
|
678
|
-
doFetch,
|
|
679
|
-
fetch: config.fetch,
|
|
680
|
-
getChannel: () => runtime.channel,
|
|
681
|
-
getChallenge: () => runtime.lastChallenge,
|
|
682
|
-
assertVoucherWithinLocalLimit,
|
|
683
|
-
managementInput,
|
|
684
|
-
acceptReceipt(receipt) {
|
|
685
|
-
updateSpentFromReceipt(receipt)
|
|
686
|
-
},
|
|
687
|
-
topUpIfNeeded,
|
|
688
|
-
})
|
|
764
|
+
return openSseSession(input, init, sseDriver)
|
|
689
765
|
},
|
|
690
766
|
|
|
691
767
|
async ws(input, init) {
|
|
@@ -766,6 +842,38 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
766
842
|
},
|
|
767
843
|
}
|
|
768
844
|
|
|
845
|
+
registerSessionManagerInternals(self, {
|
|
846
|
+
consumeSseResponse(input, response, options) {
|
|
847
|
+
return consumeSseSessionResponse(input, response, options, sseDriver)
|
|
848
|
+
},
|
|
849
|
+
rehydrate({ channel, challenge, input, spent }) {
|
|
850
|
+
if (!channel.opened) throw new Error('Cannot restore a closed session channel.')
|
|
851
|
+
if (!isTempoSessionChallenge(challenge)) {
|
|
852
|
+
throw new Error('Cannot restore session: challenge is not tempo/session.')
|
|
853
|
+
}
|
|
854
|
+
if (spent < 0n) throw new Error('Cannot restore session: spent must not be negative.')
|
|
855
|
+
if (spent > channel.cumulativeAmount) {
|
|
856
|
+
throw new Error('Cannot restore session: spent exceeds local voucher state.')
|
|
857
|
+
}
|
|
858
|
+
assertVoucherWithinLocalLimit(channel.cumulativeAmount)
|
|
859
|
+
const snapshot = validateCloseSnapshot(channel, challenge)
|
|
860
|
+
const restoredSpent = snapshot && snapshot.spent > spent ? snapshot.spent : spent
|
|
861
|
+
|
|
862
|
+
runtime.lastUrl = input
|
|
863
|
+
runtime.lastChallenge = challenge
|
|
864
|
+
runtime.channel = channel
|
|
865
|
+
runtime.spent = restoredSpent
|
|
866
|
+
dispatch({ type: 'challengeReceived', challengeId: challenge.id })
|
|
867
|
+
dispatch({
|
|
868
|
+
type: 'activated',
|
|
869
|
+
challengeId: challenge.id,
|
|
870
|
+
entry: channel,
|
|
871
|
+
spent: restoredSpent.toString(),
|
|
872
|
+
units: 0,
|
|
873
|
+
})
|
|
874
|
+
},
|
|
875
|
+
})
|
|
876
|
+
|
|
769
877
|
return self
|
|
770
878
|
}
|
|
771
879
|
|