mppx 0.8.9 → 0.8.12
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 +24 -0
- 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 +21 -9
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +50 -19
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +1 -0
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +11 -0
- 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 +4 -0
- 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 +2 -2
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +35 -6
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodResponse.d.ts +23 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -0
- package/dist/client/internal/MethodResponse.js +15 -0
- package/dist/client/internal/MethodResponse.js.map +1 -0
- package/dist/tempo/internal/fee-payer.d.ts +39 -2
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +45 -1
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/internal/fee-token.d.ts +8 -0
- package/dist/tempo/internal/fee-token.d.ts.map +1 -1
- package/dist/tempo/internal/fee-token.js +19 -6
- package/dist/tempo/internal/fee-token.js.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts +2 -0
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +4 -0
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/legacy/client/Session.d.ts.map +1 -1
- package/dist/tempo/legacy/client/Session.js +7 -0
- package/dist/tempo/legacy/client/Session.js.map +1 -1
- package/dist/tempo/legacy/session/Chain.d.ts.map +1 -1
- package/dist/tempo/legacy/session/Chain.js +53 -50
- package/dist/tempo/legacy/session/Chain.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +8 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +49 -45
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +7 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +24 -16
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +53 -1
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +3 -0
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +3 -0
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +30 -0
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +118 -19
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +25 -19
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/index.d.ts +2 -0
- package/dist/tempo/session/server/index.d.ts.map +1 -1
- package/dist/tempo/session/server/index.js +2 -0
- package/dist/tempo/session/server/index.js.map +1 -1
- package/dist/validation/core.d.ts +1 -0
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +5 -0
- package/dist/validation/core.js.map +1 -1
- package/dist/x402/Header.js +1 -1
- package/dist/x402/Header.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/validate/challenge.ts +20 -8
- package/src/cli/validate/discovery.test.ts +128 -0
- package/src/cli/validate/discovery.ts +50 -15
- package/src/cli/validate/helpers.ts +10 -0
- package/src/cli/validate/index.ts +4 -0
- package/src/cli/validate/payment.ts +4 -1
- package/src/cli/validate.test.ts +23 -2
- package/src/client/Mppx.test-d.ts +3 -1
- package/src/client/internal/Fetch.test.ts +29 -0
- package/src/client/internal/Fetch.ts +51 -6
- package/src/client/internal/MethodResponse.ts +39 -0
- package/src/tempo/PublicExports.test-d.ts +5 -0
- package/src/tempo/internal/fee-payer.test.ts +93 -0
- package/src/tempo/internal/fee-payer.ts +64 -2
- package/src/tempo/internal/fee-token.test.ts +37 -0
- package/src/tempo/internal/fee-token.ts +26 -5
- package/src/tempo/legacy/client/ChannelOps.test.ts +21 -0
- package/src/tempo/legacy/client/ChannelOps.ts +7 -0
- package/src/tempo/legacy/client/Session.test.ts +102 -1
- package/src/tempo/legacy/client/Session.ts +10 -0
- package/src/tempo/legacy/session/Chain.test.ts +78 -2
- package/src/tempo/legacy/session/Chain.ts +56 -52
- package/src/tempo/server/Charge.test.ts +121 -6
- package/src/tempo/server/Charge.ts +61 -48
- package/src/tempo/server/Methods.ts +6 -0
- package/src/tempo/server/Subscription.test.ts +79 -4
- package/src/tempo/server/Subscription.ts +24 -16
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/Session.test.ts +83 -1
- package/src/tempo/session/client/Session.ts +73 -1
- package/src/tempo/session/client/SessionManager.test.ts +12 -1
- package/src/tempo/session/client/SessionManager.ts +5 -0
- package/src/tempo/session/client/Transports.test.ts +109 -0
- package/src/tempo/session/client/Transports.ts +153 -29
- package/src/tempo/session/precompile/Chain.test.ts +9 -0
- package/src/tempo/session/precompile/Chain.ts +28 -20
- package/src/tempo/session/server/Session.test.ts +23 -3
- package/src/tempo/session/server/index.ts +2 -0
- package/src/validation/core.ts +6 -0
- package/src/x402/Header.ts +1 -1
|
@@ -304,4 +304,132 @@ describe('extractRequestBodyFromDiscovery', () => {
|
|
|
304
304
|
})
|
|
305
305
|
expect(JSON.parse(body!)).toEqual({ model: 'gpt-4' })
|
|
306
306
|
})
|
|
307
|
+
|
|
308
|
+
test('resolves $ref to components/schemas', () => {
|
|
309
|
+
const doc = {
|
|
310
|
+
paths: {
|
|
311
|
+
'/api/orders': {
|
|
312
|
+
post: {
|
|
313
|
+
requestBody: {
|
|
314
|
+
content: {
|
|
315
|
+
'application/json': {
|
|
316
|
+
schema: { $ref: '#/components/schemas/OrderRequest' },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
components: {
|
|
324
|
+
schemas: {
|
|
325
|
+
OrderRequest: {
|
|
326
|
+
type: 'object',
|
|
327
|
+
required: ['product', 'quantity'],
|
|
328
|
+
properties: {
|
|
329
|
+
product: { type: 'string', example: 'widget' },
|
|
330
|
+
quantity: { type: 'integer', minimum: 1 },
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
path: '/api/orders',
|
|
339
|
+
})
|
|
340
|
+
expect(JSON.parse(body!)).toEqual({ product: 'widget', quantity: 1 })
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
test('resolves nested $ref in properties', () => {
|
|
344
|
+
const doc = {
|
|
345
|
+
paths: {
|
|
346
|
+
'/api/orders': {
|
|
347
|
+
post: {
|
|
348
|
+
requestBody: {
|
|
349
|
+
content: {
|
|
350
|
+
'application/json': {
|
|
351
|
+
schema: { $ref: '#/components/schemas/OrderRequest' },
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
components: {
|
|
359
|
+
schemas: {
|
|
360
|
+
OrderRequest: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
required: ['customer'],
|
|
363
|
+
properties: {
|
|
364
|
+
customer: { $ref: '#/components/schemas/Customer' },
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
Customer: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
required: ['name', 'email'],
|
|
370
|
+
properties: {
|
|
371
|
+
name: { type: 'string' },
|
|
372
|
+
email: { type: 'string', format: 'email' },
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
|
|
379
|
+
method: 'POST',
|
|
380
|
+
path: '/api/orders',
|
|
381
|
+
})
|
|
382
|
+
expect(JSON.parse(body!)).toEqual({ customer: { name: 'test', email: 'test@example.com' } })
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('uses first named example from examples map', () => {
|
|
386
|
+
const doc = {
|
|
387
|
+
paths: {
|
|
388
|
+
'/api/test': {
|
|
389
|
+
post: {
|
|
390
|
+
requestBody: {
|
|
391
|
+
content: {
|
|
392
|
+
'application/json': {
|
|
393
|
+
examples: {
|
|
394
|
+
basic: { value: { prompt: 'hello', model: 'gpt-4' } },
|
|
395
|
+
advanced: { value: { prompt: 'complex', model: 'gpt-4', temp: 0.9 } },
|
|
396
|
+
},
|
|
397
|
+
schema: { type: 'object' },
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
|
|
406
|
+
method: 'POST',
|
|
407
|
+
path: '/api/test',
|
|
408
|
+
})
|
|
409
|
+
expect(JSON.parse(body!)).toEqual({ prompt: 'hello', model: 'gpt-4' })
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('returns undefined for unresolvable $ref', () => {
|
|
413
|
+
const doc = {
|
|
414
|
+
paths: {
|
|
415
|
+
'/api/test': {
|
|
416
|
+
post: {
|
|
417
|
+
requestBody: {
|
|
418
|
+
content: {
|
|
419
|
+
'application/json': {
|
|
420
|
+
schema: { $ref: '#/components/schemas/DoesNotExist' },
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
components: { schemas: {} },
|
|
428
|
+
}
|
|
429
|
+
const body = extractRequestBodyFromDiscovery(doc as Record<string, unknown>, {
|
|
430
|
+
method: 'POST',
|
|
431
|
+
path: '/api/test',
|
|
432
|
+
})
|
|
433
|
+
expect(body).toBeUndefined()
|
|
434
|
+
})
|
|
307
435
|
})
|
|
@@ -77,10 +77,19 @@ export function extractRequestBodyFromDiscovery(
|
|
|
77
77
|
|
|
78
78
|
if (jsonContent.example) return JSON.stringify(jsonContent.example)
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
if (jsonContent.examples && typeof jsonContent.examples === 'object') {
|
|
81
|
+
const first = Object.values(jsonContent.examples as Record<string, unknown>)[0] as
|
|
82
|
+
| Record<string, unknown>
|
|
83
|
+
| undefined
|
|
84
|
+
if (first?.value) return JSON.stringify(first.value)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let schema = jsonContent.schema as Record<string, unknown> | undefined
|
|
88
|
+
const seen = new Set<string>()
|
|
89
|
+
schema = resolveRef(schema, doc, seen)
|
|
81
90
|
if (!schema || schema.type !== 'object') return undefined
|
|
82
91
|
|
|
83
|
-
const result = generateValueFromSchema(schema)
|
|
92
|
+
const result = generateValueFromSchema(schema, doc, seen)
|
|
84
93
|
if (result && typeof result === 'object' && Object.keys(result as object).length > 0) {
|
|
85
94
|
return JSON.stringify(result)
|
|
86
95
|
}
|
|
@@ -149,34 +158,60 @@ function substitutePathParams(path: string, params: PathParameter[]): string {
|
|
|
149
158
|
})
|
|
150
159
|
}
|
|
151
160
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
// Dereferences a JSON Schema $ref (e.g. "#/components/schemas/Foo") against the root doc.
|
|
162
|
+
function resolveRef(
|
|
163
|
+
schema: Record<string, unknown> | undefined,
|
|
164
|
+
doc: Record<string, unknown>,
|
|
165
|
+
seen?: Set<string>,
|
|
166
|
+
): Record<string, unknown> | undefined {
|
|
167
|
+
if (!schema || typeof schema.$ref !== 'string') return schema
|
|
168
|
+
if (seen?.has(schema.$ref)) return undefined
|
|
169
|
+
const path = schema.$ref.replace(/^#\//, '').split('/')
|
|
170
|
+
let resolved: unknown = doc
|
|
171
|
+
for (const segment of path) {
|
|
172
|
+
if (resolved && typeof resolved === 'object') resolved = (resolved as any)[segment]
|
|
173
|
+
else return undefined
|
|
174
|
+
}
|
|
175
|
+
return resolved as Record<string, unknown> | undefined
|
|
176
|
+
}
|
|
156
177
|
|
|
157
|
-
|
|
178
|
+
// Generates a plausible value for a JSON Schema node (required fields only for objects).
|
|
179
|
+
function generateValueFromSchema(
|
|
180
|
+
schema: Record<string, unknown>,
|
|
181
|
+
doc: Record<string, unknown>,
|
|
182
|
+
seen?: Set<string>,
|
|
183
|
+
): unknown {
|
|
184
|
+
const resolved = resolveRef(schema, doc, seen) ?? schema
|
|
185
|
+
if (resolved.const !== undefined) return resolved.const
|
|
186
|
+
if (resolved.example !== undefined) return resolved.example
|
|
187
|
+
if (resolved.default !== undefined) return resolved.default
|
|
188
|
+
|
|
189
|
+
switch (resolved.type) {
|
|
158
190
|
case 'string': {
|
|
159
|
-
if (
|
|
160
|
-
if (
|
|
161
|
-
if (
|
|
162
|
-
if (
|
|
191
|
+
if (resolved.enum && Array.isArray(resolved.enum)) return resolved.enum[0]
|
|
192
|
+
if (resolved.format === 'email') return 'test@example.com'
|
|
193
|
+
if (resolved.format === 'uuid') return '00000000-0000-0000-0000-000000000000'
|
|
194
|
+
if (resolved.format === 'uri' || resolved.format === 'url') return 'https://example.com'
|
|
195
|
+
if (resolved.format === 'date') return '2026-01-01'
|
|
196
|
+
if (resolved.pattern === '^\\d{5}(?:-\\d{4})?$') return '10001'
|
|
197
|
+
if (resolved.pattern === '^[A-Z]{2}$') return 'US'
|
|
163
198
|
return 'test'
|
|
164
199
|
}
|
|
165
200
|
case 'number':
|
|
166
201
|
case 'integer':
|
|
167
|
-
return (
|
|
202
|
+
return (resolved.minimum as number) ?? 1
|
|
168
203
|
case 'boolean':
|
|
169
204
|
return true
|
|
170
205
|
case 'array':
|
|
171
206
|
return []
|
|
172
207
|
case 'object': {
|
|
173
|
-
const properties =
|
|
208
|
+
const properties = resolved.properties as Record<string, Record<string, unknown>> | undefined
|
|
174
209
|
if (!properties) return {}
|
|
175
|
-
const required = (
|
|
210
|
+
const required = (resolved.required as string[]) || []
|
|
176
211
|
const obj: Record<string, unknown> = {}
|
|
177
212
|
for (const key of required) {
|
|
178
213
|
const prop = properties[key]
|
|
179
|
-
if (prop) obj[key] = generateValueFromSchema(prop)
|
|
214
|
+
if (prop) obj[key] = generateValueFromSchema(prop, doc, seen)
|
|
180
215
|
}
|
|
181
216
|
return obj
|
|
182
217
|
}
|
|
@@ -154,3 +154,13 @@ export function resolveBodyForEndpoint(
|
|
|
154
154
|
} catch {}
|
|
155
155
|
return rawBody
|
|
156
156
|
}
|
|
157
|
+
|
|
158
|
+
export function parseHeaders(raw: string[] | undefined): Record<string, string> {
|
|
159
|
+
if (!raw) return {}
|
|
160
|
+
const headers: Record<string, string> = {}
|
|
161
|
+
for (const h of raw) {
|
|
162
|
+
const idx = h.indexOf(':')
|
|
163
|
+
if (idx > 0) headers[h.slice(0, idx).trim().toLowerCase()] = h.slice(idx + 1).trim()
|
|
164
|
+
}
|
|
165
|
+
return headers
|
|
166
|
+
}
|
|
@@ -19,12 +19,14 @@ const validate = Cli.create('validate', {
|
|
|
19
19
|
'Request body. With --endpoint, used directly. In discovery mode, JSON with / keys is a per-path mapping.',
|
|
20
20
|
),
|
|
21
21
|
query: z.array(z.string()).optional().describe('Query parameter (key=value, repeatable)'),
|
|
22
|
+
header: z.array(z.string()).optional().describe('Request header (key:value, repeatable)'),
|
|
22
23
|
verbose: z.number().default(0).meta({ count: true }).describe('Verbosity level'),
|
|
23
24
|
yes: z.boolean().default(false).describe('Auto-approve mainnet payments'),
|
|
24
25
|
outputJson: z.boolean().default(false).describe('Output results as JSON'),
|
|
25
26
|
}),
|
|
26
27
|
alias: {
|
|
27
28
|
endpoint: 'e',
|
|
29
|
+
header: 'H',
|
|
28
30
|
verbose: 'v',
|
|
29
31
|
yes: 'y',
|
|
30
32
|
outputJson: 'j',
|
|
@@ -37,6 +39,7 @@ const validate = Cli.create('validate', {
|
|
|
37
39
|
endpoint: c.options.endpoint,
|
|
38
40
|
body: c.options.body,
|
|
39
41
|
query: c.options.query,
|
|
42
|
+
headers: c.options.header,
|
|
40
43
|
verbose: c.options.verbose > 0,
|
|
41
44
|
yes: c.options.yes,
|
|
42
45
|
interactive: false,
|
|
@@ -66,6 +69,7 @@ const validate = Cli.create('validate', {
|
|
|
66
69
|
endpoint: c.options.endpoint,
|
|
67
70
|
body: c.options.body,
|
|
68
71
|
query: c.options.query,
|
|
72
|
+
headers: c.options.header,
|
|
69
73
|
verbose: c.options.verbose > 0,
|
|
70
74
|
yes: c.options.yes,
|
|
71
75
|
interactive: !!process.stdin.isTTY,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
fetchWithTimeout,
|
|
27
27
|
formatBytes,
|
|
28
28
|
isValidIntegerAmount,
|
|
29
|
+
parseHeaders,
|
|
29
30
|
skip,
|
|
30
31
|
warn,
|
|
31
32
|
} from './helpers.js'
|
|
@@ -129,6 +130,7 @@ export async function validatePaymentFlow(
|
|
|
129
130
|
options: {
|
|
130
131
|
body?: string | undefined
|
|
131
132
|
query?: string[] | undefined
|
|
133
|
+
extraHeaders?: string[] | undefined
|
|
132
134
|
yes?: boolean | undefined
|
|
133
135
|
silent?: boolean | undefined
|
|
134
136
|
interactive?: boolean | undefined
|
|
@@ -137,7 +139,7 @@ export async function validatePaymentFlow(
|
|
|
137
139
|
): Promise<CheckResult[]> {
|
|
138
140
|
const results: CheckResult[] = []
|
|
139
141
|
const url = buildUrl(baseUrl, endpoint, options.query)
|
|
140
|
-
const fetchHeaders: Record<string, string> =
|
|
142
|
+
const fetchHeaders: Record<string, string> = parseHeaders(options.extraHeaders)
|
|
141
143
|
let fetchBody: string | undefined
|
|
142
144
|
if (options.body) {
|
|
143
145
|
fetchBody = options.body
|
|
@@ -293,6 +295,7 @@ type PaymentContext = {
|
|
|
293
295
|
options: {
|
|
294
296
|
body?: string | undefined
|
|
295
297
|
query?: string[] | undefined
|
|
298
|
+
extraHeaders?: string[] | undefined
|
|
296
299
|
yes?: boolean | undefined
|
|
297
300
|
silent?: boolean | undefined
|
|
298
301
|
interactive?: boolean | undefined
|
package/src/cli/validate.test.ts
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import type * as http from 'node:http'
|
|
2
2
|
|
|
3
|
-
import { afterEach, describe, expect, test } from 'vp/test'
|
|
3
|
+
import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
4
4
|
import * as Http from '~test/Http.js'
|
|
5
5
|
|
|
6
6
|
import * as Challenge from '../Challenge.js'
|
|
7
7
|
import * as Constants from '../Constants.js'
|
|
8
8
|
import * as Receipt from '../Receipt.js'
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
// Keep CLI orchestration tests independent of the public Tempo RPC and faucet.
|
|
11
|
+
vi.doMock('viem/tempo', async () => {
|
|
12
|
+
const actual = await vi.importActual<typeof import('viem/tempo')>('viem/tempo')
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
Actions: {
|
|
16
|
+
...actual.Actions,
|
|
17
|
+
faucet: { ...actual.Actions.faucet, fund: vi.fn(async () => []) },
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
vi.doMock('viem/actions', async () => {
|
|
23
|
+
const actual = await vi.importActual<typeof import('viem/actions')>('viem/actions')
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
prepareTransactionRequest: vi.fn(async () => ({})),
|
|
27
|
+
signTransaction: vi.fn(async () => '0xdeadbeef'),
|
|
28
|
+
}
|
|
29
|
+
})
|
|
10
30
|
|
|
11
31
|
// Auto-cleanup for test servers
|
|
12
32
|
const servers: Http.TestServer[] = []
|
|
@@ -22,6 +42,7 @@ async function testServer(handler: http.RequestListener) {
|
|
|
22
42
|
}
|
|
23
43
|
|
|
24
44
|
async function serve(argv: string[]) {
|
|
45
|
+
const { default: validate } = await import('./validate/index.js')
|
|
25
46
|
let output = ''
|
|
26
47
|
let exitCode: number | undefined
|
|
27
48
|
const origLog = console.log
|
|
@@ -19,7 +19,9 @@ describe('Mppx', () => {
|
|
|
19
19
|
test('exports low-level session method and explicit managed session helper', () => {
|
|
20
20
|
expectTypeOf(session({ account: {} as Account })).toMatchTypeOf<Method.AnyClient>()
|
|
21
21
|
expectTypeOf(session.manager({ account: {} as Account })).toHaveProperty('fetch')
|
|
22
|
-
expectTypeOf(
|
|
22
|
+
expectTypeOf(
|
|
23
|
+
sessionManager({ account: {} as Account, resolveAccount: ({ account }) => account }),
|
|
24
|
+
).toHaveProperty('fetch')
|
|
23
25
|
expectTypeOf(sessionMethod({ account: {} as Account })).toMatchTypeOf<Method.AnyClient>()
|
|
24
26
|
expectTypeOf(tempo.session({ account: {} as Account })).toMatchTypeOf<Method.AnyClient>()
|
|
25
27
|
expectTypeOf(tempo.session.manager({ account: {} as Account })).toHaveProperty('fetch')
|
|
@@ -10,6 +10,7 @@ import { rpcUrl } from '~test/tempo/rpc.js'
|
|
|
10
10
|
import { accounts, asset, chain, client, http } from '~test/tempo/viem.js'
|
|
11
11
|
|
|
12
12
|
import * as Fetch from './Fetch.js'
|
|
13
|
+
import * as MethodResponse from './MethodResponse.js'
|
|
13
14
|
|
|
14
15
|
const realm = 'api.example.com'
|
|
15
16
|
const secretKey = 'test-secret-key-test-secret-key-32'
|
|
@@ -391,6 +392,34 @@ function encodeRawPaymentRequiredHeader(value: unknown): string {
|
|
|
391
392
|
return Base64.fromString(JSON.stringify(value))
|
|
392
393
|
}
|
|
393
394
|
|
|
395
|
+
describe('Fetch.from: method responses', () => {
|
|
396
|
+
test('handles successful paid responses without touching free responses', async () => {
|
|
397
|
+
const method = { ...noopMethod }
|
|
398
|
+
const handle = vi.fn(
|
|
399
|
+
async ({ response }: MethodResponse.HandlerParameters) =>
|
|
400
|
+
new Response(`handled:${await response.text()}`, response),
|
|
401
|
+
)
|
|
402
|
+
MethodResponse.register(method, handle)
|
|
403
|
+
|
|
404
|
+
let paidResponse: Response | undefined
|
|
405
|
+
const mockFetch: typeof globalThis.fetch = async (input, init) => {
|
|
406
|
+
if (input.toString().endsWith('/free')) return new Response('free')
|
|
407
|
+
if (!new Headers(init?.headers).has('Authorization')) return make402()
|
|
408
|
+
paidResponse = new Response('paid')
|
|
409
|
+
vi.spyOn(paidResponse, 'clone')
|
|
410
|
+
return paidResponse
|
|
411
|
+
}
|
|
412
|
+
const fetch = Fetch.from({ fetch: mockFetch, methods: [method] })
|
|
413
|
+
|
|
414
|
+
expect(await (await fetch('https://example.com/free')).text()).toBe('free')
|
|
415
|
+
expect(handle).not.toHaveBeenCalled()
|
|
416
|
+
expect(await (await fetch('https://example.com/paid')).text()).toBe('handled:paid')
|
|
417
|
+
expect(handle).toHaveBeenCalledOnce()
|
|
418
|
+
expect(handle).toHaveBeenCalledWith(expect.objectContaining({ credential: 'credential' }))
|
|
419
|
+
expect(paidResponse?.clone).not.toHaveBeenCalled()
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
394
423
|
describe('Fetch.from: init passthrough (non-402)', () => {
|
|
395
424
|
test('preserves init object identity while adding Accept-Payment', async () => {
|
|
396
425
|
const receivedInits: (RequestInit | undefined)[] = []
|
|
@@ -6,6 +6,7 @@ import type { MaybePromise } from '../../internal/types.js'
|
|
|
6
6
|
import type * as Method from '../../Method.js'
|
|
7
7
|
import type * as z from '../../zod.js'
|
|
8
8
|
import * as Transport from '../Transport.js'
|
|
9
|
+
import * as MethodResponse from './MethodResponse.js'
|
|
9
10
|
|
|
10
11
|
// We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
|
|
11
12
|
// even across multiple module instances/bundles. This lets restore() avoid clobbering
|
|
@@ -18,6 +19,7 @@ type WrappedFetch = typeof globalThis.fetch & {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
let originalFetch: typeof globalThis.fetch | undefined
|
|
22
|
+
const eventObserverChecks = new WeakMap<object, (name: keyof ClientEventMap) => boolean>()
|
|
21
23
|
|
|
22
24
|
export type ClientEventMap<
|
|
23
25
|
methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[],
|
|
@@ -174,7 +176,11 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
174
176
|
// which can duplicate retries and make restore semantics fragile.
|
|
175
177
|
const baseFetch = unwrapFetch(fetch)
|
|
176
178
|
|
|
177
|
-
const
|
|
179
|
+
const request = async (
|
|
180
|
+
input: RequestInfo | URL,
|
|
181
|
+
init: from.RequestInit<methods> | undefined,
|
|
182
|
+
canRefetch: boolean,
|
|
183
|
+
): Promise<Response> => {
|
|
178
184
|
const callerHeaders = getCallerHeaders(input, init?.headers)
|
|
179
185
|
const hasExplicitAcceptPayment = callerHeaders.has(Constants.Headers.acceptPayment)
|
|
180
186
|
const paymentPreferences = resolvePaymentPreferences(callerHeaders, resolvedAcceptPayment)
|
|
@@ -272,8 +278,13 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
272
278
|
}),
|
|
273
279
|
)
|
|
274
280
|
|
|
281
|
+
const paymentInput = resolvePaymentRetryInput(
|
|
282
|
+
response,
|
|
283
|
+
initialRequest.input,
|
|
284
|
+
initialRequest.input,
|
|
285
|
+
)
|
|
275
286
|
response = await baseFetch(
|
|
276
|
-
|
|
287
|
+
paymentInput,
|
|
277
288
|
transport.setCredential(
|
|
278
289
|
{
|
|
279
290
|
...fetchInit,
|
|
@@ -283,7 +294,8 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
283
294
|
{ challenge: selectedChallenge },
|
|
284
295
|
),
|
|
285
296
|
)
|
|
286
|
-
|
|
297
|
+
// Cloning an unobserved stream would tee and buffer it indefinitely.
|
|
298
|
+
if (response.ok && hasEventObservers(events, 'payment.response'))
|
|
287
299
|
await events.emit(
|
|
288
300
|
'payment.response',
|
|
289
301
|
createPaymentResponsePayload({
|
|
@@ -295,8 +307,23 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
295
307
|
response,
|
|
296
308
|
}),
|
|
297
309
|
)
|
|
298
|
-
if (!(await transport.isPaymentRequired(response, transportRequest as never)))
|
|
299
|
-
return response
|
|
310
|
+
if (!(await transport.isPaymentRequired(response, transportRequest as never))) {
|
|
311
|
+
if (!response.ok) return response
|
|
312
|
+
return MethodResponse.handle(selected.method, {
|
|
313
|
+
challenge: selectedChallenge,
|
|
314
|
+
credential,
|
|
315
|
+
fetch: baseFetch,
|
|
316
|
+
headers: initialRequest.headers,
|
|
317
|
+
input: paymentInput,
|
|
318
|
+
...(canRefetch
|
|
319
|
+
? {
|
|
320
|
+
refetch: () => request(cloneRequestInput(initialRequest.input), init, false),
|
|
321
|
+
}
|
|
322
|
+
: {}),
|
|
323
|
+
response,
|
|
324
|
+
signal: resolveRequestSignal(input, init),
|
|
325
|
+
})
|
|
326
|
+
}
|
|
300
327
|
}
|
|
301
328
|
return response
|
|
302
329
|
} catch (error) {
|
|
@@ -315,6 +342,8 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
315
342
|
throw error
|
|
316
343
|
}
|
|
317
344
|
}
|
|
345
|
+
const wrappedFetch = (input: RequestInfo | URL, init?: from.RequestInit<methods>) =>
|
|
346
|
+
request(input, init, true)
|
|
318
347
|
|
|
319
348
|
// Record the wrapped target so future polyfill() / restore() calls can detect origin
|
|
320
349
|
// and safely unwrap only mppx-installed wrappers.
|
|
@@ -491,7 +520,7 @@ export function createEventDispatcher<
|
|
|
491
520
|
}
|
|
492
521
|
}
|
|
493
522
|
|
|
494
|
-
|
|
523
|
+
const dispatcher: ClientEventDispatcher<methods, response> = {
|
|
495
524
|
async emit(name, payload) {
|
|
496
525
|
switch (name) {
|
|
497
526
|
case 'challenge.received': {
|
|
@@ -525,6 +554,15 @@ export function createEventDispatcher<
|
|
|
525
554
|
},
|
|
526
555
|
on,
|
|
527
556
|
}
|
|
557
|
+
eventObserverChecks.set(
|
|
558
|
+
dispatcher,
|
|
559
|
+
(name) => handlers[name as keyof typeof handlers].size > 0 || handlers['*'].size > 0,
|
|
560
|
+
)
|
|
561
|
+
return dispatcher
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function hasEventObservers(dispatcher: ClientEventDispatcher, name: keyof ClientEventMap): boolean {
|
|
565
|
+
return eventObserverChecks.get(dispatcher)?.(name) ?? true
|
|
528
566
|
}
|
|
529
567
|
|
|
530
568
|
async function emitChallengeReceived<methods extends readonly Method.AnyClient[], response>(
|
|
@@ -812,6 +850,13 @@ function cloneRequestInput(input: RequestInfo | URL): RequestInfo | URL {
|
|
|
812
850
|
}
|
|
813
851
|
}
|
|
814
852
|
|
|
853
|
+
function resolveRequestSignal(
|
|
854
|
+
input: RequestInfo | URL,
|
|
855
|
+
init: RequestInit | undefined,
|
|
856
|
+
): AbortSignal | undefined {
|
|
857
|
+
return init?.signal ?? (input instanceof Request ? input.signal : undefined)
|
|
858
|
+
}
|
|
859
|
+
|
|
815
860
|
/** @internal */
|
|
816
861
|
function isWrappedFetch(fetch: typeof globalThis.fetch): fetch is WrappedFetch {
|
|
817
862
|
return Boolean((fetch as WrappedFetch)[MPPX_FETCH_WRAPPER])
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type * as Challenge from '../../Challenge.js'
|
|
2
|
+
import type { MaybePromise } from '../../internal/types.js'
|
|
3
|
+
import type * as Method from '../../Method.js'
|
|
4
|
+
|
|
5
|
+
const handlers = new WeakMap<Method.AnyClient, Handler>()
|
|
6
|
+
|
|
7
|
+
/** Inputs available when a client method handles a successful paid response. */
|
|
8
|
+
export type HandlerParameters = {
|
|
9
|
+
challenge: Challenge.Challenge
|
|
10
|
+
credential: string
|
|
11
|
+
fetch: typeof globalThis.fetch
|
|
12
|
+
headers: Headers
|
|
13
|
+
input: RequestInfo | URL
|
|
14
|
+
refetch?: (() => Promise<Response>) | undefined
|
|
15
|
+
response: Response
|
|
16
|
+
signal?: AbortSignal | undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Internal client-method response adapter. */
|
|
20
|
+
export type Handler = (parameters: HandlerParameters) => MaybePromise<Response>
|
|
21
|
+
|
|
22
|
+
/** Registers an internal response adapter without changing the public method shape. */
|
|
23
|
+
export function register<const method extends Method.AnyClient>(
|
|
24
|
+
method: method,
|
|
25
|
+
handler: Handler,
|
|
26
|
+
): method {
|
|
27
|
+
handlers.set(method, handler)
|
|
28
|
+
return method
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Removes response handling from a method whose caller owns the response lifecycle. */
|
|
32
|
+
export function unregister(method: Method.AnyClient): void {
|
|
33
|
+
handlers.delete(method)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Lets the selected client method handle a successful paid response. */
|
|
37
|
+
export function handle(method: Method.AnyClient, parameters: HandlerParameters): Promise<Response> {
|
|
38
|
+
return Promise.resolve(handlers.get(method)?.(parameters) ?? parameters.response)
|
|
39
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tempo as serverTempo } from 'mppx/server'
|
|
1
2
|
import { expectTypeOf, test } from 'vp/test'
|
|
2
3
|
|
|
3
4
|
import type {
|
|
@@ -22,6 +23,7 @@ import type { SessionTransition as MachineSessionTransition } from './session/cl
|
|
|
22
23
|
import type { ChannelTransactionOptions } from './session/precompile/Chain.js'
|
|
23
24
|
import type { RawAmountString } from './session/precompile/index.js'
|
|
24
25
|
import type { SettlementSchedule as SessionSettlementSchedule } from './session/server/index.js'
|
|
26
|
+
import type { SessionController } from './session/server/Sse.js'
|
|
25
27
|
|
|
26
28
|
test('tempo session public barrels expose manager and schedule interfaces', () => {
|
|
27
29
|
expectTypeOf(Tempo.Session).toBeObject()
|
|
@@ -43,6 +45,8 @@ test('tempo session public barrels expose manager and schedule interfaces', () =
|
|
|
43
45
|
units?: number | undefined
|
|
44
46
|
}>()
|
|
45
47
|
expectTypeOf<RawAmountString>().toEqualTypeOf<string>()
|
|
48
|
+
expectTypeOf<serverTempo.Sse.SessionController>().toEqualTypeOf<SessionController>()
|
|
49
|
+
expectTypeOf<Tempo.Session.Server.Sse.SessionController>().toEqualTypeOf<SessionController>()
|
|
46
50
|
expectTypeOf<ActiveSessionState['status']>().toEqualTypeOf<'active'>()
|
|
47
51
|
expectTypeOf<VoucherNeededSessionState['status']>().toEqualTypeOf<'voucherNeeded'>()
|
|
48
52
|
expectTypeOf<ClosedSessionState['status']>().toEqualTypeOf<'closed'>()
|
|
@@ -83,6 +87,7 @@ test('tempo session public barrel hides internal session drivers', () => {
|
|
|
83
87
|
expectTypeOf<SessionPublic>().not.toHaveProperty('settle')
|
|
84
88
|
expectTypeOf<SessionClientPublic>().not.toHaveProperty('ChannelOps')
|
|
85
89
|
expectTypeOf<SessionClientPublic>().not.toHaveProperty('Chain')
|
|
90
|
+
expectTypeOf<SessionClientPublic>().not.toHaveProperty('driveSseResponse')
|
|
86
91
|
expectTypeOf<SessionServerPublic>().not.toHaveProperty('ChannelOps')
|
|
87
92
|
expectTypeOf<SessionServerPublic>().not.toHaveProperty('Chain')
|
|
88
93
|
})
|