mppx 0.8.14 → 0.8.15

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 (80) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cli/validate/payment.d.ts.map +1 -1
  3. package/dist/cli/validate/payment.js +18 -4
  4. package/dist/cli/validate/payment.js.map +1 -1
  5. package/dist/client/internal/Fetch.d.ts.map +1 -1
  6. package/dist/client/internal/Fetch.js +93 -10
  7. package/dist/client/internal/Fetch.js.map +1 -1
  8. package/dist/client/internal/MethodChallenge.d.ts +2 -0
  9. package/dist/client/internal/MethodChallenge.d.ts.map +1 -1
  10. package/dist/client/internal/MethodChallenge.js +4 -0
  11. package/dist/client/internal/MethodChallenge.js.map +1 -1
  12. package/dist/client/internal/MethodResponse.d.ts +15 -0
  13. package/dist/client/internal/MethodResponse.d.ts.map +1 -1
  14. package/dist/client/internal/MethodResponse.js +10 -0
  15. package/dist/client/internal/MethodResponse.js.map +1 -1
  16. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  17. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  18. package/dist/stripe/server/internal/html.gen.js +1 -1
  19. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  20. package/dist/tempo/server/Charge.d.ts +9 -1
  21. package/dist/tempo/server/Charge.d.ts.map +1 -1
  22. package/dist/tempo/server/Charge.js.map +1 -1
  23. package/dist/tempo/server/Relay.d.ts +13 -4
  24. package/dist/tempo/server/Relay.d.ts.map +1 -1
  25. package/dist/tempo/server/Relay.js +4 -2
  26. package/dist/tempo/server/Relay.js.map +1 -1
  27. package/dist/tempo/server/SponsorBudget.d.ts.map +1 -1
  28. package/dist/tempo/server/SponsorBudget.js +7 -1
  29. package/dist/tempo/server/SponsorBudget.js.map +1 -1
  30. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  31. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  32. package/dist/tempo/server/internal/html.gen.js +1 -1
  33. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  34. package/dist/tempo/session/client/Session.d.ts.map +1 -1
  35. package/dist/tempo/session/client/Session.js +128 -14
  36. package/dist/tempo/session/client/Session.js.map +1 -1
  37. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  38. package/dist/tempo/session/client/SessionManager.js +62 -20
  39. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  40. package/dist/tempo/session/client/Transports.d.ts +9 -1
  41. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  42. package/dist/tempo/session/client/Transports.js +36 -13
  43. package/dist/tempo/session/client/Transports.js.map +1 -1
  44. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  45. package/dist/tempo/session/server/Ws.js +3 -0
  46. package/dist/tempo/session/server/Ws.js.map +1 -1
  47. package/dist/tempo/subscription/KeyAuthorization.d.ts +1 -829
  48. package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
  49. package/dist/tempo/subscription/KeyAuthorization.js +14 -3
  50. package/dist/tempo/subscription/KeyAuthorization.js.map +1 -1
  51. package/dist/viem/Client.d.ts.map +1 -1
  52. package/dist/viem/Client.js +6 -1
  53. package/dist/viem/Client.js.map +1 -1
  54. package/package.json +2 -2
  55. package/src/cli/validate/payment.ts +18 -4
  56. package/src/cli/validate.test.ts +16 -0
  57. package/src/client/internal/Fetch.test.ts +77 -0
  58. package/src/client/internal/Fetch.ts +113 -11
  59. package/src/client/internal/MethodChallenge.ts +5 -0
  60. package/src/client/internal/MethodResponse.ts +28 -0
  61. package/src/stripe/server/internal/html.gen.ts +1 -1
  62. package/src/tempo/server/Charge.ts +9 -1
  63. package/src/tempo/server/Relay.test.ts +156 -2
  64. package/src/tempo/server/Relay.ts +13 -4
  65. package/src/tempo/server/SponsorBudget.test.ts +29 -0
  66. package/src/tempo/server/SponsorBudget.ts +7 -1
  67. package/src/tempo/server/internal/html.gen.ts +1 -1
  68. package/src/tempo/session/README.md +7 -0
  69. package/src/tempo/session/client/Session.test.ts +299 -33
  70. package/src/tempo/session/client/Session.ts +152 -16
  71. package/src/tempo/session/client/SessionManager.test.ts +216 -9
  72. package/src/tempo/session/client/SessionManager.ts +63 -17
  73. package/src/tempo/session/client/Transports.test.ts +22 -0
  74. package/src/tempo/session/client/Transports.ts +58 -22
  75. package/src/tempo/session/server/Session.test.ts +66 -4
  76. package/src/tempo/session/server/Ws.ts +3 -0
  77. package/src/tempo/subscription/KeyAuthorization.test.ts +70 -1
  78. package/src/tempo/subscription/KeyAuthorization.ts +14 -3
  79. package/src/viem/Client.test.ts +18 -1
  80. package/src/viem/Client.ts +7 -1
