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
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
|
})
|
|
@@ -10,6 +10,7 @@ import { rpcUrl } from '~test/tempo/rpc.js'
|
|
|
10
10
|
import { accounts, asset, chain, client, http } from '~test/tempo/viem.js'
|
|
11
11
|
|
|
12
12
|
import * as Fetch from './Fetch.js'
|
|
13
|
+
import * as MethodResponse from './MethodResponse.js'
|
|
13
14
|
|
|
14
15
|
const realm = 'api.example.com'
|
|
15
16
|
const secretKey = 'test-secret-key-test-secret-key-32'
|
|
@@ -391,6 +392,34 @@ function encodeRawPaymentRequiredHeader(value: unknown): string {
|
|
|
391
392
|
return Base64.fromString(JSON.stringify(value))
|
|
392
393
|
}
|
|
393
394
|
|
|
395
|
+
describe('Fetch.from: method responses', () => {
|
|
396
|
+
test('handles successful paid responses without touching free responses', async () => {
|
|
397
|
+
const method = { ...noopMethod }
|
|
398
|
+
const handle = vi.fn(
|
|
399
|
+
async ({ response }: MethodResponse.HandlerParameters) =>
|
|
400
|
+
new Response(`handled:${await response.text()}`, response),
|
|
401
|
+
)
|
|
402
|
+
MethodResponse.register(method, handle)
|
|
403
|
+
|
|
404
|
+
let paidResponse: Response | undefined
|
|
405
|
+
const mockFetch: typeof globalThis.fetch = async (input, init) => {
|
|
406
|
+
if (input.toString().endsWith('/free')) return new Response('free')
|
|
407
|
+
if (!new Headers(init?.headers).has('Authorization')) return make402()
|
|
408
|
+
paidResponse = new Response('paid')
|
|
409
|
+
vi.spyOn(paidResponse, 'clone')
|
|
410
|
+
return paidResponse
|
|
411
|
+
}
|
|
412
|
+
const fetch = Fetch.from({ fetch: mockFetch, methods: [method] })
|
|
413
|
+
|
|
414
|
+
expect(await (await fetch('https://example.com/free')).text()).toBe('free')
|
|
415
|
+
expect(handle).not.toHaveBeenCalled()
|
|
416
|
+
expect(await (await fetch('https://example.com/paid')).text()).toBe('handled:paid')
|
|
417
|
+
expect(handle).toHaveBeenCalledOnce()
|
|
418
|
+
expect(handle).toHaveBeenCalledWith(expect.objectContaining({ credential: 'credential' }))
|
|
419
|
+
expect(paidResponse?.clone).not.toHaveBeenCalled()
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
394
423
|
describe('Fetch.from: init passthrough (non-402)', () => {
|
|
395
424
|
test('preserves init object identity while adding Accept-Payment', async () => {
|
|
396
425
|
const receivedInits: (RequestInit | undefined)[] = []
|
|
@@ -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 MethodResponse from './MethodResponse.js'
|
|
9
10
|
|
|
10
11
|
// We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
|
|
11
12
|
// even across multiple module instances/bundles. This lets restore() avoid clobbering
|
|
@@ -18,6 +19,7 @@ type WrappedFetch = typeof globalThis.fetch & {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
let originalFetch: typeof globalThis.fetch | undefined
|
|
22
|
+
const eventObserverChecks = new WeakMap<object, (name: keyof ClientEventMap) => boolean>()
|
|
21
23
|
|
|
22
24
|
export type ClientEventMap<
|
|
23
25
|
methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[],
|
|
@@ -174,7 +176,11 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
174
176
|
// which can duplicate retries and make restore semantics fragile.
|
|
175
177
|
const baseFetch = unwrapFetch(fetch)
|
|
176
178
|
|
|
177
|
-
const
|
|
179
|
+
const request = async (
|
|
180
|
+
input: RequestInfo | URL,
|
|
181
|
+
init: from.RequestInit<methods> | undefined,
|
|
182
|
+
canRefetch: boolean,
|
|
183
|
+
): Promise<Response> => {
|
|
178
184
|
const callerHeaders = getCallerHeaders(input, init?.headers)
|
|
179
185
|
const hasExplicitAcceptPayment = callerHeaders.has(Constants.Headers.acceptPayment)
|
|
180
186
|
const paymentPreferences = resolvePaymentPreferences(callerHeaders, resolvedAcceptPayment)
|
|
@@ -272,8 +278,13 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
272
278
|
}),
|
|
273
279
|
)
|
|
274
280
|
|
|
281
|
+
const paymentInput = resolvePaymentRetryInput(
|
|
282
|
+
response,
|
|
283
|
+
initialRequest.input,
|
|
284
|
+
initialRequest.input,
|
|
285
|
+
)
|
|
275
286
|
response = await baseFetch(
|
|
276
|
-
|
|
287
|
+
paymentInput,
|
|
277
288
|
transport.setCredential(
|
|
278
289
|
{
|
|
279
290
|
...fetchInit,
|
|
@@ -283,7 +294,8 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
283
294
|
{ challenge: selectedChallenge },
|
|
284
295
|
),
|
|
285
296
|
)
|
|
286
|
-
|
|
297
|
+
// Cloning an unobserved stream would tee and buffer it indefinitely.
|
|
298
|
+
if (response.ok && hasEventObservers(events, 'payment.response'))
|
|
287
299
|
await events.emit(
|
|
288
300
|
'payment.response',
|
|
289
301
|
createPaymentResponsePayload({
|
|
@@ -295,8 +307,23 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
295
307
|
response,
|
|
296
308
|
}),
|
|
297
309
|
)
|
|
298
|
-
if (!(await transport.isPaymentRequired(response, transportRequest as never)))
|
|
299
|
-
return response
|
|
310
|
+
if (!(await transport.isPaymentRequired(response, transportRequest as never))) {
|
|
311
|
+
if (!response.ok) return response
|
|
312
|
+
return MethodResponse.handle(selected.method, {
|
|
313
|
+
challenge: selectedChallenge,
|
|
314
|
+
credential,
|
|
315
|
+
fetch: baseFetch,
|
|
316
|
+
headers: initialRequest.headers,
|
|
317
|
+
input: paymentInput,
|
|
318
|
+
...(canRefetch
|
|
319
|
+
? {
|
|
320
|
+
refetch: () => request(cloneRequestInput(initialRequest.input), init, false),
|
|
321
|
+
}
|
|
322
|
+
: {}),
|
|
323
|
+
response,
|
|
324
|
+
signal: resolveRequestSignal(input, init),
|
|
325
|
+
})
|
|
326
|
+
}
|
|
300
327
|
}
|
|
301
328
|
return response
|
|
302
329
|
} catch (error) {
|
|
@@ -315,6 +342,8 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
315
342
|
throw error
|
|
316
343
|
}
|
|
317
344
|
}
|
|
345
|
+
const wrappedFetch = (input: RequestInfo | URL, init?: from.RequestInit<methods>) =>
|
|
346
|
+
request(input, init, true)
|
|
318
347
|
|
|
319
348
|
// Record the wrapped target so future polyfill() / restore() calls can detect origin
|
|
320
349
|
// and safely unwrap only mppx-installed wrappers.
|
|
@@ -491,7 +520,7 @@ export function createEventDispatcher<
|
|
|
491
520
|
}
|
|
492
521
|
}
|
|
493
522
|
|
|
494
|
-
|
|
523
|
+
const dispatcher: ClientEventDispatcher<methods, response> = {
|
|
495
524
|
async emit(name, payload) {
|
|
496
525
|
switch (name) {
|
|
497
526
|
case 'challenge.received': {
|
|
@@ -525,6 +554,15 @@ export function createEventDispatcher<
|
|
|
525
554
|
},
|
|
526
555
|
on,
|
|
527
556
|
}
|
|
557
|
+
eventObserverChecks.set(
|
|
558
|
+
dispatcher,
|
|
559
|
+
(name) => handlers[name as keyof typeof handlers].size > 0 || handlers['*'].size > 0,
|
|
560
|
+
)
|
|
561
|
+
return dispatcher
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function hasEventObservers(dispatcher: ClientEventDispatcher, name: keyof ClientEventMap): boolean {
|
|
565
|
+
return eventObserverChecks.get(dispatcher)?.(name) ?? true
|
|
528
566
|
}
|
|
529
567
|
|
|
530
568
|
async function emitChallengeReceived<methods extends readonly Method.AnyClient[], response>(
|
|
@@ -812,6 +850,13 @@ function cloneRequestInput(input: RequestInfo | URL): RequestInfo | URL {
|
|
|
812
850
|
}
|
|
813
851
|
}
|
|
814
852
|
|
|
853
|
+
function resolveRequestSignal(
|
|
854
|
+
input: RequestInfo | URL,
|
|
855
|
+
init: RequestInit | undefined,
|
|
856
|
+
): AbortSignal | undefined {
|
|
857
|
+
return init?.signal ?? (input instanceof Request ? input.signal : undefined)
|
|
858
|
+
}
|
|
859
|
+
|
|
815
860
|
/** @internal */
|
|
816
861
|
function isWrappedFetch(fetch: typeof globalThis.fetch): fetch is WrappedFetch {
|
|
817
862
|
return Boolean((fetch as WrappedFetch)[MPPX_FETCH_WRAPPER])
|
|
@@ -0,0 +1,39 @@
|
|
|
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 when a client method handles a successful paid response. */
|
|
8
|
+
export type HandlerParameters = {
|
|
9
|
+
challenge: Challenge.Challenge
|
|
10
|
+
credential: string
|
|
11
|
+
fetch: typeof globalThis.fetch
|
|
12
|
+
headers: Headers
|
|
13
|
+
input: RequestInfo | URL
|
|
14
|
+
refetch?: (() => Promise<Response>) | undefined
|
|
15
|
+
response: Response
|
|
16
|
+
signal?: AbortSignal | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Internal client-method response adapter. */
|
|
20
|
+
export type Handler = (parameters: HandlerParameters) => MaybePromise<Response>
|
|
21
|
+
|
|
22
|
+
/** Registers an internal response adapter without changing the public method shape. */
|
|
23
|
+
export function register<const method extends Method.AnyClient>(
|
|
24
|
+
method: method,
|
|
25
|
+
handler: Handler,
|
|
26
|
+
): method {
|
|
27
|
+
handlers.set(method, handler)
|
|
28
|
+
return method
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Removes response handling from a method whose caller owns the response lifecycle. */
|
|
32
|
+
export function unregister(method: Method.AnyClient): void {
|
|
33
|
+
handlers.delete(method)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Lets the selected client method handle a successful paid response. */
|
|
37
|
+
export function handle(method: Method.AnyClient, parameters: HandlerParameters): Promise<Response> {
|
|
38
|
+
return Promise.resolve(handlers.get(method)?.(parameters) ?? parameters.response)
|
|
39
|
+
}
|
|
@@ -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
|
+
})
|