mppx 0.3.12 → 0.3.14
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/dist/Challenge.d.ts +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +107 -15
- package/dist/Challenge.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +55 -9
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/constantTimeEqual.d.ts.map +1 -1
- package/dist/internal/constantTimeEqual.js +7 -4
- package/dist/internal/constantTimeEqual.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +2 -1
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/Methods.d.ts +0 -3
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js +0 -2
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/stripe/client/Charge.d.ts +0 -3
- package/dist/stripe/client/Charge.d.ts.map +1 -1
- package/dist/stripe/client/Charge.js +2 -2
- package/dist/stripe/client/Charge.js.map +1 -1
- package/dist/stripe/client/Methods.d.ts +0 -3
- package/dist/stripe/client/Methods.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts +0 -3
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +2 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/Methods.d.ts +0 -3
- package/dist/stripe/server/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.d.ts +0 -3
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +3 -3
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +13 -3
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +18 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +4 -3
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Charge.d.ts +0 -3
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +2 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +0 -3
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.test.ts +94 -18
- package/src/Challenge.ts +118 -15
- package/src/PaymentRequest.test.ts +0 -5
- package/src/client/Mppx.test.ts +5 -5
- package/src/client/Transport.test.ts +5 -8
- package/src/client/internal/Fetch.browser.test.ts +135 -0
- package/src/client/internal/Fetch.test.ts +16 -60
- package/src/client/internal/Fetch.ts +66 -9
- package/src/internal/constantTimeEqual.ts +6 -4
- package/src/mcp-sdk/client/McpClient.test.ts +1 -1
- package/src/server/Mppx.ts +3 -1
- package/src/server/Transport.test.ts +6 -9
- package/src/stripe/Methods.ts +0 -2
- package/src/stripe/client/Charge.ts +2 -2
- package/src/stripe/server/Charge.ts +2 -2
- package/src/tempo/Methods.test.ts +22 -0
- package/src/tempo/Methods.ts +3 -3
- package/src/tempo/client/Charge.ts +29 -1
- package/src/tempo/server/Charge.test.ts +34 -72
- package/src/tempo/server/Charge.ts +2 -1
package/src/Challenge.test.ts
CHANGED
|
@@ -250,12 +250,12 @@ describe('fromMethod', () => {
|
|
|
250
250
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
251
251
|
id: 'abc123',
|
|
252
252
|
realm: 'api.example.com',
|
|
253
|
+
expires: '2025-01-06T12:00:00Z',
|
|
253
254
|
request: {
|
|
254
255
|
amount: '1',
|
|
255
256
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
256
257
|
decimals: 6,
|
|
257
258
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
258
|
-
expires: '2025-01-06T12:00:00Z',
|
|
259
259
|
},
|
|
260
260
|
})
|
|
261
261
|
|
|
@@ -269,7 +269,6 @@ describe('fromMethod', () => {
|
|
|
269
269
|
"request": {
|
|
270
270
|
"amount": "1000000",
|
|
271
271
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
272
|
-
"expires": "2025-01-06T12:00:00Z",
|
|
273
272
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
274
273
|
},
|
|
275
274
|
}
|
|
@@ -280,12 +279,12 @@ describe('fromMethod', () => {
|
|
|
280
279
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
281
280
|
id: 'abc123',
|
|
282
281
|
realm: 'api.example.com',
|
|
282
|
+
expires: '2025-01-06T12:00:00Z',
|
|
283
283
|
request: {
|
|
284
284
|
amount: '1',
|
|
285
285
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
286
286
|
decimals: 6,
|
|
287
287
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
288
|
-
expires: '2025-01-06T12:00:00Z',
|
|
289
288
|
chainId: 42431,
|
|
290
289
|
feePayer: true,
|
|
291
290
|
},
|
|
@@ -301,7 +300,6 @@ describe('fromMethod', () => {
|
|
|
301
300
|
"request": {
|
|
302
301
|
"amount": "1000000",
|
|
303
302
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
304
|
-
"expires": "2025-01-06T12:00:00Z",
|
|
305
303
|
"methodDetails": {
|
|
306
304
|
"chainId": 42431,
|
|
307
305
|
"feePayer": true,
|
|
@@ -321,7 +319,6 @@ describe('fromMethod', () => {
|
|
|
321
319
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
322
320
|
decimals: 6,
|
|
323
321
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
324
|
-
expires: '2025-01-06T12:00:00Z',
|
|
325
322
|
},
|
|
326
323
|
digest: 'sha-256=abc',
|
|
327
324
|
expires: '2025-01-06T12:00:00Z',
|
|
@@ -334,12 +331,12 @@ describe('fromMethod', () => {
|
|
|
334
331
|
test('behavior: creates challenge with HMAC-bound id via secretKey', () => {
|
|
335
332
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
336
333
|
realm: 'api.example.com',
|
|
334
|
+
expires: '2025-01-06T12:00:00Z',
|
|
337
335
|
request: {
|
|
338
336
|
amount: '1',
|
|
339
337
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
340
338
|
decimals: 6,
|
|
341
339
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
342
|
-
expires: '2025-01-06T12:00:00Z',
|
|
343
340
|
},
|
|
344
341
|
secretKey: 'my-secret',
|
|
345
342
|
})
|
|
@@ -358,7 +355,6 @@ describe('fromMethod', () => {
|
|
|
358
355
|
amount: 123,
|
|
359
356
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
360
357
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
361
|
-
expires: '2025-01-06T12:00:00Z',
|
|
362
358
|
} as any,
|
|
363
359
|
}),
|
|
364
360
|
).toThrow()
|
|
@@ -402,17 +398,18 @@ describe('serialize', () => {
|
|
|
402
398
|
})
|
|
403
399
|
|
|
404
400
|
describe('deserialize', () => {
|
|
405
|
-
|
|
406
|
-
|
|
401
|
+
const paymentHeader = Challenge.serialize(
|
|
402
|
+
Challenge.from({
|
|
407
403
|
id: 'abc123',
|
|
408
404
|
realm: 'api.example.com',
|
|
409
405
|
method: 'tempo',
|
|
410
406
|
intent: 'charge',
|
|
411
407
|
request: { amount: '1000000', currency: 'USD' },
|
|
412
|
-
})
|
|
408
|
+
}),
|
|
409
|
+
)
|
|
413
410
|
|
|
414
|
-
|
|
415
|
-
const challenge = Challenge.deserialize(
|
|
411
|
+
test('behavior: deserializes WWW-Authenticate header', () => {
|
|
412
|
+
const challenge = Challenge.deserialize(paymentHeader)
|
|
416
413
|
|
|
417
414
|
expect(challenge).toMatchInlineSnapshot(`
|
|
418
415
|
{
|
|
@@ -446,20 +443,100 @@ describe('deserialize', () => {
|
|
|
446
443
|
expect(challenge?.expires).toBe('2025-01-06T12:00:00Z')
|
|
447
444
|
})
|
|
448
445
|
|
|
449
|
-
test(
|
|
450
|
-
|
|
446
|
+
test.each([
|
|
447
|
+
{ name: 'single Payment scheme', header: paymentHeader },
|
|
448
|
+
{ name: 'Payment after another scheme', header: `Bearer realm="api", ${paymentHeader}` },
|
|
449
|
+
{
|
|
450
|
+
name: 'scheme token is case-insensitive',
|
|
451
|
+
header: paymentHeader.replace(/^Payment /, 'payment '),
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: 'quoted values in previous schemes do not interfere',
|
|
455
|
+
header: `Bearer error_description="retry with Payment challenge", ${paymentHeader}`,
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: 'parses Payment params before the next scheme token',
|
|
459
|
+
header: `${paymentHeader}, Bearer realm="fallback"`,
|
|
460
|
+
},
|
|
461
|
+
])('behavior: extracts challenge for $name', ({ header }) => {
|
|
462
|
+
const challenge = Challenge.deserialize(header)
|
|
463
|
+
|
|
464
|
+
expect(challenge.id).toBe('abc123')
|
|
465
|
+
expect(challenge.method).toBe('tempo')
|
|
466
|
+
expect(challenge.intent).toBe('charge')
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
const request = /request="([^"]+)"/.exec(paymentHeader)?.[1]
|
|
470
|
+
if (!request) throw new Error('request missing from serialized challenge')
|
|
471
|
+
|
|
472
|
+
test.each([
|
|
473
|
+
{
|
|
474
|
+
name: 'escaped quotes',
|
|
475
|
+
headerValue: 'premium \\"access\\"',
|
|
476
|
+
expected: 'premium "access"',
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: 'escaped comma',
|
|
480
|
+
headerValue: 'tier\\,premium',
|
|
481
|
+
expected: 'tier,premium',
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: 'escaped backslash',
|
|
485
|
+
headerValue: 'path\\\\alpha',
|
|
486
|
+
expected: 'path\\alpha',
|
|
487
|
+
},
|
|
488
|
+
])('behavior: deserializes $name in quoted-string values', ({ headerValue, expected }) => {
|
|
489
|
+
const header =
|
|
490
|
+
'Payment id="abc123", realm="api.example.com", method="tempo", intent="charge", request="' +
|
|
491
|
+
request +
|
|
492
|
+
`", description="${headerValue}"`
|
|
493
|
+
|
|
494
|
+
const challenge = Challenge.deserialize(header)
|
|
495
|
+
expect(challenge.description).toBe(expected)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
test.each([
|
|
499
|
+
{
|
|
500
|
+
name: 'missing Payment scheme',
|
|
501
|
+
header: 'Bearer token',
|
|
502
|
+
error: 'Missing Payment scheme.',
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'duplicate parameters',
|
|
506
|
+
header: 'Payment id="a", realm="api", method="tempo", intent="charge", request="e30", id="b"',
|
|
507
|
+
error: 'Duplicate parameter: id',
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: 'unterminated quoted-string',
|
|
511
|
+
header:
|
|
512
|
+
'Payment id="a", realm="api", method="tempo", intent="charge", request="e30", description="oops',
|
|
513
|
+
error: 'Unterminated quoted-string.',
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: 'invalid method casing',
|
|
517
|
+
header: 'Payment id="a", realm="api", method="Tempo", intent="charge", request="e30"',
|
|
518
|
+
error: 'Invalid method: "Tempo". Must be lowercase per spec.',
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: 'malformed auth-param',
|
|
522
|
+
header:
|
|
523
|
+
'Payment id="a", realm="api", method="tempo", intent="charge", request="e30", ="value"',
|
|
524
|
+
error: 'Malformed auth-param.',
|
|
525
|
+
},
|
|
526
|
+
])('error: throws for $name', ({ header, error }) => {
|
|
527
|
+
expect(() => Challenge.deserialize(header)).toThrow(error)
|
|
451
528
|
})
|
|
452
529
|
|
|
453
530
|
test('error: missing required fields', () => {
|
|
454
531
|
expect(() => Challenge.deserialize('Payment realm="test"')).toThrow()
|
|
455
532
|
})
|
|
456
533
|
|
|
457
|
-
test('error:
|
|
534
|
+
test('error: missing request parameter after valid Payment scheme', () => {
|
|
458
535
|
expect(() =>
|
|
459
536
|
Challenge.deserialize(
|
|
460
|
-
'Payment id="a", realm="api", method="tempo", intent="charge"
|
|
537
|
+
'Bearer realm="api", Payment id="a", realm="api", method="tempo", intent="charge"',
|
|
461
538
|
),
|
|
462
|
-
).toThrow('
|
|
539
|
+
).toThrow('Missing request parameter.')
|
|
463
540
|
})
|
|
464
541
|
})
|
|
465
542
|
|
|
@@ -584,7 +661,6 @@ describe('opaque', () => {
|
|
|
584
661
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
585
662
|
decimals: 6,
|
|
586
663
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
587
|
-
expires: '2025-01-06T12:00:00Z',
|
|
588
664
|
},
|
|
589
665
|
meta: { payment_intent: 'pi_3abc123XYZ' },
|
|
590
666
|
})
|
package/src/Challenge.ts
CHANGED
|
@@ -125,7 +125,7 @@ export function from<
|
|
|
125
125
|
secretKey,
|
|
126
126
|
} = parameters
|
|
127
127
|
|
|
128
|
-
const expires =
|
|
128
|
+
const expires = parameters.expires as string
|
|
129
129
|
const id = secretKey
|
|
130
130
|
? computeId({ ...parameters, expires, ...(meta && { opaque: meta }) }, { secretKey })
|
|
131
131
|
: (parameters as { id: string }).id
|
|
@@ -205,11 +205,11 @@ export declare namespace from {
|
|
|
205
205
|
* Methods.charge,
|
|
206
206
|
* {
|
|
207
207
|
* realm: 'api.example.com',
|
|
208
|
+
* expires: '2025-01-06T12:00:00Z',
|
|
208
209
|
* request: {
|
|
209
210
|
* amount: '1000000',
|
|
210
211
|
* currency: '0x20c0000000000000000000000000000000000001',
|
|
211
212
|
* recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
212
|
-
* expires: '2025-01-06T12:00:00Z',
|
|
213
213
|
* },
|
|
214
214
|
* },
|
|
215
215
|
* { secretKey: 'my-secret' },
|
|
@@ -319,20 +319,10 @@ export function deserialize<const methods extends readonly Method.Method[] | und
|
|
|
319
319
|
value: string,
|
|
320
320
|
options?: from.Options<methods>,
|
|
321
321
|
): from.ReturnType<from.Parameters, methods> {
|
|
322
|
-
const
|
|
323
|
-
if (!
|
|
322
|
+
const params = extractPaymentAuthParams(value)
|
|
323
|
+
if (!params) throw new Error('Missing Payment scheme.')
|
|
324
324
|
|
|
325
|
-
const
|
|
326
|
-
const result: Record<string, string> = {}
|
|
327
|
-
|
|
328
|
-
for (const match of params.matchAll(/(\w+)="([^"]+)"/g)) {
|
|
329
|
-
const key = match[1]
|
|
330
|
-
const value = match[2]
|
|
331
|
-
if (key && value) {
|
|
332
|
-
if (key in result) throw new Error(`Duplicate parameter: ${key}`)
|
|
333
|
-
result[key] = value
|
|
334
|
-
}
|
|
335
|
-
}
|
|
325
|
+
const result = parseAuthParams(params)
|
|
336
326
|
|
|
337
327
|
const { request, opaque, ...rest } = result
|
|
338
328
|
if (!request) throw new Error('Missing request parameter.')
|
|
@@ -349,6 +339,119 @@ export function deserialize<const methods extends readonly Method.Method[] | und
|
|
|
349
339
|
)
|
|
350
340
|
}
|
|
351
341
|
|
|
342
|
+
/** @internal Extracts the `Payment` scheme from a WWW-Authenticate value that may contain multiple schemes. */
|
|
343
|
+
function extractPaymentAuthParams(header: string): string | null {
|
|
344
|
+
const token = 'Payment'
|
|
345
|
+
let inQuotes = false
|
|
346
|
+
let escaped = false
|
|
347
|
+
|
|
348
|
+
for (let i = 0; i < header.length; i++) {
|
|
349
|
+
const char = header[i]
|
|
350
|
+
|
|
351
|
+
if (inQuotes) {
|
|
352
|
+
if (escaped) escaped = false
|
|
353
|
+
else if (char === '\\') escaped = true
|
|
354
|
+
else if (char === '"') inQuotes = false
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (char === '"') {
|
|
359
|
+
inQuotes = true
|
|
360
|
+
continue
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!startsWithSchemeToken(header, i, token)) continue
|
|
364
|
+
|
|
365
|
+
const prefix = header.slice(0, i)
|
|
366
|
+
if (prefix.trim() && !prefix.trimEnd().endsWith(',')) continue
|
|
367
|
+
|
|
368
|
+
let paramsStart = i + token.length
|
|
369
|
+
while (paramsStart < header.length && /\s/.test(header[paramsStart] ?? '')) paramsStart++
|
|
370
|
+
return header.slice(paramsStart)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return null
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** @internal Parses auth-params with support for escaped quoted-string values. */
|
|
377
|
+
function parseAuthParams(input: string): Record<string, string> {
|
|
378
|
+
const result: Record<string, string> = {}
|
|
379
|
+
let i = 0
|
|
380
|
+
|
|
381
|
+
while (i < input.length) {
|
|
382
|
+
while (i < input.length && /[\s,]/.test(input[i] ?? '')) i++
|
|
383
|
+
if (i >= input.length) break
|
|
384
|
+
|
|
385
|
+
const keyStart = i
|
|
386
|
+
while (i < input.length && /[A-Za-z0-9_-]/.test(input[i] ?? '')) i++
|
|
387
|
+
const key = input.slice(keyStart, i)
|
|
388
|
+
if (!key) throw new Error('Malformed auth-param.')
|
|
389
|
+
|
|
390
|
+
while (i < input.length && /\s/.test(input[i] ?? '')) i++
|
|
391
|
+
|
|
392
|
+
// If there is no '=' after a token, this is likely another auth scheme.
|
|
393
|
+
if (input[i] !== '=') break
|
|
394
|
+
i++
|
|
395
|
+
|
|
396
|
+
while (i < input.length && /\s/.test(input[i] ?? '')) i++
|
|
397
|
+
|
|
398
|
+
const [value, nextIndex] = readAuthParamValue(input, i)
|
|
399
|
+
i = nextIndex
|
|
400
|
+
|
|
401
|
+
if (key in result) throw new Error(`Duplicate parameter: ${key}`)
|
|
402
|
+
result[key] = value
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** @internal */
|
|
409
|
+
function readAuthParamValue(input: string, start: number): [value: string, nextIndex: number] {
|
|
410
|
+
if (input[start] === '"') return readQuotedAuthParamValue(input, start + 1)
|
|
411
|
+
|
|
412
|
+
let i = start
|
|
413
|
+
while (i < input.length && input[i] !== ',') i++
|
|
414
|
+
return [input.slice(start, i).trim(), i]
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** @internal */
|
|
418
|
+
function readQuotedAuthParamValue(
|
|
419
|
+
input: string,
|
|
420
|
+
start: number,
|
|
421
|
+
): [value: string, nextIndex: number] {
|
|
422
|
+
let i = start
|
|
423
|
+
let value = ''
|
|
424
|
+
let escaped = false
|
|
425
|
+
|
|
426
|
+
while (i < input.length) {
|
|
427
|
+
const char = input[i]!
|
|
428
|
+
i++
|
|
429
|
+
|
|
430
|
+
if (escaped) {
|
|
431
|
+
value += char
|
|
432
|
+
escaped = false
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (char === '\\') {
|
|
437
|
+
escaped = true
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (char === '"') return [value, i]
|
|
442
|
+
value += char
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
throw new Error('Unterminated quoted-string.')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** @internal */
|
|
449
|
+
function startsWithSchemeToken(value: string, index: number, token: string): boolean {
|
|
450
|
+
if (!value.slice(index).toLowerCase().startsWith(token.toLowerCase())) return false
|
|
451
|
+
const next = value[index + token.length]
|
|
452
|
+
return Boolean(next && /\s/.test(next))
|
|
453
|
+
}
|
|
454
|
+
|
|
352
455
|
/**
|
|
353
456
|
* Extracts the challenge from a Headers object.
|
|
354
457
|
*
|
|
@@ -26,13 +26,11 @@ describe('fromMethod', () => {
|
|
|
26
26
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
27
27
|
decimals: 6,
|
|
28
28
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
29
|
-
expires: '2025-01-06T12:00:00Z',
|
|
30
29
|
})
|
|
31
30
|
expect(request).toMatchInlineSnapshot(`
|
|
32
31
|
{
|
|
33
32
|
"amount": "1000000",
|
|
34
33
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
35
|
-
"expires": "2025-01-06T12:00:00Z",
|
|
36
34
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
37
35
|
}
|
|
38
36
|
`)
|
|
@@ -44,14 +42,12 @@ describe('fromMethod', () => {
|
|
|
44
42
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
45
43
|
decimals: 6,
|
|
46
44
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
47
|
-
expires: '2025-01-06T12:00:00Z',
|
|
48
45
|
chainId: 42431,
|
|
49
46
|
})
|
|
50
47
|
expect(request).toMatchInlineSnapshot(`
|
|
51
48
|
{
|
|
52
49
|
"amount": "1000000",
|
|
53
50
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
54
|
-
"expires": "2025-01-06T12:00:00Z",
|
|
55
51
|
"methodDetails": {
|
|
56
52
|
"chainId": 42431,
|
|
57
53
|
},
|
|
@@ -66,7 +62,6 @@ describe('fromMethod', () => {
|
|
|
66
62
|
amount: 123,
|
|
67
63
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
68
64
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
69
|
-
expires: '2025-01-06T12:00:00Z',
|
|
70
65
|
} as any),
|
|
71
66
|
).toThrowErrorMatchingInlineSnapshot(`
|
|
72
67
|
[$ZodError: [
|
package/src/client/Mppx.test.ts
CHANGED
|
@@ -82,12 +82,12 @@ describe('createCredential', () => {
|
|
|
82
82
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
83
83
|
realm,
|
|
84
84
|
secretKey,
|
|
85
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
85
86
|
request: {
|
|
86
87
|
amount: '1000',
|
|
87
88
|
currency: '0x1234567890123456789012345678901234567890',
|
|
88
89
|
decimals: 6,
|
|
89
90
|
recipient: '0x1234567890123456789012345678901234567890',
|
|
90
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
91
91
|
},
|
|
92
92
|
})
|
|
93
93
|
|
|
@@ -164,11 +164,11 @@ describe('createCredential', () => {
|
|
|
164
164
|
realm,
|
|
165
165
|
method: 'stripe',
|
|
166
166
|
intent: 'charge',
|
|
167
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
167
168
|
request: {
|
|
168
169
|
amount: '2000',
|
|
169
170
|
currency: '0xabcd',
|
|
170
171
|
recipient: '0xefgh',
|
|
171
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
172
172
|
},
|
|
173
173
|
})
|
|
174
174
|
|
|
@@ -195,12 +195,12 @@ describe('createCredential', () => {
|
|
|
195
195
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
196
196
|
realm,
|
|
197
197
|
secretKey,
|
|
198
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
198
199
|
request: {
|
|
199
200
|
amount: '1000',
|
|
200
201
|
currency: '0x1234567890123456789012345678901234567890',
|
|
201
202
|
decimals: 6,
|
|
202
203
|
recipient: '0x1234567890123456789012345678901234567890',
|
|
203
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
204
204
|
},
|
|
205
205
|
})
|
|
206
206
|
|
|
@@ -227,12 +227,12 @@ describe('createCredential', () => {
|
|
|
227
227
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
228
228
|
realm,
|
|
229
229
|
secretKey,
|
|
230
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
230
231
|
request: {
|
|
231
232
|
amount: '1000',
|
|
232
233
|
currency: '0x1234567890123456789012345678901234567890',
|
|
233
234
|
decimals: 6,
|
|
234
235
|
recipient: '0x1234567890123456789012345678901234567890',
|
|
235
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
236
236
|
},
|
|
237
237
|
})
|
|
238
238
|
|
|
@@ -258,12 +258,12 @@ describe('createCredential', () => {
|
|
|
258
258
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
259
259
|
realm,
|
|
260
260
|
secretKey,
|
|
261
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
261
262
|
request: {
|
|
262
263
|
amount: '1000',
|
|
263
264
|
currency: '0x1234567890123456789012345678901234567890',
|
|
264
265
|
decimals: 6,
|
|
265
266
|
recipient: '0x1234567890123456789012345678901234567890',
|
|
266
|
-
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
267
267
|
},
|
|
268
268
|
})
|
|
269
269
|
|
|
@@ -9,12 +9,12 @@ const secretKey = 'test-secret-key'
|
|
|
9
9
|
const challenge = Challenge.fromMethod(Methods.charge, {
|
|
10
10
|
realm,
|
|
11
11
|
secretKey,
|
|
12
|
+
expires: '2025-01-01T00:00:00.000Z',
|
|
12
13
|
request: {
|
|
13
14
|
amount: '0.001',
|
|
14
15
|
currency: '0x20c0000000000000000000000000000000000001',
|
|
15
16
|
decimals: 6,
|
|
16
17
|
recipient: '0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00',
|
|
17
|
-
expires: '2025-01-01T00:00:00.000Z',
|
|
18
18
|
},
|
|
19
19
|
})
|
|
20
20
|
|
|
@@ -60,14 +60,13 @@ describe('http', () => {
|
|
|
60
60
|
expect(transport.getChallenge(response)).toMatchInlineSnapshot(`
|
|
61
61
|
{
|
|
62
62
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
63
|
-
"id": "
|
|
63
|
+
"id": "0hnrySRDqWfttlDIJpuxV4mJsRJIS7d7RjnufuonJOE",
|
|
64
64
|
"intent": "charge",
|
|
65
65
|
"method": "tempo",
|
|
66
66
|
"realm": "api.example.com",
|
|
67
67
|
"request": {
|
|
68
68
|
"amount": "1000",
|
|
69
69
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
70
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
71
70
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
72
71
|
},
|
|
73
72
|
}
|
|
@@ -91,7 +90,7 @@ describe('http', () => {
|
|
|
91
90
|
const headers = result.headers as Headers
|
|
92
91
|
|
|
93
92
|
expect(headers.get('Authorization')).toMatchInlineSnapshot(
|
|
94
|
-
`"Payment
|
|
93
|
+
`"Payment eyJjaGFsbGVuZ2UiOnsiZXhwaXJlcyI6IjIwMjUtMDEtMDFUMDA6MDA6MDAuMDAwWiIsImlkIjoiMGhucnlTUkRxV2Z0dGxESUpwdXhWNG1Kc1JKSVM3ZDdSam51ZnVvbkpPRSIsImludGVudCI6ImNoYXJnZSIsIm1ldGhvZCI6InRlbXBvIiwicmVhbG0iOiJhcGkuZXhhbXBsZS5jb20iLCJyZXF1ZXN0IjoiZXlKaGJXOTFiblFpT2lJeE1EQXdJaXdpWTNWeWNtVnVZM2tpT2lJd2VESXdZekF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREVpTENKeVpXTnBjR2xsYm5RaU9pSXdlRGMwTW1Rek5VTmpOall6TkVNd05UTXlPVEkxWVROaU9EUTBRbU01WlRjMU9UVm1PR1pGTURBaWZRIn0sInBheWxvYWQiOnsic2lnbmF0dXJlIjoiMHhhYmMxMjMiLCJ0eXBlIjoidHJhbnNhY3Rpb24ifX0"`,
|
|
95
94
|
)
|
|
96
95
|
})
|
|
97
96
|
|
|
@@ -182,14 +181,13 @@ describe('mcp', () => {
|
|
|
182
181
|
expect(transport.getChallenge(response)).toMatchInlineSnapshot(`
|
|
183
182
|
{
|
|
184
183
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
185
|
-
"id": "
|
|
184
|
+
"id": "0hnrySRDqWfttlDIJpuxV4mJsRJIS7d7RjnufuonJOE",
|
|
186
185
|
"intent": "charge",
|
|
187
186
|
"method": "tempo",
|
|
188
187
|
"realm": "api.example.com",
|
|
189
188
|
"request": {
|
|
190
189
|
"amount": "1000",
|
|
191
190
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
192
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
193
191
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
194
192
|
},
|
|
195
193
|
}
|
|
@@ -239,14 +237,13 @@ describe('mcp', () => {
|
|
|
239
237
|
"org.paymentauth/credential": {
|
|
240
238
|
"challenge": {
|
|
241
239
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
242
|
-
"id": "
|
|
240
|
+
"id": "0hnrySRDqWfttlDIJpuxV4mJsRJIS7d7RjnufuonJOE",
|
|
243
241
|
"intent": "charge",
|
|
244
242
|
"method": "tempo",
|
|
245
243
|
"realm": "api.example.com",
|
|
246
244
|
"request": {
|
|
247
245
|
"amount": "1000",
|
|
248
246
|
"currency": "0x20c0000000000000000000000000000000000001",
|
|
249
|
-
"expires": "2025-01-01T00:00:00.000Z",
|
|
250
247
|
"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f8fE00",
|
|
251
248
|
},
|
|
252
249
|
},
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
2
|
+
import * as Fetch from './Fetch.js'
|
|
3
|
+
|
|
4
|
+
const noopMethod = {
|
|
5
|
+
name: 'test',
|
|
6
|
+
intent: 'test',
|
|
7
|
+
context: undefined,
|
|
8
|
+
createCredential: async () => 'credential',
|
|
9
|
+
} as any
|
|
10
|
+
|
|
11
|
+
function make402() {
|
|
12
|
+
const request = btoa(JSON.stringify({ amount: '1' }))
|
|
13
|
+
.replace(/\+/g, '-')
|
|
14
|
+
.replace(/\//g, '_')
|
|
15
|
+
.replace(/=+$/, '')
|
|
16
|
+
return new Response(null, {
|
|
17
|
+
status: 402,
|
|
18
|
+
headers: {
|
|
19
|
+
'WWW-Authenticate': `Payment id="abc", realm="test", method="test", intent="test", request="${request}"`,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Returns a fetch wrapper and the init captured from the 402 retry call. */
|
|
25
|
+
function setup() {
|
|
26
|
+
const calls: (RequestInit | undefined)[] = []
|
|
27
|
+
let callCount = 0
|
|
28
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
29
|
+
calls.push(init)
|
|
30
|
+
callCount++
|
|
31
|
+
if (callCount === 1) return make402()
|
|
32
|
+
return new Response('OK', { status: 200 })
|
|
33
|
+
}
|
|
34
|
+
const fetch = Fetch.from({ fetch: mockFetch, methods: [noopMethod] })
|
|
35
|
+
return {
|
|
36
|
+
fetch,
|
|
37
|
+
/** Headers sent on the retry (second) request. */
|
|
38
|
+
retryHeaders: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
39
|
+
await fetch(input, init)
|
|
40
|
+
return (calls[1] as Record<string, unknown>)?.headers as Record<string, string>
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('Fetch.from: browser header normalization', () => {
|
|
46
|
+
test('preserves Headers instance', async () => {
|
|
47
|
+
const { retryHeaders } = setup()
|
|
48
|
+
const h = await retryHeaders('https://example.com', {
|
|
49
|
+
headers: new Headers({ 'X-Custom': 'value', 'Content-Type': 'application/json' }),
|
|
50
|
+
})
|
|
51
|
+
expect(h['x-custom']).toBe('value')
|
|
52
|
+
expect(h['content-type']).toBe('application/json')
|
|
53
|
+
expect(h.Authorization).toBe('credential')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('preserves header tuples', async () => {
|
|
57
|
+
const { retryHeaders } = setup()
|
|
58
|
+
const h = await retryHeaders('https://example.com', {
|
|
59
|
+
headers: [
|
|
60
|
+
['X-Custom', 'value'],
|
|
61
|
+
['Accept', 'application/json'],
|
|
62
|
+
],
|
|
63
|
+
})
|
|
64
|
+
expect(h['X-Custom']).toBe('value')
|
|
65
|
+
expect(h.Accept).toBe('application/json')
|
|
66
|
+
expect(h.Authorization).toBe('credential')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('replaces authorization case-insensitively', async () => {
|
|
70
|
+
const { retryHeaders } = setup()
|
|
71
|
+
const h = await retryHeaders('https://example.com', {
|
|
72
|
+
headers: { authorization: 'Bearer stale', 'X-Custom': 'value' },
|
|
73
|
+
})
|
|
74
|
+
expect(h.authorization).toBeUndefined()
|
|
75
|
+
expect(h.Authorization).toBe('credential')
|
|
76
|
+
expect(h['X-Custom']).toBe('value')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('preserves plain object headers', async () => {
|
|
80
|
+
const { retryHeaders } = setup()
|
|
81
|
+
const h = await retryHeaders('https://example.com', { headers: { 'X-Custom': 'val' } })
|
|
82
|
+
expect(h['X-Custom']).toBe('val')
|
|
83
|
+
expect(h.Authorization).toBe('credential')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('adds Authorization when no headers provided', async () => {
|
|
87
|
+
const { retryHeaders } = setup()
|
|
88
|
+
const h = await retryHeaders('https://example.com')
|
|
89
|
+
expect(h.Authorization).toBe('credential')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('Fetch.polyfill / restore: browser', () => {
|
|
94
|
+
test('restore is a no-op when polyfill was never called', () => {
|
|
95
|
+
const before = globalThis.fetch
|
|
96
|
+
Fetch.restore()
|
|
97
|
+
expect(globalThis.fetch).toBe(before)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('restore reverts to original fetch', () => {
|
|
101
|
+
const original = globalThis.fetch
|
|
102
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
103
|
+
expect(globalThis.fetch).not.toBe(original)
|
|
104
|
+
Fetch.restore()
|
|
105
|
+
expect(globalThis.fetch).toBe(original)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('stacked polyfill calls preserve the true original', () => {
|
|
109
|
+
const original = globalThis.fetch
|
|
110
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
111
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
112
|
+
Fetch.restore()
|
|
113
|
+
expect(globalThis.fetch).toBe(original)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('double restore does not clobber fetch', () => {
|
|
117
|
+
const original = globalThis.fetch
|
|
118
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
119
|
+
Fetch.restore()
|
|
120
|
+
Fetch.restore()
|
|
121
|
+
expect(globalThis.fetch).toBe(original)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('restore is a no-op when fetch was replaced externally', () => {
|
|
125
|
+
const original = globalThis.fetch
|
|
126
|
+
const external = vi.fn(
|
|
127
|
+
async () => new Response('external'),
|
|
128
|
+
) as unknown as typeof globalThis.fetch
|
|
129
|
+
Fetch.polyfill({ methods: [noopMethod] })
|
|
130
|
+
globalThis.fetch = external
|
|
131
|
+
Fetch.restore()
|
|
132
|
+
expect(globalThis.fetch).toBe(external)
|
|
133
|
+
globalThis.fetch = original
|
|
134
|
+
})
|
|
135
|
+
})
|