@@ -2421,12 +2421,15 @@ describe('precompile server session unit guardrails', () => {
2421
2421
  })
2422
2422
 
2423
2423
  describe('WebSocket parity', () => {
2424
- async function createManagedWsHarness(options: { maxDeposit?: bigint } = {}) {
2424
+ async function createManagedWsHarness(
2425
+ options: { challengeTtlMs?: number; maxDeposit?: bigint } = {},
2426
+ ) {
2425
2427
  const rawStore = Store.memory()
2428
+ let challengeRequests = 0
2426
2429
  let currentPayload: SessionCredentialPayload | undefined
2427
2430
  let voucherPosts = 0
2428
2431
  const maxDeposit = options.maxDeposit ?? 3n
2429
- const routeHandler = Mppx_server.create({
2432
+ const payment = Mppx_server.create({
2430
2433
  methods: [
2431
2434
  tempo_server.session({
2432
2435
  amount: '1',
@@ -2463,7 +2466,7 @@ describe('precompile server session unit guardrails', () => {
2463
2466
  ],
2464
2467
  realm: 'api.example.com',
2465
2468
  secretKey: 'test-secret-key-test-secret-key-32',
2466
- }).session({ amount: '1', decimals: 0, suggestedDeposit: maxDeposit.toString() })
2469
+ })
2467
2470
 
2468
2471
  const route = async (request: Request) => {
2469
2472
  currentPayload = undefined
@@ -2472,7 +2475,15 @@ describe('precompile server session unit guardrails', () => {
2472
2475
  currentPayload = Credential.fromRequest<SessionCredentialPayload>(request).payload
2473
2476
  if (currentPayload.action === 'voucher') voucherPosts++
2474
2477
  } catch {}
2475
- }
2478
+ } else challengeRequests++
2479
+ const routeHandler = payment.session({
2480
+ amount: '1',
2481
+ decimals: 0,
2482
+ suggestedDeposit: maxDeposit.toString(),
2483
+ ...(options.challengeTtlMs === undefined
2484
+ ? {}
2485
+ : { expires: new Date(Date.now() + options.challengeTtlMs) }),
2486
+ })
2476
2487
  return routeHandler(request)
2477
2488
  }
2478
2489
 
@@ -2510,6 +2521,9 @@ describe('precompile server session unit guardrails', () => {
2510
2521
  get port() {
2511
2522
  return port
2512
2523
  },
2524
+ get challengeRequests() {
2525
+ return challengeRequests
2526
+ },
2513
2527
  get voucherPosts() {
2514
2528
  return voucherPosts
2515
2529
  },
@@ -2520,6 +2534,54 @@ describe('precompile server session unit guardrails', () => {
2520
2534
  }
2521
2535
  }
2522
2536
 
2537
+ test('refreshes an expired challenge before sending a later websocket voucher', async () => {
2538
+ const harness = await createManagedWsHarness({ challengeTtlMs: 2_000, maxDeposit: 2n })
2539
+ harness.wsServer.on('connection', (socket: import('ws').WebSocket) => {
2540
+ void TempoWs.serve({
2541
+ socket,
2542
+ store: harness.rawStore,
2543
+ url: `${harness.server.url}/ws`,
2544
+ route: harness.route,
2545
+ generate: async function* (stream: TempoWs.SessionController) {
2546
+ await stream.charge()
2547
+ yield 'chunk-1'
2548
+ await new Promise((resolve) => setTimeout(resolve, 2_200))
2549
+ await stream.charge()
2550
+ yield 'chunk-2'
2551
+ },
2552
+ })
2553
+ })
2554
+
2555
+ try {
2556
+ const manager = precompileSessionManager({
2557
+ account: payer,
2558
+ client: createSigningClient(),
2559
+ decimals: 0,
2560
+ fetch: globalThis.fetch,
2561
+ maxDeposit: '2',
2562
+ webSocket: WebSocket as never,
2563
+ })
2564
+ const ws = await manager.ws(`ws://localhost:${harness.port}/ws`)
2565
+ const chunks: string[] = []
2566
+
2567
+ await new Promise<void>((resolve, reject) => {
2568
+ ws.addEventListener('message', (event) => {
2569
+ if (typeof event.data === 'string') chunks.push(event.data)
2570
+ })
2571
+ ws.addEventListener('close', () => resolve(), { once: true })
2572
+ ws.addEventListener('error', () => reject(new Error('websocket stream failed')), {
2573
+ once: true,
2574
+ })
2575
+ })
2576
+
2577
+ expect(chunks).toEqual(['chunk-1', 'chunk-2'])
2578
+ expect(harness.challengeRequests).toBeGreaterThan(1)
2579
+ expect(harness.voucherPosts).toBe(1)
2580
+ } finally {
2581
+ harness.close()
2582
+ }
2583
+ })
2584
+
2523
2585
  test('open -> stream -> need-voucher -> resume -> close', async () => {
2524
2586
  const harness = await createManagedWsHarness({ maxDeposit: 3n })
2525
2587
  harness.wsServer.on('connection', (socket: import('ws').WebSocket) => {
@@ -220,6 +220,9 @@ export async function serve(options: serve.Options): Promise<void> {
220
220
  }
221
221
 
222
222
  const { receipt } = authorizationResult
223
+ if (payload.action === 'voucher' && runtime.streamContext) {
224
+ runtime.streamContext.challengeId = credential.challenge.id
225
+ }
223
226
  await send(socket, formatReceiptMessage(receipt))
224
227
 
225
228
  if (payload.action === 'close') {
@@ -1,4 +1,5 @@
1
- import { KeyAuthorization } from 'ox/tempo'
1
+ import { Rlp } from 'ox'
2
+ import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
2
3
  import { privateKeyToAccount } from 'viem/accounts'
3
4
  import { describe, expect, test } from 'vp/test'
4
5
 
@@ -69,6 +70,25 @@ async function createPayload(request = parseRequest()) {
69
70
  } as const
70
71
  }
71
72
 
73
+ function serializeCompositeSignature(
74
+ type: 'keychain' | 'multisig',
75
+ inner: SignatureEnvelope.Primitive,
76
+ ) {
77
+ return SignatureEnvelope.serialize(
78
+ type === 'keychain'
79
+ ? {
80
+ inner,
81
+ type,
82
+ userAddress: rootAccount.address,
83
+ }
84
+ : {
85
+ account: rootAccount.address,
86
+ signatures: [inner],
87
+ type,
88
+ },
89
+ )
90
+ }
91
+
72
92
  describe('tempo subscription key authorization', () => {
73
93
  test('signs and verifies a scoped key authorization', async () => {
74
94
  const request = parseRequest()
@@ -87,6 +107,55 @@ describe('tempo subscription key authorization', () => {
87
107
  )
88
108
  })
89
109
 
110
+ test.each(['keychain', 'multisig'] as const)(
111
+ 'rejects %s authorization signatures',
112
+ async (type) => {
113
+ const request = parseRequest()
114
+
115
+ await expect(
116
+ signSubscriptionKeyAuthorization({
117
+ accessKey,
118
+ account: {
119
+ async sign({ hash }) {
120
+ const inner = SignatureEnvelope.from(await rootAccount.sign({ hash }))
121
+ if (inner.type === 'keychain' || inner.type === 'multisig') {
122
+ throw new Error('expected primitive signature')
123
+ }
124
+ return serializeCompositeSignature(type, inner)
125
+ },
126
+ },
127
+ chainId: 4217,
128
+ request,
129
+ }),
130
+ ).rejects.toThrow('keyAuthorization must use a primitive signature')
131
+ },
132
+ )
133
+
134
+ test.each(['keychain', 'multisig'] as const)(
135
+ 'rejects serialized %s authorization signatures',
136
+ async (type) => {
137
+ const request = parseRequest()
138
+ const payload = await createPayload(request)
139
+ const authorization = KeyAuthorization.deserialize(payload.signature)
140
+ const [authorizationTuple] = KeyAuthorization.toTuple(authorization)
141
+
142
+ expect(() =>
143
+ verifySubscriptionKeyAuthorization({
144
+ accessKey,
145
+ chainId: 4217,
146
+ payload: {
147
+ ...payload,
148
+ signature: Rlp.fromHex([
149
+ authorizationTuple,
150
+ serializeCompositeSignature(type, authorization.signature!),
151
+ ]),
152
+ },
153
+ request,
154
+ }),
155
+ ).toThrow('keyAuthorization must use a primitive signature')
156
+ },
157
+ )
158
+
90
159
  test('builds wallet allowed calls from the subscription request', () => {
91
160
  const request = parseRequest()
92
161
 
@@ -168,8 +168,14 @@ export async function signSubscriptionKeyAuthorization(parameters: {
168
168
  const signature = await account.sign({
169
169
  hash: KeyAuthorization.getSignPayload(authorization),
170
170
  })
171
+ const signatureEnvelope = SignatureEnvelope.from(signature)
172
+ if (signatureEnvelope.type === 'keychain' || signatureEnvelope.type === 'multisig') {
173
+ throw new VerificationFailedError({
174
+ reason: 'keyAuthorization must use a primitive signature',
175
+ })
176
+ }
171
177
  return KeyAuthorization.from(authorization, {
172
- signature: SignatureEnvelope.from(signature),
178
+ signature,
173
179
  })
174
180
  }
175
181
 
@@ -238,14 +244,19 @@ function createUnsignedAuthorization(parameters: {
238
244
  function deserializeAuthorization(signature: `0x${string}`) {
239
245
  try {
240
246
  return KeyAuthorization.deserialize(signature)
241
- } catch {
247
+ } catch (error) {
248
+ if (error instanceof KeyAuthorization.InvalidSignatureTypeError) {
249
+ throw new VerificationFailedError({
250
+ reason: 'keyAuthorization must use a primitive signature',
251
+ })
252
+ }
242
253
  throw new VerificationFailedError({ reason: 'invalid keyAuthorization payload' })
243
254
  }
244
255
  }
245
256
 
246
257
  function getPrimitiveSignature(authorization: Authorization) {
247
258
  const signature = authorization.signature
248
- if (!signature || signature.type === 'keychain') {
259
+ if (!signature) {
249
260
  throw new VerificationFailedError({
250
261
  reason: 'keyAuthorization must use a primitive signature',
251
262
  })
@@ -2,7 +2,7 @@ import { createClient, custom, defineChain, type Hex } from 'viem'
2
2
  import { privateKeyToAccount } from 'viem/accounts'
3
3
  import { signTransaction } from 'viem/actions'
4
4
  import { Account as TempoAccount, Transaction } from 'viem/tempo'
5
- import { tempoLocalnet } from 'viem/tempo/chains'
5
+ import { tempo as tempoMainnetChain, tempoLocalnet, tempoModerato } from 'viem/tempo/chains'
6
6
  import { afterEach, describe, expect, test, vi } from 'vp/test'
7
7
 
8
8
  import * as Client from './Client.js'
@@ -48,6 +48,23 @@ describe('getResolver', () => {
48
48
  expect(client.chain?.name).toBe('test')
49
49
  })
50
50
 
51
+ test('behavior: resolves the correct named Tempo chain even when the default chain differs', async () => {
52
+ const getClient = Client.getResolver({
53
+ // Default chain is mainnet, but the resolved chain ID below is testnet.
54
+ chain: tempoMainnetChain as never,
55
+ rpcUrl: {
56
+ [tempoMainnetChain.id]: 'https://rpc.example.com',
57
+ [tempoModerato.id]: 'https://rpc2.example.com',
58
+ },
59
+ })
60
+
61
+ const client = await getClient({ chainId: tempoModerato.id })
62
+
63
+ expect(client.chain?.id).toBe(tempoModerato.id)
64
+ expect(client.chain?.name).toBe(tempoModerato.name)
65
+ expect(client.chain?.name).not.toBe(tempoMainnetChain.name)
66
+ })
67
+
51
68
  test('error: throws when no rpcUrl provided', () => {
52
69
  const getClient = Client.getResolver({})
53
70
 
@@ -1,8 +1,14 @@
1
1
  import { type Chain, type Client, createClient, createTransport, custom, http } from 'viem'
2
2
  import { withFeePayer } from 'viem/tempo'
3
+ import { tempo as tempoMainnetChain, tempoModerato } from 'viem/tempo/chains'
3
4
 
4
5
  import type { MaybePromise } from '../internal/types.js'
5
6
 
7
+ const knownTempoChains: Record<number, Chain> = {
8
+ [tempoMainnetChain.id]: tempoMainnetChain,
9
+ [tempoModerato.id]: tempoModerato,
10
+ }
11
+
6
12
  export function getResolver(
7
13
  parameters: getResolver.Parameters & {
8
14
  /** Default chain to use if not provided. */
@@ -73,7 +79,7 @@ export function getResolver(
73
79
  if (!url) throw new Error(`No \`rpcUrl\` configured for \`chainId\` (${resolvedChainId}).`)
74
80
  const transport = feePayerUrl ? withFeePayer(http(url), http(feePayerUrl)) : http(url)
75
81
  return createClient({
76
- chain: { ...chain, id: resolvedChainId } as never,
82
+ chain: (knownTempoChains[resolvedChainId] ?? { ...chain, id: resolvedChainId }) as never,
77
83
  transport,
78
84
  })
79
85
  }