mppx 0.4.9 → 0.4.10
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 +17 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +155 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/discovery/Discovery.d.ts +146 -0
- package/dist/discovery/Discovery.d.ts.map +1 -0
- package/dist/discovery/Discovery.js +60 -0
- package/dist/discovery/Discovery.js.map +1 -0
- package/dist/discovery/OpenApi.d.ts +61 -0
- package/dist/discovery/OpenApi.d.ts.map +1 -0
- package/dist/discovery/OpenApi.js +139 -0
- package/dist/discovery/OpenApi.js.map +1 -0
- package/dist/discovery/Validate.d.ts +10 -0
- package/dist/discovery/Validate.d.ts.map +1 -0
- package/dist/discovery/Validate.js +63 -0
- package/dist/discovery/Validate.js.map +1 -0
- package/dist/discovery/index.d.ts +4 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +4 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/middlewares/elysia.d.ts +52 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +17 -0
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts +13 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +18 -0
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts +19 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js +51 -0
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +4 -2
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +10 -3
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts +11 -0
- package/dist/middlewares/nextjs.d.ts.map +1 -1
- package/dist/middlewares/nextjs.js +15 -0
- package/dist/middlewares/nextjs.js.map +1 -1
- package/dist/proxy/Proxy.d.ts +6 -0
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +56 -80
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.d.ts +16 -23
- package/dist/proxy/Service.d.ts.map +1 -1
- package/dist/proxy/Service.js +19 -83
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.js +1 -1
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/proxy/services/anthropic.d.ts.map +1 -1
- package/dist/proxy/services/anthropic.js +5 -0
- package/dist/proxy/services/anthropic.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +6 -3
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/proxy/services/stripe.d.ts.map +1 -1
- package/dist/proxy/services/stripe.js +6 -3
- package/dist/proxy/services/stripe.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +18 -5
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +8 -0
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.js +1 -1
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +6 -1
- package/src/BodyDigest.test.ts +1 -1
- package/src/Challenge.fuzz.test.ts +121 -0
- package/src/Challenge.test-d.ts +1 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Credential.fuzz.test.ts +62 -0
- package/src/Credential.test.ts +1 -1
- package/src/Errors.test.ts +1 -1
- package/src/Expires.test.ts +1 -1
- package/src/Method.test.ts +1 -1
- package/src/PaymentRequest.test.ts +1 -1
- package/src/Receipt.test.ts +1 -1
- package/src/Store.test-d.ts +1 -1
- package/src/Store.test.ts +1 -1
- package/src/cli/cli.test.ts +212 -1
- package/src/cli/cli.ts +162 -0
- package/src/client/Mppx.test-d.ts +1 -1
- package/src/client/Mppx.test.ts +1 -1
- package/src/client/Transport.test.ts +1 -1
- package/src/client/internal/Fetch.browser.test.ts +1 -1
- package/src/client/internal/Fetch.test-d.ts +1 -1
- package/src/client/internal/Fetch.test.ts +2 -1
- package/src/discovery/Discovery.test.ts +152 -0
- package/src/discovery/Discovery.ts +72 -0
- package/src/discovery/OpenApi.test.ts +425 -0
- package/src/discovery/OpenApi.ts +224 -0
- package/src/discovery/Validate.test.ts +188 -0
- package/src/discovery/Validate.ts +76 -0
- package/src/discovery/index.ts +3 -0
- package/src/internal/constantTimeEqual.test.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
- package/src/mcp-sdk/client/McpClient.test.ts +1 -1
- package/src/mcp-sdk/server/Transport.test.ts +1 -1
- package/src/middlewares/elysia.test.ts +27 -2
- package/src/middlewares/elysia.ts +35 -1
- package/src/middlewares/express.test.ts +35 -7
- package/src/middlewares/express.ts +34 -0
- package/src/middlewares/hono.test.ts +28 -6
- package/src/middlewares/hono.ts +73 -1
- package/src/middlewares/internal/mppx.test.ts +1 -1
- package/src/middlewares/internal/mppx.ts +14 -6
- package/src/middlewares/nextjs.test.ts +31 -6
- package/src/middlewares/nextjs.ts +28 -0
- package/src/proxy/Proxy.test.ts +54 -270
- package/src/proxy/Proxy.ts +71 -93
- package/src/proxy/Service.test.ts +23 -1
- package/src/proxy/Service.ts +40 -86
- package/src/proxy/internal/Headers.test.ts +1 -1
- package/src/proxy/internal/Route.test.ts +9 -1
- package/src/proxy/internal/Route.ts +1 -1
- package/src/proxy/services/anthropic.test.ts +132 -0
- package/src/proxy/services/anthropic.ts +5 -0
- package/src/proxy/services/openai.test.ts +1 -1
- package/src/proxy/services/openai.ts +6 -4
- package/src/proxy/services/stripe.test.ts +132 -0
- package/src/proxy/services/stripe.ts +6 -4
- package/src/server/Mppx.test-d.ts +1 -1
- package/src/server/Mppx.test.ts +2 -1
- package/src/server/NodeListener.test.ts +1 -1
- package/src/server/Request.test.ts +1 -1
- package/src/server/Response.test.ts +1 -1
- package/src/server/Transport.test.ts +1 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/Methods.test.ts +1 -1
- package/src/stripe/client/Charge.test.ts +1 -1
- package/src/stripe/server/Charge.test.ts +1 -1
- package/src/tempo/Attribution.test.ts +1 -1
- package/src/tempo/Methods.test.ts +1 -1
- package/src/tempo/client/ChannelOps.test.ts +6 -3
- package/src/tempo/client/Session.test.ts +5 -2
- package/src/tempo/client/SessionManager.test.ts +1 -1
- package/src/tempo/internal/auto-swap.test.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +1 -1
- package/src/tempo/internal/fee-payer.test.ts +1 -1
- package/src/tempo/server/Charge.test.ts +1 -1
- package/src/tempo/server/Session.test.ts +87 -37
- package/src/tempo/server/Session.ts +25 -8
- package/src/tempo/server/Sse.test.ts +1 -1
- package/src/tempo/server/internal/transport.test.ts +24 -1
- package/src/tempo/server/internal/transport.ts +11 -0
- package/src/tempo/session/Chain.test.ts +5 -2
- package/src/tempo/session/Chain.ts +1 -1
- package/src/tempo/session/Channel.test.ts +1 -1
- package/src/tempo/session/ChannelStore.test.ts +1 -1
- package/src/tempo/session/Receipt.test.ts +1 -1
- package/src/tempo/session/Sse.fuzz.test.ts +138 -0
- package/src/tempo/session/Sse.test.ts +1 -1
- package/src/tempo/session/Voucher.test.ts +1 -1
- package/src/viem/Account.test.ts +1 -1
- package/src/viem/Client.test.ts +1 -1
- package/src/zod.test.ts +147 -0
package/src/Expires.test.ts
CHANGED
package/src/Method.test.ts
CHANGED
package/src/Receipt.test.ts
CHANGED
package/src/Store.test-d.ts
CHANGED
package/src/Store.test.ts
CHANGED
package/src/cli/cli.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as path from 'node:path'
|
|
|
6
6
|
import { parseUnits } from 'viem'
|
|
7
7
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
8
8
|
import { Addresses } from 'viem/tempo'
|
|
9
|
-
import { afterAll, describe, expect, test } from '
|
|
9
|
+
import { afterAll, describe, expect, test } from 'vp/test'
|
|
10
10
|
import * as Http from '~test/Http.js'
|
|
11
11
|
import { rpcUrl } from '~test/tempo/prool.js'
|
|
12
12
|
import { deployEscrow } from '~test/tempo/session.js'
|
|
@@ -76,6 +76,217 @@ async function serve(argv: string[], options?: { env?: Record<string, string | u
|
|
|
76
76
|
return { output, stderr, exitCode }
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
describe('discover validate', () => {
|
|
80
|
+
test('validates a local discovery document', async () => {
|
|
81
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-discovery-'))
|
|
82
|
+
const file = path.join(dir, 'openapi.json')
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
file,
|
|
85
|
+
JSON.stringify({
|
|
86
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
87
|
+
openapi: '3.1.0',
|
|
88
|
+
paths: {
|
|
89
|
+
'/search': {
|
|
90
|
+
post: {
|
|
91
|
+
'x-payment-info': {
|
|
92
|
+
amount: '100',
|
|
93
|
+
intent: 'charge',
|
|
94
|
+
method: 'tempo',
|
|
95
|
+
},
|
|
96
|
+
requestBody: {
|
|
97
|
+
content: { 'application/json': { schema: { type: 'object' } } },
|
|
98
|
+
},
|
|
99
|
+
responses: {
|
|
100
|
+
'200': { description: 'OK' },
|
|
101
|
+
'402': { description: 'Payment Required' },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const { output, exitCode } = await serve(['discover', 'validate', file])
|
|
110
|
+
expect(exitCode).toBeUndefined()
|
|
111
|
+
expect(output).toContain('Discovery document is valid.')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('returns non-zero for invalid discovery documents', async () => {
|
|
115
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-discovery-'))
|
|
116
|
+
const file = path.join(dir, 'openapi.json')
|
|
117
|
+
fs.writeFileSync(
|
|
118
|
+
file,
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
121
|
+
openapi: '3.1.0',
|
|
122
|
+
paths: {
|
|
123
|
+
'/search': {
|
|
124
|
+
post: {
|
|
125
|
+
'x-payment-info': {
|
|
126
|
+
amount: '100',
|
|
127
|
+
intent: 'charge',
|
|
128
|
+
method: 'tempo',
|
|
129
|
+
},
|
|
130
|
+
responses: {
|
|
131
|
+
'200': { description: 'OK' },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const { output, exitCode } = await serve(['discover', 'validate', file])
|
|
140
|
+
expect(exitCode).toBe(1)
|
|
141
|
+
expect(output).toContain('[error]')
|
|
142
|
+
expect(output).toContain('402')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test(
|
|
146
|
+
'validates remote discovery documents and reports warnings',
|
|
147
|
+
{ timeout: 20_000 },
|
|
148
|
+
async () => {
|
|
149
|
+
const body = JSON.stringify({
|
|
150
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
151
|
+
openapi: '3.1.0',
|
|
152
|
+
paths: {
|
|
153
|
+
'/search': {
|
|
154
|
+
post: {
|
|
155
|
+
'x-payment-info': {
|
|
156
|
+
amount: '100',
|
|
157
|
+
intent: 'charge',
|
|
158
|
+
method: 'tempo',
|
|
159
|
+
},
|
|
160
|
+
responses: {
|
|
161
|
+
'200': { description: 'OK' },
|
|
162
|
+
'402': { description: 'Payment Required' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
const server = await Http.createServer((_req, res) => {
|
|
169
|
+
res.setHeader('Content-Type', 'application/json')
|
|
170
|
+
res.end(body)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const { output, exitCode } = await serve(['discover', 'validate', server.url])
|
|
175
|
+
expect(exitCode).toBeUndefined()
|
|
176
|
+
expect(output).toContain('[warning]')
|
|
177
|
+
expect(output).toContain('requestBody')
|
|
178
|
+
expect(output).toContain('valid with 1 warning')
|
|
179
|
+
} finally {
|
|
180
|
+
server.close()
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
test(
|
|
186
|
+
'rejects oversized discovery documents via content-length',
|
|
187
|
+
{ timeout: 20_000 },
|
|
188
|
+
async () => {
|
|
189
|
+
const server = await Http.createServer((_req, res) => {
|
|
190
|
+
res.setHeader('Content-Type', 'application/json')
|
|
191
|
+
res.setHeader('Content-Length', String(11 * 1024 * 1024))
|
|
192
|
+
res.end('{}')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const { exitCode, output } = await serve(['discover', 'validate', server.url])
|
|
197
|
+
expect(exitCode).toBe(1)
|
|
198
|
+
expect(output).toContain('10 MB')
|
|
199
|
+
} finally {
|
|
200
|
+
server.close()
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('discover generate', () => {
|
|
207
|
+
test('generates from a pre-built OpenAPI document module', async () => {
|
|
208
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-generate-'))
|
|
209
|
+
const mod = path.join(dir, 'doc.mjs')
|
|
210
|
+
fs.writeFileSync(
|
|
211
|
+
mod,
|
|
212
|
+
`export default ${JSON.stringify({
|
|
213
|
+
openapi: '3.1.0',
|
|
214
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
215
|
+
paths: {
|
|
216
|
+
'/pay': {
|
|
217
|
+
post: {
|
|
218
|
+
'x-payment-info': { amount: '100', intent: 'charge', method: 'tempo' },
|
|
219
|
+
responses: {
|
|
220
|
+
'200': { description: 'OK' },
|
|
221
|
+
'402': { description: 'Payment Required' },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
})}`,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
const { output, exitCode } = await serve(['discover', 'generate', mod])
|
|
230
|
+
expect(exitCode).toBeUndefined()
|
|
231
|
+
const doc = JSON.parse(output)
|
|
232
|
+
expect(doc.openapi).toBe('3.1.0')
|
|
233
|
+
expect(doc.paths['/pay'].post['x-payment-info'].amount).toBe('100')
|
|
234
|
+
|
|
235
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test('writes to file with --output', async () => {
|
|
239
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-generate-'))
|
|
240
|
+
const mod = path.join(dir, 'doc.mjs')
|
|
241
|
+
const outFile = path.join(dir, 'openapi.json')
|
|
242
|
+
fs.writeFileSync(
|
|
243
|
+
mod,
|
|
244
|
+
`export default ${JSON.stringify({
|
|
245
|
+
openapi: '3.1.0',
|
|
246
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
247
|
+
paths: {},
|
|
248
|
+
})}`,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
const { output, stderr, exitCode } = await serve([
|
|
252
|
+
'discover',
|
|
253
|
+
'generate',
|
|
254
|
+
mod,
|
|
255
|
+
'--output',
|
|
256
|
+
outFile,
|
|
257
|
+
])
|
|
258
|
+
expect(exitCode).toBeUndefined()
|
|
259
|
+
expect(output).toBe('')
|
|
260
|
+
expect(stderr).toContain(outFile)
|
|
261
|
+
const written = JSON.parse(fs.readFileSync(outFile, 'utf-8'))
|
|
262
|
+
expect(written.openapi).toBe('3.1.0')
|
|
263
|
+
|
|
264
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('errors when module not found', async () => {
|
|
268
|
+
const { output, exitCode } = await serve([
|
|
269
|
+
'discover',
|
|
270
|
+
'generate',
|
|
271
|
+
'/tmp/nonexistent-mppx-module.mjs',
|
|
272
|
+
])
|
|
273
|
+
expect(exitCode).toBe(1)
|
|
274
|
+
expect(output).toContain('MODULE_NOT_FOUND')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
test('errors when module has no mppx or openapi export', async () => {
|
|
278
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-generate-'))
|
|
279
|
+
const mod = path.join(dir, 'bad.mjs')
|
|
280
|
+
fs.writeFileSync(mod, 'export default { foo: "bar" }')
|
|
281
|
+
|
|
282
|
+
const { output, exitCode } = await serve(['discover', 'generate', mod])
|
|
283
|
+
expect(exitCode).toBe(1)
|
|
284
|
+
expect(output).toContain('INVALID_MODULE')
|
|
285
|
+
|
|
286
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
79
290
|
describe('basic charge (examples/basic)', () => {
|
|
80
291
|
test('happy path: makes payment and receives response', { timeout: 120_000 }, async () => {
|
|
81
292
|
const { Actions } = await import('viem/tempo')
|
package/src/cli/cli.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { tempo as tempoMainnet } from 'viem/chains'
|
|
|
11
11
|
import * as Challenge from '../Challenge.js'
|
|
12
12
|
import { normalizeHeaders } from '../client/internal/Fetch.js'
|
|
13
13
|
import * as Mppx from '../client/Mppx.js'
|
|
14
|
+
import { validate as validateDiscovery } from '../discovery/Validate.js'
|
|
14
15
|
import { createDefaultStore, createKeychain, resolveAccountName } from './account.js'
|
|
15
16
|
import { loadConfig, resolvePlugin } from './internal.js'
|
|
16
17
|
import type { Plugin } from './plugins/plugin.js'
|
|
@@ -915,7 +916,168 @@ export default defineConfig({
|
|
|
915
916
|
},
|
|
916
917
|
})
|
|
917
918
|
|
|
919
|
+
const discover = Cli.create('discover', {
|
|
920
|
+
description: 'Discovery tooling',
|
|
921
|
+
})
|
|
922
|
+
.command('generate', {
|
|
923
|
+
description: 'Generate a static OpenAPI discovery document from a module',
|
|
924
|
+
args: z.object({
|
|
925
|
+
module: z.string().describe('Path to a module that default-exports a discovery config'),
|
|
926
|
+
}),
|
|
927
|
+
options: z.object({
|
|
928
|
+
output: z.string().optional().describe('Write output to a file instead of stdout'),
|
|
929
|
+
}),
|
|
930
|
+
alias: { output: 'o' },
|
|
931
|
+
async run(c) {
|
|
932
|
+
const modulePath = path.resolve(c.args.module)
|
|
933
|
+
if (!fs.existsSync(modulePath)) {
|
|
934
|
+
return c.error({
|
|
935
|
+
code: 'MODULE_NOT_FOUND',
|
|
936
|
+
message: `Module not found: ${modulePath}`,
|
|
937
|
+
exitCode: 1,
|
|
938
|
+
})
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let mod: Record<string, unknown>
|
|
942
|
+
try {
|
|
943
|
+
mod = await import(modulePath)
|
|
944
|
+
} catch (error) {
|
|
945
|
+
return c.error({
|
|
946
|
+
code: 'MODULE_IMPORT_FAILED',
|
|
947
|
+
message: `Failed to import module: ${(error as Error).message}`,
|
|
948
|
+
exitCode: 1,
|
|
949
|
+
})
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const exported = (mod.default ?? mod) as Record<string, unknown>
|
|
953
|
+
|
|
954
|
+
// If the export is already a plain OpenAPI doc (has `openapi` key), use it directly.
|
|
955
|
+
// Otherwise, expect { mppx, ...GenerateConfig } and call generate().
|
|
956
|
+
let doc: Record<string, unknown>
|
|
957
|
+
if (typeof exported.openapi === 'string') {
|
|
958
|
+
doc = exported
|
|
959
|
+
} else {
|
|
960
|
+
const { generate } = await import('../discovery/OpenApi.js')
|
|
961
|
+
const mppx = exported.mppx as { methods: readonly any[]; realm: string }
|
|
962
|
+
if (!mppx) {
|
|
963
|
+
return c.error({
|
|
964
|
+
code: 'INVALID_MODULE',
|
|
965
|
+
message:
|
|
966
|
+
'Module must default-export an OpenAPI document (with `openapi` key) or an object with `mppx` (server instance) and `routes`.',
|
|
967
|
+
exitCode: 1,
|
|
968
|
+
})
|
|
969
|
+
}
|
|
970
|
+
doc = generate(mppx, exported as any)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const json = JSON.stringify(doc, null, 2)
|
|
974
|
+
if (c.options.output) {
|
|
975
|
+
const outPath = path.resolve(c.options.output)
|
|
976
|
+
fs.writeFileSync(outPath, `${json}\n`)
|
|
977
|
+
process.stderr.write(`Wrote ${outPath}\n`)
|
|
978
|
+
} else {
|
|
979
|
+
console.log(json)
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
})
|
|
983
|
+
.command('validate', {
|
|
984
|
+
description: 'Validate an OpenAPI discovery document from a file or URL',
|
|
985
|
+
args: z.object({
|
|
986
|
+
input: z.string().describe('Path or URL to a discovery document'),
|
|
987
|
+
}),
|
|
988
|
+
async run(c) {
|
|
989
|
+
const input = c.args.input
|
|
990
|
+
let raw: string
|
|
991
|
+
if (/^https?:\/\//.test(input)) {
|
|
992
|
+
const controller = new AbortController()
|
|
993
|
+
const timeout = setTimeout(() => controller.abort(), 30_000)
|
|
994
|
+
let response: Response
|
|
995
|
+
try {
|
|
996
|
+
response = await globalThis.fetch(input, { signal: controller.signal })
|
|
997
|
+
} catch (error) {
|
|
998
|
+
clearTimeout(timeout)
|
|
999
|
+
const msg =
|
|
1000
|
+
error instanceof DOMException && error.name === 'AbortError'
|
|
1001
|
+
? 'Request timed out after 30s'
|
|
1002
|
+
: (error as Error).message
|
|
1003
|
+
return c.error({
|
|
1004
|
+
code: 'DISCOVERY_FETCH_FAILED',
|
|
1005
|
+
message: `Failed to fetch discovery document: ${msg}`,
|
|
1006
|
+
exitCode: 1,
|
|
1007
|
+
})
|
|
1008
|
+
}
|
|
1009
|
+
clearTimeout(timeout)
|
|
1010
|
+
if (!response.ok) {
|
|
1011
|
+
return c.error({
|
|
1012
|
+
code: 'DISCOVERY_FETCH_FAILED',
|
|
1013
|
+
message: `Failed to fetch discovery document: HTTP ${response.status}`,
|
|
1014
|
+
exitCode: 1,
|
|
1015
|
+
})
|
|
1016
|
+
}
|
|
1017
|
+
const maxSize = 10 * 1024 * 1024 // 10 MB
|
|
1018
|
+
const contentLength = response.headers.get('content-length')
|
|
1019
|
+
if (contentLength && Number(contentLength) > maxSize) {
|
|
1020
|
+
return c.error({
|
|
1021
|
+
code: 'DISCOVERY_TOO_LARGE',
|
|
1022
|
+
message: `Discovery document exceeds 10 MB limit`,
|
|
1023
|
+
exitCode: 1,
|
|
1024
|
+
})
|
|
1025
|
+
}
|
|
1026
|
+
raw = await response.text()
|
|
1027
|
+
if (raw.length > maxSize) {
|
|
1028
|
+
return c.error({
|
|
1029
|
+
code: 'DISCOVERY_TOO_LARGE',
|
|
1030
|
+
message: `Discovery document exceeds 10 MB limit`,
|
|
1031
|
+
exitCode: 1,
|
|
1032
|
+
})
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
const resolved = path.resolve(input)
|
|
1036
|
+
if (!fs.existsSync(resolved)) {
|
|
1037
|
+
return c.error({
|
|
1038
|
+
code: 'DISCOVERY_NOT_FOUND',
|
|
1039
|
+
message: `Discovery document not found: ${resolved}`,
|
|
1040
|
+
exitCode: 1,
|
|
1041
|
+
})
|
|
1042
|
+
}
|
|
1043
|
+
raw = fs.readFileSync(resolved, 'utf-8')
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
let doc: unknown
|
|
1047
|
+
try {
|
|
1048
|
+
doc = JSON.parse(raw)
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
return c.error({
|
|
1051
|
+
code: 'DISCOVERY_INVALID_JSON',
|
|
1052
|
+
message: `Invalid discovery JSON: ${(error as Error).message}`,
|
|
1053
|
+
exitCode: 1,
|
|
1054
|
+
})
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const issues = validateDiscovery(doc)
|
|
1058
|
+
for (const issue of issues) console.log(`[${issue.severity}] ${issue.path}: ${issue.message}`)
|
|
1059
|
+
|
|
1060
|
+
const errorCount = issues.filter((issue) => issue.severity === 'error').length
|
|
1061
|
+
const warningCount = issues.filter((issue) => issue.severity === 'warning').length
|
|
1062
|
+
|
|
1063
|
+
if (errorCount > 0) {
|
|
1064
|
+
return c.error({
|
|
1065
|
+
code: 'DISCOVERY_INVALID',
|
|
1066
|
+
message: `Discovery document has ${errorCount} error(s) and ${warningCount} warning(s).`,
|
|
1067
|
+
exitCode: 1,
|
|
1068
|
+
})
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
console.log(
|
|
1072
|
+
warningCount > 0
|
|
1073
|
+
? `Discovery document is valid with ${warningCount} warning(s).`
|
|
1074
|
+
: 'Discovery document is valid.',
|
|
1075
|
+
)
|
|
1076
|
+
},
|
|
1077
|
+
})
|
|
1078
|
+
|
|
918
1079
|
cli.command(account)
|
|
1080
|
+
cli.command(discover)
|
|
919
1081
|
cli.command(init)
|
|
920
1082
|
cli.command(sign)
|
|
921
1083
|
|
package/src/client/Mppx.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Challenge, Credential, Mcp, Method, Receipt } from 'mppx'
|
|
|
2
2
|
import { Mppx, Transport, tempo } from 'mppx/client'
|
|
3
3
|
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
4
4
|
import { Methods } from 'mppx/tempo'
|
|
5
|
-
import { afterEach, describe, expect, test } from '
|
|
5
|
+
import { afterEach, describe, expect, test } from 'vp/test'
|
|
6
6
|
import * as Http from '~test/Http.js'
|
|
7
7
|
import { accounts, asset, client } from '~test/tempo/viem.js'
|
|
8
8
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Challenge, Credential, Mcp } from 'mppx'
|
|
2
2
|
import { Transport } from 'mppx/client'
|
|
3
3
|
import { Methods } from 'mppx/tempo'
|
|
4
|
-
import { describe, expect, test } from '
|
|
4
|
+
import { describe, expect, test } from 'vp/test'
|
|
5
5
|
|
|
6
6
|
const realm = 'api.example.com'
|
|
7
7
|
const secretKey = 'test-secret-key'
|
|
@@ -2,7 +2,7 @@ import { Receipt } from 'mppx'
|
|
|
2
2
|
import { tempo } from 'mppx/client'
|
|
3
3
|
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
4
4
|
import { createClient, defineChain } from 'viem'
|
|
5
|
-
import { describe, expect, test, vi } from '
|
|
5
|
+
import { describe, expect, test, vi } from 'vp/test'
|
|
6
6
|
import * as Http from '~test/Http.js'
|
|
7
7
|
import { rpcUrl } from '~test/tempo/prool.js'
|
|
8
8
|
import { accounts, asset, chain, client, http } from '~test/tempo/viem.js'
|
|
@@ -16,6 +16,7 @@ const server = Mppx_server.create({
|
|
|
16
16
|
methods: [
|
|
17
17
|
tempo_server({
|
|
18
18
|
getClient: () => client,
|
|
19
|
+
account: accounts[0],
|
|
19
20
|
}),
|
|
20
21
|
],
|
|
21
22
|
realm,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { DiscoveryDocument, PaymentInfo, ServiceInfo } from './Discovery.js'
|
|
2
|
+
|
|
3
|
+
describe('PaymentInfo', () => {
|
|
4
|
+
test('parses a valid charge payment info', () => {
|
|
5
|
+
const result = PaymentInfo.safeParse({
|
|
6
|
+
amount: '1000',
|
|
7
|
+
intent: 'charge',
|
|
8
|
+
method: 'tempo',
|
|
9
|
+
})
|
|
10
|
+
expect(result.success).toBe(true)
|
|
11
|
+
expect(result.data).toEqual({ amount: '1000', intent: 'charge', method: 'tempo' })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('parses a session with null amount', () => {
|
|
15
|
+
const result = PaymentInfo.safeParse({
|
|
16
|
+
amount: null,
|
|
17
|
+
intent: 'session',
|
|
18
|
+
method: 'tempo',
|
|
19
|
+
})
|
|
20
|
+
expect(result.success).toBe(true)
|
|
21
|
+
expect(result.data?.amount).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('accepts custom intents', () => {
|
|
25
|
+
const result = PaymentInfo.safeParse({
|
|
26
|
+
amount: '100',
|
|
27
|
+
intent: 'subscribe',
|
|
28
|
+
method: 'tempo',
|
|
29
|
+
})
|
|
30
|
+
expect(result.success).toBe(true)
|
|
31
|
+
expect(result.data?.intent).toBe('subscribe')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('rejects invalid amount pattern', () => {
|
|
35
|
+
const result = PaymentInfo.safeParse({
|
|
36
|
+
amount: '01',
|
|
37
|
+
intent: 'charge',
|
|
38
|
+
method: 'tempo',
|
|
39
|
+
})
|
|
40
|
+
expect(result.success).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('accepts x402 format with unknown fields', () => {
|
|
44
|
+
const result = PaymentInfo.safeParse({
|
|
45
|
+
price: '0.54',
|
|
46
|
+
pricingMode: 'fixed',
|
|
47
|
+
protocols: ['x402', 'mpp'],
|
|
48
|
+
})
|
|
49
|
+
expect(result.success).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('ServiceInfo', () => {
|
|
54
|
+
test('parses a full service info', () => {
|
|
55
|
+
const result = ServiceInfo.safeParse({
|
|
56
|
+
categories: ['ai', 'search'],
|
|
57
|
+
docs: {
|
|
58
|
+
apiReference: 'https://example.com/api',
|
|
59
|
+
homepage: 'https://example.com',
|
|
60
|
+
llms: 'https://example.com/llms.txt',
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
expect(result.success).toBe(true)
|
|
64
|
+
expect(result.data?.categories).toEqual(['ai', 'search'])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('accepts relative paths for doc links', () => {
|
|
68
|
+
const result = ServiceInfo.safeParse({
|
|
69
|
+
docs: {
|
|
70
|
+
llms: '/llms.txt',
|
|
71
|
+
apiReference: '/docs/api',
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
expect(result.success).toBe(true)
|
|
75
|
+
expect(result.data?.docs?.llms).toBe('/llms.txt')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('rejects invalid doc URIs', () => {
|
|
79
|
+
const result = ServiceInfo.safeParse({
|
|
80
|
+
docs: {
|
|
81
|
+
homepage: 'not-a-uri',
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
expect(result.success).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('DiscoveryDocument', () => {
|
|
89
|
+
test('parses a minimal document', () => {
|
|
90
|
+
const result = DiscoveryDocument.safeParse({
|
|
91
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
92
|
+
openapi: '3.1.0',
|
|
93
|
+
})
|
|
94
|
+
expect(result.success).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('parses a document with discovery extensions', () => {
|
|
98
|
+
const result = DiscoveryDocument.safeParse({
|
|
99
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
100
|
+
openapi: '3.1.0',
|
|
101
|
+
paths: {
|
|
102
|
+
'/search': {
|
|
103
|
+
post: {
|
|
104
|
+
'x-payment-info': {
|
|
105
|
+
amount: '100',
|
|
106
|
+
intent: 'charge',
|
|
107
|
+
method: 'tempo',
|
|
108
|
+
},
|
|
109
|
+
responses: {
|
|
110
|
+
'200': { description: 'OK' },
|
|
111
|
+
'402': { description: 'Payment Required' },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
'x-service-info': {
|
|
117
|
+
categories: ['search'],
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
expect(result.success).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('accepts path items with summary, parameters, and extensions', () => {
|
|
124
|
+
const result = DiscoveryDocument.safeParse({
|
|
125
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
126
|
+
openapi: '3.1.0',
|
|
127
|
+
paths: {
|
|
128
|
+
'/search': {
|
|
129
|
+
summary: 'Search endpoints',
|
|
130
|
+
parameters: [{ name: 'q', in: 'query' }],
|
|
131
|
+
'x-custom': 'hello',
|
|
132
|
+
post: {
|
|
133
|
+
'x-payment-info': {
|
|
134
|
+
amount: '100',
|
|
135
|
+
intent: 'charge',
|
|
136
|
+
method: 'tempo',
|
|
137
|
+
},
|
|
138
|
+
responses: { '402': { description: 'Payment Required' } },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
expect(result.success).toBe(true)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('rejects missing info', () => {
|
|
147
|
+
const result = DiscoveryDocument.safeParse({
|
|
148
|
+
openapi: '3.1.0',
|
|
149
|
+
})
|
|
150
|
+
expect(result.success).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
})
|