mppx 0.6.22 → 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 (48) hide show
  1. package/CHANGELOG.md +13 -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/Subscription.d.ts +7 -0
  25. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  26. package/dist/tempo/server/Subscription.js +47 -6
  27. package/dist/tempo/server/Subscription.js.map +1 -1
  28. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  29. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  30. package/dist/tempo/server/internal/html.gen.js +1 -1
  31. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/Credential.test.ts +23 -0
  34. package/src/Credential.ts +2 -1
  35. package/src/client/Mppx.test-d.ts +13 -0
  36. package/src/client/Mppx.test.ts +52 -0
  37. package/src/client/Mppx.ts +22 -4
  38. package/src/client/internal/Fetch.test-d.ts +11 -0
  39. package/src/client/internal/Fetch.test.ts +117 -1
  40. package/src/client/internal/Fetch.ts +24 -2
  41. package/src/internal/AcceptPayment.test.ts +26 -0
  42. package/src/internal/AcceptPayment.ts +55 -10
  43. package/src/server/Mppx.test.ts +24 -0
  44. package/src/server/Mppx.ts +6 -5
  45. package/src/tempo/client/SessionManager.test.ts +84 -3
  46. package/src/tempo/client/SessionManager.ts +35 -5
  47. package/src/tempo/server/Subscription.ts +62 -3
  48. 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
+ }
@@ -3,6 +3,7 @@ import { KeyAuthorization } from 'ox/tempo'
3
3
  import { encodeFunctionData, isAddressEqual, type Address, type Client as ViemClient } from 'viem'
4
4
  import {
5
5
  call as viem_call,
6
+ prepareTransactionRequest,
6
7
  sendRawTransaction,
7
8
  sendRawTransactionSync,
8
9
  signTransaction,
@@ -19,6 +20,7 @@ import * as ClientResolver from '../../viem/Client.js'
19
20
  import * as Attribution from '../Attribution.js'
20
21
  import * as Account from '../internal/account.js'
21
22
  import * as defaults from '../internal/defaults.js'
23
+ import * as FeePayer from '../internal/fee-payer.js'
22
24
  import * as Proof from '../internal/proof.js'
23
25
  import type * as types from '../internal/types.js'
24
26
  import * as Methods from '../Methods.js'
@@ -72,7 +74,7 @@ export function subscription<const parameters extends subscription.Parameters>(
72
74
  activationTimeoutMs: parameters.activationTimeoutMs,
73
75
  renewalTimeoutMs: parameters.renewalTimeoutMs,
74
76
  })
