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.
Files changed (158) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +155 -0
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/discovery/Discovery.d.ts +146 -0
  6. package/dist/discovery/Discovery.d.ts.map +1 -0
  7. package/dist/discovery/Discovery.js +60 -0
  8. package/dist/discovery/Discovery.js.map +1 -0
  9. package/dist/discovery/OpenApi.d.ts +61 -0
  10. package/dist/discovery/OpenApi.d.ts.map +1 -0
  11. package/dist/discovery/OpenApi.js +139 -0
  12. package/dist/discovery/OpenApi.js.map +1 -0
  13. package/dist/discovery/Validate.d.ts +10 -0
  14. package/dist/discovery/Validate.d.ts.map +1 -0
  15. package/dist/discovery/Validate.js +63 -0
  16. package/dist/discovery/Validate.js.map +1 -0
  17. package/dist/discovery/index.d.ts +4 -0
  18. package/dist/discovery/index.d.ts.map +1 -0
  19. package/dist/discovery/index.js +4 -0
  20. package/dist/discovery/index.js.map +1 -0
  21. package/dist/middlewares/elysia.d.ts +52 -1
  22. package/dist/middlewares/elysia.d.ts.map +1 -1
  23. package/dist/middlewares/elysia.js +17 -0
  24. package/dist/middlewares/elysia.js.map +1 -1
  25. package/dist/middlewares/express.d.ts +13 -1
  26. package/dist/middlewares/express.d.ts.map +1 -1
  27. package/dist/middlewares/express.js +18 -0
  28. package/dist/middlewares/express.js.map +1 -1
  29. package/dist/middlewares/hono.d.ts +19 -1
  30. package/dist/middlewares/hono.d.ts.map +1 -1
  31. package/dist/middlewares/hono.js +51 -0
  32. package/dist/middlewares/hono.js.map +1 -1
  33. package/dist/middlewares/internal/mppx.d.ts +4 -2
  34. package/dist/middlewares/internal/mppx.d.ts.map +1 -1
  35. package/dist/middlewares/internal/mppx.js +10 -3
  36. package/dist/middlewares/internal/mppx.js.map +1 -1
  37. package/dist/middlewares/nextjs.d.ts +11 -0
  38. package/dist/middlewares/nextjs.d.ts.map +1 -1
  39. package/dist/middlewares/nextjs.js +15 -0
  40. package/dist/middlewares/nextjs.js.map +1 -1
  41. package/dist/proxy/Proxy.d.ts +6 -0
  42. package/dist/proxy/Proxy.d.ts.map +1 -1
  43. package/dist/proxy/Proxy.js +56 -80
  44. package/dist/proxy/Proxy.js.map +1 -1
  45. package/dist/proxy/Service.d.ts +16 -23
  46. package/dist/proxy/Service.d.ts.map +1 -1
  47. package/dist/proxy/Service.js +19 -83
  48. package/dist/proxy/Service.js.map +1 -1
  49. package/dist/proxy/internal/Route.js +1 -1
  50. package/dist/proxy/internal/Route.js.map +1 -1
  51. package/dist/proxy/services/anthropic.d.ts.map +1 -1
  52. package/dist/proxy/services/anthropic.js +5 -0
  53. package/dist/proxy/services/anthropic.js.map +1 -1
  54. package/dist/proxy/services/openai.d.ts.map +1 -1
  55. package/dist/proxy/services/openai.js +6 -3
  56. package/dist/proxy/services/openai.js.map +1 -1
  57. package/dist/proxy/services/stripe.d.ts.map +1 -1
  58. package/dist/proxy/services/stripe.js +6 -3
  59. package/dist/proxy/services/stripe.js.map +1 -1
  60. package/dist/tempo/server/Session.d.ts.map +1 -1
  61. package/dist/tempo/server/Session.js +18 -5
  62. package/dist/tempo/server/Session.js.map +1 -1
  63. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  64. package/dist/tempo/server/internal/transport.js +8 -0
  65. package/dist/tempo/server/internal/transport.js.map +1 -1
  66. package/dist/tempo/session/Chain.js +1 -1
  67. package/dist/tempo/session/Chain.js.map +1 -1
  68. package/package.json +6 -1
  69. package/src/BodyDigest.test.ts +1 -1
  70. package/src/Challenge.fuzz.test.ts +121 -0
  71. package/src/Challenge.test-d.ts +1 -1
  72. package/src/Challenge.test.ts +1 -1
  73. package/src/Credential.fuzz.test.ts +62 -0
  74. package/src/Credential.test.ts +1 -1
  75. package/src/Errors.test.ts +1 -1
  76. package/src/Expires.test.ts +1 -1
  77. package/src/Method.test.ts +1 -1
  78. package/src/PaymentRequest.test.ts +1 -1
  79. package/src/Receipt.test.ts +1 -1
  80. package/src/Store.test-d.ts +1 -1
  81. package/src/Store.test.ts +1 -1
  82. package/src/cli/cli.test.ts +212 -1
  83. package/src/cli/cli.ts +162 -0
  84. package/src/client/Mppx.test-d.ts +1 -1
  85. package/src/client/Mppx.test.ts +1 -1
  86. package/src/client/Transport.test.ts +1 -1
  87. package/src/client/internal/Fetch.browser.test.ts +1 -1
  88. package/src/client/internal/Fetch.test-d.ts +1 -1
  89. package/src/client/internal/Fetch.test.ts +2 -1
  90. package/src/discovery/Discovery.test.ts +152 -0
  91. package/src/discovery/Discovery.ts +72 -0
  92. package/src/discovery/OpenApi.test.ts +425 -0
  93. package/src/discovery/OpenApi.ts +224 -0
  94. package/src/discovery/Validate.test.ts +188 -0
  95. package/src/discovery/Validate.ts +76 -0
  96. package/src/discovery/index.ts +3 -0
  97. package/src/internal/constantTimeEqual.test.ts +1 -1
  98. package/src/mcp-sdk/client/McpClient.test-d.ts +1 -1
  99. package/src/mcp-sdk/client/McpClient.test.ts +1 -1
  100. package/src/mcp-sdk/server/Transport.test.ts +1 -1
  101. package/src/middlewares/elysia.test.ts +27 -2
  102. package/src/middlewares/elysia.ts +35 -1
  103. package/src/middlewares/express.test.ts +35 -7
  104. package/src/middlewares/express.ts +34 -0
  105. package/src/middlewares/hono.test.ts +28 -6
  106. package/src/middlewares/hono.ts +73 -1
  107. package/src/middlewares/internal/mppx.test.ts +1 -1
  108. package/src/middlewares/internal/mppx.ts +14 -6
  109. package/src/middlewares/nextjs.test.ts +31 -6
  110. package/src/middlewares/nextjs.ts +28 -0
  111. package/src/proxy/Proxy.test.ts +54 -270
  112. package/src/proxy/Proxy.ts +71 -93
  113. package/src/proxy/Service.test.ts +23 -1
  114. package/src/proxy/Service.ts +40 -86
  115. package/src/proxy/internal/Headers.test.ts +1 -1
  116. package/src/proxy/internal/Route.test.ts +9 -1
  117. package/src/proxy/internal/Route.ts +1 -1
  118. package/src/proxy/services/anthropic.test.ts +132 -0
  119. package/src/proxy/services/anthropic.ts +5 -0
  120. package/src/proxy/services/openai.test.ts +1 -1
  121. package/src/proxy/services/openai.ts +6 -4
  122. package/src/proxy/services/stripe.test.ts +132 -0
  123. package/src/proxy/services/stripe.ts +6 -4
  124. package/src/server/Mppx.test-d.ts +1 -1
  125. package/src/server/Mppx.test.ts +2 -1
  126. package/src/server/NodeListener.test.ts +1 -1
  127. package/src/server/Request.test.ts +1 -1
  128. package/src/server/Response.test.ts +1 -1
  129. package/src/server/Transport.test.ts +1 -1
  130. package/src/stripe/Charge.integration.test.ts +1 -1
  131. package/src/stripe/Methods.test.ts +1 -1
  132. package/src/stripe/client/Charge.test.ts +1 -1
  133. package/src/stripe/server/Charge.test.ts +1 -1
  134. package/src/tempo/Attribution.test.ts +1 -1
  135. package/src/tempo/Methods.test.ts +1 -1
  136. package/src/tempo/client/ChannelOps.test.ts +6 -3
  137. package/src/tempo/client/Session.test.ts +5 -2
  138. package/src/tempo/client/SessionManager.test.ts +1 -1
  139. package/src/tempo/internal/auto-swap.test.ts +1 -1
  140. package/src/tempo/internal/defaults.test.ts +1 -1
  141. package/src/tempo/internal/fee-payer.test.ts +1 -1
  142. package/src/tempo/server/Charge.test.ts +1 -1
  143. package/src/tempo/server/Session.test.ts +87 -37
  144. package/src/tempo/server/Session.ts +25 -8
  145. package/src/tempo/server/Sse.test.ts +1 -1
  146. package/src/tempo/server/internal/transport.test.ts +24 -1
  147. package/src/tempo/server/internal/transport.ts +11 -0
  148. package/src/tempo/session/Chain.test.ts +5 -2
  149. package/src/tempo/session/Chain.ts +1 -1
  150. package/src/tempo/session/Channel.test.ts +1 -1
  151. package/src/tempo/session/ChannelStore.test.ts +1 -1
  152. package/src/tempo/session/Receipt.test.ts +1 -1
  153. package/src/tempo/session/Sse.fuzz.test.ts +138 -0
  154. package/src/tempo/session/Sse.test.ts +1 -1
  155. package/src/tempo/session/Voucher.test.ts +1 -1
  156. package/src/viem/Account.test.ts +1 -1
  157. package/src/viem/Client.test.ts +1 -1
  158. package/src/zod.test.ts +147 -0
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vp/test'
2
2
 
