mppx 0.6.23 → 0.6.24

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 (43) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/Credential.d.ts.map +1 -1
  3. package/dist/Credential.js +1 -1
  4. package/dist/Credential.js.map +1 -1
  5. package/dist/client/Mppx.d.ts +6 -2
  6. package/dist/client/Mppx.d.ts.map +1 -1
  7. package/dist/client/Mppx.js +8 -2
  8. package/dist/client/Mppx.js.map +1 -1
  9. package/dist/client/internal/Fetch.d.ts +4 -0
  10. package/dist/client/internal/Fetch.d.ts.map +1 -1
  11. package/dist/client/internal/Fetch.js +9 -3
  12. package/dist/client/internal/Fetch.js.map +1 -1
  13. package/dist/internal/AcceptPayment.d.ts +16 -0
  14. package/dist/internal/AcceptPayment.d.ts.map +1 -1
  15. package/dist/internal/AcceptPayment.js +31 -8
  16. package/dist/internal/AcceptPayment.js.map +1 -1
  17. package/dist/server/Mppx.d.ts.map +1 -1
  18. package/dist/server/Mppx.js +6 -5
  19. package/dist/server/Mppx.js.map +1 -1
  20. package/dist/tempo/client/SessionManager.d.ts +12 -2
  21. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  22. package/dist/tempo/client/SessionManager.js +8 -1
  23. package/dist/tempo/client/SessionManager.js.map +1 -1
  24. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  25. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  26. package/dist/tempo/server/internal/html.gen.js +1 -1
  27. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/Credential.test.ts +23 -0
  30. package/src/Credential.ts +2 -1
  31. package/src/client/Mppx.test-d.ts +13 -0
  32. package/src/client/Mppx.test.ts +52 -0
  33. package/src/client/Mppx.ts +22 -4
  34. package/src/client/internal/Fetch.test-d.ts +11 -0
  35. package/src/client/internal/Fetch.test.ts +117 -1
  36. package/src/client/internal/Fetch.ts +24 -2
  37. package/src/internal/AcceptPayment.test.ts +26 -0
  38. package/src/internal/AcceptPayment.ts +55 -10
  39. package/src/server/Mppx.test.ts +24 -0
  40. package/src/server/Mppx.ts +6 -5
  41. package/src/tempo/client/SessionManager.test.ts +84 -3
  42. package/src/tempo/client/SessionManager.ts +35 -5
  43. package/src/tempo/server/internal/html.gen.ts +1 -1
@@ -30,11 +30,11 @@ function makeChallenge(overrides: Record<string, unknown> = {}): Challenge.Chall
30
30
  })
31
31
  }
32
32
 
33
- function make402Response(challenge?: Challenge.Challenge): Response {
34
- const c = challenge ?? makeChallenge()
33
+ function make402Response(...challenges: Challenge.Challenge[]): Response {
34
+ const entries = challenges.length ? challenges : [makeChallenge()]
35
35
  return new Response(null, {
36
36
  status: 402,
37
- headers: { 'WWW-Authenticate': Challenge.serialize(c) },
37
+ headers: { 'WWW-Authenticate': entries.map(Challenge.serialize).join(', ') },
38
38
  })
39
39
  }
40
40
 
@@ -111,6 +111,87 @@ describe('Session', () => {
111
111
  'no `deposit` or `maxDeposit` configured',
112
112
  )
113
113
  })
