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.
Files changed (134) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/Receipt.d.ts +20 -2
  3. package/dist/Receipt.d.ts.map +1 -1
  4. package/dist/Receipt.js +19 -12
  5. package/dist/Receipt.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +6 -4
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/internal.js +2 -2
  10. package/dist/cli/internal.js.map +1 -1
  11. package/dist/cli/plugins/evm.d.ts +2 -0
  12. package/dist/cli/plugins/evm.d.ts.map +1 -0
  13. package/dist/cli/plugins/evm.js +51 -0
  14. package/dist/cli/plugins/evm.js.map +1 -0
  15. package/dist/cli/plugins/index.d.ts +1 -0
  16. package/dist/cli/plugins/index.d.ts.map +1 -1
  17. package/dist/cli/plugins/index.js +1 -0
  18. package/dist/cli/plugins/index.js.map +1 -1
  19. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  20. package/dist/cli/plugins/tempo.js +4 -2
  21. package/dist/cli/plugins/tempo.js.map +1 -1
  22. package/dist/cli/validate/challenge.d.ts +2 -0
  23. package/dist/cli/validate/challenge.d.ts.map +1 -1
  24. package/dist/cli/validate/challenge.js +184 -78
  25. package/dist/cli/validate/challenge.js.map +1 -1
  26. package/dist/cli/validate/discovery.d.ts +1 -0
  27. package/dist/cli/validate/discovery.d.ts.map +1 -1
  28. package/dist/cli/validate/discovery.js +99 -35
  29. package/dist/cli/validate/discovery.js.map +1 -1
  30. package/dist/cli/validate/helpers.d.ts +14 -1
  31. package/dist/cli/validate/helpers.d.ts.map +1 -1
  32. package/dist/cli/validate/helpers.js +0 -13
  33. package/dist/cli/validate/helpers.js.map +1 -1
  34. package/dist/cli/validate/index.d.ts +1 -0
  35. package/dist/cli/validate/index.d.ts.map +1 -1
  36. package/dist/cli/validate/index.js +98 -151
  37. package/dist/cli/validate/index.js.map +1 -1
  38. package/dist/cli/validate/payment.d.ts +1 -0
  39. package/dist/cli/validate/payment.d.ts.map +1 -1
  40. package/dist/cli/validate/payment.js +215 -144
  41. package/dist/cli/validate/payment.js.map +1 -1
  42. package/dist/client/internal/Fetch.d.ts.map +1 -1
  43. package/dist/client/internal/Fetch.js +6 -1
  44. package/dist/client/internal/Fetch.js.map +1 -1
  45. package/dist/evm/Chains.d.ts +4 -0
  46. package/dist/evm/Chains.d.ts.map +1 -1
  47. package/dist/evm/Chains.js +4 -0
  48. package/dist/evm/Chains.js.map +1 -1
  49. package/dist/internal/AcceptPayment.d.ts.map +1 -1
  50. package/dist/internal/AcceptPayment.js +7 -1
  51. package/dist/internal/AcceptPayment.js.map +1 -1
  52. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  53. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  54. package/dist/stripe/server/internal/html.gen.js +1 -1
  55. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  56. package/dist/tempo/client/Charge.d.ts.map +1 -1
  57. package/dist/tempo/client/Charge.js +6 -2
  58. package/dist/tempo/client/Charge.js.map +1 -1
  59. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  60. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  61. package/dist/tempo/server/internal/html.gen.js +1 -1
  62. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  63. package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
  64. package/dist/tempo/server/internal/request-body.js +2 -0
  65. package/dist/tempo/server/internal/request-body.js.map +1 -1
  66. package/dist/tempo/session/precompile/Chain.d.ts +26 -2
  67. package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
  68. package/dist/tempo/session/precompile/Chain.js +38 -5
  69. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  70. package/dist/tempo/session/server/ChannelStore.js +4 -4
  71. package/dist/tempo/session/server/ChannelStore.js.map +1 -1
  72. package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
  73. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  74. package/dist/tempo/session/server/CredentialVerification.js +21 -1
  75. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  76. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  77. package/dist/tempo/session/server/Session.js +1 -0
  78. package/dist/tempo/session/server/Session.js.map +1 -1
  79. package/dist/validation/core.d.ts +79 -0
  80. package/dist/validation/core.d.ts.map +1 -0
  81. package/dist/validation/core.js +256 -0
  82. package/dist/validation/core.js.map +1 -0
  83. package/dist/validation/index.d.ts +3 -0
  84. package/dist/validation/index.d.ts.map +1 -0
  85. package/dist/validation/index.js +2 -0
  86. package/dist/validation/index.js.map +1 -0
  87. package/dist/x402/Assets.d.ts +9 -0
  88. package/dist/x402/Assets.d.ts.map +1 -1
  89. package/dist/x402/Assets.js +38 -0
  90. package/dist/x402/Assets.js.map +1 -1
  91. package/package.json +9 -4
  92. package/src/Receipt.test.ts +53 -0
  93. package/src/Receipt.ts +26 -13
  94. package/src/cli/cli.test.ts +41 -5
  95. package/src/cli/cli.ts +7 -4
  96. package/src/cli/internal.ts +2 -2
  97. package/src/cli/plugins/evm.ts +55 -0
  98. package/src/cli/plugins/index.ts +1 -0
  99. package/src/cli/plugins/tempo.ts +6 -2
  100. package/src/cli/validate/challenge.ts +334 -121
  101. package/src/cli/validate/discovery.test.ts +307 -0
  102. package/src/cli/validate/discovery.ts +104 -38
  103. package/src/cli/validate/helpers.ts +15 -13
  104. package/src/cli/validate/index.ts +117 -240
  105. package/src/cli/validate/payment.ts +261 -178
  106. package/src/cli/validate.test.ts +143 -3
  107. package/src/client/internal/Fetch.test.ts +186 -2
  108. package/src/client/internal/Fetch.ts +5 -1
  109. package/src/evm/Chains.ts +6 -0
  110. package/src/evm/PublicInterface.test-d.ts +7 -0
  111. package/src/internal/AcceptPayment.test.ts +45 -0
  112. package/src/internal/AcceptPayment.ts +11 -1
  113. package/src/stripe/server/internal/html/package.json +1 -1
  114. package/src/stripe/server/internal/html.gen.ts +1 -1
  115. package/src/tempo/client/Charge.test.ts +61 -0
  116. package/src/tempo/client/Charge.ts +6 -1
  117. package/src/tempo/server/internal/html/package.json +1 -1
  118. package/src/tempo/server/internal/html.gen.ts +1 -1
  119. package/src/tempo/server/internal/request-body.test.ts +9 -0
  120. package/src/tempo/server/internal/request-body.ts +2 -0
  121. package/src/tempo/session/precompile/Chain.test.ts +121 -2
  122. package/src/tempo/session/precompile/Chain.ts +56 -6
  123. package/src/tempo/session/server/ChannelStore.test.ts +17 -0
  124. package/src/tempo/session/server/ChannelStore.ts +4 -4
  125. package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
  126. package/src/tempo/session/server/CredentialVerification.ts +30 -0
  127. package/src/tempo/session/server/Session.integration.test.ts +5 -0
  128. package/src/tempo/session/server/Session.test.ts +119 -53
  129. package/src/tempo/session/server/Session.ts +1 -0
  130. package/src/validation/core.ts +348 -0
  131. package/src/validation/index.ts +11 -0
  132. package/src/x402/Assets.test.ts +42 -0
  133. package/src/x402/Assets.ts +40 -0
  134. 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,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
- 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',
@@ -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
@@ -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
  }