mppx 0.8.6 → 0.8.8

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 (108) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cli/internal.js +2 -2
  3. package/dist/cli/internal.js.map +1 -1
  4. package/dist/cli/plugins/evm.d.ts +2 -0
  5. package/dist/cli/plugins/evm.d.ts.map +1 -0
  6. package/dist/cli/plugins/evm.js +51 -0
  7. package/dist/cli/plugins/evm.js.map +1 -0
  8. package/dist/cli/plugins/index.d.ts +1 -0
  9. package/dist/cli/plugins/index.d.ts.map +1 -1
  10. package/dist/cli/plugins/index.js +1 -0
  11. package/dist/cli/plugins/index.js.map +1 -1
  12. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  13. package/dist/cli/plugins/stripe.js +3 -2
  14. package/dist/cli/plugins/stripe.js.map +1 -1
  15. package/dist/cli/validate/challenge.d.ts +2 -0
  16. package/dist/cli/validate/challenge.d.ts.map +1 -1
  17. package/dist/cli/validate/challenge.js +191 -84
  18. package/dist/cli/validate/challenge.js.map +1 -1
  19. package/dist/cli/validate/discovery.d.ts +1 -0
  20. package/dist/cli/validate/discovery.d.ts.map +1 -1
  21. package/dist/cli/validate/discovery.js +99 -35
  22. package/dist/cli/validate/discovery.js.map +1 -1
  23. package/dist/cli/validate/helpers.d.ts +14 -1
  24. package/dist/cli/validate/helpers.d.ts.map +1 -1
  25. package/dist/cli/validate/helpers.js +0 -13
  26. package/dist/cli/validate/helpers.js.map +1 -1
  27. package/dist/cli/validate/index.d.ts +1 -0
  28. package/dist/cli/validate/index.d.ts.map +1 -1
  29. package/dist/cli/validate/index.js +102 -153
  30. package/dist/cli/validate/index.js.map +1 -1
  31. package/dist/cli/validate/payment.d.ts +4 -1
  32. package/dist/cli/validate/payment.d.ts.map +1 -1
  33. package/dist/cli/validate/payment.js +312 -106
  34. package/dist/cli/validate/payment.js.map +1 -1
  35. package/dist/evm/Chains.d.ts +4 -0
  36. package/dist/evm/Chains.d.ts.map +1 -1
  37. package/dist/evm/Chains.js +4 -0
  38. package/dist/evm/Chains.js.map +1 -1
  39. package/dist/internal/AcceptPayment.d.ts.map +1 -1
  40. package/dist/internal/AcceptPayment.js +7 -1
  41. package/dist/internal/AcceptPayment.js.map +1 -1
  42. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  43. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  44. package/dist/stripe/server/internal/html.gen.js +1 -1
  45. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  46. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  47. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/html.gen.js +1 -1
  49. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  50. package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
  51. package/dist/tempo/server/internal/request-body.js +2 -0
  52. package/dist/tempo/server/internal/request-body.js.map +1 -1
  53. package/dist/tempo/session/server/ChannelStore.js +4 -4
  54. package/dist/tempo/session/server/ChannelStore.js.map +1 -1
  55. package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
  56. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  57. package/dist/tempo/session/server/CredentialVerification.js +21 -1
  58. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  59. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  60. package/dist/tempo/session/server/Session.js +1 -0
  61. package/dist/tempo/session/server/Session.js.map +1 -1
  62. package/dist/validation/core.d.ts +75 -0
  63. package/dist/validation/core.d.ts.map +1 -0
  64. package/dist/validation/core.js +269 -0
  65. package/dist/validation/core.js.map +1 -0
  66. package/dist/validation/index.d.ts +3 -0
  67. package/dist/validation/index.d.ts.map +1 -0
  68. package/dist/validation/index.js +2 -0
  69. package/dist/validation/index.js.map +1 -0
  70. package/dist/x402/Assets.d.ts +9 -0
  71. package/dist/x402/Assets.d.ts.map +1 -1
  72. package/dist/x402/Assets.js +38 -0
  73. package/dist/x402/Assets.js.map +1 -1
  74. package/package.json +9 -4
  75. package/src/cli/internal.ts +2 -2
  76. package/src/cli/plugins/evm.ts +55 -0
  77. package/src/cli/plugins/index.ts +1 -0
  78. package/src/cli/plugins/stripe.ts +3 -2
  79. package/src/cli/validate/challenge.ts +345 -127
  80. package/src/cli/validate/discovery.test.ts +307 -0
  81. package/src/cli/validate/discovery.ts +104 -38
  82. package/src/cli/validate/helpers.ts +15 -13
  83. package/src/cli/validate/index.ts +121 -242
  84. package/src/cli/validate/payment.ts +413 -144
  85. package/src/cli/validate.test.ts +237 -3
  86. package/src/client/internal/Fetch.test.ts +142 -2
  87. package/src/evm/Chains.ts +6 -0
  88. package/src/evm/PublicInterface.test-d.ts +7 -0
  89. package/src/internal/AcceptPayment.test.ts +45 -0
  90. package/src/internal/AcceptPayment.ts +11 -1
  91. package/src/stripe/server/internal/html/package.json +1 -1
  92. package/src/stripe/server/internal/html.gen.ts +1 -1
  93. package/src/tempo/server/internal/html/package.json +1 -1
  94. package/src/tempo/server/internal/html.gen.ts +1 -1
  95. package/src/tempo/server/internal/request-body.test.ts +9 -0
  96. package/src/tempo/server/internal/request-body.ts +2 -0
  97. package/src/tempo/session/server/ChannelStore.test.ts +17 -0
  98. package/src/tempo/session/server/ChannelStore.ts +4 -4
  99. package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
  100. package/src/tempo/session/server/CredentialVerification.ts +30 -0
  101. package/src/tempo/session/server/Session.integration.test.ts +5 -0
  102. package/src/tempo/session/server/Session.test.ts +119 -53
  103. package/src/tempo/session/server/Session.ts +1 -0
  104. package/src/validation/core.ts +374 -0
  105. package/src/validation/index.ts +11 -0
  106. package/src/x402/Assets.test.ts +42 -0
  107. package/src/x402/Assets.ts +40 -0
  108. package/src/x402/Exact.localnet.test.ts +475 -0
