mppx 0.8.12 → 0.8.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +16 -1
- 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/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 +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/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/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/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 +2 -1
- 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/internal/types.d.ts +10 -1
- package/dist/tempo/internal/types.d.ts.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/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 +40 -78
- 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 +13 -23
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +6 -2
- 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/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 +7 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +17 -3
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +26 -2
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +99 -16
- 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 +16 -6
- 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 +10 -3
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +176 -50
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +18 -7
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +81 -33
- 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.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +109 -12
- 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/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 +13 -5
- 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/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/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/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.ts +16 -7
- package/src/client/internal/MethodChallenge.ts +30 -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 +44 -0
- package/src/server/Mppx.test.ts +205 -0
- package/src/server/Mppx.ts +167 -103
- 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/PublicExports.test-d.ts +20 -0
- package/src/tempo/internal/fee-payer.ts +7 -3
- package/src/tempo/internal/types.ts +13 -2
- package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
- package/src/tempo/legacy/client/ChannelOps.ts +2 -1
- 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.test.ts +337 -63
- package/src/tempo/server/Charge.ts +440 -270
- package/src/tempo/server/Methods.ts +16 -4
- 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/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 +55 -0
- package/src/tempo/session/client/ChannelOps.ts +21 -3
- package/src/tempo/session/client/CredentialState.test.ts +2 -2
- package/src/tempo/session/client/CredentialState.ts +159 -16
- 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 +238 -5
- package/src/tempo/session/client/Session.ts +130 -41
- package/src/tempo/session/client/SessionManager.test.ts +239 -8
- package/src/tempo/session/client/SessionManager.ts +204 -54
- package/src/tempo/session/client/Transports.test.ts +181 -8
- package/src/tempo/session/client/Transports.ts +127 -45
- package/src/tempo/session/client/internal/SessionManager.ts +36 -0
- package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
- package/src/tempo/session/precompile/Chain.test.ts +223 -10
- package/src/tempo/session/precompile/Chain.ts +119 -12
- package/src/tempo/session/precompile/Channel.test.ts +9 -0
- package/src/tempo/session/precompile/Channel.ts +5 -0
- 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 +47 -4
- package/src/tempo/session/server/RequestState.ts +15 -8
- 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
package/src/cli/utils.test.ts
CHANGED
|
@@ -3,12 +3,35 @@ import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
fetchTokenInfo,
|
|
6
|
+
isAgentEnvironment,
|
|
6
7
|
networkRpcUrls,
|
|
7
8
|
resolveChain,
|
|
8
9
|
resolveFundingNetwork,
|
|
9
10
|
resolveRpcUrl,
|
|
10
11
|
} from './utils.js'
|
|
11
12
|
|
|
13
|
+
describe('isAgentEnvironment', () => {
|
|
14
|
+
test('detects Claude Code', () => {
|
|
15
|
+
expect(isAgentEnvironment({ CLAUDECODE: '1' })).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('detects Codex by exact env var', () => {
|
|
19
|
+
expect(isAgentEnvironment({ CODEX: '1' })).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('detects Codex wrapper env vars', () => {
|
|
23
|
+
expect(isAgentEnvironment({ CODEX_CI: '1' })).toBe(true)
|
|
24
|
+
expect(isAgentEnvironment({ CODEX_SANDBOX: 'seatbelt' })).toBe(true)
|
|
25
|
+
expect(isAgentEnvironment({ CODEX_THREAD_ID: '019f6bc0-b80f-7061-ac1b-7af8ba1ea513' })).toBe(
|
|
26
|
+
true,
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('ignores empty and unrelated env vars', () => {
|
|
31
|
+
expect(isAgentEnvironment({ CODEX_CI: '', CI: '1' })).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
12
35
|
describe('resolveRpcUrl', () => {
|
|
13
36
|
afterEach(() => {
|
|
14
37
|
delete process.env.MPPX_RPC_URL
|
package/src/cli/utils.ts
CHANGED
|
@@ -179,6 +179,16 @@ export function parseMethodOpts(raw: string | string[] | undefined): Record<stri
|
|
|
179
179
|
return result
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/** Returns true when the CLI is running inside an agent wrapper that prefers machine-readable output. */
|
|
183
|
+
export function isAgentEnvironment(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
184
|
+
if (env.CLAUDECODE) return true
|
|
185
|
+
for (const [key, value] of Object.entries(env)) {
|
|
186
|
+
if (!value) continue
|
|
187
|
+
if (key === 'CODEX' || key.startsWith('CODEX_')) return true
|
|
188
|
+
}
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
|
|
182
192
|
export function isTempoAccount(accountName: string): boolean {
|
|
183
193
|
return accountName.startsWith('tempo:')
|
|
184
194
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Cli, z } from 'incur'
|
|
2
2
|
|
|
3
3
|
import { validate as validateCore, validateStream } from '../../validation/core.js'
|
|
4
|
-
import { pc } from '../utils.js'
|
|
4
|
+
import { isAgentEnvironment, pc } from '../utils.js'
|
|
5
5
|
import type { Counts } from './helpers.js'
|
|
6
6
|
import { printResults, printSection } from './helpers.js'
|
|
7
|
+
import { missingDiscoverySuggestion } from './messages.js'
|
|
7
8
|
|
|
8
9
|
const validate = Cli.create('validate', {
|
|
9
10
|
description: 'Validate an MPP server implementation end-to-end',
|
|
@@ -22,7 +23,10 @@ const validate = Cli.create('validate', {
|
|
|
22
23
|
header: z.array(z.string()).optional().describe('Request header (key:value, repeatable)'),
|
|
23
24
|
verbose: z.number().default(0).meta({ count: true }).describe('Verbosity level'),
|
|
24
25
|
yes: z.boolean().default(false).describe('Auto-approve mainnet payments'),
|
|
25
|
-
outputJson: z
|
|
26
|
+
outputJson: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.default(false)
|
|
29
|
+
.describe('Output results as JSON (auto-enabled in known agent environments)'),
|
|
26
30
|
}),
|
|
27
31
|
alias: {
|
|
28
32
|
endpoint: 'e',
|
|
@@ -32,8 +36,10 @@ const validate = Cli.create('validate', {
|
|
|
32
36
|
outputJson: 'j',
|
|
33
37
|
},
|
|
34
38
|
async run(c) {
|
|
39
|
+
const outputJson = c.options.outputJson || isAgentEnvironment()
|
|
40
|
+
|
|
35
41
|
// JSON mode: batch everything
|
|
36
|
-
if (
|
|
42
|
+
if (outputJson) {
|
|
37
43
|
const result = await validateCore({
|
|
38
44
|
url: c.args.url,
|
|
39
45
|
endpoint: c.options.endpoint,
|
|
@@ -82,17 +88,9 @@ const validate = Cli.create('validate', {
|
|
|
82
88
|
discoveryFound = event.discovery.found
|
|
83
89
|
if (!discoveryFound && !c.options.endpoint) {
|
|
84
90
|
console.log('')
|
|
85
|
-
|
|
86
|
-
console.log(
|
|
87
|
-
|
|
88
|
-
' MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
|
|
89
|
-
),
|
|
90
|
-
)
|
|
91
|
-
console.log(
|
|
92
|
-
pc.dim(
|
|
93
|
-
' To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path',
|
|
94
|
-
),
|
|
95
|
-
)
|
|
91
|
+
const [headline, ...lines] = missingDiscoverySuggestion.split('\n')
|
|
92
|
+
console.log(pc.yellow(` ${headline}`))
|
|
93
|
+
for (const line of lines) console.log(line ? pc.dim(` ${line}`) : '')
|
|
96
94
|
console.log('')
|
|
97
95
|
process.exit(1)
|
|
98
96
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const missingDiscoverySuggestion = [
|
|
2
|
+
'No discovery document found, endpoints could not be automatically found.',
|
|
3
|
+
'MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
|
|
4
|
+
'The MPP SDKs can automatically expose this endpoint. See https://mpp.dev/sdk.',
|
|
5
|
+
'',
|
|
6
|
+
'To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path',
|
|
7
|
+
].join('\n')
|
package/src/cli/validate.test.ts
CHANGED
|
@@ -6,6 +6,10 @@ import * as Http from '~test/Http.js'
|
|
|
6
6
|
import * as Challenge from '../Challenge.js'
|
|
7
7
|
import * as Constants from '../Constants.js'
|
|
8
8
|
import * as Receipt from '../Receipt.js'
|
|
9
|
+
import { missingDiscoverySuggestion } from './validate/messages.js'
|
|
10
|
+
|
|
11
|
+
// Keep validate tests out of JSON mode by default, even when the suite runs inside an agent.
|
|
12
|
+
const isAgentEnvironmentMock = vi.fn(() => false)
|
|
9
13
|
|
|
10
14
|
// Keep CLI orchestration tests independent of the public Tempo RPC and faucet.
|
|
11
15
|
vi.doMock('viem/tempo', async () => {
|
|
@@ -28,11 +32,20 @@ vi.doMock('viem/actions', async () => {
|
|
|
28
32
|
}
|
|
29
33
|
})
|
|
30
34
|
|
|
35
|
+
vi.doMock('./utils.js', async () => {
|
|
36
|
+
const actual = await vi.importActual<typeof import('./utils.js')>('./utils.js')
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
isAgentEnvironment: isAgentEnvironmentMock,
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
31
43
|
// Auto-cleanup for test servers
|
|
32
44
|
const servers: Http.TestServer[] = []
|
|
33
45
|
afterEach(() => {
|
|
34
46
|
servers.forEach((s) => s.close())
|
|
35
47
|
servers.length = 0
|
|
48
|
+
isAgentEnvironmentMock.mockReturnValue(false)
|
|
36
49
|
})
|
|
37
50
|
|
|
38
51
|
async function testServer(handler: http.RequestListener) {
|
|
@@ -167,11 +180,8 @@ describe('validate: discovery', () => {
|
|
|
167
180
|
})
|
|
168
181
|
const { output, exitCode } = await serve(['validate', server.url])
|
|
169
182
|
expect(exitCode).toBe(1)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect(output).toContain(
|
|
173
|
-
'To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path',
|
|
174
|
-
)
|
|
183
|
+
for (const line of missingDiscoverySuggestion.split('\n').filter(Boolean))
|
|
184
|
+
expect(output).toContain(line)
|
|
175
185
|
})
|
|
176
186
|
|
|
177
187
|
test('strips /openapi.json from input URL', { timeout: 15_000 }, async () => {
|
|
@@ -774,6 +784,19 @@ describe('validate: JSON mode', () => {
|
|
|
774
784
|
expect(allPayment.every((r: { severity: string }) => r.severity === 'skip')).toBe(true)
|
|
775
785
|
})
|
|
776
786
|
|
|
787
|
+
test(
|
|
788
|
+
'auto-enables JSON output when an agent environment is detected',
|
|
789
|
+
{ timeout: 15_000 },
|
|
790
|
+
async () => {
|
|
791
|
+
const server = await mppServer(mainnetChallenge())
|
|
792
|
+
isAgentEnvironmentMock.mockReturnValue(true)
|
|
793
|
+
const { output } = await serve(['validate', server.url])
|
|
794
|
+
const result = parseJson(output)
|
|
795
|
+
expect(result.summary.skipped).toBeGreaterThan(0)
|
|
796
|
+
expect(result.endpoints.length).toBeGreaterThan(0)
|
|
797
|
+
},
|
|
798
|
+
)
|
|
799
|
+
|
|
777
800
|
test('no console leaks before JSON', { timeout: 15_000 }, async () => {
|
|
778
801
|
const server = await mppServer(mainnetChallenge())
|
|
779
802
|
const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
|
|
@@ -791,4 +814,14 @@ describe('validate: JSON mode', () => {
|
|
|
791
814
|
const result = parseJson(output)
|
|
792
815
|
expect(Array.isArray(result.suggestions)).toBe(true)
|
|
793
816
|
})
|
|
817
|
+
|
|
818
|
+
test('includes missing discovery suggestions', { timeout: 15_000 }, async () => {
|
|
819
|
+
const server = await testServer((_req, res) => {
|
|
820
|
+
res.writeHead(404)
|
|
821
|
+
res.end()
|
|
822
|
+
})
|
|
823
|
+
const { output } = await serve(['validate', server.url, '--outputJson'])
|
|
824
|
+
const result = parseJson(output)
|
|
825
|
+
expect(result.suggestions).toEqual([missingDiscoverySuggestion])
|
|
826
|
+
})
|
|
794
827
|
})
|
|
@@ -262,9 +262,9 @@ describe('fetch context', () => {
|
|
|
262
262
|
type FetchInit = NonNullable<Parameters<typeof _mppx.fetch>[1]>
|
|
263
263
|
type Context = NonNullable<FetchInit['context']>
|
|
264
264
|
|
|
265
|
-
// Context is a union of charge and session contexts.
|
|
266
|
-
// `account` exists on both; `autoSwap` only on charge.
|
|
265
|
+
// Context is a union of charge and session contexts; both support auto-swap.
|
|
267
266
|
expectTypeOf<Context>().toHaveProperty('account')
|
|
268
|
-
expectTypeOf<
|
|
267
|
+
expectTypeOf<Context>().toHaveProperty('autoSwap')
|
|
268
|
+
expectTypeOf<Context['autoSwap']>().toEqualTypeOf<AutoSwap.resolve.Value | undefined>()
|
|
269
269
|
})
|
|
270
270
|
})
|
|
@@ -6,6 +6,7 @@ import type { MaybePromise } from '../../internal/types.js'
|
|
|
6
6
|
import type * as Method from '../../Method.js'
|
|
7
7
|
import type * as z from '../../zod.js'
|
|
8
8
|
import * as Transport from '../Transport.js'
|
|
9
|
+
import * as MethodChallenge from './MethodChallenge.js'
|
|
9
10
|
import * as MethodResponse from './MethodResponse.js'
|
|
10
11
|
|
|
11
12
|
// We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
|
|
@@ -241,9 +242,22 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
241
242
|
mi = selected.method
|
|
242
243
|
if (challenge.expires) Expires.assert(challenge.expires, challenge.id)
|
|
243
244
|
|
|
245
|
+
const paymentInput = resolvePaymentRetryInput(
|
|
246
|
+
response,
|
|
247
|
+
initialRequest.input,
|
|
248
|
+
initialRequest.input,
|
|
249
|
+
)
|
|
244
250
|
const createCredential = memoizeCreateCredential(
|
|
245
|
-
(overrideContext?: AnyContextFor<methods>) =>
|
|
246
|
-
|
|
251
|
+
async (overrideContext?: AnyContextFor<methods>) => {
|
|
252
|
+
const credentialContext = overrideContext ?? context
|
|
253
|
+
await MethodChallenge.handle(selected.method, {
|
|
254
|
+
challenge: selectedChallenge,
|
|
255
|
+
context: credentialContext,
|
|
256
|
+
fetch: baseFetch,
|
|
257
|
+
input: paymentInput,
|
|
258
|
+
})
|
|
259
|
+
return resolveCredential(selectedChallenge, selected.method, credentialContext)
|
|
260
|
+
},
|
|
247
261
|
)
|
|
248
262
|
const eventCredential = await events.emit(
|
|
249
263
|
'challenge.received',
|
|
@@ -278,11 +292,6 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
278
292
|
}),
|
|
279
293
|
)
|
|
280
294
|
|
|
281
|
-
const paymentInput = resolvePaymentRetryInput(
|
|
282
|
-
response,
|
|
283
|
-
initialRequest.input,
|
|
284
|
-
initialRequest.input,
|
|
285
|
-
)
|
|
286
295
|
response = await baseFetch(
|
|
287
296
|
paymentInput,
|
|
288
297
|
transport.setCredential(
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type * as Challenge from '../../Challenge.js'
|
|
2
|
+
import type { MaybePromise } from '../../internal/types.js'
|
|
3
|
+
import type * as Method from '../../Method.js'
|
|
4
|
+
|
|
5
|
+
const handlers = new WeakMap<Method.AnyClient, Handler>()
|
|
6
|
+
|
|
7
|
+
/** Inputs available before a client method creates a challenge credential. */
|
|
8
|
+
export type HandlerParameters = {
|
|
9
|
+
challenge: Challenge.Challenge
|
|
10
|
+
context?: unknown
|
|
11
|
+
fetch: typeof globalThis.fetch
|
|
12
|
+
input: RequestInfo | URL
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Internal client-method challenge hook. */
|
|
16
|
+
export type Handler = (parameters: HandlerParameters) => MaybePromise<void>
|
|
17
|
+
|
|
18
|
+
/** Registers an internal challenge hook without changing the public method shape. */
|
|
19
|
+
export function register<const method extends Method.AnyClient>(
|
|
20
|
+
method: method,
|
|
21
|
+
handler: Handler,
|
|
22
|
+
): method {
|
|
23
|
+
handlers.set(method, handler)
|
|
24
|
+
return method
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Runs method-specific work before creating a challenge credential. */
|
|
28
|
+
export function handle(method: Method.AnyClient, parameters: HandlerParameters): Promise<void> {
|
|
29
|
+
return Promise.resolve(handlers.get(method)?.(parameters))
|
|
30
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
2
|
+
import { homedir, tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'vp/test'
|
|
7
|
+
|
|
8
|
+
import type { ChannelEntry } from '../tempo/session/client/ChannelOps.js'
|
|
9
|
+
import { entryKey } from '../tempo/session/client/ChannelStore.js'
|
|
10
|
+
import { createSqliteChannelStore, defaultChannelDatabasePath } from './node.js'
|
|
11
|
+
|
|
12
|
+
const channel: ChannelEntry = {
|
|
13
|
+
channelId: `0x${'11'.repeat(32)}`,
|
|
14
|
+
cumulativeAmount: 2_000_000n,
|
|
15
|
+
deposit: 10_000_000n,
|
|
16
|
+
descriptor: {
|
|
17
|
+
authorizedSigner: '0x0000000000000000000000000000000000000001',
|
|
18
|
+
expiringNonceHash: `0x${'22'.repeat(32)}`,
|
|
19
|
+
operator: '0x0000000000000000000000000000000000000000',
|
|
20
|
+
payee: '0x0000000000000000000000000000000000000002',
|
|
21
|
+
payer: '0x0000000000000000000000000000000000000003',
|
|
22
|
+
salt: `0x${'33'.repeat(32)}`,
|
|
23
|
+
token: '0x0000000000000000000000000000000000000004',
|
|
24
|
+
},
|
|
25
|
+
escrow: '0x0000000000000000000000000000000000000005',
|
|
26
|
+
chainId: 4217,
|
|
27
|
+
opened: true,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('SQLite ChannelStore', () => {
|
|
31
|
+
test('defaults to the shared Tempo wallet channels database', () => {
|
|
32
|
+
expect(defaultChannelDatabasePath()).toBe(join(homedir(), '.tempo', 'wallet', 'channels.db'))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('persists a namespaced channel across fresh client instances', () => {
|
|
36
|
+
const directory = mkdtempSync(join(tmpdir(), 'mppx-channels-'))
|
|
37
|
+
const path = join(directory, 'channels.db')
|
|
38
|
+
try {
|
|
39
|
+
const first = createSqliteChannelStore({ namespace: 'https://api.example.com', path })
|
|
40
|
+
first.set(channel)
|
|
41
|
+
first.close()
|
|
42
|
+
|
|
43
|
+
const second = createSqliteChannelStore({ namespace: 'https://api.example.com', path })
|
|
44
|
+
expect(second.get(entryKey(channel))).toEqual(channel)
|
|
45
|
+
second.set({ ...channel, cumulativeAmount: 1_000_000n, deposit: 5_000_000n })
|
|
46
|
+
expect(second.get(entryKey(channel))).toMatchObject({
|
|
47
|
+
cumulativeAmount: 2_000_000n,
|
|
48
|
+
deposit: 10_000_000n,
|
|
49
|
+
})
|
|
50
|
+
second.close()
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(directory, { recursive: true })
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('rehydrates an existing wallet-cli v2 channel row', () => {
|
|
57
|
+
const directory = mkdtempSync(join(tmpdir(), 'mppx-wallet-channels-'))
|
|
58
|
+
const path = join(directory, 'channels.db')
|
|
59
|
+
try {
|
|
60
|
+
const database = new DatabaseSync(path)
|
|
61
|
+
database.exec(`CREATE TABLE channels (
|
|
62
|
+
channel_id TEXT PRIMARY KEY,
|
|
63
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
64
|
+
origin TEXT NOT NULL,
|
|
65
|
+
request_url TEXT NOT NULL DEFAULT '',
|
|
66
|
+
chain_id INTEGER NOT NULL,
|
|
67
|
+
escrow_contract TEXT NOT NULL,
|
|
68
|
+
token TEXT NOT NULL,
|
|
69
|
+
payee TEXT NOT NULL,
|
|
70
|
+
payer TEXT NOT NULL,
|
|
71
|
+
authorized_signer TEXT NOT NULL,
|
|
72
|
+
salt TEXT NOT NULL,
|
|
73
|
+
deposit TEXT NOT NULL,
|
|
74
|
+
cumulative_amount TEXT NOT NULL,
|
|
75
|
+
challenge_echo TEXT NOT NULL,
|
|
76
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
77
|
+
close_requested_at INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
grace_ready_at INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
created_at INTEGER NOT NULL,
|
|
80
|
+
last_used_at INTEGER NOT NULL,
|
|
81
|
+
accepted_cumulative TEXT NOT NULL DEFAULT '0',
|
|
82
|
+
server_spent TEXT NOT NULL DEFAULT '0',
|
|
83
|
+
session_protocol TEXT NOT NULL DEFAULT 'v1',
|
|
84
|
+
descriptor_json TEXT
|
|
85
|
+
)`)
|
|
86
|
+
database
|
|
87
|
+
.prepare(`INSERT INTO channels (
|
|
88
|
+
channel_id, origin, chain_id, escrow_contract, token, payee, payer,
|
|
89
|
+
authorized_signer, salt, deposit, cumulative_amount, challenge_echo,
|
|
90
|
+
state, created_at, last_used_at, session_protocol, descriptor_json
|
|
91
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '{}', 'active', 1, 1, 'v2', ?)`)
|
|
92
|
+
.run(
|
|
93
|
+
channel.channelId,
|
|
94
|
+
'https://api.example.com',
|
|
95
|
+
channel.chainId,
|
|
96
|
+
channel.escrow,
|
|
97
|
+
channel.descriptor.token,
|
|
98
|
+
channel.descriptor.payee,
|
|
99
|
+
channel.descriptor.payer,
|
|
100
|
+
channel.descriptor.authorizedSigner,
|
|
101
|
+
channel.descriptor.salt,
|
|
102
|
+
channel.deposit.toString(),
|
|
103
|
+
channel.cumulativeAmount.toString(),
|
|
104
|
+
JSON.stringify(channel.descriptor),
|
|
105
|
+
)
|
|
106
|
+
database.close()
|
|
107
|
+
|
|
108
|
+
const store = createSqliteChannelStore({ namespace: 'https://api.example.com', path })
|
|
109
|
+
expect(store.get(entryKey(channel))).toEqual(channel)
|
|
110
|
+
store.close()
|
|
111
|
+
} finally {
|
|
112
|
+
rmSync(directory, { recursive: true })
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
5
|
+
|
|
6
|
+
import type { ChannelEntry } from '../tempo/session/client/ChannelOps.js'
|
|
7
|
+
import {
|
|
8
|
+
deserializeEntry,
|
|
9
|
+
entryKey,
|
|
10
|
+
serializeEntry,
|
|
11
|
+
type ChannelStore,
|
|
12
|
+
type StoredChannel,
|
|
13
|
+
} from '../tempo/session/client/ChannelStore.js'
|
|
14
|
+
|
|
15
|
+
const schemaVersion = 2
|
|
16
|
+
|
|
17
|
+
/** Options for the Node SQLite-backed payer channel store. */
|
|
18
|
+
export type SqliteChannelStoreOptions = {
|
|
19
|
+
/** Service namespace, normally the protected API origin. */
|
|
20
|
+
namespace?: string | undefined
|
|
21
|
+
/** SQLite file path. Defaults to Tempo Wallet's shared channel database. */
|
|
22
|
+
path?: string | undefined
|
|
23
|
+
/** Full protected URL retained for CLI session-management requests. */
|
|
24
|
+
requestUrl?: string | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A SQLite-backed channel store that can release its database handle. */
|
|
28
|
+
export type SqliteChannelStore = ChannelStore & {
|
|
29
|
+
/** Absolute or caller-supplied path opened by this store. */
|
|
30
|
+
readonly path: string
|
|
31
|
+
/** Closes the underlying SQLite connection. */
|
|
32
|
+
close(): void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ChannelRow = {
|
|
36
|
+
chain_id: number
|
|
37
|
+
channel_id: string
|
|
38
|
+
cumulative_amount: string
|
|
39
|
+
deposit: string
|
|
40
|
+
descriptor_json: string | null
|
|
41
|
+
entry_json: string | null
|
|
42
|
+
escrow_contract: string
|
|
43
|
+
state: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns the channel database shared by Tempo command-line applications. */
|
|
47
|
+
export function defaultChannelDatabasePath(): string {
|
|
48
|
+
return join(homedir(), '.tempo', 'wallet', 'channels.db')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a synchronous Node SQLite implementation of {@link ChannelStore}.
|
|
53
|
+
*
|
|
54
|
+
* The schema is compatible with Tempo Wallet's existing `channels` table, so
|
|
55
|
+
* a fresh MPPx client can reuse v2 session records without a separate migration
|
|
56
|
+
* command. A namespace keeps identical payment scopes at different services
|
|
57
|
+
* isolated from one another.
|
|
58
|
+
*/
|
|
59
|
+
export function createSqliteChannelStore(
|
|
60
|
+
options: SqliteChannelStoreOptions = {},
|
|
61
|
+
): SqliteChannelStore {
|
|
62
|
+
const path = options.path ?? defaultChannelDatabasePath()
|
|
63
|
+
const namespace = options.namespace ?? ''
|
|
64
|
+
const requestUrl = options.requestUrl ?? namespace
|
|
65
|
+
const origin = resolveOrigin(requestUrl, namespace)
|
|
66
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
67
|
+
const database = new DatabaseSync(path)
|
|
68
|
+
database.exec('PRAGMA journal_mode = WAL')
|
|
69
|
+
database.exec('PRAGMA busy_timeout = 5000')
|
|
70
|
+
ensureSchema(database)
|
|
71
|
+
|
|
72
|
+
const getRow = database.prepare(`SELECT channel_id, chain_id, escrow_contract,
|
|
73
|
+
cumulative_amount, deposit, descriptor_json, entry_json, state
|
|
74
|
+
FROM channels
|
|
75
|
+
WHERE scope_key = ?`)
|
|
76
|
+
const deleteScope = database.prepare('DELETE FROM channels WHERE scope_key = ?')
|
|
77
|
+
const deleteOtherScopeChannel = database.prepare(
|
|
78
|
+
'DELETE FROM channels WHERE scope_key = ? AND channel_id <> ?',
|
|
79
|
+
)
|
|
80
|
+
const upsert = database.prepare(`INSERT INTO channels (
|
|
81
|
+
channel_id, version, scope_key, origin, request_url, chain_id, escrow_contract, token,
|
|
82
|
+
payee, payer, authorized_signer, salt, session_protocol, descriptor_json, entry_json,
|
|
83
|
+
deposit, cumulative_amount, accepted_cumulative, challenge_echo, state,
|
|
84
|
+
close_requested_at, grace_ready_at, created_at, last_used_at, server_spent
|
|
85
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'v2', ?, ?, ?, ?, '0', '{}', 'active',
|
|
86
|
+
0, 0, ?, ?, '0')
|
|
87
|
+
ON CONFLICT(channel_id) DO UPDATE SET
|
|
88
|
+
version = excluded.version,
|
|
89
|
+
scope_key = excluded.scope_key,
|
|
90
|
+
origin = excluded.origin,
|
|
91
|
+
request_url = excluded.request_url,
|
|
92
|
+
chain_id = excluded.chain_id,
|
|
93
|
+
escrow_contract = excluded.escrow_contract,
|
|
94
|
+
token = excluded.token,
|
|
95
|
+
payee = excluded.payee,
|
|
96
|
+
payer = excluded.payer,
|
|
97
|
+
authorized_signer = excluded.authorized_signer,
|
|
98
|
+
salt = excluded.salt,
|
|
99
|
+
session_protocol = excluded.session_protocol,
|
|
100
|
+
descriptor_json = excluded.descriptor_json,
|
|
101
|
+
entry_json = excluded.entry_json,
|
|
102
|
+
deposit = excluded.deposit,
|
|
103
|
+
cumulative_amount = excluded.cumulative_amount,
|
|
104
|
+
state = 'active',
|
|
105
|
+
close_requested_at = 0,
|
|
106
|
+
last_used_at = excluded.last_used_at`)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
path,
|
|
110
|
+
get(key) {
|
|
111
|
+
const row = getRow.get(scopedKey(namespace, key)) as ChannelRow | undefined
|
|
112
|
+
return row ? entryFromRow(row) : undefined
|
|
113
|
+
},
|
|
114
|
+
set(entry) {
|
|
115
|
+
const scopeKey = scopedKey(namespace, entryKey(entry))
|
|
116
|
+
database.exec('BEGIN IMMEDIATE')
|
|
117
|
+
try {
|
|
118
|
+
const existing = getRow.get(scopeKey) as ChannelRow | undefined
|
|
119
|
+
const merged = mergeEntry(existing ? entryFromRow(existing) : undefined, entry)
|
|
120
|
+
const stored = serializeEntry(merged)
|
|
121
|
+
const now = Math.floor(Date.now() / 1_000)
|
|
122
|
+
deleteOtherScopeChannel.run(scopeKey, merged.channelId)
|
|
123
|
+
upsert.run(
|
|
124
|
+
merged.channelId,
|
|
125
|
+
schemaVersion,
|
|
126
|
+
scopeKey,
|
|
127
|
+
origin,
|
|
128
|
+
requestUrl,
|
|
129
|
+
merged.chainId,
|
|
130
|
+
merged.escrow,
|
|
131
|
+
merged.descriptor.token,
|
|
132
|
+
merged.descriptor.payee,
|
|
133
|
+
merged.descriptor.payer,
|
|
134
|
+
merged.descriptor.authorizedSigner,
|
|
135
|
+
merged.descriptor.salt,
|
|
136
|
+
JSON.stringify(merged.descriptor),
|
|
137
|
+
JSON.stringify(stored),
|
|
138
|
+
merged.deposit.toString(),
|
|
139
|
+
merged.cumulativeAmount.toString(),
|
|
140
|
+
now,
|
|
141
|
+
now,
|
|
142
|
+
)
|
|
143
|
+
database.exec('COMMIT')
|
|
144
|
+
} catch (error) {
|
|
145
|
+
database.exec('ROLLBACK')
|
|
146
|
+
throw error
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
delete(key) {
|
|
150
|
+
deleteScope.run(scopedKey(namespace, key))
|
|
151
|
+
},
|
|
152
|
+
close() {
|
|
153
|
+
database.close()
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveOrigin(requestUrl: string, fallback: string): string {
|
|
159
|
+
try {
|
|
160
|
+
return new URL(requestUrl).origin
|
|
161
|
+
} catch {
|
|
162
|
+
return fallback
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function scopedKey(namespace: string, key: string): string {
|
|
167
|
+
return `${namespace}\n${key}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function mergeEntry(current: ChannelEntry | undefined, incoming: ChannelEntry): ChannelEntry {
|
|
171
|
+
if (!current || current.channelId.toLowerCase() !== incoming.channelId.toLowerCase())
|
|
172
|
+
return incoming
|
|
173
|
+
return {
|
|
174
|
+
...incoming,
|
|
175
|
+
cumulativeAmount:
|
|
176
|
+
current.cumulativeAmount > incoming.cumulativeAmount
|
|
177
|
+
? current.cumulativeAmount
|
|
178
|
+
: incoming.cumulativeAmount,
|
|
179
|
+
deposit: current.deposit > incoming.deposit ? current.deposit : incoming.deposit,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function entryFromRow(row: ChannelRow): ChannelEntry {
|
|
184
|
+
if (row.entry_json) {
|
|
185
|
+
const stored = JSON.parse(row.entry_json) as StoredChannel
|
|
186
|
+
return deserializeEntry({ ...stored, opened: row.state === 'active' })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!row.descriptor_json) throw new Error('v2 channel row is missing its descriptor')
|
|
190
|
+
const descriptor = JSON.parse(row.descriptor_json) as ChannelEntry['descriptor']
|
|
191
|
+
return {
|
|
192
|
+
channelId: row.channel_id as ChannelEntry['channelId'],
|
|
193
|
+
cumulativeAmount: BigInt(row.cumulative_amount),
|
|
194
|
+
deposit: BigInt(row.deposit),
|
|
195
|
+
descriptor,
|
|
196
|
+
escrow: row.escrow_contract as ChannelEntry['escrow'],
|
|
197
|
+
chainId: row.chain_id,
|
|
198
|
+
opened: row.state === 'active',
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function ensureSchema(database: DatabaseSync): void {
|
|
203
|
+
database.exec(`CREATE TABLE IF NOT EXISTS channels (
|
|
204
|
+
channel_id TEXT PRIMARY KEY,
|
|
205
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
206
|
+
scope_key TEXT,
|
|
207
|
+
origin TEXT NOT NULL,
|
|
208
|
+
request_url TEXT NOT NULL DEFAULT '',
|
|
209
|
+
chain_id INTEGER NOT NULL,
|
|
210
|
+
escrow_contract TEXT NOT NULL,
|
|
211
|
+
token TEXT NOT NULL,
|
|
212
|
+
payee TEXT NOT NULL,
|
|
213
|
+
payer TEXT NOT NULL,
|
|
214
|
+
authorized_signer TEXT NOT NULL,
|
|
215
|
+
salt TEXT NOT NULL,
|
|
216
|
+
deposit TEXT NOT NULL,
|
|
217
|
+
cumulative_amount TEXT NOT NULL,
|
|
218
|
+
challenge_echo TEXT NOT NULL,
|
|
219
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
220
|
+
close_requested_at INTEGER NOT NULL DEFAULT 0,
|
|
221
|
+
grace_ready_at INTEGER NOT NULL DEFAULT 0,
|
|
222
|
+
created_at INTEGER NOT NULL,
|
|
223
|
+
last_used_at INTEGER NOT NULL,
|
|
224
|
+
accepted_cumulative TEXT NOT NULL DEFAULT '0',
|
|
225
|
+
server_spent TEXT NOT NULL DEFAULT '0',
|
|
226
|
+
session_protocol TEXT NOT NULL DEFAULT 'v1',
|
|
227
|
+
descriptor_json TEXT,
|
|
228
|
+
entry_json TEXT
|
|
229
|
+
)`)
|
|
230
|
+
addColumn(database, 'scope_key TEXT')
|
|
231
|
+
addColumn(database, 'entry_json TEXT')
|
|
232
|
+
database.exec(`UPDATE channels
|
|
233
|
+
SET scope_key = origin || char(10) || lower(payee) || ':' || lower(token) || ':' ||
|
|
234
|
+
lower(escrow_contract) || ':' || chain_id
|
|
235
|
+
WHERE scope_key IS NULL AND session_protocol = 'v2' AND descriptor_json IS NOT NULL`)
|
|
236
|
+
database.exec(
|
|
237
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS idx_channels_scope_key ON channels(scope_key) WHERE scope_key IS NOT NULL',
|
|
238
|
+
)
|
|
239
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_channels_origin ON channels(origin)')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function addColumn(database: DatabaseSync, definition: string): void {
|
|
243
|
+
const name = definition.slice(0, definition.indexOf(' '))
|
|
244
|
+
const columns = database.prepare('PRAGMA table_info(channels)').all() as Array<{ name: string }>
|
|
245
|
+
if (columns.some((column) => column.name === name)) return
|
|
246
|
+
database.exec(`ALTER TABLE channels ADD COLUMN ${definition}`)
|
|
247
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { expectTypeOf, test } from 'vp/test'
|
|
2
|
+
|
|
3
|
+
import type { ConfiguredDefaults } from './types.js'
|
|
4
|
+
|
|
5
|
+
test('ConfiguredDefaults retains only known defined values', () => {
|
|
6
|
+
type Parameters = {
|
|
7
|
+
configured: string
|
|
8
|
+
maybeDefined?: number | undefined
|
|
9
|
+
explicitlyUndefined: boolean | undefined
|
|
10
|
+
unrelated: bigint
|
|
11
|
+
}
|
|
12
|
+
type Defaults = {
|
|
13
|
+
configured?: string | undefined
|
|
14
|
+
maybeDefined?: number | undefined
|
|
15
|
+
explicitlyUndefined?: boolean | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
expectTypeOf<ConfiguredDefaults<Parameters, Defaults>>().toEqualTypeOf<{
|
|
19
|
+
configured: string
|
|
20
|
+
}>()
|
|
21
|
+
})
|