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.
- package/CHANGELOG.md +21 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +18 -4
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +93 -10
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +2 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -1
- package/dist/client/internal/MethodChallenge.js +4 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -1
- package/dist/client/internal/MethodResponse.d.ts +15 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -1
- package/dist/client/internal/MethodResponse.js +10 -0
- package/dist/client/internal/MethodResponse.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +9 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Relay.d.ts +13 -4
- package/dist/tempo/server/Relay.d.ts.map +1 -1
- package/dist/tempo/server/Relay.js +4 -2
- package/dist/tempo/server/Relay.js.map +1 -1
- package/dist/tempo/server/SponsorBudget.d.ts.map +1 -1
- package/dist/tempo/server/SponsorBudget.js +7 -1
- package/dist/tempo/server/SponsorBudget.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +128 -14
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +62 -20
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +9 -1
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +36 -13
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js +3 -0
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +1 -829
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.js +14 -3
- package/dist/tempo/subscription/KeyAuthorization.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +6 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/validate/payment.ts +18 -4
- package/src/cli/validate.test.ts +16 -0
- package/src/client/internal/Fetch.test.ts +77 -0
- package/src/client/internal/Fetch.ts +113 -11
- package/src/client/internal/MethodChallenge.ts +5 -0
- package/src/client/internal/MethodResponse.ts +28 -0
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/Charge.ts +9 -1
- package/src/tempo/server/Relay.test.ts +156 -2
- package/src/tempo/server/Relay.ts +13 -4
- package/src/tempo/server/SponsorBudget.test.ts +29 -0
- package/src/tempo/server/SponsorBudget.ts +7 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +7 -0
- package/src/tempo/session/client/Session.test.ts +299 -33
- package/src/tempo/session/client/Session.ts +152 -16
- package/src/tempo/session/client/SessionManager.test.ts +216 -9
- package/src/tempo/session/client/SessionManager.ts +63 -17
- package/src/tempo/session/client/Transports.test.ts +22 -0
- package/src/tempo/session/client/Transports.ts +58 -22
- package/src/tempo/session/server/Session.test.ts +66 -4
- package/src/tempo/session/server/Ws.ts +3 -0
- package/src/tempo/subscription/KeyAuthorization.test.ts +70 -1
- package/src/tempo/subscription/KeyAuthorization.ts +14 -3
- package/src/viem/Client.test.ts +18 -1
- 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(
|
|
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
|
|
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
|
-
})
|
|
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 {
|
|
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
|
|
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
|
|
259
|
+
if (!signature) {
|
|
249
260
|
throw new VerificationFailedError({
|
|
250
261
|
reason: 'keyAuthorization must use a primitive signature',
|
|
251
262
|
})
|
package/src/viem/Client.test.ts
CHANGED
|
@@ -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
|
|
package/src/viem/Client.ts
CHANGED
|
@@ -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
|
}
|