mppx 0.5.11 → 0.5.13
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 +16 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +41 -16
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/config.d.ts +6 -4
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/internal.d.ts +8 -0
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js +33 -3
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/plugin.d.ts +2 -0
- package/dist/cli/plugins/plugin.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +3 -0
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +3 -0
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/client/Mppx.d.ts +10 -1
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +17 -5
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/Transport.d.ts +2 -0
- package/dist/client/Transport.d.ts.map +1 -1
- package/dist/client/Transport.js +11 -0
- package/dist/client/Transport.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +3 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +65 -19
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts +72 -0
- package/dist/internal/AcceptPayment.d.ts.map +1 -0
- package/dist/internal/AcceptPayment.js +185 -0
- package/dist/internal/AcceptPayment.js.map +1 -0
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +8 -4
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +33 -24
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/internal/html/config.d.ts.map +1 -1
- package/dist/server/internal/html/config.js +8 -1
- package/dist/server/internal/html/config.js.map +1 -1
- package/dist/stripe/internal/constants.d.ts +8 -0
- package/dist/stripe/internal/constants.d.ts.map +1 -0
- package/dist/stripe/internal/constants.js +8 -0
- package/dist/stripe/internal/constants.js.map +1 -0
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +23 -5
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +6 -2
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/Proof.d.ts +12 -0
- package/dist/tempo/Proof.d.ts.map +1 -0
- package/dist/tempo/Proof.js +10 -0
- package/dist/tempo/Proof.js.map +1 -0
- package/dist/tempo/client/Charge.d.ts +11 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +14 -2
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +6 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/index.d.ts +1 -0
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +1 -0
- package/dist/tempo/index.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +8 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +29 -3
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +17 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +69 -4
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +6 -0
- package/dist/tempo/server/Methods.d.ts.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/package.json +2 -2
- package/src/cli/cli.test.ts +278 -0
- package/src/cli/cli.ts +47 -16
- package/src/cli/config.ts +10 -4
- package/src/cli/internal.ts +59 -3
- package/src/cli/plugins/plugin.ts +3 -0
- package/src/cli/plugins/stripe.ts +3 -0
- package/src/cli/plugins/tempo.ts +3 -0
- package/src/client/Mppx.test-d.ts +33 -0
- package/src/client/Mppx.test.ts +130 -1
- package/src/client/Mppx.ts +35 -5
- package/src/client/Transport.test.ts +88 -55
- package/src/client/Transport.ts +13 -0
- package/src/client/internal/Fetch.browser.test.ts +16 -13
- package/src/client/internal/Fetch.test.ts +307 -10
- package/src/client/internal/Fetch.ts +85 -19
- package/src/internal/AcceptPayment.test.ts +211 -0
- package/src/internal/AcceptPayment.ts +304 -0
- package/src/mcp-sdk/client/McpClient.ts +11 -5
- package/src/server/Mppx.test.ts +141 -44
- package/src/server/Mppx.ts +43 -23
- package/src/server/Transport.test.ts +20 -0
- package/src/server/internal/html/config.ts +9 -1
- package/src/stripe/internal/constants.ts +7 -0
- package/src/stripe/server/Charge.ts +22 -4
- package/src/tempo/Methods.test.ts +25 -0
- package/src/tempo/Methods.ts +30 -22
- package/src/tempo/Proof.test-d.ts +13 -0
- package/src/tempo/Proof.test.ts +31 -0
- package/src/tempo/Proof.ts +13 -0
- package/src/tempo/client/Charge.ts +20 -6
- package/src/tempo/client/SessionManager.test.ts +4 -7
- package/src/tempo/index.ts +1 -0
- package/src/tempo/internal/fee-payer.test.ts +75 -1
- package/src/tempo/internal/fee-payer.ts +41 -3
- package/src/tempo/server/Charge.test.ts +309 -1
- package/src/tempo/server/Charge.ts +99 -1
- package/src/tempo/server/internal/html/main.ts +2 -2
- package/src/tempo/server/internal/html.gen.ts +1 -1
|
@@ -350,7 +350,7 @@ function make402(overrides?: { method?: string; intent?: string }) {
|
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
describe('Fetch.from: init passthrough (non-402)', () => {
|
|
353
|
-
test('
|
|
353
|
+
test('preserves init object identity while adding Accept-Payment', async () => {
|
|
354
354
|
const receivedInits: (RequestInit | undefined)[] = []
|
|
355
355
|
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
356
356
|
receivedInits.push(init)
|
|
@@ -371,6 +371,9 @@ describe('Fetch.from: init passthrough (non-402)', () => {
|
|
|
371
371
|
await fetch('https://example.com/ws-upgrade', customInit)
|
|
372
372
|
|
|
373
373
|
expect(receivedInits[0]).toBe(customInit)
|
|
374
|
+
const headers = new Headers(receivedInits[0]?.headers)
|
|
375
|
+
expect(headers.get('X-Custom')).toBe('value')
|
|
376
|
+
expect(headers.get('Accept-Payment')).toBe('test/test')
|
|
374
377
|
})
|
|
375
378
|
|
|
376
379
|
test('preserves extra properties on init for non-402 responses', async () => {
|
|
@@ -395,7 +398,8 @@ describe('Fetch.from: init passthrough (non-402)', () => {
|
|
|
395
398
|
|
|
396
399
|
const received = receivedInits[0]!
|
|
397
400
|
expect(received.method).toBe('GET')
|
|
398
|
-
expect((received.headers
|
|
401
|
+
expect(new Headers(received.headers).get('Authorization')).toBe('Bearer token123')
|
|
402
|
+
expect(new Headers(received.headers).get('Accept-Payment')).toBe('test/test')
|
|
399
403
|
expect(received.signal).toBe(customInit.signal)
|
|
400
404
|
})
|
|
401
405
|
|
|
@@ -412,7 +416,7 @@ describe('Fetch.from: init passthrough (non-402)', () => {
|
|
|
412
416
|
})
|
|
413
417
|
|
|
414
418
|
await fetch('https://example.com/api')
|
|
415
|
-
expect(receivedInits[0]).
|
|
419
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
416
420
|
})
|
|
417
421
|
|
|
418
422
|
test('passes init with context through untouched', async () => {
|
|
@@ -431,9 +435,153 @@ describe('Fetch.from: init passthrough (non-402)', () => {
|
|
|
431
435
|
await fetch('https://example.com/api', customInit as any)
|
|
432
436
|
|
|
433
437
|
expect(receivedInits[0]).toBe(customInit)
|
|
438
|
+
expect((receivedInits[0] as Record<string, unknown>).context).toEqual({ account: '0xabc' })
|
|
439
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
434
440
|
})
|
|
435
441
|
|
|
436
|
-
test('preserves
|
|
442
|
+
test('preserves Request-carried headers when injecting Accept-Payment', async () => {
|
|
443
|
+
const receivedInputs: (RequestInfo | URL)[] = []
|
|
444
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
445
|
+
const mockFetch: typeof globalThis.fetch = async (input, init) => {
|
|
446
|
+
receivedInputs.push(input)
|
|
447
|
+
receivedInits.push(init)
|
|
448
|
+
return new Response('OK', { status: 200 })
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const fetch = Fetch.from({
|
|
452
|
+
fetch: mockFetch,
|
|
453
|
+
methods: [noopMethod],
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
const request = new Request('https://example.com/api', {
|
|
457
|
+
headers: { Authorization: 'Bearer token123', 'X-Custom': 'value' },
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
await fetch(request)
|
|
461
|
+
|
|
462
|
+
expect(receivedInputs[0]).toBe(request)
|
|
463
|
+
const headers = new Headers(receivedInits[0]?.headers)
|
|
464
|
+
expect(headers.get('Authorization')).toBe('Bearer token123')
|
|
465
|
+
expect(headers.get('X-Custom')).toBe('value')
|
|
466
|
+
expect(headers.get('Accept-Payment')).toBe('test/test')
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
test('does not overwrite an explicit Accept-Payment header', async () => {
|
|
470
|
+
const receivedInits: (RequestInit | undefined)[] = []
|
|
471
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
472
|
+
receivedInits.push(init)
|
|
473
|
+
return new Response('OK', { status: 200 })
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const fetch = Fetch.from({
|
|
477
|
+
fetch: mockFetch,
|
|
478
|
+
methods: [noopMethod],
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
await fetch('https://example.com/api', {
|
|
482
|
+
headers: { 'Accept-Payment': 'custom/charge;q=0.5' },
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('custom/charge;q=0.5')
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
test('uses an explicit Accept-Payment header to reprioritize challenge selection', async () => {
|
|
489
|
+
let callCount = 0
|
|
490
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
491
|
+
callCount++
|
|
492
|
+
if (callCount === 1) {
|
|
493
|
+
expect(new Headers(init?.headers).get('Accept-Payment')).toBe(
|
|
494
|
+
'stripe/charge, tempo/charge;q=0.1',
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
return new Response(null, {
|
|
498
|
+
status: 402,
|
|
499
|
+
headers: {
|
|
500
|
+
'WWW-Authenticate': [
|
|
501
|
+
'Payment id="tempo", realm="test", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxIn0"',
|
|
502
|
+
'Payment id="stripe", realm="test", method="stripe", intent="charge", request="eyJhbW91bnQiOiIxIn0"',
|
|
503
|
+
].join(', '),
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
expect(new Headers(init?.headers).get('Authorization')).toBe('stripe-credential')
|
|
509
|
+
return new Response('OK', { status: 200 })
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const fetch = Fetch.from({
|
|
513
|
+
fetch: mockFetch,
|
|
514
|
+
methods: [
|
|
515
|
+
{
|
|
516
|
+
name: 'tempo',
|
|
517
|
+
intent: 'charge',
|
|
518
|
+
context: undefined,
|
|
519
|
+
createCredential: async () => 'tempo-credential',
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: 'stripe',
|
|
523
|
+
intent: 'charge',
|
|
524
|
+
context: undefined,
|
|
525
|
+
createCredential: async () => 'stripe-credential',
|
|
526
|
+
},
|
|
527
|
+
] as const,
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
const response = await fetch('https://example.com/api', {
|
|
531
|
+
headers: { 'Accept-Payment': 'stripe/charge, tempo/charge;q=0.1' },
|
|
532
|
+
})
|
|
533
|
+
expect(response.status).toBe(200)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
test('applies an explicit method opt-out before broader wildcard preferences', async () => {
|
|
537
|
+
let callCount = 0
|
|
538
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
539
|
+
callCount++
|
|
540
|
+
if (callCount === 1) {
|
|
541
|
+
expect(new Headers(init?.headers).get('Accept-Payment')).toBe(
|
|
542
|
+
'tempo/*;q=1, tempo/charge;q=0, stripe/*;q=0.5',
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
return new Response(null, {
|
|
546
|
+
status: 402,
|
|
547
|
+
headers: {
|
|
548
|
+
'WWW-Authenticate': [
|
|
549
|
+
'Payment id="tempo", realm="test", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxIn0"',
|
|
550
|
+
'Payment id="stripe", realm="test", method="stripe", intent="charge", request="eyJhbW91bnQiOiIxIn0"',
|
|
551
|
+
].join(', '),
|
|
552
|
+
},
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
expect(new Headers(init?.headers).get('Authorization')).toBe('stripe-credential')
|
|
557
|
+
return new Response('OK', { status: 200 })
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const fetch = Fetch.from({
|
|
561
|
+
fetch: mockFetch,
|
|
562
|
+
methods: [
|
|
563
|
+
{
|
|
564
|
+
name: 'tempo',
|
|
565
|
+
intent: 'charge',
|
|
566
|
+
context: undefined,
|
|
567
|
+
createCredential: async () => 'tempo-credential',
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: 'stripe',
|
|
571
|
+
intent: 'charge',
|
|
572
|
+
context: undefined,
|
|
573
|
+
createCredential: async () => 'stripe-credential',
|
|
574
|
+
},
|
|
575
|
+
] as const,
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
const response = await fetch('https://example.com/api', {
|
|
579
|
+
headers: { 'Accept-Payment': 'tempo/*;q=1, tempo/charge;q=0, stripe/*;q=0.5' },
|
|
580
|
+
})
|
|
581
|
+
expect(response.status).toBe(200)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
test('preserves non-header init fields across all non-402 status codes', async () => {
|
|
437
585
|
for (const status of [200, 201, 204, 301, 400, 401, 403, 404, 500, 503]) {
|
|
438
586
|
const receivedInits: (RequestInit | undefined)[] = []
|
|
439
587
|
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
@@ -448,7 +596,8 @@ describe('Fetch.from: init passthrough (non-402)', () => {
|
|
|
448
596
|
|
|
449
597
|
const customInit = { method: 'GET' }
|
|
450
598
|
await fetch('https://example.com/api', customInit)
|
|
451
|
-
expect(receivedInits[0]).toBe(customInit)
|
|
599
|
+
expect(receivedInits[0]?.method).toBe(customInit.method)
|
|
600
|
+
expect(new Headers(receivedInits[0]?.headers).get('Accept-Payment')).toBe('test/test')
|
|
452
601
|
}
|
|
453
602
|
})
|
|
454
603
|
})
|
|
@@ -521,10 +670,11 @@ describe('Fetch.from: 402 retry path', () => {
|
|
|
521
670
|
})
|
|
522
671
|
|
|
523
672
|
const retryInit = calls[1]!.init as Record<string, unknown>
|
|
524
|
-
const headers = retryInit.headers as
|
|
525
|
-
expect(headers
|
|
526
|
-
expect(headers
|
|
527
|
-
expect(headers.
|
|
673
|
+
const headers = new Headers(retryInit.headers as HeadersInit)
|
|
674
|
+
expect(headers.get('X-Custom')).toBe('value')
|
|
675
|
+
expect(headers.get('Content-Type')).toBe('application/json')
|
|
676
|
+
expect(headers.get('Accept-Payment')).toBe('test/test')
|
|
677
|
+
expect(headers.get('Authorization')).toBe('credential')
|
|
528
678
|
})
|
|
529
679
|
|
|
530
680
|
test('preserves method and other init properties on retry', async () => {
|
|
@@ -575,7 +725,154 @@ describe('Fetch.from: 402 retry path', () => {
|
|
|
575
725
|
|
|
576
726
|
expect(calls).toHaveLength(2)
|
|
577
727
|
const retryInit = calls[1]!.init as Record<string, unknown>
|
|
578
|
-
|
|
728
|
+
const headers = new Headers(retryInit.headers as HeadersInit)
|
|
729
|
+
expect(headers.get('Accept-Payment')).toBe('test/test')
|
|
730
|
+
expect(headers.get('Authorization')).toBe('credential')
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
test('preserves Request-carried headers on retry after injecting Accept-Payment', async () => {
|
|
734
|
+
let callCount = 0
|
|
735
|
+
const calls: { init: RequestInit | undefined; input: RequestInfo | URL }[] = []
|
|
736
|
+
const mockFetch: typeof globalThis.fetch = async (input, init) => {
|
|
737
|
+
calls.push({ init, input })
|
|
738
|
+
callCount++
|
|
739
|
+
if (callCount === 1) return make402()
|
|
740
|
+
return new Response('OK', { status: 200 })
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const fetch = Fetch.from({
|
|
744
|
+
fetch: mockFetch,
|
|
745
|
+
methods: [noopMethod],
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
const request = new Request('https://example.com/api', {
|
|
749
|
+
headers: { Authorization: 'Bearer token123', 'X-Custom': 'value' },
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
await fetch(request)
|
|
753
|
+
|
|
754
|
+
expect(calls[0]?.input).toBe(request)
|
|
755
|
+
expect(calls[1]?.input).toBe(request)
|
|
756
|
+
const headers = new Headers(calls[1]?.init?.headers)
|
|
757
|
+
expect(headers.get('X-Custom')).toBe('value')
|
|
758
|
+
expect(headers.get('Accept-Payment')).toBe('test/test')
|
|
759
|
+
expect(headers.get('Authorization')).toBe('credential')
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
test('selects the highest-ranked supported challenge', async () => {
|
|
763
|
+
let callCount = 0
|
|
764
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
765
|
+
callCount++
|
|
766
|
+
if (callCount === 1) {
|
|
767
|
+
expect(new Headers(init?.headers).get('Accept-Payment')).toBe(
|
|
768
|
+
'tempo/charge, stripe/charge;q=0.5',
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
return new Response(null, {
|
|
772
|
+
status: 402,
|
|
773
|
+
headers: {
|
|
774
|
+
'WWW-Authenticate': [
|
|
775
|
+
'Payment id="stripe", realm="test", method="stripe", intent="charge", request="eyJhbW91bnQiOiIxIn0"',
|
|
776
|
+
'Payment id="tempo", realm="test", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxIn0"',
|
|
777
|
+
].join(', '),
|
|
778
|
+
},
|
|
779
|
+
})
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
expect(new Headers(init?.headers).get('Authorization')).toBe('tempo-credential')
|
|
783
|
+
return new Response('OK', { status: 200 })
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const fetch = Fetch.from({
|
|
787
|
+
fetch: mockFetch,
|
|
788
|
+
methods: [
|
|
789
|
+
{
|
|
790
|
+
name: 'tempo',
|
|
791
|
+
intent: 'charge',
|
|
792
|
+
context: undefined,
|
|
793
|
+
createCredential: async () => 'tempo-credential',
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: 'stripe',
|
|
797
|
+
intent: 'charge',
|
|
798
|
+
context: undefined,
|
|
799
|
+
createCredential: async () => 'stripe-credential',
|
|
800
|
+
},
|
|
801
|
+
] as const,
|
|
802
|
+
acceptPayment: {
|
|
803
|
+
definition: { 'stripe/charge': 0.5 },
|
|
804
|
+
entries: [
|
|
805
|
+
{ intent: 'charge', method: 'tempo', q: 1, index: 0 },
|
|
806
|
+
{ intent: 'charge', method: 'stripe', q: 0.5, index: 1 },
|
|
807
|
+
],
|
|
808
|
+
header: 'tempo/charge, stripe/charge;q=0.5',
|
|
809
|
+
keys: {
|
|
810
|
+
stripe: { charge: 'stripe/charge' },
|
|
811
|
+
tempo: { charge: 'tempo/charge' },
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
const response = await fetch('https://example.com/api')
|
|
817
|
+
expect(response.status).toBe(200)
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
test('falls back to configured preferences when explicit Accept-Payment is invalid', async () => {
|
|
821
|
+
let callCount = 0
|
|
822
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
823
|
+
callCount++
|
|
824
|
+
if (callCount === 1) {
|
|
825
|
+
expect(new Headers(init?.headers).get('Accept-Payment')).toBe('not a valid header')
|
|
826
|
+
|
|
827
|
+
return new Response(null, {
|
|
828
|
+
status: 402,
|
|
829
|
+
headers: {
|
|
830
|
+
'WWW-Authenticate': [
|
|
831
|
+
'Payment id="stripe", realm="test", method="stripe", intent="charge", request="eyJhbW91bnQiOiIxIn0"',
|
|
832
|
+
'Payment id="tempo", realm="test", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxIn0"',
|
|
833
|
+
].join(', '),
|
|
834
|
+
},
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
expect(new Headers(init?.headers).get('Authorization')).toBe('tempo-credential')
|
|
839
|
+
return new Response('OK', { status: 200 })
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const fetch = Fetch.from({
|
|
843
|
+
fetch: mockFetch,
|
|
844
|
+
methods: [
|
|
845
|
+
{
|
|
846
|
+
name: 'tempo',
|
|
847
|
+
intent: 'charge',
|
|
848
|
+
context: undefined,
|
|
849
|
+
createCredential: async () => 'tempo-credential',
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: 'stripe',
|
|
853
|
+
intent: 'charge',
|
|
854
|
+
context: undefined,
|
|
855
|
+
createCredential: async () => 'stripe-credential',
|
|
856
|
+
},
|
|
857
|
+
] as const,
|
|
858
|
+
acceptPayment: {
|
|
859
|
+
definition: { 'stripe/charge': 0.5 },
|
|
860
|
+
entries: [
|
|
861
|
+
{ intent: 'charge', method: 'tempo', q: 1, index: 0 },
|
|
862
|
+
{ intent: 'charge', method: 'stripe', q: 0.5, index: 1 },
|
|
863
|
+
],
|
|
864
|
+
header: 'tempo/charge, stripe/charge;q=0.5',
|
|
865
|
+
keys: {
|
|
866
|
+
stripe: { charge: 'stripe/charge' },
|
|
867
|
+
tempo: { charge: 'tempo/charge' },
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
const response = await fetch('https://example.com/api', {
|
|
873
|
+
headers: { 'Accept-Payment': 'not a valid header' },
|
|
874
|
+
})
|
|
875
|
+
expect(response.status).toBe(200)
|
|
579
876
|
})
|
|
580
877
|
|
|
581
878
|
test('throws when no matching method for 402 challenge', async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Challenge from '../../Challenge.js'
|
|
2
|
+
import * as AcceptPayment from '../../internal/AcceptPayment.js'
|
|
2
3
|
import type * as Method from '../../Method.js'
|
|
3
4
|
import type * as z from '../../zod.js'
|
|
4
5
|
|
|
@@ -37,41 +38,42 @@ let originalFetch: typeof globalThis.fetch | undefined
|
|
|
37
38
|
export function from<const methods extends readonly Method.AnyClient[]>(
|
|
38
39
|
config: from.Config<methods>,
|
|
39
40
|
): from.Fetch<methods> {
|
|
40
|
-
const { fetch = globalThis.fetch, methods, onChallenge } = config
|
|
41
|
+
const { acceptPayment, fetch = globalThis.fetch, methods, onChallenge } = config
|
|
42
|
+
const resolvedAcceptPayment = acceptPayment ?? AcceptPayment.resolve(methods)
|
|
41
43
|
// Always operate on the true underlying fetch to avoid wrapper-on-wrapper stacking,
|
|
42
44
|
// which can duplicate retries and make restore semantics fragile.
|
|
43
45
|
const baseFetch = unwrapFetch(fetch)
|
|
44
46
|
|
|
45
47
|
const wrappedFetch = async (input: RequestInfo | URL, init?: from.RequestInit<methods>) => {
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
+
const callerHeaders = getCallerHeaders(input, init?.headers)
|
|
49
|
+
const hasExplicitAcceptPayment = callerHeaders.has('Accept-Payment')
|
|
50
|
+
const paymentPreferences = resolvePaymentPreferences(callerHeaders, resolvedAcceptPayment)
|
|
51
|
+
const initialRequest = prepareInitialRequest(
|
|
52
|
+
input,
|
|
53
|
+
init,
|
|
54
|
+
callerHeaders,
|
|
55
|
+
paymentPreferences.header,
|
|
56
|
+
hasExplicitAcceptPayment,
|
|
57
|
+
)
|
|
58
|
+
const response = await baseFetch(initialRequest.input, initialRequest.init)
|
|
48
59
|
|
|
49
60
|
if (response.status !== 402) return response
|
|
50
61
|
|
|
51
62
|
// Only extract context for payment handling after confirming 402.
|
|
52
63
|
const context = (init as Record<string, unknown> | undefined)?.context
|
|
53
|
-
const { context: _, ...fetchInit } = (init ?? {}) as Record<string, unknown>
|
|
64
|
+
const { context: _, ...fetchInit } = (initialRequest.init ?? {}) as Record<string, unknown>
|
|
54
65
|
|
|
55
66
|
// Parse all challenges from the response (supports merged WWW-Authenticate headers).
|
|
56
|
-
// Match in client preference order: iterate the client's methods array and pick the
|
|
57
|
-
// first method that has a matching challenge, so the client controls priority.
|
|
58
67
|
const challenges = Challenge.fromResponseList(response)
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
for (const m of methods) {
|
|
63
|
-
const match = challenges.find((c) => c.method === m.name && c.intent === m.intent)
|
|
64
|
-
if (match) {
|
|
65
|
-
challenge = match
|
|
66
|
-
mi = m
|
|
67
|
-
break
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (!challenge || !mi)
|
|
69
|
+
const selected = AcceptPayment.selectChallenge(challenges, methods, paymentPreferences.entries)
|
|
70
|
+
if (!selected)
|
|
71
71
|
throw new Error(
|
|
72
72
|
`No method found for challenges: ${challenges.map((c) => `${c.method}.${c.intent}`).join(', ')}. Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`,
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
+
const { challenge, method: mi } = selected
|
|
76
|
+
|
|
75
77
|
const onChallengeCredential = onChallenge
|
|
76
78
|
? await onChallenge(challenge, {
|
|
77
79
|
createCredential: async (overrideContext?: AnyContextFor<methods>) =>
|
|
@@ -81,9 +83,9 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
81
83
|
const credential = onChallengeCredential ?? (await resolveCredential(challenge, mi, context))
|
|
82
84
|
validateCredentialHeaderValue(credential)
|
|
83
85
|
|
|
84
|
-
return baseFetch(input, {
|
|
86
|
+
return baseFetch(initialRequest.input, {
|
|
85
87
|
...fetchInit,
|
|
86
|
-
headers: withAuthorizationHeader(
|
|
88
|
+
headers: withAuthorizationHeader(initialRequest.headers, credential),
|
|
87
89
|
})
|
|
88
90
|
}
|
|
89
91
|
|
|
@@ -104,6 +106,8 @@ type AnyContextFor<methods extends readonly Method.AnyClient[]> = {
|
|
|
104
106
|
|
|
105
107
|
export declare namespace from {
|
|
106
108
|
type Config<methods extends readonly Method.AnyClient[] = readonly Method.AnyClient[]> = {
|
|
109
|
+
/** Resolved `Accept-Payment` header and selection preferences. */
|
|
110
|
+
acceptPayment?: AcceptPayment.Resolved<methods> | undefined
|
|
107
111
|
/** Custom fetch function to wrap. Defaults to `globalThis.fetch`. */
|
|
108
112
|
fetch?: typeof globalThis.fetch
|
|
109
113
|
/** Array of methods to use. */
|
|
@@ -218,6 +222,47 @@ function withAuthorizationHeader(headers: unknown, credential: string): Record<s
|
|
|
218
222
|
return normalized
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
/** @internal */
|
|
226
|
+
function prepareInitialRequest<methods extends readonly Method.AnyClient[]>(
|
|
227
|
+
input: RequestInfo | URL,
|
|
228
|
+
init: from.RequestInit<methods> | undefined,
|
|
229
|
+
callerHeaders: Headers,
|
|
230
|
+
header: string,
|
|
231
|
+
hasExplicitAcceptPayment: boolean,
|
|
232
|
+
): { headers: Headers; init: from.RequestInit<methods> | undefined; input: RequestInfo | URL } {
|
|
233
|
+
const shouldInjectAcceptPayment = Boolean(header) && !hasExplicitAcceptPayment
|
|
234
|
+
if (!shouldInjectAcceptPayment) return { headers: callerHeaders, init, input }
|
|
235
|
+
|
|
236
|
+
const headers = new Headers(input instanceof Request ? input.headers : undefined)
|
|
237
|
+
callerHeaders.forEach((value, key) => {
|
|
238
|
+
headers.set(key, value)
|
|
239
|
+
})
|
|
240
|
+
headers.set('Accept-Payment', header)
|
|
241
|
+
|
|
242
|
+
if (init) {
|
|
243
|
+
// Preserve init identity for callers like websocket upgrade helpers that
|
|
244
|
+
// depend on the original RequestInit object reaching the underlying fetch.
|
|
245
|
+
;(init as from.RequestInit<methods> & { headers?: HeadersInit }).headers = headers
|
|
246
|
+
return {
|
|
247
|
+
headers,
|
|
248
|
+
init,
|
|
249
|
+
input,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
headers,
|
|
255
|
+
init: shouldInjectAcceptPayment ? ({ headers } as from.RequestInit<methods>) : undefined,
|
|
256
|
+
input,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** @internal */
|
|
261
|
+
function getCallerHeaders(input: RequestInfo | URL, headers: HeadersInit | undefined): Headers {
|
|
262
|
+
if (headers) return new Headers(headers)
|
|
263
|
+
return new Headers(input instanceof Request ? input.headers : undefined)
|
|
264
|
+
}
|
|
265
|
+
|
|
221
266
|
/** @internal */
|
|
222
267
|
function unwrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch {
|
|
223
268
|
let current = fetch as WrappedFetch
|
|
@@ -251,3 +296,24 @@ async function resolveCredential(
|
|
|
251
296
|
parsedContext !== undefined ? { challenge, context: parsedContext } : ({ challenge } as never),
|
|
252
297
|
)
|
|
253
298
|
}
|
|
299
|
+
|
|
300
|
+
function resolvePaymentPreferences<methods extends readonly Method.AnyClient[]>(
|
|
301
|
+
headers: Headers,
|
|
302
|
+
acceptPayment: AcceptPayment.Resolved<methods>,
|
|
303
|
+
): AcceptPayment.Resolved<methods> {
|
|
304
|
+
const header = headers.get('Accept-Payment')
|
|
305
|
+
if (!header) return acceptPayment
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
return {
|
|
309
|
+
...acceptPayment,
|
|
310
|
+
entries: AcceptPayment.parse(header),
|
|
311
|
+
header,
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
// Fail open for explicit malformed headers: preserve the caller's header on
|
|
315
|
+
// the wire, but continue automatic challenge selection with configured
|
|
316
|
+
// defaults instead of throwing from the wrapper.
|
|
317
|
+
return acceptPayment
|
|
318
|
+
}
|
|
319
|
+
}
|