@@ -0,0 +1,307 @@
1
+ import { describe, expect, test } from 'vp/test'
2
+
3
+ import {
4
+ buildUrl,
5
+ extractEndpointsFromDiscovery,
6
+ extractRequestBodyFromDiscovery,
7
+ } from './discovery.js'
8
+
9
+ describe('buildUrl', () => {
10
+ describe('path parameters', () => {
11
+ test('substitutes param with example from parameter', () => {
12
+ const endpoint = {
13
+ method: 'GET',
14
+ path: '/users/{userId}/posts',
15
+ parameters: [{ name: 'userId', in: 'path' as const, example: 'abc-123' }],
16
+ }
17
+ expect(buildUrl('http://localhost', endpoint)).toBe('http://localhost/users/abc-123/posts')
18
+ })
19
+
20
+ test('substitutes param with example from schema', () => {
21
+ const endpoint = {
22
+ method: 'GET',
23
+ path: '/orders/{orderId}',
24
+ parameters: [{ name: 'orderId', in: 'path' as const, schema: { example: 'ORD-001' } }],
25
+ }
26
+ expect(buildUrl('http://localhost', endpoint)).toBe('http://localhost/orders/ORD-001')
27
+ })
28
+
29
+ test('substitutes param with default from schema', () => {
30
+ const endpoint = {
31
+ method: 'GET',
32
+ path: '/items/{itemId}',
33
+ parameters: [{ name: 'itemId', in: 'path' as const, schema: { default: '42' } }],
34
+ }
35
+ expect(buildUrl('http://localhost', endpoint)).toBe('http://localhost/items/42')
36
+ })
37
+
38
+ test('leaves placeholder when no example available', () => {
39
+ const endpoint = {
40
+ method: 'GET',
41
+ path: '/users/{userId}',
42
+ parameters: [{ name: 'userId', in: 'path' as const, schema: { type: 'string' } }],
43
+ }
44
+ expect(buildUrl('http://localhost', endpoint)).toBe('http://localhost/users/%7BuserId%7D')
45
+ })
46
+
47
+ test('handles multiple path params', () => {
48
+ const endpoint = {
49
+ method: 'GET',
50
+ path: '/orgs/{orgId}/repos/{repoId}',
51
+ parameters: [
52
+ { name: 'orgId', in: 'path' as const, example: 'stripe' },
53
+ { name: 'repoId', in: 'path' as const, example: 'mppx' },
54
+ ],
55
+ }
56
+ expect(buildUrl('http://localhost', endpoint)).toBe('http://localhost/orgs/stripe/repos/mppx')
57
+ })
58
+
59
+ test('encodes special characters', () => {
60
+ const endpoint = {
61
+ method: 'GET',
62
+ path: '/search/{query}',
63
+ parameters: [{ name: 'query', in: 'path' as const, example: 'hello world' }],
64
+ }
65
+ expect(buildUrl('http://localhost', endpoint)).toBe('http://localhost/search/hello%20world')
66
+ })
67
+
68
+ test('no parameters field leaves path as-is', () => {
69
+ const endpoint = { method: 'GET', path: '/simple' }
70
+ expect(buildUrl('http://localhost', endpoint)).toBe('http://localhost/simple')
71
+ })
72
+ })
73
+
74
+ describe('subpath base URL', () => {
75
+ test('preserves base URL subpath', () => {
76
+ const endpoint = { method: 'POST', path: '/plan' }
77
+ expect(buildUrl('http://localhost/mpp', endpoint)).toBe('http://localhost/mpp/plan')
78
+ })
79
+
80
+ test('preserves nested subpath', () => {
81
+ const endpoint = { method: 'GET', path: '/users/123' }
82
+ expect(buildUrl('http://localhost/api/v1', endpoint)).toBe(
83
+ 'http://localhost/api/v1/users/123',
84
+ )
85
+ })
86
+
87
+ test('works with trailing slash on base', () => {
88
+ const endpoint = { method: 'POST', path: '/plan' }
89
+ expect(buildUrl('http://localhost/mpp/', endpoint)).toBe('http://localhost/mpp/plan')
90
+ })
91
+ })
92
+ })
93
+
94
+ describe('extractEndpointsFromDiscovery', () => {
95
+ describe('path parameters', () => {
96
+ test('extracts parameters from OpenAPI doc', () => {
97
+ const doc = {
98
+ openapi: '3.1.0',
99
+ paths: {
100
+ '/users/{userId}': {
101
+ parameters: [
102
+ { name: 'userId', in: 'path', schema: { type: 'string', example: 'u-1' } },
103
+ ],
104
+ get: {
105
+ 'x-payment-info': { amount: '100' },
106
+ responses: { '402': {} },
107
+ },
108
+ },
109
+ },
110
+ }
111
+ const endpoints = extractEndpointsFromDiscovery(doc as Record<string, unknown>)
112
+ expect(endpoints).toHaveLength(1)
113
+ expect(endpoints[0]!.parameters).toHaveLength(1)
114
+ expect(endpoints[0]!.parameters![0]!.name).toBe('userId')
115
+ expect(buildUrl('http://localhost', endpoints[0]!)).toBe('http://localhost/users/u-1')
116
+ })
117
+
118
+ test('operation params override path-level params', () => {
119
+ const doc = {
120
+ openapi: '3.1.0',
121
+ paths: {
122
+ '/items/{id}': {
123
+ parameters: [{ name: 'id', in: 'path', example: 'path-level' }],
124
+ post: {
125
+ 'x-payment-info': { amount: '100' },
126
+ parameters: [{ name: 'id', in: 'path', example: 'op-level' }],
127
+ },
128
+ },
129
+ },
130
+ }
131
+ const endpoints = extractEndpointsFromDiscovery(doc as Record<string, unknown>)
132
+ expect(buildUrl('http://localhost', endpoints[0]!)).toBe('http://localhost/items/op-level')
133
+ })
134
+ })
135
+
136
+ describe('endpoint selection', () => {
137
+ test('prefers x-payment-info endpoints over 402 heuristic', () => {
138
+ const doc = {
139
+ paths: {
140
+ '/paid': { post: { 'x-payment-info': { amount: '100' }, responses: { '402': {} } } },
141
+ '/maybe': { get: { responses: { '402': {} } } },
142
+ },
143
+ }
144
+ const endpoints = extractEndpointsFromDiscovery(doc as Record<string, unknown>)
145
+ expect(endpoints).toHaveLength(1)
146
+ expect(endpoints[0]!.path).toBe('/paid')
147
+ })
148
+
149
+ test('falls back to 402 heuristic when no x-payment-info', () => {
150
+ const doc = {
151
+ paths: {
152
+ '/a': { get: { responses: { '402': {} } } },
153
+ '/b': { post: { responses: { '402': {} } } },
154
+ },
155
+ }
156
+ const endpoints = extractEndpointsFromDiscovery(doc as Record<string, unknown>)
157
+ expect(endpoints).toHaveLength(2)
158
+ })
159
+
160
+ test('returns empty for no paths', () => {
161
+ expect(extractEndpointsFromDiscovery({} as Record<string, unknown>)).toEqual([])
162
+ })
163
+
164
+ test('captures amount from x-payment-info', () => {
165
+ const doc = {
166
+ paths: {
167
+ '/api/generate': {
168
+ post: { 'x-payment-info': { amount: '50000' }, responses: { '402': {} } },
169
+ },
170
+ },
171
+ }
172
+ const endpoints = extractEndpointsFromDiscovery(doc as Record<string, unknown>)
173
+ expect(endpoints[0]!.amount).toBe('50000')
174
+ })
175
+ })
176
+ })
177
+
178
+ describe('extractRequestBodyFromDiscovery', () => {
179
+ test('returns explicit example from content', () => {
180
+ const doc = {
181
+ paths: {
182
+ '/api/test': {
183
+ post: {
184
+ requestBody: {
185
+ content: {
186
+ 'application/json': { example: { prompt: 'hello' } },
187
+ },
188
+ },
189
+ },
190
+ },
191
+ },
192
+ }
193
+ const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
194
+ method: 'POST',
195
+ path: '/api/test',
196
+ })
197
+ expect(body).toBe('{"prompt":"hello"}')
198
+ })
199
+
200
+ test('generates body from schema with required fields', () => {
201
+ const doc = {
202
+ paths: {
203
+ '/api/test': {
204
+ post: {
205
+ requestBody: {
206
+ content: {
207
+ 'application/json': {
208
+ schema: {
209
+ type: 'object',
210
+ required: ['query', 'count'],
211
+ properties: {
212
+ query: { type: 'string', example: 'test query' },
213
+ count: { type: 'integer', default: 10 },
214
+ optional: { type: 'string' },
215
+ },
216
+ },
217
+ },
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ }
224
+ const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
225
+ method: 'POST',
226
+ path: '/api/test',
227
+ })
228
+ expect(JSON.parse(body!)).toEqual({ query: 'test query', count: 10 })
229
+ })
230
+
231
+ test('uses schema defaults for scalar types', () => {
232
+ const doc = {
233
+ paths: {
234
+ '/api/test': {
235
+ post: {
236
+ requestBody: {
237
+ content: {
238
+ 'application/json': {
239
+ schema: {
240
+ type: 'object',
241
+ required: ['email', 'active'],
242
+ properties: {
243
+ email: { type: 'string', format: 'email' },
244
+ active: { type: 'boolean' },
245
+ },
246
+ },
247
+ },
248
+ },
249
+ },
250
+ },
251
+ },
252
+ },
253
+ }
254
+ const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
255
+ method: 'POST',
256
+ path: '/api/test',
257
+ })
258
+ expect(JSON.parse(body!)).toEqual({ email: 'test@example.com', active: true })
259
+ })
260
+
261
+ test('returns undefined when no requestBody', () => {
262
+ const doc = { paths: { '/api/test': { post: {} } } }
263
+ const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
264
+ method: 'POST',
265
+ path: '/api/test',
266
+ })
267
+ expect(body).toBeUndefined()
268
+ })
269
+
270
+ test('returns undefined when path not found', () => {
271
+ const doc = { paths: { '/other': { get: {} } } }
272
+ const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
273
+ method: 'POST',
274
+ path: '/api/test',
275
+ })
276
+ expect(body).toBeUndefined()
277
+ })
278
+
279
+ test('handles enum in schema', () => {
280
+ const doc = {
281
+ paths: {
282
+ '/api/test': {
283
+ post: {
284
+ requestBody: {
285
+ content: {
286
+ 'application/json': {
287
+ schema: {
288
+ type: 'object',
289
+ required: ['model'],
290
+ properties: {
291
+ model: { type: 'string', enum: ['gpt-4', 'gpt-3.5'] },
292
+ },
293
+ },
294
+ },
295
+ },
296
+ },
297
+ },
298
+ },
299
+ },
300
+ }
301
+ const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
302
+ method: 'POST',
303
+ path: '/api/test',
304
+ })
305
+ expect(JSON.parse(body!)).toEqual({ model: 'gpt-4' })
306
+ })
307
+ })
@@ -1,10 +1,11 @@
1
- import type { EndpointSpec } from './helpers.js'
1
+ import type { EndpointSpec, PathParameter } from './helpers.js'
2
2
  import { fetchWithTimeout, HTTP_METHODS } from './helpers.js'