114
+
115
+ test('passes supported challenges through the ordering hook', async () => {
116
+ const first = makeChallenge({ currency: 'pathusd' })
117
+ const second = makeChallenge({ currency: 'usdc' })
118
+ const mockFetch = vi.fn().mockResolvedValue(make402Response(first, second))
119
+ const orderChallenges = vi.fn(
120
+ (candidates: Parameters<NonNullable<sessionManager.Parameters['orderChallenges']>>[0]) =>
121
+ candidates.slice(1, 1),
122
+ )
123
+
124
+ const s = sessionManager({
125
+ account: '0x0000000000000000000000000000000000000001',
126
+ fetch: mockFetch as typeof globalThis.fetch,
127
+ orderChallenges,
128
+ })
129
+
130
+ await expect(s.fetch('https://api.example.com/data')).rejects.toThrow(
131
+ 'No method found for challenges: tempo.session, tempo.session',
132
+ )
133
+ expect(orderChallenges).toHaveBeenCalledOnce()
134
+ expect(
135
+ orderChallenges.mock.calls[0]?.[0].map(({ challenge, index }) => ({
136
+ currency: challenge.request.currency,
137
+ index,
138
+ })),
139
+ ).toEqual([
140
+ { currency: 'pathusd', index: 0 },
141
+ { currency: 'usdc', index: 1 },
142
+ ])
143
+ })
144
+
145
+ test('request-local ordering overrides the session manager ordering hook', async () => {
146
+ const mockFetch = vi.fn().mockResolvedValue(make402Response())
147
+ const configuredOrderChallenges = vi.fn(
148
+ (candidates: Parameters<NonNullable<sessionManager.Parameters['orderChallenges']>>[0]) =>
149
+ candidates,
150
+ )
151
+ const requestOrderChallenges = vi.fn(
152
+ (
153
+ _candidates: Parameters<NonNullable<sessionManager.Parameters['orderChallenges']>>[0],
154
+ ) => [],
155
+ )
156
+
157
+ const s = sessionManager({
158
+ account: '0x0000000000000000000000000000000000000001',
159
+ fetch: mockFetch as typeof globalThis.fetch,
160
+ orderChallenges: configuredOrderChallenges,
161
+ })
162
+
163
+ await expect(
164
+ s.fetch('https://api.example.com/data', {
165
+ orderChallenges: requestOrderChallenges,
166
+ }),
167
+ ).rejects.toThrow('No method found for challenges: tempo.session')
168
+ expect(configuredOrderChallenges).not.toHaveBeenCalled()
169
+ expect(requestOrderChallenges).toHaveBeenCalledOnce()
170
+ })
171
+ })
172
+
173
+ describe('.ws()', () => {
174
+ test('applies challenge ordering to the HTTP probe', async () => {
175
+ const mockFetch = vi.fn().mockResolvedValue(make402Response())
176
+ const orderChallenges = vi.fn(
177
+ (
178
+ _candidates: Parameters<NonNullable<sessionManager.Parameters['orderChallenges']>>[0],
179
+ ) => [],
180
+ )
181
+
182
+ const s = sessionManager({
183
+ account: '0x0000000000000000000000000000000000000001',
184
+ fetch: mockFetch as typeof globalThis.fetch,
185
+ maxDeposit: '10',
186
+ orderChallenges,
187
+ webSocket: vi.fn() as never,
188
+ })
189
+
190
+ await expect(s.ws('wss://api.example.com/session')).rejects.toThrow(
191
+ 'No payment challenge received from HTTP endpoint for this WebSocket URL.',
192
+ )
193
+ expect(orderChallenges).toHaveBeenCalledOnce()
194
+ })
114
195
  })
115
196
 
