mppx 0.8.12 → 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 +18 -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/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/Session.d.ts +2 -2
- 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/SessionManager.d.ts +2 -2
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +128 -30
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +11 -2
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +33 -9
- 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/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/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 +3 -3
- package/src/tempo/session/client/SessionManager.test.ts +137 -4
- package/src/tempo/session/client/SessionManager.ts +139 -33
- package/src/tempo/session/client/Transports.test.ts +150 -8
- package/src/tempo/session/client/Transports.ts +48 -10
- 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
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { Cli, Errors, z } from 'incur'
|
|
2
|
+
import { createClient, http } from 'viem'
|
|
3
|
+
import { tempo as tempoMainnet, tempoModerato } from 'viem/tempo/chains'
|
|
4
|
+
|
|
5
|
+
import { normalizeHeaders } from '../../client/internal/Fetch.js'
|
|
6
|
+
import { canSignDescriptor } from '../../tempo/session/client/CredentialState.js'
|
|
7
|
+
import * as Chain from '../../tempo/session/precompile/Chain.js'
|
|
8
|
+
import * as Channel from '../../tempo/session/precompile/Channel.js'
|
|
9
|
+
import { resolvePersistentAccount } from '../account.js'
|
|
10
|
+
import { resolveChain, resolveRpcUrl, type Network } from '../utils.js'
|
|
11
|
+
import { closeWithSessionManager } from './Manager.js'
|
|
12
|
+
import {
|
|
13
|
+
createSessionRegistry,
|
|
14
|
+
SessionBusyError,
|
|
15
|
+
SessionStateError,
|
|
16
|
+
type ManagedSession,
|
|
17
|
+
type SessionPersistenceContext,
|
|
18
|
+
sessionResourceUrl,
|
|
19
|
+
sessionScope,
|
|
20
|
+
toChannelStore,
|
|
21
|
+
} from './store.js'
|
|
22
|
+
|
|
23
|
+
type SessionCloseOptions = {
|
|
24
|
+
account?: string | undefined
|
|
25
|
+
headers?: readonly string[] | undefined
|
|
26
|
+
network?: Network | undefined
|
|
27
|
+
resourceUrl?: string | undefined
|
|
28
|
+
rpcUrl?: string | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sessionOutputSchema = z.object({
|
|
32
|
+
status: z.enum(['opening', 'open', 'closing', 'stale']),
|
|
33
|
+
channelId: z.string(),
|
|
34
|
+
account: z.string().optional(),
|
|
35
|
+
payer: z.string(),
|
|
36
|
+
payee: z.string(),
|
|
37
|
+
authorizedSigner: z.string(),
|
|
38
|
+
token: z.string(),
|
|
39
|
+
escrow: z.string(),
|
|
40
|
+
chainId: z.number(),
|
|
41
|
+
cumulativeAmount: z.string(),
|
|
42
|
+
confirmedSpend: z.string(),
|
|
43
|
+
deposit: z.string(),
|
|
44
|
+
units: z.number(),
|
|
45
|
+
resourceUrl: z.string(),
|
|
46
|
+
createdAt: z.string(),
|
|
47
|
+
updatedAt: z.string(),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const sessionCloseOutputSchema = z.object({
|
|
51
|
+
channelId: z.string(),
|
|
52
|
+
status: z.enum(['closed', 'already-closed']),
|
|
53
|
+
spent: z.string(),
|
|
54
|
+
txHash: z.string().optional(),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const sessionBulkCloseOutputSchema = z.object({
|
|
58
|
+
closed: z.array(sessionCloseOutputSchema),
|
|
59
|
+
failed: z.array(z.object({ channelId: z.string(), message: z.string() })),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
type SessionOutput = z.infer<typeof sessionOutputSchema>
|
|
63
|
+
type SessionCloseOutput = z.infer<typeof sessionCloseOutputSchema>
|
|
64
|
+
|
|
65
|
+
function networkChainId(network: Network): number {
|
|
66
|
+
return network === 'mainnet' ? tempoMainnet.id : tempoModerato.id
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function networkForChain(chainId: number): Network | undefined {
|
|
70
|
+
if (chainId === tempoMainnet.id) return 'mainnet'
|
|
71
|
+
if (chainId === tempoModerato.id) return 'testnet'
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseHeaders(values: readonly string[] | undefined): Record<string, string> {
|
|
76
|
+
const headers: Record<string, string> = {}
|
|
77
|
+
for (const value of values ?? []) {
|
|
78
|
+
const index = value.indexOf(':')
|
|
79
|
+
if (index === -1)
|
|
80
|
+
throw new Errors.IncurError({
|
|
81
|
+
code: 'INVALID_HEADER',
|
|
82
|
+
message: `Invalid header format: ${value}`,
|
|
83
|
+
exitCode: 2,
|
|
84
|
+
})
|
|
85
|
+
headers[value.slice(0, index).trim()] = value.slice(index + 1).trim()
|
|
86
|
+
}
|
|
87
|
+
return headers
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function outputSession(record: ManagedSession): SessionOutput {
|
|
91
|
+
return {
|
|
92
|
+
status: record.status,
|
|
93
|
+
channelId: record.channel.channelId,
|
|
94
|
+
...(record.account.name && { account: record.account.name }),
|
|
95
|
+
payer: record.channel.descriptor.payer,
|
|
96
|
+
payee: record.channel.descriptor.payee,
|
|
97
|
+
authorizedSigner: record.channel.descriptor.authorizedSigner,
|
|
98
|
+
token: record.channel.descriptor.token,
|
|
99
|
+
escrow: record.channel.escrow,
|
|
100
|
+
chainId: record.channel.chainId,
|
|
101
|
+
cumulativeAmount: record.channel.cumulativeAmount.toString(),
|
|
102
|
+
confirmedSpend: record.spent.toString(),
|
|
103
|
+
deposit: record.channel.deposit.toString(),
|
|
104
|
+
units: record.units,
|
|
105
|
+
resourceUrl: record.endpoint,
|
|
106
|
+
createdAt: record.createdAt,
|
|
107
|
+
updatedAt: record.updatedAt,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Normalizes persistent session failures for CLI output. */
|
|
112
|
+
export function sessionCommandError(error: unknown, fallbackCode: string): never {
|
|
113
|
+
if (error instanceof Errors.IncurError) throw error
|
|
114
|
+
if (error instanceof SessionBusyError)
|
|
115
|
+
throw new Errors.IncurError({
|
|
116
|
+
code: error.code,
|
|
117
|
+
message: error.message,
|
|
118
|
+
exitCode: error.exitCode,
|
|
119
|
+
cause: error,
|
|
120
|
+
})
|
|
121
|
+
if (error instanceof SessionStateError)
|
|
122
|
+
throw new Errors.IncurError({
|
|
123
|
+
code: error.code,
|
|
124
|
+
message: error.message,
|
|
125
|
+
exitCode: 65,
|
|
126
|
+
cause: error,
|
|
127
|
+
})
|
|
128
|
+
throw new Errors.IncurError({
|
|
129
|
+
code: fallbackCode,
|
|
130
|
+
message: error instanceof Error ? error.message : String(error),
|
|
131
|
+
exitCode: 75,
|
|
132
|
+
...(error instanceof Error && { cause: error }),
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function resolveCloseAccount(record: ManagedSession, accountOverride?: string | undefined) {
|
|
137
|
+
const accountName = accountOverride ?? record.account.name
|
|
138
|
+
const resolved = await resolvePersistentAccount(accountName)
|
|
139
|
+
if (!canSignDescriptor(resolved.account, record.channel.descriptor))
|
|
140
|
+
throw new Errors.IncurError({
|
|
141
|
+
code: 'SESSION_ACCOUNT_MISMATCH',
|
|
142
|
+
message: `Account ${resolved.account.address} cannot sign for session ${record.channel.channelId}.`,
|
|
143
|
+
exitCode: 2,
|
|
144
|
+
})
|
|
145
|
+
return resolved
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function resolveCloseClient(
|
|
149
|
+
chainId: number,
|
|
150
|
+
options: Pick<SessionCloseOptions, 'network' | 'rpcUrl'>,
|
|
151
|
+
) {
|
|
152
|
+
if (options.network && networkChainId(options.network) !== chainId)
|
|
153
|
+
throw new Errors.IncurError({
|
|
154
|
+
code: 'CHAIN_MISMATCH',
|
|
155
|
+
message: `Session uses chainId ${chainId}, not ${options.network}.`,
|
|
156
|
+
exitCode: 2,
|
|
157
|
+
})
|
|
158
|
+
const network = options.network ?? networkForChain(chainId)
|
|
159
|
+
const rpcUrl = resolveRpcUrl(options.rpcUrl, { network })
|
|
160
|
+
const chain = await resolveChain({ network, rpcUrl })
|
|
161
|
+
if (chain.id !== chainId)
|
|
162
|
+
throw new Errors.IncurError({
|
|
163
|
+
code: 'CHAIN_MISMATCH',
|
|
164
|
+
message: `Session uses chainId ${chainId}, but RPC is chainId ${chain.id}.`,
|
|
165
|
+
exitCode: 2,
|
|
166
|
+
})
|
|
167
|
+
return createClient({ chain, transport: http(rpcUrl) })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const sessions = Cli.create('sessions', {
|
|
171
|
+
description: 'Manage persistent payment sessions (list, view, close)',
|
|
172
|
+
})
|
|
173
|
+
.command('list', {
|
|
174
|
+
description: 'List persistent payment sessions',
|
|
175
|
+
options: z.object({
|
|
176
|
+
account: z.string().optional().describe('Filter by account name'),
|
|
177
|
+
network: z.enum(['mainnet', 'testnet']).optional().describe('Filter by Tempo network'),
|
|
178
|
+
}),
|
|
179
|
+
output: z.object({ sessions: z.array(sessionOutputSchema) }),
|
|
180
|
+
alias: { account: 'a' },
|
|
181
|
+
async run(c) {
|
|
182
|
+
try {
|
|
183
|
+
const records = (await createSessionRegistry().list())
|
|
184
|
+
.filter((record) => {
|
|
185
|
+
if (c.options.account && record.account.name !== c.options.account) return false
|
|
186
|
+
if (c.options.network && record.channel.chainId !== networkChainId(c.options.network))
|
|
187
|
+
return false
|
|
188
|
+
return true
|
|
189
|
+
})
|
|
190
|
+
.map(outputSession)
|
|
191
|
+
const result = { sessions: records }
|
|
192
|
+
if (c.format === 'json' && c.formatExplicit) return c.ok(result)
|
|
193
|
+
if (records.length === 0) console.log('No sessions.')
|
|
194
|
+
for (const [index, record] of records.entries()) {
|
|
195
|
+
if (index > 0) console.log('')
|
|
196
|
+
printSession(record)
|
|
197
|
+
}
|
|
198
|
+
return undefined as never
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return sessionCommandError(error, 'SESSION_LIST_FAILED')
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
.command('view', {
|
|
205
|
+
description: 'View a persistent payment session',
|
|
206
|
+
args: z.object({ channelId: z.string().describe('Full session channel ID') }),
|
|
207
|
+
output: sessionOutputSchema,
|
|
208
|
+
async run(c) {
|
|
209
|
+
try {
|
|
210
|
+
const record = await createSessionRegistry().get(c.args.channelId)
|
|
211
|
+
if (!record)
|
|
212
|
+
throw new Errors.IncurError({
|
|
213
|
+
code: 'SESSION_NOT_FOUND',
|
|
214
|
+
message: `Session ${c.args.channelId} was not found.`,
|
|
215
|
+
exitCode: 2,
|
|
216
|
+
})
|
|
217
|
+
const result = outputSession(record)
|
|
218
|
+
if (c.format === 'json' && c.formatExplicit) return c.ok(result)
|
|
219
|
+
printSession(result)
|
|
220
|
+
return undefined as never
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return sessionCommandError(error, 'SESSION_VIEW_FAILED')
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
.command('close', {
|
|
227
|
+
description: 'Cooperatively close persistent payment sessions',
|
|
228
|
+
usage: [{ suffix: '<channel-id> [options]' }, { suffix: '--all --yes [options]' }],
|
|
229
|
+
args: z.object({ channelId: z.string().optional().describe('Full session channel ID') }),
|
|
230
|
+
options: z.object({
|
|
231
|
+
account: z.string().optional().describe('Account name'),
|
|
232
|
+
all: z.boolean().optional().default(false).describe('Close every matching session'),
|
|
233
|
+
header: z.array(z.string()).optional().describe('Add close request header (repeatable)'),
|
|
234
|
+
network: z.enum(['mainnet', 'testnet']).optional().describe('Tempo network'),
|
|
235
|
+
rpcUrl: z.string().optional().describe('RPC endpoint (env: MPPX_RPC_URL)'),
|
|
236
|
+
url: z.string().optional().describe('Override the stored resource URL'),
|
|
237
|
+
yes: z.boolean().optional().default(false).describe('Confirm closing every session'),
|
|
238
|
+
}),
|
|
239
|
+
output: z.union([sessionCloseOutputSchema, sessionBulkCloseOutputSchema]),
|
|
240
|
+
alias: { account: 'a', header: 'H', rpcUrl: 'r' },
|
|
241
|
+
async run(c) {
|
|
242
|
+
if (c.options.all && c.args.channelId)
|
|
243
|
+
return c.error({
|
|
244
|
+
code: 'INVALID_SESSION_CLOSE',
|
|
245
|
+
message: 'Specify a channel ID or --all, not both.',
|
|
246
|
+
exitCode: 2,
|
|
247
|
+
})
|
|
248
|
+
if (!c.options.all && !c.args.channelId)
|
|
249
|
+
return c.error({
|
|
250
|
+
code: 'INVALID_SESSION_CLOSE',
|
|
251
|
+
message: 'Specify a channel ID or --all.',
|
|
252
|
+
exitCode: 2,
|
|
253
|
+
})
|
|
254
|
+
if (c.options.all && !c.options.yes)
|
|
255
|
+
return c.error({
|
|
256
|
+
code: 'CONFIRMATION_REQUIRED',
|
|
257
|
+
message: 'Closing all sessions requires --yes.',
|
|
258
|
+
exitCode: 2,
|
|
259
|
+
})
|
|
260
|
+
if (c.options.all && (c.options.header || c.options.rpcUrl || c.options.url))
|
|
261
|
+
return c.error({
|
|
262
|
+
code: 'INVALID_SESSION_CLOSE',
|
|
263
|
+
message: '--header, --rpc-url, and --url only apply to a single session.',
|
|
264
|
+
exitCode: 2,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const registry = createSessionRegistry()
|
|
268
|
+
async function closeSession(
|
|
269
|
+
channelId: string,
|
|
270
|
+
options: SessionCloseOptions = {},
|
|
271
|
+
): Promise<SessionCloseOutput> {
|
|
272
|
+
const candidate = await registry.get(channelId)
|
|
273
|
+
if (!candidate)
|
|
274
|
+
throw new Errors.IncurError({
|
|
275
|
+
code: 'SESSION_NOT_FOUND',
|
|
276
|
+
message: `Session ${channelId} was not found.`,
|
|
277
|
+
exitCode: 2,
|
|
278
|
+
})
|
|
279
|
+
const scope = sessionScope(candidate.channel)
|
|
280
|
+
const lock = await registry.acquire(scope)
|
|
281
|
+
try {
|
|
282
|
+
const record = await registry.get(channelId)
|
|
283
|
+
if (!record)
|
|
284
|
+
throw new Errors.IncurError({
|
|
285
|
+
code: 'SESSION_NOT_FOUND',
|
|
286
|
+
message: `Session ${channelId} was not found.`,
|
|
287
|
+
exitCode: 2,
|
|
288
|
+
})
|
|
289
|
+
const resolvedAccount = await resolveCloseAccount(record, options.account)
|
|
290
|
+
const client = await resolveCloseClient(record.channel.chainId, options)
|
|
291
|
+
const expectedId = Channel.computeId({
|
|
292
|
+
...record.channel.descriptor,
|
|
293
|
+
escrow: record.channel.escrow,
|
|
294
|
+
chainId: record.channel.chainId,
|
|
295
|
+
})
|
|
296
|
+
if (expectedId.toLowerCase() !== record.channel.channelId.toLowerCase())
|
|
297
|
+
throw new Errors.IncurError({
|
|
298
|
+
code: 'SESSION_STATE_INVALID',
|
|
299
|
+
message: 'Stored descriptor does not derive the session channel ID.',
|
|
300
|
+
exitCode: 65,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const state = await Chain.getChannelState(
|
|
304
|
+
client as never,
|
|
305
|
+
record.channel.channelId,
|
|
306
|
+
record.channel.escrow,
|
|
307
|
+
)
|
|
308
|
+
if (state.deposit === 0n) {
|
|
309
|
+
await registry.remove(record.channel.channelId)
|
|
310
|
+
return {
|
|
311
|
+
channelId: record.channel.channelId,
|
|
312
|
+
status: 'already-closed',
|
|
313
|
+
spent: record.spent.toString(),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const endpoint = sessionResourceUrl(options.resourceUrl ?? record.endpoint)
|
|
318
|
+
const account = {
|
|
319
|
+
...(resolvedAccount.source === 'keychain' && { name: resolvedAccount.accountName }),
|
|
320
|
+
address: resolvedAccount.account.address,
|
|
321
|
+
}
|
|
322
|
+
const closingContext = (challenge = record.challenge): SessionPersistenceContext => ({
|
|
323
|
+
status: 'closing',
|
|
324
|
+
account,
|
|
325
|
+
endpoint,
|
|
326
|
+
challenge,
|
|
327
|
+
...(record.receipt && { receipt: record.receipt }),
|
|
328
|
+
spent: record.spent,
|
|
329
|
+
units: record.units,
|
|
330
|
+
})
|
|
331
|
+
const closing = await registry.upsert({
|
|
332
|
+
...closingContext(),
|
|
333
|
+
channel: record.channel,
|
|
334
|
+
})
|
|
335
|
+
const headers = parseHeaders(options.headers)
|
|
336
|
+
let latestChallenge = closing.challenge
|
|
337
|
+
const closeFetch: typeof globalThis.fetch = async (input, init) =>
|
|
338
|
+
globalThis.fetch(input, {
|
|
339
|
+
...init,
|
|
340
|
+
headers: { ...headers, ...normalizeHeaders(init?.headers) },
|
|
341
|
+
})
|
|
342
|
+
const result = await closeWithSessionManager({
|
|
343
|
+
channel: closing.channel,
|
|
344
|
+
challenge: closing.challenge,
|
|
345
|
+
fetch: closeFetch,
|
|
346
|
+
input: endpoint,
|
|
347
|
+
spent: closing.spent,
|
|
348
|
+
async onChallenge(challenge) {
|
|
349
|
+
latestChallenge = challenge
|
|
350
|
+
await registry.upsert({
|
|
351
|
+
...closingContext(challenge),
|
|
352
|
+
channel: closing.channel,
|
|
353
|
+
})
|
|
354
|
+
},
|
|
355
|
+
manager: {
|
|
356
|
+
account: resolvedAccount.account,
|
|
357
|
+
client,
|
|
358
|
+
channelStore: toChannelStore(registry, {
|
|
359
|
+
scope,
|
|
360
|
+
selection: closing.channel.channelId,
|
|
361
|
+
context: () => closingContext(latestChallenge),
|
|
362
|
+
}),
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
await registry.remove(closing.channel.channelId)
|
|
366
|
+
return {
|
|
367
|
+
channelId: closing.channel.channelId,
|
|
368
|
+
status: 'closed',
|
|
369
|
+
spent: result.receipt.spent,
|
|
370
|
+
txHash: result.receipt.txHash,
|
|
371
|
+
}
|
|
372
|
+
} finally {
|
|
373
|
+
await lock.release()
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
if (c.options.all) {
|
|
379
|
+
const records = (await registry.list()).filter(
|
|
380
|
+
(record) =>
|
|
381
|
+
(!c.options.account || record.account.name === c.options.account) &&
|
|
382
|
+
(!c.options.network || record.channel.chainId === networkChainId(c.options.network)),
|
|
383
|
+
)
|
|
384
|
+
const closed: SessionCloseOutput[] = []
|
|
385
|
+
const failed: { channelId: string; message: string }[] = []
|
|
386
|
+
for (const record of records) {
|
|
387
|
+
try {
|
|
388
|
+
closed.push(
|
|
389
|
+
await closeSession(record.channel.channelId, {
|
|
390
|
+
account: c.options.account,
|
|
391
|
+
network: c.options.network,
|
|
392
|
+
}),
|
|
393
|
+
)
|
|
394
|
+
} catch (error) {
|
|
395
|
+
failed.push({
|
|
396
|
+
channelId: record.channel.channelId,
|
|
397
|
+
message: error instanceof Error ? error.message : String(error),
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const result = { closed, failed }
|
|
402
|
+
if (failed.length > 0) {
|
|
403
|
+
for (const failure of failed)
|
|
404
|
+
process.stderr.write(`${failure.channelId}: ${failure.message}\n`)
|
|
405
|
+
process.exitCode = 1
|
|
406
|
+
}
|
|
407
|
+
if (c.format === 'json' && c.formatExplicit) return c.ok(result)
|
|
408
|
+
for (const item of closed) console.log(`${item.channelId} ${item.status} ${item.spent}`)
|
|
409
|
+
for (const failure of failed)
|
|
410
|
+
console.log(`${failure.channelId} failed ${failure.message}`)
|
|
411
|
+
return undefined as never
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const result = await closeSession(c.args.channelId!, {
|
|
415
|
+
account: c.options.account,
|
|
416
|
+
headers: c.options.header,
|
|
417
|
+
network: c.options.network,
|
|
418
|
+
resourceUrl: c.options.url,
|
|
419
|
+
rpcUrl: c.options.rpcUrl,
|
|
420
|
+
})
|
|
421
|
+
if (c.format === 'json' && c.formatExplicit) return c.ok(result)
|
|
422
|
+
console.log(`${result.channelId} ${result.status} ${result.spent}`)
|
|
423
|
+
if (result.txHash) console.log(` transaction ${result.txHash}`)
|
|
424
|
+
return undefined as never
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return sessionCommandError(error, 'SESSION_CLOSE_FAILED')
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
function printSession(record: SessionOutput): void {
|
|
432
|
+
console.log(record.channelId)
|
|
433
|
+
console.log(` status ${record.status}`)
|
|
434
|
+
console.log(` cumulative amount ${record.cumulativeAmount}`)
|
|
435
|
+
console.log(` confirmed spend ${record.confirmedSpend}`)
|
|
436
|
+
console.log(` deposit ${record.deposit}`)
|
|
437
|
+
console.log(` chain ${record.chainId}`)
|
|
438
|
+
console.log(` payer ${record.payer}`)
|
|
439
|
+
console.log(` payee ${record.payee}`)
|
|
440
|
+
console.log(` token ${record.token}`)
|
|
441
|
+
console.log(` resource ${record.resourceUrl}`)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export default sessions
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Hex } from 'viem'
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
|
+
|
|
4
|
+
import type * as Challenge from '../../Challenge.js'
|
|
5
|
+
import { resolveSessionMaxDeposit, resolveSessionSelection } from './request.js'
|
|
6
|
+
|
|
7
|
+
const channelId = `0x${'12'.repeat(32)}` as Hex
|
|
8
|
+
describe('resolveSessionSelection', () => {
|
|
9
|
+
test('uses auto by default and accepts new or an explicit channel', () => {
|
|
10
|
+
expect(resolveSessionSelection('auto', undefined)).toBe('auto')
|
|
11
|
+
expect(resolveSessionSelection('new', undefined)).toBe('new')
|
|
12
|
+
expect(resolveSessionSelection(channelId.toUpperCase().replace('0X', '0x'), undefined)).toBe(
|
|
13
|
+
channelId,
|
|
14
|
+
)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('supports the channel method compatibility alias', () => {
|
|
18
|
+
expect(resolveSessionSelection('auto', channelId)).toBe(channelId)
|
|
19
|
+
expect(resolveSessionSelection(channelId, channelId)).toBe(channelId)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('rejects conflicting selectors', () => {
|
|
23
|
+
expect(() => resolveSessionSelection('new', channelId)).toThrow(
|
|
24
|
+
'--session and -M channel= select different sessions.',
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('resolveSessionMaxDeposit', () => {
|
|
30
|
+
const challenge = {
|
|
31
|
+
id: 'challenge-1',
|
|
32
|
+
realm: 'api.example.test',
|
|
33
|
+
method: 'tempo',
|
|
34
|
+
intent: 'session',
|
|
35
|
+
request: {
|
|
36
|
+
amount: '1000000',
|
|
37
|
+
currency: '0x3333333333333333333333333333333333333333',
|
|
38
|
+
decimals: 6,
|
|
39
|
+
recipient: '0x2222222222222222222222222222222222222222',
|
|
40
|
+
suggestedDeposit: '7000000',
|
|
41
|
+
},
|
|
42
|
+
} satisfies Challenge.Challenge
|
|
43
|
+
|
|
44
|
+
test('converts the raw server suggestion to human-readable token units', () => {
|
|
45
|
+
expect(resolveSessionMaxDeposit(challenge, {}, false)).toBe('7')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('prefers the human-readable CLI deposit override', () => {
|
|
49
|
+
expect(resolveSessionMaxDeposit(challenge, { deposit: '10' }, false)).toBe('10')
|
|
50
|
+
})
|
|
51
|
+
})
|