mppx 0.4.8 → 0.4.10
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 +26 -3
- package/README.md +13 -13
- package/dist/BodyDigest.d.ts.map +1 -1
- package/dist/BodyDigest.js.map +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js.map +1 -1
- package/dist/Credential.d.ts.map +1 -1
- package/dist/Credential.js.map +1 -1
- package/dist/Errors.js +64 -67
- package/dist/Errors.js.map +1 -1
- package/dist/PaymentRequest.d.ts.map +1 -1
- package/dist/PaymentRequest.js.map +1 -1
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js.map +1 -1
- package/dist/Store.d.ts +9 -0
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +17 -0
- package/dist/Store.js.map +1 -1
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +40 -5
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +157 -1
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.d.ts.map +1 -1
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +2 -1
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +1 -1
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/discovery/Discovery.d.ts +146 -0
- package/dist/discovery/Discovery.d.ts.map +1 -0
- package/dist/discovery/Discovery.js +60 -0
- package/dist/discovery/Discovery.js.map +1 -0
- package/dist/discovery/OpenApi.d.ts +61 -0
- package/dist/discovery/OpenApi.d.ts.map +1 -0
- package/dist/discovery/OpenApi.js +139 -0
- package/dist/discovery/OpenApi.js.map +1 -0
- package/dist/discovery/Validate.d.ts +10 -0
- package/dist/discovery/Validate.d.ts.map +1 -0
- package/dist/discovery/Validate.js +63 -0
- package/dist/discovery/Validate.js.map +1 -0
- package/dist/discovery/index.d.ts +4 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +4 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +1 -1
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/middlewares/elysia.d.ts +52 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +17 -0
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.d.ts +13 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +23 -2
- package/dist/middlewares/express.js.map +1 -1
- package/dist/middlewares/hono.d.ts +19 -1
- package/dist/middlewares/hono.d.ts.map +1 -1
- package/dist/middlewares/hono.js +51 -0
- package/dist/middlewares/hono.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts +4 -2
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +10 -3
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/middlewares/nextjs.d.ts +11 -0
- package/dist/middlewares/nextjs.d.ts.map +1 -1
- package/dist/middlewares/nextjs.js +15 -0
- package/dist/middlewares/nextjs.js.map +1 -1
- package/dist/proxy/Proxy.d.ts +6 -0
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +56 -80
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/Service.d.ts +16 -23
- package/dist/proxy/Service.d.ts.map +1 -1
- package/dist/proxy/Service.js +20 -84
- package/dist/proxy/Service.js.map +1 -1
- package/dist/proxy/internal/Route.js +1 -1
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/proxy/services/anthropic.d.ts.map +1 -1
- package/dist/proxy/services/anthropic.js +5 -0
- package/dist/proxy/services/anthropic.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +6 -3
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/proxy/services/stripe.d.ts.map +1 -1
- package/dist/proxy/services/stripe.js +6 -3
- package/dist/proxy/services/stripe.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +35 -17
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Request.d.ts.map +1 -1
- package/dist/server/Request.js.map +1 -1
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +1 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.js +1 -1
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +1 -1
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +18 -5
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +8 -0
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +1 -1
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Receipt.d.ts.map +1 -1
- package/dist/tempo/session/Receipt.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +6 -1
- package/src/BodyDigest.test.ts +1 -1
- package/src/BodyDigest.ts +1 -0
- package/src/Challenge.fuzz.test.ts +121 -0
- package/src/Challenge.test-d.ts +2 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Challenge.ts +1 -0
- package/src/Credential.fuzz.test.ts +62 -0
- package/src/Credential.test.ts +1 -1
- package/src/Credential.ts +1 -0
- package/src/Errors.test.ts +28 -40
- package/src/Expires.test.ts +2 -1
- package/src/Method.test.ts +1 -1
- package/src/PaymentRequest.test.ts +1 -1
- package/src/PaymentRequest.ts +1 -0
- package/src/Receipt.test.ts +1 -1
- package/src/Receipt.ts +1 -0
- package/src/Store.test-d.ts +2 -1
- package/src/Store.test.ts +57 -7
- package/src/Store.ts +25 -0
- package/src/cli/account.ts +65 -30
- package/src/cli/cli.test.ts +215 -2
- package/src/cli/cli.ts +166 -1
- package/src/cli/config.test.ts +1 -0
- package/src/cli/internal.ts +1 -0
- package/src/cli/plugins/stripe.ts +1 -0
- package/src/cli/plugins/tempo.ts +4 -1
- package/src/cli/utils.ts +1 -0
- package/src/client/Mppx.test-d.ts +2 -1
- package/src/client/Mppx.test.ts +1 -1
- package/src/client/Transport.test.ts +1 -1
- package/src/client/internal/Fetch.browser.test.ts +2 -1
- package/src/client/internal/Fetch.test-d.ts +2 -1
- package/src/client/internal/Fetch.test.ts +3 -1
- package/src/client/internal/Fetch.ts +1 -1
- package/src/discovery/Discovery.test.ts +152 -0
- package/src/discovery/Discovery.ts +72 -0
- package/src/discovery/OpenApi.test.ts +425 -0
- package/src/discovery/OpenApi.ts +224 -0
- package/src/discovery/Validate.test.ts +188 -0
- package/src/discovery/Validate.ts +76 -0
- package/src/discovery/index.ts +3 -0
- package/src/internal/constantTimeEqual.test.ts +2 -1
- package/src/internal/types.ts +1 -3
- package/src/mcp-sdk/client/McpClient.test-d.ts +2 -1
- package/src/mcp-sdk/client/McpClient.test.ts +2 -1
- package/src/mcp-sdk/client/McpClient.ts +2 -0
- package/src/mcp-sdk/server/Transport.test.ts +2 -1
- package/src/mcp-sdk/server/Transport.ts +1 -0
- package/src/middlewares/elysia.test.ts +28 -2
- package/src/middlewares/elysia.ts +36 -1
- package/src/middlewares/express.test.ts +95 -7
- package/src/middlewares/express.ts +40 -2
- package/src/middlewares/hono.test.ts +28 -6
- package/src/middlewares/hono.ts +74 -1
- package/src/middlewares/internal/mppx.test.ts +2 -1
- package/src/middlewares/internal/mppx.ts +14 -6
- package/src/middlewares/nextjs.test.ts +32 -6
- package/src/middlewares/nextjs.ts +28 -0
- package/src/proxy/Proxy.test.ts +55 -270
- package/src/proxy/Proxy.ts +73 -93
- package/src/proxy/Service.test.ts +24 -1
- package/src/proxy/Service.ts +48 -88
- package/src/proxy/internal/Headers.test.ts +2 -1
- package/src/proxy/internal/Route.test.ts +9 -1
- package/src/proxy/internal/Route.ts +1 -1
- package/src/proxy/services/anthropic.test.ts +132 -0
- package/src/proxy/services/anthropic.ts +5 -0
- package/src/proxy/services/openai.test.ts +2 -1
- package/src/proxy/services/openai.ts +6 -4
- package/src/proxy/services/stripe.test.ts +132 -0
- package/src/proxy/services/stripe.ts +6 -4
- package/src/server/Mppx.test-d.ts +1 -1
- package/src/server/Mppx.test.ts +194 -1
- package/src/server/Mppx.ts +38 -19
- package/src/server/NodeListener.test.ts +1 -1
- package/src/server/Request.test.ts +2 -1
- package/src/server/Request.ts +1 -0
- package/src/server/Response.test.ts +2 -1
- package/src/server/Transport.test.ts +2 -1
- package/src/stripe/Charge.integration.test.ts +1 -1
- package/src/stripe/Methods.test.ts +1 -1
- package/src/stripe/Methods.ts +1 -0
- package/src/stripe/client/Charge.test.ts +2 -1
- package/src/stripe/server/Charge.test.ts +2 -1
- package/src/tempo/Attribution.test.ts +2 -1
- package/src/tempo/Methods.test.ts +1 -1
- package/src/tempo/Methods.ts +1 -0
- package/src/tempo/client/ChannelOps.test.ts +7 -3
- package/src/tempo/client/ChannelOps.ts +1 -0
- package/src/tempo/client/Charge.ts +1 -0
- package/src/tempo/client/Session.test.ts +6 -2
- package/src/tempo/client/Session.ts +1 -0
- package/src/tempo/client/SessionManager.test.ts +29 -1
- package/src/tempo/client/SessionManager.ts +2 -1
- package/src/tempo/internal/auto-swap.test.ts +2 -1
- package/src/tempo/internal/auto-swap.ts +1 -0
- package/src/tempo/internal/defaults.test.ts +2 -1
- package/src/tempo/internal/fee-payer.test.ts +2 -1
- package/src/tempo/internal/fee-payer.ts +1 -0
- package/src/tempo/server/Charge.test.ts +2 -1
- package/src/tempo/server/Charge.ts +1 -0
- package/src/tempo/server/Session.test.ts +88 -37
- package/src/tempo/server/Session.ts +26 -8
- package/src/tempo/server/Sse.test.ts +2 -1
- package/src/tempo/server/internal/transport.test.ts +25 -1
- package/src/tempo/server/internal/transport.ts +11 -0
- package/src/tempo/session/Chain.test.ts +6 -2
- package/src/tempo/session/Chain.ts +2 -1
- package/src/tempo/session/Channel.test.ts +2 -1
- package/src/tempo/session/ChannelStore.test.ts +2 -1
- package/src/tempo/session/ChannelStore.ts +1 -0
- package/src/tempo/session/Receipt.test.ts +2 -1
- package/src/tempo/session/Receipt.ts +1 -0
- package/src/tempo/session/Sse.fuzz.test.ts +138 -0
- package/src/tempo/session/Sse.test.ts +2 -1
- package/src/tempo/session/Sse.ts +1 -0
- package/src/tempo/session/Voucher.test.ts +2 -1
- package/src/tempo/session/Voucher.ts +1 -0
- package/src/viem/Account.test.ts +2 -1
- package/src/viem/Client.test.ts +2 -1
- package/src/viem/Client.ts +1 -0
- package/src/zod.test.ts +147 -0
package/src/server/Mppx.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Challenge, Credential, Method, z } from 'mppx'
|
|
2
2
|
import { Mppx, Transport, tempo } from 'mppx/server'
|
|
3
|
-
import { describe, expect, test } from '
|
|
3
|
+
import { describe, expect, test } from 'vp/test'
|
|
4
4
|
import * as Http from '~test/Http.js'
|
|
5
5
|
import { accounts, asset, client } from '~test/tempo/viem.js'
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ const secretKey = 'test-secret-key'
|
|
|
9
9
|
|
|
10
10
|
const method = tempo({
|
|
11
11
|
getClient: () => client,
|
|
12
|
+
account: accounts[0],
|
|
12
13
|
})
|
|
13
14
|
|
|
14
15
|
describe('create', () => {
|
|
@@ -180,6 +181,198 @@ describe('request handler', () => {
|
|
|
180
181
|
expect(body.detail).toContain('does not match')
|
|
181
182
|
})
|
|
182
183
|
|
|
184
|
+
test('topUp credential bypasses cross-route amount validation', async () => {
|
|
185
|
+
// Use a session method whose schema defines action: 'topUp'
|
|
186
|
+
const sessionMethod = Method.from({
|
|
187
|
+
name: 'mock',
|
|
188
|
+
intent: 'session',
|
|
189
|
+
schema: {
|
|
190
|
+
credential: {
|
|
191
|
+
payload: z.discriminatedUnion('action', [
|
|
192
|
+
z.object({ action: z.literal('open'), token: z.string() }),
|
|
193
|
+
z.object({ action: z.literal('topUp'), token: z.string() }),
|
|
194
|
+
]),
|
|
195
|
+
},
|
|
196
|
+
request: z.object({
|
|
197
|
+
amount: z.string(),
|
|
198
|
+
currency: z.string(),
|
|
199
|
+
recipient: z.string(),
|
|
200
|
+
}),
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
const sessionServerMethod = Method.toServer(sessionMethod, {
|
|
204
|
+
async verify() {
|
|
205
|
+
return {
|
|
206
|
+
status: 'settled',
|
|
207
|
+
method: 'mock',
|
|
208
|
+
timestamp: new Date().toISOString(),
|
|
209
|
+
reference: 'ref',
|
|
210
|
+
} as any
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
const handler = Mppx.create({ methods: [sessionServerMethod], realm, secretKey })
|
|
214
|
+
|
|
215
|
+
// Get a challenge from the "cheap" route (simulates HEAD-obtained challenge)
|
|
216
|
+
const cheapHandle = handler['mock/session']({
|
|
217
|
+
amount: '1',
|
|
218
|
+
currency: asset,
|
|
219
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
220
|
+
recipient: accounts[0].address,
|
|
221
|
+
})
|
|
222
|
+
const cheapResult = await cheapHandle(new Request('https://example.com/cheap'))
|
|
223
|
+
expect(cheapResult.status).toBe(402)
|
|
224
|
+
if (cheapResult.status !== 402) throw new Error()
|
|
225
|
+
|
|
226
|
+
const cheapChallenge = Challenge.fromResponse(cheapResult.challenge)
|
|
227
|
+
|
|
228
|
+
// Build a topUp credential from the cheap challenge (echoed from HEAD)
|
|
229
|
+
const credential = Credential.from({
|
|
230
|
+
challenge: cheapChallenge,
|
|
231
|
+
payload: { action: 'topUp', token: 'valid' },
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Present it at the "expensive" route — topUp should bypass amount check
|
|
235
|
+
const expensiveHandle = handler['mock/session']({
|
|
236
|
+
amount: '1000000',
|
|
237
|
+
currency: asset,
|
|
238
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
239
|
+
recipient: accounts[0].address,
|
|
240
|
+
})
|
|
241
|
+
const result = await expensiveHandle(
|
|
242
|
+
new Request('https://example.com/expensive', {
|
|
243
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
244
|
+
}),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
// Should NOT get 402 for amount mismatch — topUp bypasses the check.
|
|
248
|
+
// It will fail at a later stage (payload validation), but not with
|
|
249
|
+
// "does not match this route's requirements".
|
|
250
|
+
if (result.status === 402) {
|
|
251
|
+
const body = (await result.challenge.json()) as { detail?: string }
|
|
252
|
+
expect(body.detail).not.toContain('does not match')
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('voucher credential bypasses cross-route amount validation', async () => {
|
|
257
|
+
const sessionMethod = Method.from({
|
|
258
|
+
name: 'mock',
|
|
259
|
+
intent: 'session',
|
|
260
|
+
schema: {
|
|
261
|
+
credential: {
|
|
262
|
+
payload: z.discriminatedUnion('action', [
|
|
263
|
+
z.object({ action: z.literal('open'), token: z.string() }),
|
|
264
|
+
z.object({
|
|
265
|
+
action: z.literal('voucher'),
|
|
266
|
+
cumulativeAmount: z.string(),
|
|
267
|
+
signature: z.string(),
|
|
268
|
+
}),
|
|
269
|
+
]),
|
|
270
|
+
},
|
|
271
|
+
request: z.object({
|
|
272
|
+
amount: z.string(),
|
|
273
|
+
currency: z.string(),
|
|
274
|
+
recipient: z.string(),
|
|
275
|
+
}),
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
const sessionServerMethod = Method.toServer(sessionMethod, {
|
|
279
|
+
async verify() {
|
|
280
|
+
return {
|
|
281
|
+
status: 'settled',
|
|
282
|
+
method: 'mock',
|
|
283
|
+
timestamp: new Date().toISOString(),
|
|
284
|
+
reference: 'ref',
|
|
285
|
+
} as any
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
const handler = Mppx.create({ methods: [sessionServerMethod], realm, secretKey })
|
|
289
|
+
|
|
290
|
+
// Get a challenge from the "cheap" route (simulates initial SSE request)
|
|
291
|
+
const cheapHandle = handler['mock/session']({
|
|
292
|
+
amount: '1',
|
|
293
|
+
currency: asset,
|
|
294
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
295
|
+
recipient: accounts[0].address,
|
|
296
|
+
})
|
|
297
|
+
const cheapResult = await cheapHandle(new Request('https://example.com/chat'))
|
|
298
|
+
expect(cheapResult.status).toBe(402)
|
|
299
|
+
if (cheapResult.status !== 402) throw new Error()
|
|
300
|
+
|
|
301
|
+
const cheapChallenge = Challenge.fromResponse(cheapResult.challenge)
|
|
302
|
+
|
|
303
|
+
// Build a voucher credential echoing the original challenge — mid-stream
|
|
304
|
+
// the server may re-price (dynamic pricing), so the route's amount differs
|
|
305
|
+
const credential = Credential.from({
|
|
306
|
+
challenge: cheapChallenge,
|
|
307
|
+
payload: { action: 'voucher', cumulativeAmount: '500', signature: '0xabc' },
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// Present it at the same route but with a higher price — voucher should
|
|
311
|
+
// bypass the cross-route amount check just like topUp does
|
|
312
|
+
const expensiveHandle = handler['mock/session']({
|
|
313
|
+
amount: '1000000',
|
|
314
|
+
currency: asset,
|
|
315
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
316
|
+
recipient: accounts[0].address,
|
|
317
|
+
})
|
|
318
|
+
const result = await expensiveHandle(
|
|
319
|
+
new Request('https://example.com/chat', {
|
|
320
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
321
|
+
}),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// Should NOT get 402 for amount mismatch — voucher bypasses the check.
|
|
325
|
+
if (result.status === 402) {
|
|
326
|
+
const body = (await result.challenge.json()) as { detail?: string }
|
|
327
|
+
expect(body.detail).not.toContain('does not match')
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test('rejects charge credential with injected action: topUp (cross-route bypass attempt)', async () => {
|
|
332
|
+
const handler = Mppx.create({ methods: [method], realm, secretKey })
|
|
333
|
+
|
|
334
|
+
// Get a challenge from the "cheap" route
|
|
335
|
+
const cheapHandle = handler.charge({
|
|
336
|
+
amount: '1',
|
|
337
|
+
currency: asset,
|
|
338
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
339
|
+
recipient: accounts[0].address,
|
|
340
|
+
})
|
|
341
|
+
const cheapResult = await cheapHandle(new Request('https://example.com/cheap'))
|
|
342
|
+
expect(cheapResult.status).toBe(402)
|
|
343
|
+
if (cheapResult.status !== 402) throw new Error()
|
|
344
|
+
|
|
345
|
+
const cheapChallenge = Challenge.fromResponse(cheapResult.challenge)
|
|
346
|
+
|
|
347
|
+
// Malicious client injects action: 'topUp' into a regular charge credential
|
|
348
|
+
// to try to bypass the cross-route amount check
|
|
349
|
+
const credential = Credential.from({
|
|
350
|
+
challenge: cheapChallenge,
|
|
351
|
+
payload: { action: 'topUp', signature: '0x123', type: 'transaction' },
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// Present it at the "expensive" route — should still be rejected
|
|
355
|
+
const expensiveHandle = handler.charge({
|
|
356
|
+
amount: '1000000',
|
|
357
|
+
currency: asset,
|
|
358
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
359
|
+
recipient: accounts[0].address,
|
|
360
|
+
})
|
|
361
|
+
const result = await expensiveHandle(
|
|
362
|
+
new Request('https://example.com/expensive', {
|
|
363
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
364
|
+
}),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
// Injecting action: 'topUp' on a charge credential must not bypass
|
|
368
|
+
// the cross-route amount check. The credential should be rejected
|
|
369
|
+
// with "does not match" just like a normal charge credential would be.
|
|
370
|
+
expect(result.status).toBe(402)
|
|
371
|
+
if (result.status !== 402) throw new Error()
|
|
372
|
+
const body = (await result.challenge.json()) as { detail: string }
|
|
373
|
+
expect(body.detail).toContain('does not match')
|
|
374
|
+
})
|
|
375
|
+
|
|
183
376
|
test('returns 402 when credential challenge is expired', async () => {
|
|
184
377
|
const pastExpires = new Date(Date.now() - 60_000).toISOString()
|
|
185
378
|
|
package/src/server/Mppx.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
+
|
|
2
3
|
import * as Challenge from '../Challenge.js'
|
|
3
4
|
import * as Credential from '../Credential.js'
|
|
4
5
|
import * as Errors from '../Errors.js'
|
|
@@ -335,6 +336,13 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
335
336
|
// Note: we compare specific payment parameters rather than the full
|
|
336
337
|
// request because the `request` hook may produce credential-dependent
|
|
337
338
|
// output (e.g. `feePayer` differs between 402 and credential calls).
|
|
339
|
+
//
|
|
340
|
+
// Skip this check for topUp and voucher actions: the route's
|
|
341
|
+
// `request` hook may produce a different amount because these
|
|
342
|
+
// requests carry no application body (e.g. no model field for
|
|
343
|
+
// dynamic pricing). The credential echoes a challenge obtained
|
|
344
|
+
// from the original request which had the correct amount; the
|
|
345
|
+
// on-chain voucher signature is the real validation.
|
|
338
346
|
{
|
|
339
347
|
for (const field of ['method', 'intent', 'realm'] as const) {
|
|
340
348
|
if (credential.challenge[field] !== challenge[field]) {
|
|
@@ -350,25 +358,36 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
350
358
|
}
|
|
351
359
|
}
|
|
352
360
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
361
|
+
// Use safeParse (not raw payload) so only methods whose schema
|
|
362
|
+
// defines `action` can trigger the skip. Without this, a client
|
|
363
|
+
// could inject `action: 'topUp'` on a charge credential to bypass
|
|
364
|
+
// the amount check. Zod strips unknown keys, so charge payloads
|
|
365
|
+
// (which don't define `action`) will have it removed.
|
|
366
|
+
const parsed = method.schema.credential.payload.safeParse(credential.payload)
|
|
367
|
+
const action = parsed.success
|
|
368
|
+
? (parsed.data as Record<string, unknown>)?.action
|
|
369
|
+
: undefined
|
|
370
|
+
if (action !== 'topUp' && action !== 'voucher') {
|
|
371
|
+
const routeReq = challenge.request as Record<string, unknown>
|
|
372
|
+
const echoedReq = credential.challenge.request as Record<string, unknown>
|
|
373
|
+
const routeDetails = (routeReq.methodDetails ?? {}) as Record<string, unknown>
|
|
374
|
+
const echoedDetails = (echoedReq.methodDetails ?? {}) as Record<string, unknown>
|
|
375
|
+
for (const field of ['amount', 'currency', 'recipient'] as const) {
|
|
376
|
+
const routeVal = routeReq[field] ?? routeDetails[field]
|
|
377
|
+
if (
|
|
378
|
+
routeVal !== undefined &&
|
|
379
|
+
String(routeVal) !== String(echoedReq[field] ?? echoedDetails[field])
|
|
380
|
+
) {
|
|
381
|
+
const response = await transport.respondChallenge({
|
|
382
|
+
challenge,
|
|
383
|
+
input,
|
|
384
|
+
error: new Errors.InvalidChallengeError({
|
|
385
|
+
id: credential.challenge.id,
|
|
386
|
+
reason: `credential ${field} does not match this route's requirements`,
|
|
387
|
+
}),
|
|
388
|
+
})
|
|
389
|
+
return { challenge: response, status: 402 }
|
|
390
|
+
}
|
|
372
391
|
}
|
|
373
392
|
}
|
|
374
393
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NodeListener, Request } from 'mppx/server'
|
|
2
|
-
import { afterEach, describe, expect, test } from '
|
|
2
|
+
import { afterEach, describe, expect, test } from 'vp/test'
|
|
3
3
|
import * as Http from '~test/Http.js'
|
|
4
4
|
|
|
5
5
|
let server: Awaited<ReturnType<typeof Http.createServer>> | undefined
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
3
|
+
|
|
3
4
|
import { Request } from 'mppx/server'
|
|
4
|
-
import { describe, expect, test } from '
|
|
5
|
+
import { describe, expect, test } from 'vp/test'
|
|
5
6
|
|
|
6
7
|
function createMockRequest(options: {
|
|
7
8
|
method?: string
|
package/src/server/Request.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Challenge, Credential, Mcp, Receipt } from 'mppx'
|
|
2
2
|
import { Transport } from 'mppx/server'
|
|
3
3
|
import { Methods } from 'mppx/tempo'
|
|
4
|
-
import { describe, expect, test } from '
|
|
4
|
+
import { describe, expect, test } from 'vp/test'
|
|
5
|
+
|
|
5
6
|
import { BadRequestError, ChannelClosedError } from '../Errors.js'
|
|
6
7
|
|
|
7
8
|
const realm = 'api.example.com'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Challenge, Credential, Receipt } from 'mppx'
|
|
2
2
|
import { Mppx as Mppx_client, stripe as stripe_client } from 'mppx/client'
|
|
3
3
|
import { Mppx as Mppx_server, stripe as stripe_server } from 'mppx/server'
|
|
4
|
-
import { afterEach, describe, expect, test } from '
|
|
4
|
+
import { afterEach, describe, expect, test } from 'vp/test'
|
|
5
5
|
import * as Http from '~test/Http.js'
|
|
6
6
|
|
|
7
7
|
const stripeSecretKey = process.env.VITE_STRIPE_SECRET_KEY
|
package/src/stripe/Methods.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Challenge, Credential } from 'mppx'
|
|
2
2
|
import { Mppx, stripe } from 'mppx/client'
|
|
3
3
|
import { Mppx as Mppx_server, stripe as stripe_server } from 'mppx/server'
|
|
4
|
-
import { describe, expect, test, vi } from '
|
|
4
|
+
import { describe, expect, test, vi } from 'vp/test'
|
|
5
|
+
|
|
5
6
|
import type { StripeJs } from '../internal/types.js'
|
|
6
7
|
import { charge as clientCharge_ } from './Charge.js'
|
|
7
8
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Challenge, Credential } from 'mppx'
|
|
2
2
|
import { Mppx, stripe } from 'mppx/server'
|
|
3
|
-
import { afterEach, describe, expect, test, vi } from '
|
|
3
|
+
import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
4
4
|
import * as Http from '~test/Http.js'
|
|
5
|
+
|
|
5
6
|
import type { StripeClient } from '../internal/types.js'
|
|
6
7
|
|
|
7
8
|
const realm = 'api.example.com'
|
package/src/tempo/Methods.ts
CHANGED
|
@@ -2,9 +2,13 @@ import { Hex } from 'ox'
|
|
|
2
2
|
import { type Address, createClient } from 'viem'
|
|
3
3
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
4
4
|
import { Addresses } from 'viem/tempo'
|
|
5
|
-
import { beforeAll, describe, expect, test } from '
|
|
5
|
+
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
6
|
+
import { nodeEnv } from '~test/config.js'
|
|
6
7
|
import { deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
7
8
|
import { accounts, asset, chain, client, fundAccount, http } from '~test/tempo/viem.js'
|
|
9
|
+
|
|
10
|
+
const isLocalnet = nodeEnv === 'localnet'
|
|
11
|
+
|
|
8
12
|
import type { Challenge } from '../../Challenge.js'
|
|
9
13
|
import * as Credential from '../../Credential.js'
|
|
10
14
|
import {
|
|
@@ -165,7 +169,7 @@ describe('createClosePayload', () => {
|
|
|
165
169
|
})
|
|
166
170
|
})
|
|
167
171
|
|
|
168
|
-
describe('createOpenPayload', () => {
|
|
172
|
+
describe.runIf(isLocalnet)('createOpenPayload', () => {
|
|
169
173
|
const payer = accounts[2]
|
|
170
174
|
const payee = accounts[1].address
|
|
171
175
|
const currency = asset
|
|
@@ -249,7 +253,7 @@ describe('createOpenPayload', () => {
|
|
|
249
253
|
})
|
|
250
254
|
})
|
|
251
255
|
|
|
252
|
-
describe('tryRecoverChannel', () => {
|
|
256
|
+
describe.runIf(isLocalnet)('tryRecoverChannel', () => {
|
|
253
257
|
const payer = accounts[3]
|
|
254
258
|
const payee = accounts[1].address
|
|
255
259
|
const currency = asset
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from 'viem'
|
|
16
16
|
import { prepareTransactionRequest, signTransaction } from 'viem/actions'
|
|
17
17
|
import { Abis } from 'viem/tempo'
|
|
18
|
+
|
|
18
19
|
import type { Challenge } from '../../Challenge.js'
|
|
19
20
|
import * as Credential from '../../Credential.js'
|
|
20
21
|
import * as defaults from '../internal/defaults.js'
|
|
@@ -3,6 +3,7 @@ import type { Address } from 'viem'
|
|
|
3
3
|
import { prepareTransactionRequest, sendCallsSync, signTransaction } from 'viem/actions'
|
|
4
4
|
import { tempo as tempo_chain } from 'viem/chains'
|
|
5
5
|
import { Actions } from 'viem/tempo'
|
|
6
|
+
|
|
6
7
|
import * as Credential from '../../Credential.js'
|
|
7
8
|
import * as Method from '../../Method.js'
|
|
8
9
|
import * as Account from '../../viem/Account.js'
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { type Address, createClient, type Hex, http } from 'viem'
|
|
2
2
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
3
|
import { Addresses } from 'viem/tempo'
|
|
4
|
-
import { beforeAll, describe, expect, test } from '
|
|
4
|
+
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
5
|
+
import { nodeEnv } from '~test/config.js'
|
|
5
6
|
import { deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
6
7
|
import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
|
|
8
|
+
|
|
9
|
+
const isLocalnet = nodeEnv === 'localnet'
|
|
10
|
+
|
|
7
11
|
import * as Challenge from '../../Challenge.js'
|
|
8
12
|
import * as Credential from '../../Credential.js'
|
|
9
13
|
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
@@ -200,7 +204,7 @@ describe('session (pure)', () => {
|
|
|
200
204
|
})
|
|
201
205
|
})
|
|
202
206
|
|
|
203
|
-
describe('session (on-chain)', () => {
|
|
207
|
+
describe.runIf(isLocalnet)('session (on-chain)', () => {
|
|
204
208
|
const payer = accounts[2]
|
|
205
209
|
const payee = accounts[1].address
|
|
206
210
|
let escrowContract: Address
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hex } from 'ox'
|
|
2
2
|
import { type Address, parseUnits, type Account as viem_Account } from 'viem'
|
|
3
3
|
import { tempo as tempo_chain } from 'viem/chains'
|
|
4
|
+
|
|
4
5
|
import type * as Challenge from '../../Challenge.js'
|
|
5
6
|
import * as Method from '../../Method.js'
|
|
6
7
|
import * as Account from '../../viem/Account.js'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Hex } from 'viem'
|
|
2
|
-
import { describe, expect, test, vi } from '
|
|
2
|
+
import { describe, expect, test, vi } from 'vp/test'
|
|
3
|
+
|
|
3
4
|
import * as Challenge from '../../Challenge.js'
|
|
4
5
|
import { formatNeedVoucherEvent, parseEvent } from '../session/Sse.js'
|
|
5
6
|
import type { NeedVoucherEvent, SessionReceipt } from '../session/Types.js'
|
|
@@ -207,6 +208,33 @@ describe('Session', () => {
|
|
|
207
208
|
})
|
|
208
209
|
})
|
|
209
210
|
|
|
211
|
+
describe('.sse() headers normalization', () => {
|
|
212
|
+
test('preserves Headers instance properties when passed as headers', async () => {
|
|
213
|
+
const mockFetch = vi.fn().mockResolvedValue(makeSseResponse(['event: message\ndata: ok\n\n']))
|
|
214
|
+
|
|
215
|
+
const s = sessionManager({
|
|
216
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
217
|
+
fetch: mockFetch as typeof globalThis.fetch,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const iterable = await s.sse('https://api.example.com/stream', {
|
|
221
|
+
headers: new Headers({ 'Content-Type': 'application/json', 'X-Custom': 'value' }),
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
for await (const _ of iterable) {
|
|
225
|
+
// drain
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const calledHeaders = (mockFetch.mock.calls[0]![1] as RequestInit).headers as Record<
|
|
229
|
+
string,
|
|
230
|
+
string
|
|
231
|
+
>
|
|
232
|
+
expect(calledHeaders['content-type']).toBe('application/json')
|
|
233
|
+
expect(calledHeaders['x-custom']).toBe('value')
|
|
234
|
+
expect(calledHeaders.Accept).toBe('text/event-stream')
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
210
238
|
describe('.close()', () => {
|
|
211
239
|
test('is no-op when not opened', async () => {
|
|
212
240
|
const mockFetch = vi.fn()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Hex } from 'ox'
|
|
2
2
|
import type { Address } from 'viem'
|
|
3
|
+
|
|
3
4
|
import type * as Challenge from '../../Challenge.js'
|
|
4
5
|
import * as Fetch from '../../client/internal/Fetch.js'
|
|
5
6
|
import type * as Account from '../../viem/Account.js'
|
|
@@ -157,7 +158,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
157
158
|
const sseInit = {
|
|
158
159
|
...fetchInit,
|
|
159
160
|
headers: {
|
|
160
|
-
...fetchInit.headers,
|
|
161
|
+
...Fetch.normalizeHeaders(fetchInit.headers),
|
|
161
162
|
Accept: 'text/event-stream',
|
|
162
163
|
},
|
|
163
164
|
...(signal ? { signal } : {}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { encodeFunctionData } from 'viem'
|
|
2
2
|
import { Abis, Addresses } from 'viem/tempo'
|
|
3
|
-
import { describe, expect, test } from '
|
|
3
|
+
import { describe, expect, test } from 'vp/test'
|
|
4
|
+
|
|
4
5
|
import { callScopes, FeePayerValidationError, validateCalls } from './fee-payer.js'
|
|
5
6
|
import * as Selectors from './selectors.js'
|
|
6
7
|
|
|
@@ -2,6 +2,7 @@ import type { TempoAddress } from 'ox/tempo'
|
|
|
2
2
|
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
3
3
|
import { decodeFunctionData } from 'viem'
|
|
4
4
|
import { Abis, Addresses } from 'viem/tempo'
|
|
5
|
+
|
|
5
6
|
import * as TempoAddress_internal from './address.js'
|
|
6
7
|
import * as Selectors from './selectors.js'
|
|
7
8
|
|
|
@@ -7,10 +7,11 @@ import { Handler } from 'tempo.ts/server'
|
|
|
7
7
|
import { createClient, custom, encodeFunctionData, parseUnits } from 'viem'
|
|
8
8
|
import { getTransactionReceipt, prepareTransactionRequest, signTransaction } from 'viem/actions'
|
|
9
9
|
import { Abis, Account, Actions, Addresses, Secp256k1, Tick, Transaction } from 'viem/tempo'
|
|
10
|
-
import { beforeAll, describe, expect, test } from '
|
|
10
|
+
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
11
11
|
import * as Http from '~test/Http.js'
|
|
12
12
|
import { closeChannelOnChain, deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
13
13
|
import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
|
|
14
|
+
|
|
14
15
|
import * as Store from '../../Store.js'
|
|
15
16
|
import * as Attribution from '../Attribution.js'
|
|
16
17
|
import { signVoucher } from '../session/Voucher.js'
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from 'viem/actions'
|
|
10
10
|
import { tempo as tempo_chain } from 'viem/chains'
|
|
11
11
|
import { Abis, Transaction } from 'viem/tempo'
|
|
12
|
+
|
|
12
13
|
import { PaymentExpiredError } from '../../Errors.js'
|
|
13
14
|
import type { LooseOmit } from '../../internal/types.js'
|
|
14
15
|
import * as Method from '../../Method.js'
|