3
3
 
4
4
  export async function fetchDiscoveryDoc(
5
5
  baseUrl: string,
6
6
  ): Promise<{ doc: unknown; raw: string } | { error: string }> {
7
- const url = new URL('/openapi.json', baseUrl).href
7
+ // Trailing slash makes URL treat baseUrl as a directory, so relative resolution appends rather than replaces the last segment.
8
+ const url = new URL('openapi.json', baseUrl.replace(/\/?$/, '/')).href
8
9
  try {
9
10
  const response = await fetchWithTimeout(url, {})
10
11
  if (!response.ok) return { error: `HTTP ${response.status}` }
@@ -28,21 +29,29 @@ export function extractEndpointsFromDiscovery(doc: Record<string, unknown>): End
28
29
  const paths = doc.paths as Record<string, Record<string, unknown>> | undefined
29
30
  if (!paths) return []
30
31
  for (const [pathKey, pathItem] of Object.entries(paths)) {
32
+ const pathLevelParams = extractPathParameters(pathItem.parameters)
33
+
31
34
  for (const [method, operation] of Object.entries(pathItem)) {
32
35
  if (!HTTP_METHODS.has(method)) continue
33
36
  if (!operation || typeof operation !== 'object' || Array.isArray(operation)) continue
34
37
  const op = operation as Record<string, unknown>
38
+
39
+ const opParams = extractPathParameters(op.parameters)
40
+ const merged = mergeParameters(pathLevelParams, opParams)
41
+ const params = merged.length > 0 ? merged : undefined
42
+
35
43
  if (op['x-payment-info']) {
36
44
  const payInfo = op['x-payment-info'] as Record<string, unknown>
37
45
  withPaymentInfo.push({
38
46
  method: method.toUpperCase(),
39
47
  path: pathKey,
40
48
  amount: payInfo.amount as string | undefined,
49
+ parameters: params,
41
50
  })
42
51
  } else {
43
52
  const responses = op.responses as Record<string, unknown> | undefined
44
53
  if (responses && '402' in responses) {
45
- with402Response.push({ method: method.toUpperCase(), path: pathKey })
54
+ with402Response.push({ method: method.toUpperCase(), path: pathKey, parameters: params })
46
55
  }
47
56
  }
48
57
  }
@@ -50,8 +59,96 @@ export function extractEndpointsFromDiscovery(doc: Record<string, unknown>): End
50
59
  return withPaymentInfo.length > 0 ? withPaymentInfo : with402Response
51
60
  }
52
61
 
53
- // Recursively generates a minimal valid value from a JSON Schema definition.
54
- // Fills only required fields, preferring const > example > default > synthetic.
62
+ export function extractRequestBodyFromDiscovery(
63
+ doc: Record<string, unknown>,
64
+ endpoint: EndpointSpec,
65
+ ): string | undefined {
66
+ const paths = doc.paths as Record<string, Record<string, unknown>> | undefined
67
+ if (!paths) return undefined
68
+ const pathItem = paths[endpoint.path]
69
+ if (!pathItem) return undefined
70
+ const op = pathItem[endpoint.method.toLowerCase()] as Record<string, unknown> | undefined
71
+ if (!op?.requestBody) return undefined
72
+
73
+ const rb = op.requestBody as Record<string, unknown>
74
+ const content = rb.content as Record<string, unknown> | undefined
75
+ const jsonContent = content?.['application/json'] as Record<string, unknown> | undefined
76
+ if (!jsonContent) return undefined
77
+
78
+ if (jsonContent.example) return JSON.stringify(jsonContent.example)
79
+
80
+ const schema = jsonContent.schema as Record<string, unknown> | undefined
81
+ if (!schema || schema.type !== 'object') return undefined
82
+
83
+ const result = generateValueFromSchema(schema)
84
+ if (result && typeof result === 'object' && Object.keys(result as object).length > 0) {
85
+ return JSON.stringify(result)
86
+ }
87
+ return undefined
88
+ }
89
+
90
+ export function buildUrl(baseUrl: string, endpoint: EndpointSpec, query?: string[]): string {
91
+ let path = endpoint.path
92
+ if (endpoint.parameters) {
93
+ path = substitutePathParams(path, endpoint.parameters)
94
+ }
95
+ // Strip leading slash so URL resolves relative to baseUrl's path, not root.
96
+ const relativePath = path.startsWith('/') ? path.slice(1) : path
97
+ let url = new URL(relativePath, baseUrl.replace(/\/?$/, '/')).href
98
+ if (query) {
99
+ const u = new URL(url)
100
+ for (const q of query) {
101
+ const [key, ...rest] = q.split('=')
102
+ if (key) u.searchParams.set(key, rest.join('='))
103
+ }
104
+ url = u.href
105
+ }
106
+ return url
107
+ }
108
+
109
+ // ── Internal helpers ─────────────────────────────────────────────────────────
110
+
111
+ function extractPathParameters(raw: unknown): PathParameter[] {
112
+ if (!Array.isArray(raw)) return []
113
+ const results: PathParameter[] = []
114
+ for (const p of raw) {
115
+ if (!p || typeof p !== 'object') continue
116
+ const param = p as Record<string, unknown>
117
+ if (param.in !== 'path' || typeof param.name !== 'string') continue
118
+ results.push(
119
+ param.schema && typeof param.schema === 'object'
120
+ ? {
121
+ name: param.name,
122
+ in: 'path' as const,
123
+ schema: param.schema as NonNullable<PathParameter['schema']>,
124
+ example: param.example,
125
+ }
126
+ : { name: param.name, in: 'path' as const, example: param.example },
127
+ )
128
+ }
129
+ return results
130
+ }
131
+
132
+ function mergeParameters(base: PathParameter[], override: PathParameter[]): PathParameter[] {
133
+ const merged = [...base]
134
+ for (const p of override) {
135
+ const idx = merged.findIndex((m) => m.name === p.name)
136
+ if (idx >= 0) merged[idx] = p
137
+ else merged.push(p)
138
+ }
139
+ return merged
140
+ }
141
+
142
+ function substitutePathParams(path: string, params: PathParameter[]): string {
143
+ return path.replace(/\{([^}]+)\}/g, (match, name) => {
144
+ const param = params.find((p) => p.name === name && p.in === 'path')
145
+ if (!param) return match
146
+ const value = param.example ?? param.schema?.example ?? param.schema?.default
147
+ if (value === undefined) return match
148
+ return encodeURIComponent(String(value))
149
+ })
150
+ }
151
+
55
152
  function generateValueFromSchema(schema: Record<string, unknown>): unknown {
56
153
  if (schema.const !== undefined) return schema.const
57
154
  if (schema.example !== undefined) return schema.example
@@ -59,15 +156,15 @@ function generateValueFromSchema(schema: Record<string, unknown>): unknown {
59
156
 
60
157
  switch (schema.type) {
61
158
  case 'string': {
159
+ if (schema.enum && Array.isArray(schema.enum)) return schema.enum[0]
62
160
  if (schema.format === 'email') return 'test@example.com'
63
161
  if (schema.format === 'uuid') return '00000000-0000-0000-0000-000000000000'
64
162
  if (schema.format === 'uri' || schema.format === 'url') return 'https://example.com'
65
- if (schema.enum && Array.isArray(schema.enum)) return schema.enum[0]
66
163
  return 'test'
67
164
  }
68
165
  case 'number':
69
166
  case 'integer':
70
- return schema.minimum ?? 1
167
+ return (schema.minimum as number) ?? 1
71
168
  case 'boolean':
72
169
  return true
73
170
  case 'array':
@@ -87,34 +184,3 @@ function generateValueFromSchema(schema: Record<string, unknown>): unknown {
87
184
  return null
88
185
  }
89
186
  }
90
-
91
- // Looks up the requestBody schema for an endpoint in the OpenAPI doc and
92
- // returns a JSON string suitable for the request. Uses the doc's example
93
- // if one exists, otherwise generates a minimal body from the schema.
94
- export function extractRequestBodyFromDiscovery(
95
- doc: Record<string, unknown>,
96
- endpoint: EndpointSpec,
97
- ): string | undefined {
98
- const paths = doc.paths as Record<string, Record<string, unknown>> | undefined
99
- if (!paths) return undefined
100
- const pathItem = paths[endpoint.path]
101
- if (!pathItem) return undefined
102
- const op = pathItem[endpoint.method.toLowerCase()] as Record<string, unknown> | undefined
103
- if (!op?.requestBody) return undefined
104
-
105
- const rb = op.requestBody as Record<string, unknown>
106
- const content = rb.content as Record<string, unknown> | undefined
107
- const jsonContent = content?.['application/json'] as Record<string, unknown> | undefined
108
- if (!jsonContent) return undefined
109
-
110
- if (jsonContent.example) return JSON.stringify(jsonContent.example)
111
-
112
- const schema = jsonContent.schema as Record<string, unknown> | undefined
113
- if (!schema || schema.type !== 'object') return undefined
114
-
115
- const result = generateValueFromSchema(schema)
116
- if (result && typeof result === 'object' && Object.keys(result as object).length > 0) {
117
- return JSON.stringify(result)
118
- }
119
- return undefined
120
- }
@@ -11,6 +11,21 @@ export type EndpointSpec = {
11
11
  method: string
12
12
  path: string
13
13
  amount?: string | undefined
14
+ parameters?: PathParameter[] | undefined
15
+ }
16
+
17
+ export type PathParameter = {
18
+ name: string
19
+ in: 'path' | 'query' | 'header' | 'cookie'
20
+ schema?: {
21
+ type?: string
22
+ format?: string
23
+ pattern?: string
24
+ enum?: unknown[]
25
+ example?: unknown
26
+ default?: unknown
27
+ }
28
+ example?: unknown
14
29
  }
15
30
 
16
31
  export function check(label: string, detail?: string): CheckResult {
@@ -83,19 +98,6 @@ export async function fetchWithTimeout(
83
98
  }
84
99
  }
85
100
 
86
- export function buildUrl(baseUrl: string, endpoint: EndpointSpec, query?: string[]): string {
87
- let url = new URL(endpoint.path, baseUrl).href
88
- if (query) {
89
- const u = new URL(url)
90
- for (const q of query) {
91
- const [key, ...rest] = q.split('=')
92
- if (key) u.searchParams.set(key, rest.join('='))
93
- }
94
- url = u.href
95
- }
96
- return url
97
- }
98
-
99
101
  export function formatBytes(bytes: number): string {
100
102
  if (bytes < 1024) return `${bytes}B`
101
103
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`