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.
Files changed (108) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cli/internal.js +2 -2
  3. package/dist/cli/internal.js.map +1 -1
  4. package/dist/cli/plugins/evm.d.ts +2 -0
  5. package/dist/cli/plugins/evm.d.ts.map +1 -0
  6. package/dist/cli/plugins/evm.js +51 -0
  7. package/dist/cli/plugins/evm.js.map +1 -0
  8. package/dist/cli/plugins/index.d.ts +1 -0
  9. package/dist/cli/plugins/index.d.ts.map +1 -1
  10. package/dist/cli/plugins/index.js +1 -0
  11. package/dist/cli/plugins/index.js.map +1 -1
  12. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  13. package/dist/cli/plugins/stripe.js +3 -2
  14. package/dist/cli/plugins/stripe.js.map +1 -1
  15. package/dist/cli/validate/challenge.d.ts +2 -0
  16. package/dist/cli/validate/challenge.d.ts.map +1 -1
  17. package/dist/cli/validate/challenge.js +191 -84
  18. package/dist/cli/validate/challenge.js.map +1 -1
  19. package/dist/cli/validate/discovery.d.ts +1 -0
  20. package/dist/cli/validate/discovery.d.ts.map +1 -1
  21. package/dist/cli/validate/discovery.js +99 -35
  22. package/dist/cli/validate/discovery.js.map +1 -1
  23. package/dist/cli/validate/helpers.d.ts +14 -1
  24. package/dist/cli/validate/helpers.d.ts.map +1 -1
  25. package/dist/cli/validate/helpers.js +0 -13
  26. package/dist/cli/validate/helpers.js.map +1 -1
  27. package/dist/cli/validate/index.d.ts +1 -0
  28. package/dist/cli/validate/index.d.ts.map +1 -1
  29. package/dist/cli/validate/index.js +102 -153
  30. package/dist/cli/validate/index.js.map +1 -1
  31. package/dist/cli/validate/payment.d.ts +4 -1
  32. package/dist/cli/validate/payment.d.ts.map +1 -1
  33. package/dist/cli/validate/payment.js +312 -106
  34. package/dist/cli/validate/payment.js.map +1 -1
  35. package/dist/evm/Chains.d.ts +4 -0
  36. package/dist/evm/Chains.d.ts.map +1 -1
  37. package/dist/evm/Chains.js +4 -0
  38. package/dist/evm/Chains.js.map +1 -1
  39. package/dist/internal/AcceptPayment.d.ts.map +1 -1
  40. package/dist/internal/AcceptPayment.js +7 -1
  41. package/dist/internal/AcceptPayment.js.map +1 -1
  42. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  43. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  44. package/dist/stripe/server/internal/html.gen.js +1 -1
  45. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  46. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  47. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  48. package/dist/tempo/server/internal/html.gen.js +1 -1
  49. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  50. package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
  51. package/dist/tempo/server/internal/request-body.js +2 -0
  52. package/dist/tempo/server/internal/request-body.js.map +1 -1
  53. package/dist/tempo/session/server/ChannelStore.js +4 -4
  54. package/dist/tempo/session/server/ChannelStore.js.map +1 -1
  55. package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
  56. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  57. package/dist/tempo/session/server/CredentialVerification.js +21 -1
  58. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  59. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  60. package/dist/tempo/session/server/Session.js +1 -0
  61. package/dist/tempo/session/server/Session.js.map +1 -1
  62. package/dist/validation/core.d.ts +75 -0
  63. package/dist/validation/core.d.ts.map +1 -0
  64. package/dist/validation/core.js +269 -0
  65. package/dist/validation/core.js.map +1 -0
  66. package/dist/validation/index.d.ts +3 -0
  67. package/dist/validation/index.d.ts.map +1 -0
  68. package/dist/validation/index.js +2 -0
  69. package/dist/validation/index.js.map +1 -0
  70. package/dist/x402/Assets.d.ts +9 -0
  71. package/dist/x402/Assets.d.ts.map +1 -1
  72. package/dist/x402/Assets.js +38 -0
  73. package/dist/x402/Assets.js.map +1 -1
  74. package/package.json +9 -4
  75. package/src/cli/internal.ts +2 -2
  76. package/src/cli/plugins/evm.ts +55 -0
  77. package/src/cli/plugins/index.ts +1 -0
  78. package/src/cli/plugins/stripe.ts +3 -2
  79. package/src/cli/validate/challenge.ts +345 -127
  80. package/src/cli/validate/discovery.test.ts +307 -0
  81. package/src/cli/validate/discovery.ts +104 -38
  82. package/src/cli/validate/helpers.ts +15 -13
  83. package/src/cli/validate/index.ts +121 -242
  84. package/src/cli/validate/payment.ts +413 -144
  85. package/src/cli/validate.test.ts +237 -3
  86. package/src/client/internal/Fetch.test.ts +142 -2
  87. package/src/evm/Chains.ts +6 -0
  88. package/src/evm/PublicInterface.test-d.ts +7 -0
  89. package/src/internal/AcceptPayment.test.ts +45 -0
  90. package/src/internal/AcceptPayment.ts +11 -1
  91. package/src/stripe/server/internal/html/package.json +1 -1
  92. package/src/stripe/server/internal/html.gen.ts +1 -1
  93. package/src/tempo/server/internal/html/package.json +1 -1
  94. package/src/tempo/server/internal/html.gen.ts +1 -1
  95. package/src/tempo/server/internal/request-body.test.ts +9 -0
  96. package/src/tempo/server/internal/request-body.ts +2 -0
  97. package/src/tempo/session/server/ChannelStore.test.ts +17 -0
  98. package/src/tempo/session/server/ChannelStore.ts +4 -4
  99. package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
  100. package/src/tempo/session/server/CredentialVerification.ts +30 -0
  101. package/src/tempo/session/server/Session.integration.test.ts +5 -0
  102. package/src/tempo/session/server/Session.test.ts +119 -53
  103. package/src/tempo/session/server/Session.ts +1 -0
  104. package/src/validation/core.ts +374 -0
  105. package/src/validation/index.ts +11 -0
  106. package/src/x402/Assets.test.ts +42 -0
  107. package/src/x402/Assets.ts +40 -0
  108. package/src/x402/Exact.localnet.test.ts +475 -0
@@ -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
- 'Not an MPP endpoint (No WWW-Authenticate header (may be x402 or other protocol))',
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
- const response = make402()
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
@@ -3,3 +3,9 @@ export const base = 8453
3
3
 
4
4
  /** Base Sepolia testnet EVM chain ID. */
5
5
  export const baseSepolia = 84532
6
+
7
+ /** Celo mainnet EVM chain ID. */
8
+ export const celo = 42220
9
+
10
+ /** Celo Sepolia testnet EVM chain ID. */
11
+ export const celoSepolia = 11142220
@@ -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((left, right) => right.match.q - left.match.q || left.index - right.index)
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)
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@stripe/stripe-js": "9.8.0",
6
+ "@stripe/stripe-js": "9.9.0",
7
7
  "mppx": "workspace:*"
8
8
  }
9
9
  }