mppx 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/Receipt.d.ts +20 -2
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js +19 -12
- package/dist/Receipt.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +6 -4
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.js +2 -2
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/evm.d.ts +2 -0
- package/dist/cli/plugins/evm.d.ts.map +1 -0
- package/dist/cli/plugins/evm.js +51 -0
- package/dist/cli/plugins/evm.js.map +1 -0
- package/dist/cli/plugins/index.d.ts +1 -0
- package/dist/cli/plugins/index.d.ts.map +1 -1
- package/dist/cli/plugins/index.js +1 -0
- package/dist/cli/plugins/index.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +4 -2
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +184 -78
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +99 -35
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +14 -1
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +0 -13
- package/dist/cli/validate/helpers.js.map +1 -1
- package/dist/cli/validate/index.d.ts +1 -0
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +98 -151
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +215 -144
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +6 -1
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/evm/Chains.d.ts +4 -0
- package/dist/evm/Chains.d.ts.map +1 -1
- package/dist/evm/Chains.js +4 -0
- package/dist/evm/Chains.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts.map +1 -1
- package/dist/internal/AcceptPayment.js +7 -1
- package/dist/internal/AcceptPayment.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +6 -2
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +2 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts +26 -2
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +38 -5
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/ChannelStore.js +4 -4
- package/dist/tempo/session/server/ChannelStore.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +21 -1
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +1 -0
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/validation/core.d.ts +79 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +256 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/x402/Assets.d.ts +9 -0
- package/dist/x402/Assets.d.ts.map +1 -1
- package/dist/x402/Assets.js +38 -0
- package/dist/x402/Assets.js.map +1 -1
- package/package.json +9 -4
- package/src/Receipt.test.ts +53 -0
- package/src/Receipt.ts +26 -13
- package/src/cli/cli.test.ts +41 -5
- package/src/cli/cli.ts +7 -4
- package/src/cli/internal.ts +2 -2
- package/src/cli/plugins/evm.ts +55 -0
- package/src/cli/plugins/index.ts +1 -0
- package/src/cli/plugins/tempo.ts +6 -2
- package/src/cli/validate/challenge.ts +334 -121
- package/src/cli/validate/discovery.test.ts +307 -0
- package/src/cli/validate/discovery.ts +104 -38
- package/src/cli/validate/helpers.ts +15 -13
- package/src/cli/validate/index.ts +117 -240
- package/src/cli/validate/payment.ts +261 -178
- package/src/cli/validate.test.ts +143 -3
- package/src/client/internal/Fetch.test.ts +186 -2
- package/src/client/internal/Fetch.ts +5 -1
- package/src/evm/Chains.ts +6 -0
- package/src/evm/PublicInterface.test-d.ts +7 -0
- package/src/internal/AcceptPayment.test.ts +45 -0
- package/src/internal/AcceptPayment.ts +11 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/client/Charge.test.ts +61 -0
- package/src/tempo/client/Charge.ts +6 -1
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +9 -0
- package/src/tempo/server/internal/request-body.ts +2 -0
- package/src/tempo/session/precompile/Chain.test.ts +121 -2
- package/src/tempo/session/precompile/Chain.ts +56 -6
- package/src/tempo/session/server/ChannelStore.test.ts +17 -0
- package/src/tempo/session/server/ChannelStore.ts +4 -4
- package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
- package/src/tempo/session/server/CredentialVerification.ts +30 -0
- package/src/tempo/session/server/Session.integration.test.ts +5 -0
- package/src/tempo/session/server/Session.test.ts +119 -53
- package/src/tempo/session/server/Session.ts +1 -0
- package/src/validation/core.ts +348 -0
- package/src/validation/index.ts +11 -0
- package/src/x402/Assets.test.ts +42 -0
- package/src/x402/Assets.ts +40 -0
- package/src/x402/Exact.localnet.test.ts +475 -0
package/src/cli/validate.test.ts
CHANGED
|
@@ -159,6 +159,12 @@ describe('validate: discovery', () => {
|
|
|
159
159
|
expect(output).toContain('Document found and parseable')
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
+
test('discovers openapi.json at root when subpath given', { timeout: 15_000 }, async () => {
|
|
163
|
+
const server = await mppServer(makeChallenge())
|
|
164
|
+
const { output } = await serve(['validate', `${server.url}/mpp`])
|
|
165
|
+
expect(output).toContain('Document found and parseable')
|
|
166
|
+
})
|
|
167
|
+
|
|
162
168
|
test('reports invalid JSON', { timeout: 15_000 }, async () => {
|
|
163
169
|
const server = await testServer((req, res) => {
|
|
164
170
|
if (req.url?.includes('openapi.json')) {
|
|
@@ -280,9 +286,9 @@ describe('validate: challenge', () => {
|
|
|
280
286
|
}
|
|
281
287
|
})
|
|
282
288
|
const { output, exitCode } = await serve(['validate', server.url])
|
|
283
|
-
expect(output).toContain(
|
|
284
|
-
|
|
285
|
-
)
|
|
289
|
+
expect(output).toContain('Not an MPP endpoint')
|
|
290
|
+
expect(output).toContain('x402 protocol detected')
|
|
291
|
+
expect(output).toContain('x402 payment challenge')
|
|
286
292
|
expect(exitCode).toBe(1)
|
|
287
293
|
})
|
|
288
294
|
|
|
@@ -537,3 +543,137 @@ describe('validate: summary', () => {
|
|
|
537
543
|
expect(output).toContain('failed')
|
|
538
544
|
})
|
|
539
545
|
})
|
|
546
|
+
|
|
547
|
+
describe('validate: multi-challenge', () => {
|
|
548
|
+
function makeStripeChallenge(): Challenge.Challenge {
|
|
549
|
+
return {
|
|
550
|
+
id: 'stripe-id-123',
|
|
551
|
+
realm: 'localhost',
|
|
552
|
+
method: 'stripe',
|
|
553
|
+
intent: 'charge',
|
|
554
|
+
request: {
|
|
555
|
+
amount: '100',
|
|
556
|
+
currency: 'usd',
|
|
557
|
+
methodDetails: {
|
|
558
|
+
networkId: 'profile_test123',
|
|
559
|
+
paymentMethodTypes: ['card'],
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
expires: new Date(Date.now() + 300_000).toISOString(),
|
|
563
|
+
} as Challenge.Challenge
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function makeEvmChallenge(): Challenge.Challenge {
|
|
567
|
+
return {
|
|
568
|
+
id: 'evm-id-123',
|
|
569
|
+
realm: 'localhost',
|
|
570
|
+
method: 'evm',
|
|
571
|
+
intent: 'charge',
|
|
572
|
+
request: {
|
|
573
|
+
amount: '10000',
|
|
574
|
+
currency: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
575
|
+
recipient: '0x1234567890123456789012345678901234567890',
|
|
576
|
+
methodDetails: { chainId: 8453, credentialTypes: ['authorization'], decimals: 6 },
|
|
577
|
+
},
|
|
578
|
+
expires: new Date(Date.now() + 300_000).toISOString(),
|
|
579
|
+
} as Challenge.Challenge
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function multiChallengeServer(challenges: Challenge.Challenge[]) {
|
|
583
|
+
const header = challenges.map((c) => Challenge.serialize(c)).join(', ')
|
|
584
|
+
return testServer((req, res) => {
|
|
585
|
+
const url = new URL(req.url!, 'http://localhost')
|
|
586
|
+
if (url.pathname === '/openapi.json') {
|
|
587
|
+
res.setHeader('Content-Type', 'application/json')
|
|
588
|
+
res.end(makeDiscoveryDoc({ '/api/test': {} }))
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
res.writeHead(402, { [Constants.Headers.wwwAuthenticate]: header })
|
|
592
|
+
res.end()
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
test('parses multiple challenges', { timeout: 15_000 }, async () => {
|
|
597
|
+
const server = await multiChallengeServer([makeChallenge(), makeStripeChallenge()])
|
|
598
|
+
const { output } = await serve(['validate', server.url])
|
|
599
|
+
expect(output).toContain('2 methods: tempo/charge, stripe/charge')
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
test('validates Stripe fields', { timeout: 15_000 }, async () => {
|
|
603
|
+
const server = await multiChallengeServer([makeStripeChallenge()])
|
|
604
|
+
const { output } = await serve(['validate', server.url])
|
|
605
|
+
expect(output).toContain('Amount is valid integer string')
|
|
606
|
+
expect(output).toContain('Valid currency code (USD)')
|
|
607
|
+
expect(output).toContain('Has networkId')
|
|
608
|
+
expect(output).toContain('Has paymentMethodTypes (card)')
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
test('validates EVM fields', { timeout: 15_000 }, async () => {
|
|
612
|
+
const server = await multiChallengeServer([makeEvmChallenge()])
|
|
613
|
+
const { output } = await serve(['validate', server.url])
|
|
614
|
+
expect(output).toContain('Valid recipient address')
|
|
615
|
+
expect(output).toContain('Valid currency address')
|
|
616
|
+
expect(output).toContain('Amount is valid integer string')
|
|
617
|
+
expect(output).toContain('Has chainId (8453)')
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
test('tags method names when multiple challenges present', { timeout: 15_000 }, async () => {
|
|
621
|
+
const server = await multiChallengeServer([
|
|
622
|
+
makeChallenge(),
|
|
623
|
+
makeStripeChallenge(),
|
|
624
|
+
makeEvmChallenge(),
|
|
625
|
+
])
|
|
626
|
+
const { output } = await serve(['validate', server.url])
|
|
627
|
+
expect(output).toContain('[tempo] Valid recipient address')
|
|
628
|
+
expect(output).toContain('[stripe] Valid currency code')
|
|
629
|
+
expect(output).toContain('[evm] Has chainId')
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
test('no tags with single challenge', { timeout: 15_000 }, async () => {
|
|
633
|
+
const server = await multiChallengeServer([makeChallenge()])
|
|
634
|
+
const { output } = await serve(['validate', server.url])
|
|
635
|
+
expect(output).toContain('Valid recipient address')
|
|
636
|
+
expect(output).not.toContain('[tempo]')
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
test('Tempo requires recipient or splits', { timeout: 15_000 }, async () => {
|
|
640
|
+
const noRecipient = makeChallenge({
|
|
641
|
+
request: {
|
|
642
|
+
amount: '10000',
|
|
643
|
+
currency: '0x20c0000000000000000000000000000000000000',
|
|
644
|
+
methodDetails: { chainId: 42431 },
|
|
645
|
+
},
|
|
646
|
+
} as Partial<Challenge.Challenge>)
|
|
647
|
+
const server = await multiChallengeServer([noRecipient])
|
|
648
|
+
const { output } = await serve(['validate', server.url])
|
|
649
|
+
expect(output).toContain('Missing recipient (and no splits defined)')
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
test('Tempo accepts splits without recipient', { timeout: 15_000 }, async () => {
|
|
653
|
+
const withSplits = makeChallenge({
|
|
654
|
+
request: {
|
|
655
|
+
amount: '10000',
|
|
656
|
+
currency: '0x20c0000000000000000000000000000000000000',
|
|
657
|
+
methodDetails: {
|
|
658
|
+
chainId: 42431,
|
|
659
|
+
splits: [
|
|
660
|
+
{ amount: '5000', recipient: '0x1234567890123456789012345678901234567890' },
|
|
661
|
+
{ amount: '5000', recipient: '0x0987654321098765432109876543210987654321' },
|
|
662
|
+
],
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
} as Partial<Challenge.Challenge>)
|
|
666
|
+
const server = await multiChallengeServer([withSplits])
|
|
667
|
+
const { output } = await serve(['validate', server.url])
|
|
668
|
+
expect(output).toContain('Uses splits (no single recipient)')
|
|
669
|
+
expect(output).not.toContain('Missing recipient')
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
test('fails on invalid Stripe amount', { timeout: 15_000 }, async () => {
|
|
673
|
+
const bad = makeStripeChallenge()
|
|
674
|
+
;(bad.request as Record<string, unknown>).amount = '1.50'
|
|
675
|
+
const server = await multiChallengeServer([bad])
|
|
676
|
+
const { output } = await serve(['validate', server.url])
|
|
677
|
+
expect(output).toContain('Amount is valid integer string (Got: 1.50)')
|
|
678
|
+
})
|
|
679
|
+
})
|
|
@@ -368,8 +368,13 @@ function make402(overrides?: { expires?: string; id?: string; intent?: string; m
|
|
|
368
368
|
})
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
function makeCombined402(
|
|
372
|
-
|
|
371
|
+
function makeCombined402(overrides?: {
|
|
372
|
+
expires?: string
|
|
373
|
+
id?: string
|
|
374
|
+
intent?: string
|
|
375
|
+
method?: string
|
|
376
|
+
}) {
|
|
377
|
+
const response = make402(overrides)
|
|
373
378
|
const headers = new Headers(response.headers)
|
|
374
379
|
headers.set(
|
|
375
380
|
x402_Types.paymentRequiredHeader,
|
|
@@ -729,6 +734,141 @@ describe('Fetch.from: 402 retry path', () => {
|
|
|
729
734
|
expect(retryHeaders.get(x402_Types.paymentSignatureHeader)).toBeNull()
|
|
730
735
|
})
|
|
731
736
|
|
|
737
|
+
test('prefers mpp Payment-auth over x402 when both are signable', async () => {
|
|
738
|
+
let callCount = 0
|
|
739
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
740
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
741
|
+
calls.push({ init })
|
|
742
|
+
callCount++
|
|
743
|
+
if (callCount === 1) return makeCombined402({ method: 'tempo', intent: 'charge' })
|
|
744
|
+
return new Response('OK', { status: 200 })
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const fetch = Fetch.from({
|
|
748
|
+
fetch: mockFetch,
|
|
749
|
+
methods: [
|
|
750
|
+
{
|
|
751
|
+
name: 'tempo',
|
|
752
|
+
intent: 'charge',
|
|
753
|
+
context: undefined,
|
|
754
|
+
createCredential: async () => 'tempo-credential',
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: 'evm',
|
|
758
|
+
intent: 'charge',
|
|
759
|
+
context: undefined,
|
|
760
|
+
createCredential: async () => 'evm-credential',
|
|
761
|
+
},
|
|
762
|
+
] as const,
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
const response = await fetch('https://example.com/api')
|
|
766
|
+
|
|
767
|
+
expect(response.status).toBe(200)
|
|
768
|
+
const retryHeaders = new Headers(calls[1]!.init?.headers)
|
|
769
|
+
expect(retryHeaders.get('Authorization')).toBe('tempo-credential')
|
|
770
|
+
expect(retryHeaders.get(x402_Types.paymentSignatureHeader)).toBeNull()
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
test('prefers native EVM Payment-auth over x402 for EVM-only clients', async () => {
|
|
774
|
+
let callCount = 0
|
|
775
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
776
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
777
|
+
calls.push({ init })
|
|
778
|
+
callCount++
|
|
779
|
+
if (callCount === 1) return makeCombined402({ method: 'evm', intent: 'charge' })
|
|
780
|
+
return new Response('OK', { status: 200 })
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const fetch = Fetch.from({
|
|
784
|
+
fetch: mockFetch,
|
|
785
|
+
methods: [
|
|
786
|
+
{
|
|
787
|
+
name: 'evm',
|
|
788
|
+
intent: 'charge',
|
|
789
|
+
context: undefined,
|
|
790
|
+
createCredential: async () => 'evm-credential',
|
|
791
|
+
},
|
|
792
|
+
] as const,
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
const response = await fetch('https://example.com/api')
|
|
796
|
+
|
|
797
|
+
expect(response.status).toBe(200)
|
|
798
|
+
const retryHeaders = new Headers(calls[1]!.init?.headers)
|
|
799
|
+
expect(retryHeaders.get('Authorization')).toBe('evm-credential')
|
|
800
|
+
expect(retryHeaders.get(x402_Types.paymentSignatureHeader)).toBeNull()
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
test('uses x402 when no signable Payment-auth challenge is available', async () => {
|
|
804
|
+
let callCount = 0
|
|
805
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
806
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
807
|
+
calls.push({ init })
|
|
808
|
+
callCount++
|
|
809
|
+
if (callCount === 1)
|
|
810
|
+
return new Response(null, {
|
|
811
|
+
status: 402,
|
|
812
|
+
headers: {
|
|
813
|
+
[x402_Types.paymentRequiredHeader]:
|
|
814
|
+
x402_Header.encodePaymentRequired(x402PaymentRequired),
|
|
815
|
+
},
|
|
816
|
+
})
|
|
817
|
+
return new Response('OK', { status: 200 })
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const fetch = Fetch.from({
|
|
821
|
+
fetch: mockFetch,
|
|
822
|
+
methods: [
|
|
823
|
+
{
|
|
824
|
+
name: 'evm',
|
|
825
|
+
intent: 'charge',
|
|
826
|
+
context: undefined,
|
|
827
|
+
createCredential: async () => 'x402-credential',
|
|
828
|
+
},
|
|
829
|
+
] as const,
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
const response = await fetch('https://example.com/api')
|
|
833
|
+
|
|
834
|
+
expect(response.status).toBe(200)
|
|
835
|
+
const retryHeaders = new Headers(calls[1]!.init?.headers)
|
|
836
|
+
expect(retryHeaders.get('Authorization')).toBeNull()
|
|
837
|
+
expect(retryHeaders.get(x402_Types.paymentSignatureHeader)).toBe('x402-credential')
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
test('lets orderChallenges force x402 before native Payment-auth', async () => {
|
|
841
|
+
let callCount = 0
|
|
842
|
+
const calls: { init: RequestInit | undefined }[] = []
|
|
843
|
+
const mockFetch: typeof globalThis.fetch = async (_input, init) => {
|
|
844
|
+
calls.push({ init })
|
|
845
|
+
callCount++
|
|
846
|
+
if (callCount === 1) return makeCombined402({ method: 'evm', intent: 'charge' })
|
|
847
|
+
return new Response('OK', { status: 200 })
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const fetch = Fetch.from({
|
|
851
|
+
fetch: mockFetch,
|
|
852
|
+
methods: [
|
|
853
|
+
{
|
|
854
|
+
name: 'evm',
|
|
855
|
+
intent: 'charge',
|
|
856
|
+
context: undefined,
|
|
857
|
+
createCredential: async () => 'x402-credential',
|
|
858
|
+
},
|
|
859
|
+
] as const,
|
|
860
|
+
orderChallenges: (candidates) =>
|
|
861
|
+
[...candidates].sort((left, right) => right.challenge.id.localeCompare(left.challenge.id)),
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
const response = await fetch('https://example.com/api')
|
|
865
|
+
|
|
866
|
+
expect(response.status).toBe(200)
|
|
867
|
+
const retryHeaders = new Headers(calls[1]!.init?.headers)
|
|
868
|
+
expect(retryHeaders.get('Authorization')).toBeNull()
|
|
869
|
+
expect(retryHeaders.get(x402_Types.paymentSignatureHeader)).toBe('x402-credential')
|
|
870
|
+
})
|
|
871
|
+
|
|
732
872
|
test('settles MCP-over-HTTP JSON-RPC payment challenges at the fetch boundary', async () => {
|
|
733
873
|
const mcpChallenge = Challenge.from({
|
|
734
874
|
id: 'mcp-challenge',
|
|
@@ -1742,6 +1882,50 @@ describe('Fetch.from: 402 retry path', () => {
|
|
|
1742
1882
|
expect(createCredential).toHaveBeenCalledOnce()
|
|
1743
1883
|
expect(response.status).toBe(402)
|
|
1744
1884
|
})
|
|
1885
|
+
|
|
1886
|
+
test('returns a challenge-less 402 after a rejected payment', async () => {
|
|
1887
|
+
let callCount = 0
|
|
1888
|
+
const createCredential = vi.fn(async () => 'credential')
|
|
1889
|
+
const mockFetch: typeof globalThis.fetch = async () => {
|
|
1890
|
+
callCount++
|
|
1891
|
+
if (callCount === 1) return make402()
|
|
1892
|
+
return new Response(JSON.stringify({ title: 'Verification Failed' }), {
|
|
1893
|
+
status: 402,
|
|
1894
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1895
|
+
})
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const fetch = Fetch.from({
|
|
1899
|
+
fetch: mockFetch,
|
|
1900
|
+
methods: [{ ...noopMethod, createCredential }],
|
|
1901
|
+
})
|
|
1902
|
+
|
|
1903
|
+
const response = await fetch('https://example.com/api')
|
|
1904
|
+
expect(callCount).toBe(2)
|
|
1905
|
+
expect(createCredential).toHaveBeenCalledOnce()
|
|
1906
|
+
expect(response.status).toBe(402)
|
|
1907
|
+
await expect(response.json()).resolves.toEqual({ title: 'Verification Failed' })
|
|
1908
|
+
})
|
|
1909
|
+
|
|
1910
|
+
test('returns a post-payment 402 with only unsupported challenges', async () => {
|
|
1911
|
+
let callCount = 0
|
|
1912
|
+
const createCredential = vi.fn(async () => 'credential')
|
|
1913
|
+
const mockFetch: typeof globalThis.fetch = async () => {
|
|
1914
|
+
callCount++
|
|
1915
|
+
if (callCount === 1) return make402()
|
|
1916
|
+
return make402({ method: 'stripe', intent: 'charge' })
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const fetch = Fetch.from({
|
|
1920
|
+
fetch: mockFetch,
|
|
1921
|
+
methods: [{ ...noopMethod, createCredential }],
|
|
1922
|
+
})
|
|
1923
|
+
|
|
1924
|
+
const response = await fetch('https://example.com/api')
|
|
1925
|
+
expect(callCount).toBe(2)
|
|
1926
|
+
expect(createCredential).toHaveBeenCalledOnce()
|
|
1927
|
+
expect(response.status).toBe(402)
|
|
1928
|
+
})
|
|
1745
1929
|
})
|
|
1746
1930
|
|
|
1747
1931
|
describe('Fetch.from: acceptPaymentPolicy', () => {
|
|
@@ -221,10 +221,14 @@ export function from<const methods extends readonly Method.AnyClient[]>(
|
|
|
221
221
|
orderChallenges,
|
|
222
222
|
)
|
|
223
223
|
const selected = orderedCandidates[0]
|
|
224
|
-
if (!selected)
|
|
224
|
+
if (!selected) {
|
|
225
|
+
// A post-payment 402 with no actionable challenge (e.g. rejected credential)
|
|
226
|
+
// is the server's final answer, not a new challenge round.
|
|
227
|
+
if (retry > 0) return response
|
|
225
228
|
throw new Error(
|
|
226
229
|
`No method found for challenges: ${challenges.map((c) => `${c.method}.${c.intent}`).join(', ')}. Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`,
|
|
227
230
|
)
|
|
231
|
+
}
|
|
228
232
|
|
|
229
233
|
const selectedChallenge = selected.challenge
|
|
230
234
|
challenge = selectedChallenge
|
package/src/evm/Chains.ts
CHANGED
|
@@ -40,8 +40,15 @@ describe('evm public interface', () => {
|
|
|
40
40
|
expectTypeOf(clientAssets.baseSepolia.USDC).toMatchTypeOf<
|
|
41
41
|
typeof serverAssets.baseSepolia.USDC
|
|
42
42
|
>()
|
|
43
|
+
expectTypeOf(evmRoot.assets.celo.USDC).toMatchTypeOf<typeof serverAssets.celo.USDC>()
|
|
44
|
+
expectTypeOf(evmRoot.assets.celo.USDT).toMatchTypeOf<typeof serverAssets.celo.USDT>()
|
|
45
|
+
expectTypeOf(clientAssets.celoSepolia.USDC).toMatchTypeOf<
|
|
46
|
+
typeof serverAssets.celoSepolia.USDC
|
|
47
|
+
>()
|
|
43
48
|
expectTypeOf(evmRoot.chains.base).toMatchTypeOf<number>()
|
|
44
49
|
expectTypeOf(clientChains.baseSepolia).toMatchTypeOf<number>()
|
|
50
|
+
expectTypeOf(evmRoot.chains.celo).toMatchTypeOf<number>()
|
|
51
|
+
expectTypeOf(clientChains.celoSepolia).toMatchTypeOf<number>()
|
|
45
52
|
})
|
|
46
53
|
|
|
47
54
|
test('exports root EVM charge method definition', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vp/test'
|
|
2
2
|
|
|
3
|
+
import * as x402_ChallengeBrand from '../x402/internal/ChallengeBrand.js'
|
|
3
4
|
import * as AcceptPayment from './AcceptPayment.js'
|
|
4
5
|
|
|
5
6
|
function stripIndex(entry: AcceptPayment.Entry) {
|
|
@@ -178,6 +179,50 @@ describe('AcceptPayment', () => {
|
|
|
178
179
|
])
|
|
179
180
|
})
|
|
180
181
|
|
|
182
|
+
test('selectChallengeCandidates prefers Payment-auth offers before x402 offers', () => {
|
|
183
|
+
const x402Challenge = x402_ChallengeBrand.mark({
|
|
184
|
+
id: 'x402:0',
|
|
185
|
+
intent: 'charge',
|
|
186
|
+
method: 'evm',
|
|
187
|
+
realm: 'test',
|
|
188
|
+
request: { scheme: 'exact' },
|
|
189
|
+
})
|
|
190
|
+
const nativeChallenge = {
|
|
191
|
+
id: 'native',
|
|
192
|
+
intent: 'charge',
|
|
193
|
+
method: 'tempo',
|
|
194
|
+
realm: 'test',
|
|
195
|
+
request: {},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const candidates = AcceptPayment.selectChallengeCandidates(
|
|
199
|
+
[x402Challenge, nativeChallenge],
|
|
200
|
+
[
|
|
201
|
+
{ name: 'evm', intent: 'charge' },
|
|
202
|
+
{ name: 'tempo', intent: 'charge' },
|
|
203
|
+
] as const,
|
|
204
|
+
AcceptPayment.parse('evm/charge, tempo/charge;q=0.5'),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(candidates.map(({ challenge }) => challenge.id)).toEqual(['native', 'x402:0'])
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('selectChallengeCandidates preserves preference order within Payment-auth offers', () => {
|
|
211
|
+
const candidates = AcceptPayment.selectChallengeCandidates(
|
|
212
|
+
[
|
|
213
|
+
{ id: 'tempo', intent: 'charge', method: 'tempo', realm: 'test', request: {} },
|
|
214
|
+
{ id: 'evm', intent: 'charge', method: 'evm', realm: 'test', request: {} },
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
{ name: 'tempo', intent: 'charge' },
|
|
218
|
+
{ name: 'evm', intent: 'charge' },
|
|
219
|
+
] as const,
|
|
220
|
+
AcceptPayment.parse('evm/charge, tempo/charge;q=0.5'),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
expect(candidates.map(({ challenge }) => challenge.id)).toEqual(['evm', 'tempo'])
|
|
224
|
+
})
|
|
225
|
+
|
|
181
226
|
test('selectChallengeCandidates supports duplicate method keys with challenge predicates', () => {
|
|
182
227
|
const challenges = [
|
|
183
228
|
{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type * as Challenge from '../Challenge.js'
|
|
2
|
+
import * as x402_ChallengeBrand from '../x402/internal/ChallengeBrand.js'
|
|
2
3
|
import type { MaybePromise } from './types.js'
|
|
3
4
|
|
|
4
5
|
type MethodLike = {
|
|
@@ -208,7 +209,12 @@ export function selectChallengeCandidates<const methods extends readonly MethodL
|
|
|
208
209
|
}) as ChallengeCandidate<methods[number]> & { match: Match },
|
|
209
210
|
)
|
|
210
211
|
})
|
|
211
|
-
.sort(
|
|
212
|
+
.sort(
|
|
213
|
+
(left, right) =>
|
|
214
|
+
protocolRank(left.challenge) - protocolRank(right.challenge) ||
|
|
215
|
+
right.match.q - left.match.q ||
|
|
216
|
+
left.index - right.index,
|
|
217
|
+
)
|
|
212
218
|
.map((candidate) => {
|
|
213
219
|
const { match: _match, ...rest } = candidate
|
|
214
220
|
return rest as unknown as ChallengeCandidate<methods[number]>
|
|
@@ -261,6 +267,10 @@ function specificity(preference: Pick<Entry, 'intent' | 'method'>): number {
|
|
|
261
267
|
return Number(preference.method !== '*') + Number(preference.intent !== '*')
|
|
262
268
|
}
|
|
263
269
|
|
|
270
|
+
function protocolRank(challenge: Challenge.Challenge): number {
|
|
271
|
+
return x402_ChallengeBrand.is(challenge) ? 1 : 0
|
|
272
|
+
}
|
|
273
|
+
|
|
264
274
|
function parseEntry(part: string, index: number): Entry {
|
|
265
275
|
const match =
|
|
266
276
|
/^(?<method>[^/;\s]+|\*)\s*\/\s*(?<intent>[^/;\s]+|\*)(?<params>(?:\s*;\s*.+)?)$/u.exec(part)
|