3
3
  import * as Expires from './Expires.js'
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { Method, z } from 'mppx'
2
- import { describe, expect, expectTypeOf, test } from 'vitest'
2
+ import { describe, expect, expectTypeOf, test } from 'vp/test'
3
3
 
4
4
  describe('from', () => {
5
5
  test('behavior: creates intent', () => {
@@ -1,6 +1,6 @@
1
1
  import { PaymentRequest } from 'mppx'
2
2
  import { Methods } from 'mppx/tempo'
3
- import { describe, expect, test } from 'vitest'
3
+ import { describe, expect, test } from 'vp/test'
4
4
 
5
5
  describe('from', () => {
6
6
  test('creates a request', () => {
@@ -1,5 +1,5 @@
1
1
  import { Receipt } from 'mppx'
2
- import { describe, expect, test } from 'vitest'
2
+ import { describe, expect, test } from 'vp/test'
3
3
 
4
4
  describe('from', () => {
5
5
  test('behavior: creates receipt with success status', () => {
@@ -1,4 +1,4 @@
1
- import { expectTypeOf, test } from 'vitest'
1
+ import { expectTypeOf, test } from 'vp/test'
2
2
 
3
3
  import * as Store from './Store.js'
4
4
 
package/src/Store.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from 'vitest'
1
+ import { describe, expect, test } from 'vp/test'
2
2
 
3
3
  import * as Store from './Store.js'
4
4
 
@@ -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 'vitest'
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
 
@@ -1,5 +1,5 @@
1
1
  import type { Account } from 'viem'
2
- import { describe, expectTypeOf, test } from 'vitest'
2
+ import { describe, expectTypeOf, test } from 'vp/test'
3
3
 
4
4
  import * as Method from '../Method.js'
5
5
  import { charge } from '../tempo/client/Charge.js'
@@ -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 'vitest'
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 'vitest'
4
+ import { describe, expect, test } from 'vp/test'
5
5
 
6
6
  const realm = 'api.example.com'
7
7
  const secretKey = 'test-secret-key'
@@ -1,4 +1,4 @@
1
- import { describe, expect, test, vi } from 'vitest'
1
+ import { describe, expect, test, vi } from 'vp/test'
2
2
 
3
3
  import * as Fetch from './Fetch.js'
4
4
 
@@ -1,5 +1,5 @@
1
1
  import type { Account } from 'viem'
2
- import { describe, expectTypeOf, test } from 'vitest'
2
+ import { describe, expectTypeOf, test } from 'vp/test'
3
3
 
4
4
  import { charge } from '../../tempo/client/Charge.js'
5
5
  import * as Fetch from './Fetch.js'
@@ -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 'vitest'
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
+ })