mppx 0.8.6 → 0.8.8
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 +19 -0
- package/dist/cli/internal.js +2 -2
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/evm.d.ts +2 -0
- package/dist/cli/plugins/evm.d.ts.map +1 -0
- package/dist/cli/plugins/evm.js +51 -0
- package/dist/cli/plugins/evm.js.map +1 -0
- package/dist/cli/plugins/index.d.ts +1 -0
- package/dist/cli/plugins/index.d.ts.map +1 -1
- package/dist/cli/plugins/index.js +1 -0
- package/dist/cli/plugins/index.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +3 -2
- package/dist/cli/plugins/stripe.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 +191 -84
- 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 +102 -153
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +4 -1
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +312 -106
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/evm/Chains.d.ts +4 -0
- package/dist/evm/Chains.d.ts.map +1 -1
- package/dist/evm/Chains.js +4 -0
- package/dist/evm/Chains.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts.map +1 -1
- package/dist/internal/AcceptPayment.js +7 -1
- package/dist/internal/AcceptPayment.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +2 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/session/server/ChannelStore.js +4 -4
- package/dist/tempo/session/server/ChannelStore.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +21 -1
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +1 -0
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/validation/core.d.ts +75 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +269 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/x402/Assets.d.ts +9 -0
- package/dist/x402/Assets.d.ts.map +1 -1
- package/dist/x402/Assets.js +38 -0
- package/dist/x402/Assets.js.map +1 -1
- package/package.json +9 -4
- package/src/cli/internal.ts +2 -2
- package/src/cli/plugins/evm.ts +55 -0
- package/src/cli/plugins/index.ts +1 -0
- package/src/cli/plugins/stripe.ts +3 -2
- package/src/cli/validate/challenge.ts +345 -127
- 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 +121 -242
- package/src/cli/validate/payment.ts +413 -144
- package/src/cli/validate.test.ts +237 -3
- package/src/client/internal/Fetch.test.ts +142 -2
- package/src/evm/Chains.ts +6 -0
- package/src/evm/PublicInterface.test-d.ts +7 -0
- package/src/internal/AcceptPayment.test.ts +45 -0
- package/src/internal/AcceptPayment.ts +11 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +9 -0
- package/src/tempo/server/internal/request-body.ts +2 -0
- package/src/tempo/session/server/ChannelStore.test.ts +17 -0
- package/src/tempo/session/server/ChannelStore.ts +4 -4
- package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
- package/src/tempo/session/server/CredentialVerification.ts +30 -0
- package/src/tempo/session/server/Session.integration.test.ts +5 -0
- package/src/tempo/session/server/Session.test.ts +119 -53
- package/src/tempo/session/server/Session.ts +1 -0
- package/src/validation/core.ts +374 -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,231 @@ 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
|
+
})
|
|
680
|
+
|
|
681
|
+
describe('validate: payment methods coverage', () => {
|
|
682
|
+
test(
|
|
683
|
+
'every method in Constants.Methods produces a payment result',
|
|
684
|
+
{ timeout: 15_000 },
|
|
685
|
+
async () => {
|
|
686
|
+
const methods = Object.values(Constants.Methods)
|
|
687
|
+
const challenges = methods.map((method) => ({
|
|
688
|
+
id: `${method}-id`,
|
|
689
|
+
realm: 'localhost',
|
|
690
|
+
method,
|
|
691
|
+
intent: 'charge',
|
|
692
|
+
request: { amount: '100', currency: 'usd', methodDetails: {} },
|
|
693
|
+
expires: new Date(Date.now() + 300_000).toISOString(),
|
|
694
|
+
})) as Challenge.Challenge[]
|
|
695
|
+
|
|
696
|
+
const header = challenges.map((c) => Challenge.serialize(c)).join(', ')
|
|
697
|
+
const server = await testServer((req, res) => {
|
|
698
|
+
const url = new URL(req.url!, 'http://localhost')
|
|
699
|
+
if (url.pathname === '/openapi.json') {
|
|
700
|
+
res.setHeader('Content-Type', 'application/json')
|
|
701
|
+
res.end(
|
|
702
|
+
JSON.stringify({
|
|
703
|
+
openapi: '3.1.0',
|
|
704
|
+
info: { title: 'T', version: '1' },
|
|
705
|
+
paths: {
|
|
706
|
+
'/api/test': {
|
|
707
|
+
post: { 'x-payment-info': { amount: '100' }, responses: { '402': {} } },
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
}),
|
|
711
|
+
)
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
res.writeHead(402, { [Constants.Headers.wwwAuthenticate]: header })
|
|
715
|
+
res.end()
|
|
716
|
+
})
|
|
717
|
+
const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
|
|
718
|
+
const jsonStart = output.indexOf('{')
|
|
719
|
+
const jsonEnd = output.lastIndexOf('}')
|
|
720
|
+
const result = JSON.parse(output.slice(jsonStart, jsonEnd + 1))
|
|
721
|
+
const paymentLabels = result.endpoints[0].payment.map((r: { label: string }) => r.label)
|
|
722
|
+
for (const method of methods) {
|
|
723
|
+
expect(paymentLabels.some((l: string) => l.includes(`[${method}]`))).toBe(true)
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
)
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
describe('validate: JSON mode', () => {
|
|
730
|
+
function mainnetChallenge() {
|
|
731
|
+
return makeChallenge({
|
|
732
|
+
request: {
|
|
733
|
+
amount: '10000',
|
|
734
|
+
currency: '0x20c0000000000000000000000000000000000000',
|
|
735
|
+
recipient: '0x1234567890123456789012345678901234567890',
|
|
736
|
+
methodDetails: { chainId: 4217 },
|
|
737
|
+
},
|
|
738
|
+
} as Partial<Challenge.Challenge>)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function parseJson(output: string) {
|
|
742
|
+
const jsonStart = output.indexOf('{')
|
|
743
|
+
const jsonEnd = output.lastIndexOf('}')
|
|
744
|
+
return JSON.parse(output.slice(jsonStart, jsonEnd + 1))
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
test('does not prompt (non-interactive)', { timeout: 15_000 }, async () => {
|
|
748
|
+
const server = await mppServer(mainnetChallenge())
|
|
749
|
+
const { output } = await serve(['validate', server.url, '--outputJson'])
|
|
750
|
+
const result = parseJson(output)
|
|
751
|
+
const allPayment = result.endpoints.flatMap((ep: { payment: unknown[] }) => ep.payment)
|
|
752
|
+
expect(allPayment.length).toBeGreaterThan(0)
|
|
753
|
+
expect(allPayment.every((r: { severity: string }) => r.severity === 'skip')).toBe(true)
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
test('no console leaks before JSON', { timeout: 15_000 }, async () => {
|
|
757
|
+
const server = await mppServer(mainnetChallenge())
|
|
758
|
+
const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
|
|
759
|
+
const jsonStart = output.indexOf('{')
|
|
760
|
+
expect(jsonStart).toBeGreaterThanOrEqual(0)
|
|
761
|
+
const beforeJson = output.slice(0, jsonStart)
|
|
762
|
+
expect(beforeJson).not.toContain('Using wallet')
|
|
763
|
+
expect(beforeJson).not.toContain('Auto-approved')
|
|
764
|
+
expect(beforeJson).not.toContain('Attempting')
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
test('includes suggestions', { timeout: 15_000 }, async () => {
|
|
768
|
+
const server = await mppServer(mainnetChallenge())
|
|
769
|
+
const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
|
|
770
|
+
const result = parseJson(output)
|
|
771
|
+
expect(Array.isArray(result.suggestions)).toBe(true)
|
|
772
|
+
})
|
|
773
|
+
})
|
|
@@ -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',
|
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)
|