116
197
  describe('.open()', () => {
@@ -4,6 +4,7 @@ import { parseUnits, type Address } from 'viem'
4
4
  import * as Challenge from '../../Challenge.js'
5
5
  import * as Fetch from '../../client/internal/Fetch.js'
6
6
  import * as PaymentCredential from '../../Credential.js'
7
+ import * as AcceptPayment from '../../internal/AcceptPayment.js'
7
8
  import type * as Account from '../../viem/Account.js'
8
9
  import type * as Client from '../../viem/Client.js'
9
10
  import { deserializeSessionReceipt } from '../session/Receipt.js'
@@ -28,6 +29,12 @@ type CloseReadyWaiter = {
28
29
  resolve(receipt: SessionReceipt): void
29
30
  }
30
31
 
32
+ type SessionMethod = ReturnType<typeof sessionPlugin>
33
+ type SessionOrderChallenges = AcceptPayment.OrderChallenges<readonly [SessionMethod]>
34
+ type SessionRequestInit = RequestInit & {
35
+ orderChallenges?: SessionOrderChallenges | undefined
36
+ }
37
+
31
38
  const WebSocketReadyState = {
32
39
  CONNECTING: 0,
33
40
  OPEN: 1,
@@ -45,10 +52,10 @@ export type SessionManager = {
45
52
  readonly opened: boolean
46
53
 
47
54
  open(options?: { deposit?: bigint }): Promise<void>
48
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise<PaymentResponse>
55
+ fetch(input: RequestInfo | URL, init?: SessionRequestInit): Promise<PaymentResponse>
49
56
  sse(
50
57
  input: RequestInfo | URL,
51
- init?: RequestInit & {
58
+ init?: SessionRequestInit & {
52
59
  onReceipt?: ((receipt: SessionReceipt) => void) | undefined
53
60
  signal?: AbortSignal | undefined
54
61
  },
@@ -57,6 +64,7 @@ export type SessionManager = {
57
64
  input: string | URL,
58
65
  init?: {
59
66
  onReceipt?: ((receipt: SessionReceipt) => void) | undefined
67
+ orderChallenges?: SessionOrderChallenges | undefined
60
68
  protocols?: string | string[] | undefined
61
69
  signal?: AbortSignal | undefined
62
70
  },
@@ -134,6 +142,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
134
142
  lastChallenge = challenge
135
143
  return undefined
136
144
  },
145
+ orderChallenges: parameters.orderChallenges,
137
146
  })
138
147
 
139
148
  function updateSpentFromReceipt(receipt: SessionReceipt | null | undefined) {
@@ -252,7 +261,10 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
252
261
  })
253
262
  }
254
263
 
255
- async function doFetch(input: RequestInfo | URL, init?: RequestInit): Promise<PaymentResponse> {
264
+ async function doFetch(
265
+ input: RequestInfo | URL,
266
+ init?: SessionRequestInit,
267
+ ): Promise<PaymentResponse> {
256
268
  lastUrl = input
257
269
  const response = await wrappedFetch(input, init)
258
270
  return toPaymentResponse(response)
@@ -536,9 +548,17 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
536
548
  )
537
549
  }
538
550
 
539
- const challenge = Challenge.fromResponseList(probe).find(
540
- (item) => item.method === method.name && item.intent === method.intent,
551
+ const candidates = AcceptPayment.selectChallengeCandidates(
552
+ Challenge.fromResponseList(probe),
553
+ [method] as const,
554
+ AcceptPayment.resolve([method] as const).entries,
541
555
  )
556
+ const challenge = (
557
+ await resolveSessionChallengeOrder(
558
+ candidates,
559
+ init?.orderChallenges ?? parameters.orderChallenges,
560
+ )
561
+ )[0]?.challenge
542
562
  if (!challenge) {
543
563
  throw new Error(
544
564
  'No payment challenge received from HTTP endpoint for this WebSocket URL. The server may not require payment or did not advertise a challenge.',
@@ -844,7 +864,17 @@ export declare namespace sessionManager {
844
864
  fetch?: typeof globalThis.fetch | undefined
845
865
  /** Maximum deposit in human-readable units (e.g. `'10'` for 10 tokens). Converted to raw units via `decimals`. */
846
866
  maxDeposit?: string | undefined
867
+ /** Filters and sorts supported session challenges before credential creation. */
868
+ orderChallenges?: SessionOrderChallenges | undefined
847
869
  /** Optional websocket constructor for runtimes without a global WebSocket. */
848
870
  webSocket?: WebSocketConstructor | undefined
849
871
  }
850
872
  }
873
+
874
+ async function resolveSessionChallengeOrder(
875
+ candidates: readonly AcceptPayment.ChallengeCandidate<SessionMethod>[],
876
+ override: SessionOrderChallenges | undefined,
877
+ ): Promise<readonly AcceptPayment.ChallengeCandidate<SessionMethod>[]> {
878
+ const orderChallenges = override
879
+ return orderChallenges ? orderChallenges(candidates) : candidates
880
+ }