75
- const { recipient } = Account.resolve(parameters)
77
+ const { feePayer, recipient } = Account.resolve(parameters)
76
78
  const getClient = ClientResolver.getResolver({
77
79
  chain: tempo_chain,
78
80
  getClient: parameters.getClient,
@@ -104,6 +106,8 @@ export function subscription<const parameters extends subscription.Parameters>(
104
106
  const periodIndex = getPeriodIndex(subscription)
105
107
  if (periodIndex > subscription.lastChargedPeriod) {
106
108
  const renew = resolveRenewalHandler({
109
+ feePayer,
110
+ feePayerPolicy: parameters.feePayerPolicy,
107
111
  getClient,
108
112
  parameters,
109
113
  store,
@@ -235,6 +239,8 @@ export function subscription<const parameters extends subscription.Parameters>(
235
239
  accessKey,
236
240
  auto: {
237
241
  challengeId: credential.challenge.id,
242
+ feePayer,
243
+ feePayerPolicy: parameters.feePayerPolicy,
238
244
  getClient,
239
245
  keyAuthorization: (credential.payload as SubscriptionCredentialPayload).signature,
240
246
  realm: credential.challenge.realm,
@@ -346,6 +352,8 @@ async function activateSubscription(parameters: {
346
352
  accessKey: SubscriptionAccessKey
347
353
  auto: {
348
354
  challengeId: string
355
+ feePayer?: ReturnType<typeof Account.resolve>['feePayer'] | undefined
356
+ feePayerPolicy?: Partial<FeePayer.Policy> | undefined
349
357
  getClient: (parameters: { chainId?: number | undefined }) => MaybePromise<ViemClient>
350
358
  keyAuthorization: `0x${string}`
351
359
  realm: string
@@ -391,6 +399,8 @@ async function activateSubscription(parameters: {
391
399
  // billing authority needed for request-path and background renewals.
392
400
  const reference = await submitSubscriptionPayment({
393
401
  accessKey,
402
+ feePayer: auto.feePayer,
403
+ feePayerPolicy: auto.feePayerPolicy,
394
404
  getClient: auto.getClient,
395
405
  keyAuthorization: auto.keyAuthorization,
396
406
  lookupKey: resolved.key,
@@ -689,6 +699,8 @@ function subscriptionBinding(request: SubscriptionRequest) {
689
699
  }
690
700
 
691
701
  function resolveRenewalHandler(parameters: {
702
+ feePayer?: ReturnType<typeof Account.resolve>['feePayer'] | undefined
703
+ feePayerPolicy?: Partial<FeePayer.Policy> | undefined
692
704
  getClient: (parameters: { chainId?: number | undefined }) => MaybePromise<ViemClient>
693
705
  parameters: {
694
706
  renew?:
@@ -710,6 +722,8 @@ function resolveRenewalHandler(parameters: {
710
722
  }) => Promise<subscription.RenewalResult>)
711
723
  | undefined {
712
724
  const {
725
+ feePayer,
726
+ feePayerPolicy,
713
727
  getClient,
714
728
  parameters: subscriptionParameters,
715
729
  store,
@@ -721,6 +735,8 @@ function resolveRenewalHandler(parameters: {
721
735
  return async ({ inFlightReference, periodIndex, subscription }) => {
722
736
  const reference = await submitSubscriptionPayment({
723
737
  accessKey: subscription.accessKey!,
738
+ feePayer,
739
+ feePayerPolicy,
724
740
  getClient,
725
741
  lookupKey: subscription.lookupKey,
726
742
  request: subscription,
@@ -744,6 +760,8 @@ function resolveRenewalHandler(parameters: {
744
760
 
745
761
  async function submitSubscriptionPayment(parameters: {
746
762
  accessKey: SubscriptionAccessKey
763
+ feePayer?: ReturnType<typeof Account.resolve>['feePayer'] | undefined
764
+ feePayerPolicy?: Partial<FeePayer.Policy> | undefined
747
765
  getClient: (parameters: { chainId?: number | undefined }) => MaybePromise<ViemClient>
748
766
  keyAuthorization?: `0x${string}` | undefined
749
767
  lookupKey: string
@@ -757,6 +775,8 @@ async function submitSubscriptionPayment(parameters: {
757
775
  }) {
758
776
  const {
759
777
  accessKey,
778
+ feePayer,
779
+ feePayerPolicy,
760
780
  getClient,
761
781
  keyAuthorization,
762
782
  lookupKey,
@@ -786,7 +806,7 @@ async function submitSubscriptionPayment(parameters: {
786
806
  challengeId: settlementReference,
787
807
  serverId: lookupKey,
788
808
  })
789
- const serializedTransaction = await signTransaction(client, {
809
+ const baseTransaction = {
790
810
  account,
791
811
  calls: [
792
812
  {
@@ -802,7 +822,39 @@ async function submitSubscriptionPayment(parameters: {
802
822
  ...(keyAuthorization
803
823
  ? { keyAuthorization: KeyAuthorization.deserialize(keyAuthorization) }
804
824
  : {}),
805
- } as never)
825
+ }
826
+ const serializedTransaction = await (async () => {
827
+ if (!feePayer) return await signTransaction(client, baseTransaction as never)
828
+ // For sponsored payments, prepare the tx via `prepareTransactionRequest`
829
+ // (without `feePayer: true`) so viem returns the chain's full
830
+ // proof-inclusive gas estimate. With `feePayer: true` viem sets a
831
+ // dummy sig + null feePayerSignature, dropping signature and key
832
+ // authorization verification costs — see chainConfig.js FIXME. We add
833
+ // a small buffer for fee-payer overhead, then flip `feePayer = true`
834
+ // and re-sign with the fee-payer-sponsored envelope.
835
+ const prepared = await prepareTransactionRequest(client, {
836
+ ...baseTransaction,
837
+ nonceKey: 'expiring',
838
+ } as never)
839
+ prepared.gas = (prepared.gas ?? 0n) + 5_000n
840
+ ;(prepared as Record<string, unknown>).feePayer = true
841
+ const userSerialized = await signTransaction(client, prepared as never)
842
+ const userTransaction = Transaction.deserialize(
843
+ userSerialized as Transaction.TransactionSerializedTempo,
844
+ )
845
+ const sponsored = FeePayer.prepareSponsoredTransaction({
846
+ account: feePayer,
847
+ chainId: chainId ?? client.chain!.id,
848
+ details: {
849
+ amount: String(request.amount),
850
+ currency: String(request.currency),
851
+ recipient: String(request.recipient),
852
+ },
853
+ ...(feePayerPolicy ? { policy: feePayerPolicy } : {}),
854
+ transaction: userTransaction as never,
855
+ })
856
+ return await signTransaction(client, sponsored as never)
857
+ })()
806
858
  const transaction = Transaction.deserialize(
807
859
  serializedTransaction as Transaction.TransactionSerializedTempo,
808
860
  )
@@ -810,6 +862,7 @@ async function submitSubscriptionPayment(parameters: {
810
862
  ...transaction,
811
863
  account: transaction.from,
812
864
  calls: transaction.calls,
865
+ feePayerSignature: undefined,
813
866
  } as never)
814
867
 
815
868
  if (!waitForConfirmation) {
@@ -950,6 +1003,12 @@ export declare namespace subscription {
950
1003
  * Keeps concurrent renewal safe while allowing recovery from abandoned attempts.
951
1004
  */
952
1005
  renewalTimeoutMs?: number | undefined
1006
+ /**
1007
+ * Override the fee-payer policy for sponsored subscription payments.
1008
+ * Useful when the access key + key authorization tx requires more gas
1009
+ * than the default policy allows.
1010
+ */
1011
+ feePayerPolicy?: Partial<FeePayer.Policy> | undefined
953
1012
  activate?:
954
1013
  | ((parameters: {
955
1014
  /** Custom activation must verify this access key matches the resolved subscription. */