mppx 0.8.6 → 0.8.7
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 +13 -0
- package/dist/cli/internal.js +2 -2
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/evm.d.ts +2 -0
- package/dist/cli/plugins/evm.d.ts.map +1 -0
- package/dist/cli/plugins/evm.js +51 -0
- package/dist/cli/plugins/evm.js.map +1 -0
- package/dist/cli/plugins/index.d.ts +1 -0
- package/dist/cli/plugins/index.d.ts.map +1 -1
- package/dist/cli/plugins/index.js +1 -0
- package/dist/cli/plugins/index.js.map +1 -1
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +184 -78
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +99 -35
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +14 -1
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +0 -13
- package/dist/cli/validate/helpers.js.map +1 -1
- package/dist/cli/validate/index.d.ts +1 -0
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +98 -151
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +215 -144
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/evm/Chains.d.ts +4 -0
- package/dist/evm/Chains.d.ts.map +1 -1
- package/dist/evm/Chains.js +4 -0
- package/dist/evm/Chains.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts.map +1 -1
- package/dist/internal/AcceptPayment.js +7 -1
- package/dist/internal/AcceptPayment.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/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/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +2 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/session/server/ChannelStore.js +4 -4
- package/dist/tempo/session/server/ChannelStore.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +21 -1
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +1 -0
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/validation/core.d.ts +79 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +256 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/x402/Assets.d.ts +9 -0
- package/dist/x402/Assets.d.ts.map +1 -1
- package/dist/x402/Assets.js +38 -0
- package/dist/x402/Assets.js.map +1 -1
- package/package.json +9 -4
- package/src/cli/internal.ts +2 -2
- package/src/cli/plugins/evm.ts +55 -0
- package/src/cli/plugins/index.ts +1 -0
- package/src/cli/validate/challenge.ts +334 -121
- package/src/cli/validate/discovery.test.ts +307 -0
- package/src/cli/validate/discovery.ts +104 -38
- package/src/cli/validate/helpers.ts +15 -13
- package/src/cli/validate/index.ts +117 -240
- package/src/cli/validate/payment.ts +261 -178
- package/src/cli/validate.test.ts +143 -3
- package/src/client/internal/Fetch.test.ts +142 -2
- package/src/evm/Chains.ts +6 -0
- package/src/evm/PublicInterface.test-d.ts +7 -0
- package/src/internal/AcceptPayment.test.ts +45 -0
- package/src/internal/AcceptPayment.ts +11 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +9 -0
- package/src/tempo/server/internal/request-body.ts +2 -0
- package/src/tempo/session/server/ChannelStore.test.ts +17 -0
- package/src/tempo/session/server/ChannelStore.ts +4 -4
- package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
- package/src/tempo/session/server/CredentialVerification.ts +30 -0
- package/src/tempo/session/server/Session.integration.test.ts +5 -0
- package/src/tempo/session/server/Session.test.ts +119 -53
- package/src/tempo/session/server/Session.ts +1 -0
- package/src/validation/core.ts +348 -0
- package/src/validation/index.ts +11 -0
- package/src/x402/Assets.test.ts +42 -0
- package/src/x402/Assets.ts +40 -0
- 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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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`
|