mppx 0.6.27 → 0.6.29
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 +25 -0
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +16 -10
- package/dist/Challenge.js.map +1 -1
- package/dist/Method.d.ts +1 -1
- package/dist/Method.d.ts.map +1 -1
- package/dist/Store.d.ts +32 -9
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +42 -10
- package/dist/Store.js.map +1 -1
- package/dist/client/Methods.d.ts +1 -0
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js +1 -0
- package/dist/client/Methods.js.map +1 -1
- package/dist/client/Mppx.d.ts +3 -3
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +1 -0
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/Transport.d.ts +10 -3
- package/dist/client/Transport.d.ts.map +1 -1
- package/dist/client/Transport.js +60 -7
- package/dist/client/Transport.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +3 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +12 -20
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/evm/Assets.d.ts +2 -0
- package/dist/evm/Assets.d.ts.map +1 -0
- package/dist/evm/Assets.js +2 -0
- package/dist/evm/Assets.js.map +1 -0
- package/dist/evm/Chains.d.ts +5 -0
- package/dist/evm/Chains.d.ts.map +1 -0
- package/dist/evm/Chains.js +5 -0
- package/dist/evm/Chains.js.map +1 -0
- package/dist/evm/Methods.d.ts +68 -0
- package/dist/evm/Methods.d.ts.map +1 -0
- package/dist/evm/Methods.js +28 -0
- package/dist/evm/Methods.js.map +1 -0
- package/dist/evm/Types.d.ts +143 -0
- package/dist/evm/Types.d.ts.map +1 -0
- package/dist/evm/Types.js +102 -0
- package/dist/evm/Types.js.map +1 -0
- package/dist/evm/client/Charge.d.ts +102 -0
- package/dist/evm/client/Charge.d.ts.map +1 -0
- package/dist/evm/client/Charge.js +141 -0
- package/dist/evm/client/Charge.js.map +1 -0
- package/dist/evm/client/Methods.d.ts +81 -0
- package/dist/evm/client/Methods.d.ts.map +1 -0
- package/dist/evm/client/Methods.js +16 -0
- package/dist/evm/client/Methods.js.map +1 -0
- package/dist/evm/client/index.d.ts +6 -0
- package/dist/evm/client/index.d.ts.map +1 -0
- package/dist/evm/client/index.js +6 -0
- package/dist/evm/client/index.js.map +1 -0
- package/dist/evm/index.d.ts +9 -0
- package/dist/evm/index.d.ts.map +1 -0
- package/dist/evm/index.js +8 -0
- package/dist/evm/index.js.map +1 -0
- package/dist/evm/server/Charge.d.ts +62 -0
- package/dist/evm/server/Charge.d.ts.map +1 -0
- package/dist/evm/server/Charge.js +172 -0
- package/dist/evm/server/Charge.js.map +1 -0
- package/dist/evm/server/Methods.d.ts +80 -0
- package/dist/evm/server/Methods.d.ts.map +1 -0
- package/dist/evm/server/Methods.js +16 -0
- package/dist/evm/server/Methods.js.map +1 -0
- package/dist/evm/server/index.d.ts +6 -0
- package/dist/evm/server/index.d.ts.map +1 -0
- package/dist/evm/server/index.js +6 -0
- package/dist/evm/server/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/HeaderCodec.d.ts +18 -0
- package/dist/internal/HeaderCodec.d.ts.map +1 -0
- package/dist/internal/HeaderCodec.js +31 -0
- package/dist/internal/HeaderCodec.js.map +1 -0
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +2 -3
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.js +2 -1
- package/dist/middlewares/express.js.map +1 -1
- package/dist/proxy/internal/Headers.d.ts +13 -1
- package/dist/proxy/internal/Headers.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.js +25 -2
- package/dist/proxy/internal/Headers.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +2 -0
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/server/Methods.d.ts +1 -0
- package/dist/server/Methods.d.ts.map +1 -1
- package/dist/server/Methods.js +1 -0
- package/dist/server/Methods.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +90 -12
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +10 -0
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +9 -0
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts +31 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +88 -11
- package/dist/stripe/server/Charge.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/client/ChannelOps.d.ts +3 -3
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js +13 -6
- 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 +8 -5
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +0 -1
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/client/Session.d.ts +2 -4
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js +10 -12
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +3 -3
- 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/account.d.ts +5 -0
- package/dist/tempo/internal/account.d.ts.map +1 -1
- package/dist/tempo/internal/account.js +8 -0
- package/dist/tempo/internal/account.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +5 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +6 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +30 -3
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +6 -0
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +14 -13
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +6 -0
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +1 -1
- package/dist/tempo/server/Subscription.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/Chain.d.ts +2 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +8 -8
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts +4 -3
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js +71 -44
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/tempo/session/Ws.d.ts.map +1 -1
- package/dist/tempo/session/Ws.js +15 -0
- package/dist/tempo/session/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +2 -2
- package/dist/x402/Assets.d.ts +29 -0
- package/dist/x402/Assets.d.ts.map +1 -0
- package/dist/x402/Assets.js +46 -0
- package/dist/x402/Assets.js.map +1 -0
- package/dist/x402/Header.d.ts +14 -0
- package/dist/x402/Header.d.ts.map +1 -0
- package/dist/x402/Header.js +18 -0
- package/dist/x402/Header.js.map +1 -0
- package/dist/x402/Types.d.ts +289 -0
- package/dist/x402/Types.d.ts.map +1 -0
- package/dist/x402/Types.js +139 -0
- package/dist/x402/Types.js.map +1 -0
- package/dist/x402/client/Exact.d.ts +38 -0
- package/dist/x402/client/Exact.d.ts.map +1 -0
- package/dist/x402/client/Exact.js +141 -0
- package/dist/x402/client/Exact.js.map +1 -0
- package/dist/x402/index.d.ts +6 -0
- package/dist/x402/index.d.ts.map +1 -0
- package/dist/x402/index.js +6 -0
- package/dist/x402/index.js.map +1 -0
- package/dist/x402/internal/ChallengeBrand.d.ts +5 -0
- package/dist/x402/internal/ChallengeBrand.d.ts.map +1 -0
- package/dist/x402/internal/ChallengeBrand.js +13 -0
- package/dist/x402/internal/ChallengeBrand.js.map +1 -0
- package/dist/x402/internal/RouteBinding.d.ts +8 -0
- package/dist/x402/internal/RouteBinding.d.ts.map +1 -0
- package/dist/x402/internal/RouteBinding.js +12 -0
- package/dist/x402/internal/RouteBinding.js.map +1 -0
- package/dist/x402/server/EvmCharge.d.ts +50 -0
- package/dist/x402/server/EvmCharge.d.ts.map +1 -0
- package/dist/x402/server/EvmCharge.js +301 -0
- package/dist/x402/server/EvmCharge.js.map +1 -0
- package/dist/x402/server/Facilitator.d.ts +12 -0
- package/dist/x402/server/Facilitator.d.ts.map +1 -0
- package/dist/x402/server/Facilitator.js +42 -0
- package/dist/x402/server/Facilitator.js.map +1 -0
- package/package.json +41 -21
- package/src/Challenge.test.ts +28 -0
- package/src/Challenge.ts +17 -10
- package/src/Method.ts +1 -1
- package/src/Store.test-d.ts +58 -0
- package/src/Store.test.ts +77 -0
- package/src/Store.ts +155 -74
- package/src/client/Methods.ts +1 -0
- package/src/client/Mppx.ts +4 -3
- package/src/client/Transport.test.ts +165 -30
- package/src/client/Transport.ts +76 -8
- package/src/client/index.ts +1 -1
- package/src/client/internal/Fetch.test.ts +31 -2
- package/src/client/internal/Fetch.ts +26 -19
- package/src/evm/Assets.ts +1 -0
- package/src/evm/Chains.ts +5 -0
- package/src/evm/Methods.ts +44 -0
- package/src/evm/PublicInterface.test-d.ts +109 -0
- package/src/evm/Types.ts +140 -0
- package/src/evm/client/Charge.test.ts +99 -0
- package/src/evm/client/Charge.ts +198 -0
- package/src/evm/client/Methods.ts +19 -0
- package/src/evm/client/index.ts +5 -0
- package/src/evm/index.ts +13 -0
- package/src/evm/server/Charge.test.ts +199 -0
- package/src/evm/server/Charge.ts +283 -0
- package/src/evm/server/Methods.ts +22 -0
- package/src/evm/server/index.ts +5 -0
- package/src/index.ts +2 -0
- package/src/internal/HeaderCodec.ts +36 -0
- package/src/middlewares/elysia.test.ts +25 -0
- package/src/middlewares/elysia.ts +1 -2
- package/src/middlewares/express.test.ts +28 -0
- package/src/middlewares/express.ts +1 -1
- package/src/middlewares/hono.test.ts +138 -2
- package/src/middlewares/nextjs.test.ts +22 -0
- package/src/proxy/internal/Headers.test.ts +38 -0
- package/src/proxy/internal/Headers.ts +26 -2
- package/src/proxy/services/openai.test.ts +57 -1
- package/src/proxy/services/openai.ts +2 -0
- package/src/server/Methods.ts +1 -0
- package/src/server/Mppx.test.ts +244 -1
- package/src/server/Mppx.ts +124 -11
- package/src/server/NodeListener.test.ts +28 -1
- package/src/server/Transport.test.ts +19 -0
- package/src/server/Transport.ts +20 -0
- package/src/server/index.ts +1 -1
- package/src/stripe/server/Charge.test.ts +215 -1
- package/src/stripe/server/Charge.ts +150 -20
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/AccessKeyAuthorization.test.ts +231 -0
- package/src/tempo/client/ChannelOps.test.ts +61 -7
- package/src/tempo/client/ChannelOps.ts +18 -7
- package/src/tempo/client/Charge.test.ts +126 -0
- package/src/tempo/client/Charge.ts +10 -6
- package/src/tempo/client/Session.test.ts +130 -1
- package/src/tempo/client/Session.ts +12 -19
- package/src/tempo/client/SessionManager.test.ts +69 -2
- package/src/tempo/client/SessionManager.ts +4 -4
- package/src/tempo/internal/account.ts +13 -0
- package/src/tempo/internal/fee-payer.test.ts +32 -2
- package/src/tempo/internal/fee-payer.ts +6 -2
- package/src/tempo/server/Charge.test.ts +91 -1
- package/src/tempo/server/Charge.ts +48 -2
- package/src/tempo/server/Session.test.ts +30 -0
- package/src/tempo/server/Session.ts +24 -17
- package/src/tempo/server/Subscription.ts +7 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/Chain.test.ts +4 -4
- package/src/tempo/session/Chain.ts +10 -6
- package/src/tempo/session/ChannelStore.test.ts +21 -0
- package/src/tempo/session/Voucher.test.ts +230 -1
- package/src/tempo/session/Voucher.ts +96 -48
- package/src/tempo/session/Ws.test.ts +71 -0
- package/src/tempo/session/Ws.ts +13 -0
- package/src/tempo/subscription/Store.test.ts +55 -0
- package/src/x402/Assets.ts +65 -0
- package/src/x402/Exact.e2e.test.ts +448 -0
- package/src/x402/Header.test.ts +73 -0
- package/src/x402/Header.ts +34 -0
- package/src/x402/PublicInterface.test-d.ts +39 -0
- package/src/x402/Types.ts +248 -0
- package/src/x402/client/Exact.test.ts +180 -0
- package/src/x402/client/Exact.ts +198 -0
- package/src/x402/index.ts +5 -0
- package/src/x402/internal/ChallengeBrand.ts +14 -0
- package/src/x402/internal/RouteBinding.ts +18 -0
- package/src/x402/server/EvmCharge.ts +394 -0
- package/src/x402/server/Facilitator.test.ts +111 -0
- package/src/x402/server/Facilitator.ts +56 -0
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { serve } from '@hono/node-server'
|
|
2
2
|
import { Hono } from 'hono'
|
|
3
3
|
import { Challenge, Credential, Method, Receipt, z } from 'mppx'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
evm as evm_client,
|
|
6
|
+
Mppx as Mppx_client,
|
|
7
|
+
session as sessionIntent,
|
|
8
|
+
tempo as tempo_client,
|
|
9
|
+
} from 'mppx/client'
|
|
5
10
|
import { Mppx, discovery, payment } from 'mppx/hono'
|
|
6
|
-
import { tempo as tempo_server } from 'mppx/server'
|
|
11
|
+
import { evm as evm_server, Mppx as ServerMppx, tempo as tempo_server } from 'mppx/server'
|
|
12
|
+
import {
|
|
13
|
+
paymentRequiredHeader,
|
|
14
|
+
paymentResponseHeader,
|
|
15
|
+
paymentSignatureHeader,
|
|
16
|
+
type PaymentPayload,
|
|
17
|
+
} from 'mppx/x402'
|
|
7
18
|
import type { Address } from 'viem'
|
|
8
19
|
import { Addresses } from 'viem/tempo'
|
|
9
20
|
import { beforeAll, describe, expect, test } from 'vp/test'
|
|
@@ -53,6 +64,30 @@ describe('payment', () => {
|
|
|
53
64
|
|
|
54
65
|
server.close()
|
|
55
66
|
})
|
|
67
|
+
|
|
68
|
+
test('copies transport-specific success headers', async () => {
|
|
69
|
+
const intent = () => async () => ({
|
|
70
|
+
status: 200 as const,
|
|
71
|
+
withReceipt: (response?: Response) =>
|
|
72
|
+
new Response(response?.body ?? null, {
|
|
73
|
+
headers: {
|
|
74
|
+
...(response ? Object.fromEntries(response.headers) : {}),
|
|
75
|
+
'PAYMENT-RESPONSE': 'x402-response',
|
|
76
|
+
},
|
|
77
|
+
status: response?.status ?? 200,
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const app = new Hono()
|
|
82
|
+
app.get('/', payment(intent as any, {} as any), (c) => c.json({ data: 'content' }))
|
|
83
|
+
|
|
84
|
+
const server = await createServer(app)
|
|
85
|
+
const response = await globalThis.fetch(server.url)
|
|
86
|
+
expect(response.status).toBe(200)
|
|
87
|
+
expect(response.headers.get('PAYMENT-RESPONSE')).toBe('x402-response')
|
|
88
|
+
|
|
89
|
+
server.close()
|
|
90
|
+
})
|
|
56
91
|
})
|
|
57
92
|
|
|
58
93
|
const scopeMethod = Method.toServer(
|
|
@@ -187,8 +222,109 @@ describe('charge', () => {
|
|
|
187
222
|
|
|
188
223
|
server.close()
|
|
189
224
|
})
|
|
225
|
+
|
|
226
|
+
test('serves tempo and x402 from one Hono endpoint', async () => {
|
|
227
|
+
const transaction = `0x${'2'.repeat(64)}` as const
|
|
228
|
+
const payments = ServerMppx.create({
|
|
229
|
+
methods: [
|
|
230
|
+
tempo_server.charge({
|
|
231
|
+
account: accounts[0],
|
|
232
|
+
currency: asset,
|
|
233
|
+
getClient: () => client,
|
|
234
|
+
recipient: accounts[0].address,
|
|
235
|
+
}),
|
|
236
|
+
evm_server.charge({
|
|
237
|
+
currency: evm_server.assets.baseSepolia.USDC,
|
|
238
|
+
recipient: accounts[0].address,
|
|
239
|
+
x402: {
|
|
240
|
+
facilitator: {
|
|
241
|
+
async verify(paymentPayload: PaymentPayload) {
|
|
242
|
+
return {
|
|
243
|
+
isValid: true,
|
|
244
|
+
payer: payerOf(paymentPayload),
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
async settle(paymentPayload: PaymentPayload) {
|
|
248
|
+
return {
|
|
249
|
+
network: paymentPayload.accepted.network,
|
|
250
|
+
payer: payerOf(paymentPayload),
|
|
251
|
+
success: true,
|
|
252
|
+
transaction,
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
],
|
|
259
|
+
secretKey,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
const route = payments.compose(
|
|
263
|
+
[payments.tempo.charge, { amount: '0', chainId: client.chain!.id }],
|
|
264
|
+
[payments.evm.charge, { amount: '0.01' }],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
const app = new Hono()
|
|
268
|
+
app.get('/paid', async (c) => {
|
|
269
|
+
const result = await route(c.req.raw)
|
|
270
|
+
if (result.status === 402) return result.challenge
|
|
271
|
+
return result.withReceipt(c.json({ data: 'paid' }))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const server = await createServer(app)
|
|
275
|
+
const challenge = await globalThis.fetch(`${server.url}/paid`)
|
|
276
|
+
expect(challenge.status).toBe(402)
|
|
277
|
+
expect(challenge.headers.get('WWW-Authenticate')).toContain('Payment')
|
|
278
|
+
expect(challenge.headers.get(paymentRequiredHeader)).toBeTruthy()
|
|
279
|
+
|
|
280
|
+
const tempoPayment = Mppx_client.create({
|
|
281
|
+
methods: [
|
|
282
|
+
tempo_client.charge({
|
|
283
|
+
account: accounts[0],
|
|
284
|
+
getClient: () => client,
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
polyfill: false,
|
|
288
|
+
})
|
|
289
|
+
const tempoResponse = await tempoPayment.fetch(`${server.url}/paid`)
|
|
290
|
+
expect(tempoResponse.status).toBe(200)
|
|
291
|
+
expect(await tempoResponse.json()).toEqual({ data: 'paid' })
|
|
292
|
+
expect(tempoResponse.headers.get('Payment-Receipt')).toBeTruthy()
|
|
293
|
+
|
|
294
|
+
const x402Payment = Mppx_client.create({
|
|
295
|
+
methods: [
|
|
296
|
+
evm_client.charge({
|
|
297
|
+
account: accounts[0],
|
|
298
|
+
}),
|
|
299
|
+
],
|
|
300
|
+
polyfill: false,
|
|
301
|
+
})
|
|
302
|
+
const paymentSignature = await x402Payment.createCredential(pureX402Challenge(challenge))
|
|
303
|
+
const x402Response = await x402Payment.rawFetch(`${server.url}/paid`, {
|
|
304
|
+
headers: { [paymentSignatureHeader]: paymentSignature },
|
|
305
|
+
})
|
|
306
|
+
expect(x402Response.status).toBe(200)
|
|
307
|
+
expect(await x402Response.json()).toEqual({ data: 'paid' })
|
|
308
|
+
expect(x402Response.headers.get(paymentResponseHeader)).toBeTruthy()
|
|
309
|
+
|
|
310
|
+
server.close()
|
|
311
|
+
})
|
|
190
312
|
})
|
|
191
313
|
|
|
314
|
+
function payerOf(paymentPayload: PaymentPayload): string {
|
|
315
|
+
if ('authorization' in paymentPayload.payload) return paymentPayload.payload.authorization.from
|
|
316
|
+
return paymentPayload.payload.permit2Authorization.from
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function pureX402Challenge(response: Response): Response {
|
|
320
|
+
const paymentRequired = response.headers.get(paymentRequiredHeader)
|
|
321
|
+
if (!paymentRequired) throw new Error('Missing PAYMENT-REQUIRED header.')
|
|
322
|
+
return new Response(null, {
|
|
323
|
+
headers: { [paymentRequiredHeader]: paymentRequired },
|
|
324
|
+
status: 402,
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
192
328
|
describe('scope binding', () => {
|
|
193
329
|
const scopeOpts = {
|
|
194
330
|
amount: '1',
|
|
@@ -59,6 +59,28 @@ describe('payment', () => {
|
|
|
59
59
|
|
|
60
60
|
server.close()
|
|
61
61
|
})
|
|
62
|
+
|
|
63
|
+
test('copies transport-specific success headers', async () => {
|
|
64
|
+
const intent = () => async () => ({
|
|
65
|
+
status: 200 as const,
|
|
66
|
+
withReceipt: (response?: Response) =>
|
|
67
|
+
new Response(response?.body ?? null, {
|
|
68
|
+
headers: {
|
|
69
|
+
...(response ? Object.fromEntries(response.headers) : {}),
|
|
70
|
+
'PAYMENT-RESPONSE': 'x402-response',
|
|
71
|
+
},
|
|
72
|
+
status: response?.status ?? 200,
|
|
73
|
+
}),
|
|
74
|
+
})
|
|
75
|
+
const handler = payment(intent as any, {} as any, () => Response.json({ data: 'content' }))
|
|
76
|
+
|
|
77
|
+
const server = await createServer(handler)
|
|
78
|
+
const response = await globalThis.fetch(server.url)
|
|
79
|
+
expect(response.status).toBe(200)
|
|
80
|
+
expect(response.headers.get('PAYMENT-RESPONSE')).toBe('x402-response')
|
|
81
|
+
|
|
82
|
+
server.close()
|
|
83
|
+
})
|
|
62
84
|
})
|
|
63
85
|
|
|
64
86
|
function createChargeHarness(feePayer: boolean) {
|
|
@@ -13,6 +13,26 @@ describe('scrub', () => {
|
|
|
13
13
|
expect(result.get('content-type')).toBe('application/json')
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
test('behavior: strips payment protocol headers', () => {
|
|
17
|
+
const headers = new globalThis.Headers({
|
|
18
|
+
'Accept-Payment': 'evm/charge',
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
'PAYMENT-RECEIPT': 'receipt',
|
|
21
|
+
'PAYMENT-REQUIRED': 'required',
|
|
22
|
+
'PAYMENT-RESPONSE': 'response',
|
|
23
|
+
'PAYMENT-SIGNATURE': 'signature',
|
|
24
|
+
'WWW-Authenticate': 'Payment id="abc"',
|
|
25
|
+
})
|
|
26
|
+
const result = Headers.scrub(headers)
|
|
27
|
+
expect(result.has('accept-payment')).toBe(false)
|
|
28
|
+
expect(result.has('payment-receipt')).toBe(false)
|
|
29
|
+
expect(result.has('payment-required')).toBe(false)
|
|
30
|
+
expect(result.has('payment-response')).toBe(false)
|
|
31
|
+
expect(result.has('payment-signature')).toBe(false)
|
|
32
|
+
expect(result.has('www-authenticate')).toBe(false)
|
|
33
|
+
expect(result.get('content-type')).toBe('application/json')
|
|
34
|
+
})
|
|
35
|
+
|
|
16
36
|
test('behavior: strips cookie header', () => {
|
|
17
37
|
const headers = new globalThis.Headers({
|
|
18
38
|
Cookie: 'session=abc123',
|
|
@@ -98,4 +118,22 @@ describe('scrubResponse', () => {
|
|
|
98
118
|
expect(result.statusText).toBe('Created')
|
|
99
119
|
expect(await result.text()).toBe('hello')
|
|
100
120
|
})
|
|
121
|
+
|
|
122
|
+
// Regression: an upstream service must never be able to issue a cookie
|
|
123
|
+
// under the proxy's origin. Otherwise a compromised or attacker-influenced
|
|
124
|
+
// upstream can session-fixate (`Set-Cookie: session=evil; Domain=…`) every
|
|
125
|
+
// sibling subdomain of the proxy. See the docblock on `scrubResponse`.
|
|
126
|
+
test('behavior: strips set-cookie so upstream cannot set cookies on proxy origin', () => {
|
|
127
|
+
const response = new Response('body', {
|
|
128
|
+
headers: [
|
|
129
|
+
['Set-Cookie', '__Secure-session=evil; Domain=.example.com; Secure; HttpOnly'],
|
|
130
|
+
['Set-Cookie', 'tracking=1; Path=/'],
|
|
131
|
+
['Content-Type', 'application/json'],
|
|
132
|
+
],
|
|
133
|
+
})
|
|
134
|
+
const result = Headers.scrubResponse(response)
|
|
135
|
+
expect(result.headers.has('set-cookie')).toBe(false)
|
|
136
|
+
expect(result.headers.getSetCookie?.() ?? []).toEqual([])
|
|
137
|
+
expect(result.headers.get('content-type')).toBe('application/json')
|
|
138
|
+
})
|
|
101
139
|
})
|
|
@@ -9,6 +9,17 @@ const hopByHopHeaders = new Set([
|
|
|
9
9
|
'trailer',
|
|
10
10
|
])
|
|
11
11
|
|
|
12
|
+
// Payment credentials are consumed by the proxy and must never reach upstream services.
|
|
13
|
+
const paymentHeaders = new Set([
|
|
14
|
+
'accept-payment',
|
|
15
|
+
'authorization',
|
|
16
|
+
'payment-receipt',
|
|
17
|
+
'payment-required',
|
|
18
|
+
'payment-response',
|
|
19
|
+
'payment-signature',
|
|
20
|
+
'www-authenticate',
|
|
21
|
+
])
|
|
22
|
+
|
|
12
23
|
/** Strips hop-by-hop, auth, encoding, cookie, and forwarding headers from a request before proxying upstream. */
|
|
13
24
|
export function scrub(headers: Headers): Headers {
|
|
14
25
|
const scrubbed = new Headers()
|
|
@@ -16,7 +27,7 @@ export function scrub(headers: Headers): Headers {
|
|
|
16
27
|
for (const [name, value] of headers) {
|
|
17
28
|
const lower = name.toLowerCase()
|
|
18
29
|
|
|
19
|
-
if (lower
|
|
30
|
+
if (paymentHeaders.has(lower)) continue
|
|
20
31
|
if (lower === 'accept-encoding') continue
|
|
21
32
|
if (lower === 'content-length') continue
|
|
22
33
|
if (lower === 'cookie') continue
|
|
@@ -29,11 +40,24 @@ export function scrub(headers: Headers): Headers {
|
|
|
29
40
|
return scrubbed
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Strips re-streaming headers (`content-encoding`, `content-length`) and
|
|
45
|
+
* security-sensitive headers (`set-cookie`) from an upstream response.
|
|
46
|
+
*
|
|
47
|
+
* `set-cookie` is dropped because a paid API proxy must never let an upstream
|
|
48
|
+
* service set cookies in the user's browser under the proxy's origin. If a
|
|
49
|
+
* compromised, misbehaving, or attacker-influenced upstream returned
|
|
50
|
+
* `Set-Cookie: session=evil; Domain=.example.com`, the browser would honor it
|
|
51
|
+
* for every sibling subdomain of the proxy — turning any future path-confusion
|
|
52
|
+
* or open-redirect bug in the surrounding deployment into a session-fixation
|
|
53
|
+
* primitive. Proxied services authenticate via bearer tokens / signed
|
|
54
|
+
* payloads, never cookies, so dropping `set-cookie` is purely defensive.
|
|
55
|
+
*/
|
|
33
56
|
export function scrubResponse(response: Response): Response {
|
|
34
57
|
const headers = new Headers(response.headers)
|
|
35
58
|
headers.delete('content-encoding')
|
|
36
59
|
headers.delete('content-length')
|
|
60
|
+
headers.delete('set-cookie')
|
|
37
61
|
return new Response(response.body, {
|
|
38
62
|
status: response.status,
|
|
39
63
|
statusText: response.statusText,
|
|
@@ -35,8 +35,64 @@ const mppx_client = Mppx_client.create({
|
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
let proxyServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
|
|
38
|
+
let upstreamServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
|
|
38
39
|
|
|
39
|
-
afterEach(() =>
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
proxyServer?.close()
|
|
42
|
+
upstreamServer?.close()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('openai', () => {
|
|
46
|
+
test('security: strips caller-supplied OpenAI tenant headers before proxying', async () => {
|
|
47
|
+
upstreamServer = await Http.createServer((req, res) => {
|
|
48
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
49
|
+
res.end(
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
headers: {
|
|
52
|
+
authorization: req.headers.authorization,
|
|
53
|
+
organization: req.headers['openai-organization'],
|
|
54
|
+
project: req.headers['openai-project'],
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const proxy = ApiProxy.create({
|
|
61
|
+
services: [
|
|
62
|
+
openai({
|
|
63
|
+
apiKey: 'sk-test',
|
|
64
|
+
baseUrl: upstreamServer.url,
|
|
65
|
+
routes: {
|
|
66
|
+
'POST /v1/chat/completions': true,
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
})
|
|
71
|
+
proxyServer = await Http.createServer(proxy.listener)
|
|
72
|
+
|
|
73
|
+
const res = await fetch(`${proxyServer.url}/openai/v1/chat/completions`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'OpenAI-Organization': 'org_attacker',
|
|
78
|
+
'OpenAI-Project': 'proj_attacker',
|
|
79
|
+
},
|
|
80
|
+
body: '{}',
|
|
81
|
+
})
|
|
82
|
+
expect(res.status).toBe(200)
|
|
83
|
+
|
|
84
|
+
const body = (await res.json()) as {
|
|
85
|
+
headers: {
|
|
86
|
+
authorization: string
|
|
87
|
+
organization?: string | string[] | undefined
|
|
88
|
+
project?: string | string[] | undefined
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
expect(body.headers.authorization).toBe('Bearer sk-test')
|
|
92
|
+
expect(body.headers.organization).toBeUndefined()
|
|
93
|
+
expect(body.headers.project).toBeUndefined()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
40
96
|
|
|
41
97
|
describe.skipIf(!apiKey)('openai', () => {
|
|
42
98
|
test('behavior: proxies GET /v1/models with charge', async () => {
|
|
@@ -29,6 +29,8 @@ export function openai(config: openai.Config) {
|
|
|
29
29
|
},
|
|
30
30
|
rewriteRequest(request, ctx) {
|
|
31
31
|
const apiKey = ctx.apiKey ?? config.apiKey
|
|
32
|
+
request.headers.delete('OpenAI-Organization')
|
|
33
|
+
request.headers.delete('OpenAI-Project')
|
|
32
34
|
request.headers.set('Authorization', `Bearer ${apiKey}`)
|
|
33
35
|
return request
|
|
34
36
|
},
|
package/src/server/Methods.ts
CHANGED
package/src/server/Mppx.test.ts
CHANGED
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
session as tempo_session_client,
|
|
7
7
|
tempo as tempo_client,
|
|
8
8
|
} from 'mppx/client'
|
|
9
|
-
import {
|
|
9
|
+
import { Types as evm_Types } from 'mppx/evm'
|
|
10
|
+
import { evm, Mppx, stripe, Store, Transport, tempo } from 'mppx/server'
|
|
11
|
+
import { Header as x402_Header, Types as x402_Types, type PaymentPayload } from 'mppx/x402'
|
|
10
12
|
import { getTransactionReceipt } from 'viem/actions'
|
|
11
13
|
import { describe, expect, test } from 'vp/test'
|
|
12
14
|
import * as Http from '~test/Http.js'
|
|
@@ -14,6 +16,7 @@ import { deployEscrow } from '~test/tempo/session.js'
|
|
|
14
16
|
import { accounts, asset, client } from '~test/tempo/viem.js'
|
|
15
17
|
|
|
16
18
|
import type { SessionReceipt } from '../tempo/session/Types.js'
|
|
19
|
+
import * as x402_RouteBinding from '../x402/internal/RouteBinding.js'
|
|
17
20
|
|
|
18
21
|
const realm = 'api.example.com'
|
|
19
22
|
const secretKey = 'test-secret-key'
|
|
@@ -1855,6 +1858,29 @@ describe('compose', () => {
|
|
|
1855
1858
|
},
|
|
1856
1859
|
})
|
|
1857
1860
|
|
|
1861
|
+
const x402Method = evm.charge({
|
|
1862
|
+
currency: evm.assets.baseSepolia.USDC,
|
|
1863
|
+
recipient: accounts[0].address,
|
|
1864
|
+
x402: {
|
|
1865
|
+
facilitator: {
|
|
1866
|
+
async verify(paymentPayload: PaymentPayload) {
|
|
1867
|
+
return {
|
|
1868
|
+
isValid: true,
|
|
1869
|
+
payer: payerOf(paymentPayload),
|
|
1870
|
+
}
|
|
1871
|
+
},
|
|
1872
|
+
async settle(paymentPayload: PaymentPayload) {
|
|
1873
|
+
return {
|
|
1874
|
+
network: paymentPayload.accepted.network,
|
|
1875
|
+
payer: payerOf(paymentPayload),
|
|
1876
|
+
success: true,
|
|
1877
|
+
transaction: `0x${'3'.repeat(64)}`,
|
|
1878
|
+
}
|
|
1879
|
+
},
|
|
1880
|
+
},
|
|
1881
|
+
},
|
|
1882
|
+
})
|
|
1883
|
+
|
|
1858
1884
|
const challengeOpts = {
|
|
1859
1885
|
amount: '1000',
|
|
1860
1886
|
currency: '0x0000000000000000000000000000000000000001',
|
|
@@ -1879,6 +1905,142 @@ describe('compose', () => {
|
|
|
1879
1905
|
expect(wwwAuth).toContain('method="beta"')
|
|
1880
1906
|
})
|
|
1881
1907
|
|
|
1908
|
+
test('returns composed x402 challenge headers when no credential', async () => {
|
|
1909
|
+
const mppx = Mppx.create({ methods: [x402Method], realm, secretKey })
|
|
1910
|
+
|
|
1911
|
+
const result = await mppx.compose(['evm/charge', { amount: '0.01' }])(
|
|
1912
|
+
new Request('https://example.com/resource'),
|
|
1913
|
+
)
|
|
1914
|
+
|
|
1915
|
+
expect(result.status).toBe(402)
|
|
1916
|
+
if (result.status !== 402) throw new Error()
|
|
1917
|
+
|
|
1918
|
+
expect(result.challenge.headers.get('WWW-Authenticate')).toContain('method="evm"')
|
|
1919
|
+
const header = result.challenge.headers.get(x402_Types.paymentRequiredHeader)
|
|
1920
|
+
expect(header).toBeTruthy()
|
|
1921
|
+
expect(result.challenge.headers.get('Content-Type')).toContain('application/problem+json')
|
|
1922
|
+
expect(await result.challenge.json()).toMatchObject({ status: 402 })
|
|
1923
|
+
|
|
1924
|
+
const paymentRequired = x402_Header.decodePaymentRequired(header!)
|
|
1925
|
+
expect(paymentRequired.accepts).toHaveLength(1)
|
|
1926
|
+
expect(paymentRequired.accepts[0]).toMatchObject({
|
|
1927
|
+
amount: '10000',
|
|
1928
|
+
scheme: x402_Types.schemes[0],
|
|
1929
|
+
})
|
|
1930
|
+
expect(paymentRequired.resource.url).toBe('https://example.com/resource')
|
|
1931
|
+
})
|
|
1932
|
+
|
|
1933
|
+
test('merges Payment auth and x402 challenge headers in compose()', async () => {
|
|
1934
|
+
const mppx = Mppx.create({ methods: [alphaMethod, x402Method], realm, secretKey })
|
|
1935
|
+
|
|
1936
|
+
const result = await mppx.compose(
|
|
1937
|
+
[alphaMethod, challengeOpts],
|
|
1938
|
+
['evm/charge', { amount: '0.01' }],
|
|
1939
|
+
)(new Request('https://example.com/resource'))
|
|
1940
|
+
|
|
1941
|
+
expect(result.status).toBe(402)
|
|
1942
|
+
if (result.status !== 402) throw new Error()
|
|
1943
|
+
|
|
1944
|
+
const wwwAuth = result.challenge.headers.get('WWW-Authenticate')
|
|
1945
|
+
expect(wwwAuth).toContain('method="alpha"')
|
|
1946
|
+
|
|
1947
|
+
const header = result.challenge.headers.get(x402_Types.paymentRequiredHeader)
|
|
1948
|
+
expect(header).toBeTruthy()
|
|
1949
|
+
const paymentRequired = x402_Header.decodePaymentRequired(header!)
|
|
1950
|
+
expect(paymentRequired.accepts.map((accepted) => accepted.amount)).toEqual(['10000'])
|
|
1951
|
+
})
|
|
1952
|
+
|
|
1953
|
+
test('keeps pure x402 challenge headers when Payment auth challenges are present', async () => {
|
|
1954
|
+
const mppx = Mppx.create({ methods: [alphaMethod], realm, secretKey })
|
|
1955
|
+
const pureX402 = async () =>
|
|
1956
|
+
({
|
|
1957
|
+
status: 402,
|
|
1958
|
+
challenge: new Response('{}', {
|
|
1959
|
+
status: 402,
|
|
1960
|
+
headers: {
|
|
1961
|
+
[x402_Types.paymentRequiredHeader]: x402_Header.encodePaymentRequired({
|
|
1962
|
+
accepts: [
|
|
1963
|
+
{
|
|
1964
|
+
amount: '10000',
|
|
1965
|
+
asset: evm.assets.baseSepolia.USDC.address,
|
|
1966
|
+
extra: {
|
|
1967
|
+
assetTransferMethod: evm_Types.eip3009,
|
|
1968
|
+
name: 'USDC',
|
|
1969
|
+
version: '2',
|
|
1970
|
+
},
|
|
1971
|
+
maxTimeoutSeconds: 300,
|
|
1972
|
+
network: 'eip155:84532',
|
|
1973
|
+
payTo: accounts[0].address,
|
|
1974
|
+
scheme: 'exact',
|
|
1975
|
+
},
|
|
1976
|
+
],
|
|
1977
|
+
resource: { url: 'https://example.com/resource' },
|
|
1978
|
+
x402Version: 2,
|
|
1979
|
+
}),
|
|
1980
|
+
},
|
|
1981
|
+
}),
|
|
1982
|
+
}) as const
|
|
1983
|
+
|
|
1984
|
+
const alpha = (mppx as unknown as Record<string, (options: unknown) => any>)['alpha/charge']!(
|
|
1985
|
+
challengeOpts,
|
|
1986
|
+
)
|
|
1987
|
+
|
|
1988
|
+
const result = await Mppx.compose(alpha, pureX402)(new Request('https://example.com/resource'))
|
|
1989
|
+
|
|
1990
|
+
expect(result.status).toBe(402)
|
|
1991
|
+
if (result.status !== 402) throw new Error()
|
|
1992
|
+
|
|
1993
|
+
expect(result.challenge.headers.get('WWW-Authenticate')).toContain('method="alpha"')
|
|
1994
|
+
expect(result.challenge.headers.get(x402_Types.paymentRequiredHeader)).toBeTruthy()
|
|
1995
|
+
})
|
|
1996
|
+
|
|
1997
|
+
test('merges multiple x402 exact offers in compose()', async () => {
|
|
1998
|
+
const mppx = Mppx.create({ methods: [x402Method], realm, secretKey })
|
|
1999
|
+
|
|
2000
|
+
const result = await mppx.compose(
|
|
2001
|
+
['evm/charge', { amount: '0.01' }],
|
|
2002
|
+
['evm/charge', { amount: '0.02' }],
|
|
2003
|
+
)(new Request('https://example.com/resource'))
|
|
2004
|
+
|
|
2005
|
+
expect(result.status).toBe(402)
|
|
2006
|
+
if (result.status !== 402) throw new Error()
|
|
2007
|
+
|
|
2008
|
+
const header = result.challenge.headers.get(x402_Types.paymentRequiredHeader)
|
|
2009
|
+
expect(header).toBeTruthy()
|
|
2010
|
+
const paymentRequired = x402_Header.decodePaymentRequired(header!)
|
|
2011
|
+
expect(paymentRequired.accepts.map((accepted) => accepted.amount)).toEqual(['10000', '20000'])
|
|
2012
|
+
})
|
|
2013
|
+
|
|
2014
|
+
test('dispatches x402 credentials through compose()', async () => {
|
|
2015
|
+
const mppx = Mppx.create({ methods: [alphaMethod, x402Method], realm, secretKey })
|
|
2016
|
+
const handle = mppx.compose([alphaMethod, challengeOpts], ['evm/charge', { amount: '0.01' }])
|
|
2017
|
+
|
|
2018
|
+
const firstResult = await handle(new Request('https://example.com/resource'))
|
|
2019
|
+
expect(firstResult.status).toBe(402)
|
|
2020
|
+
if (firstResult.status !== 402) throw new Error()
|
|
2021
|
+
|
|
2022
|
+
const paymentRequired = x402_Header.decodePaymentRequired(
|
|
2023
|
+
firstResult.challenge.headers.get(x402_Types.paymentRequiredHeader)!,
|
|
2024
|
+
)
|
|
2025
|
+
const credential = await x402PaymentSignature(
|
|
2026
|
+
paymentRequired.accepts[0]!,
|
|
2027
|
+
paymentRequired.resource,
|
|
2028
|
+
paymentRequired.extensions,
|
|
2029
|
+
)
|
|
2030
|
+
|
|
2031
|
+
const result = await handle(
|
|
2032
|
+
new Request('https://example.com/resource', {
|
|
2033
|
+
headers: { [x402_Types.paymentSignatureHeader]: credential },
|
|
2034
|
+
}),
|
|
2035
|
+
)
|
|
2036
|
+
|
|
2037
|
+
expect(result.status).toBe(200)
|
|
2038
|
+
if (result.status !== 200) throw new Error()
|
|
2039
|
+
const response = result.withReceipt(new Response('paid'))
|
|
2040
|
+
expect(response.headers.get(x402_Types.paymentResponseHeader)).toBeTruthy()
|
|
2041
|
+
expect(await response.text()).toBe('paid')
|
|
2042
|
+
})
|
|
2043
|
+
|
|
1882
2044
|
test('filters compose challenges using Accept-Payment', async () => {
|
|
1883
2045
|
const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
|
|
1884
2046
|
|
|
@@ -1899,6 +2061,27 @@ describe('compose', () => {
|
|
|
1899
2061
|
expect(challenges[0]?.method).toBe('beta')
|
|
1900
2062
|
})
|
|
1901
2063
|
|
|
2064
|
+
test('filters compose x402 challenge headers using Accept-Payment', async () => {
|
|
2065
|
+
const mppx = Mppx.create({ methods: [alphaMethod, x402Method], realm, secretKey })
|
|
2066
|
+
|
|
2067
|
+
const result = await mppx.compose(
|
|
2068
|
+
[alphaMethod, challengeOpts],
|
|
2069
|
+
['evm/charge', { amount: '0.01' }],
|
|
2070
|
+
)(
|
|
2071
|
+
new Request('https://example.com/resource', {
|
|
2072
|
+
headers: { 'Accept-Payment': 'alpha/charge' },
|
|
2073
|
+
}),
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
expect(result.status).toBe(402)
|
|
2077
|
+
if (result.status !== 402) throw new Error()
|
|
2078
|
+
|
|
2079
|
+
const challenges = Challenge.fromResponseList(result.challenge)
|
|
2080
|
+
expect(challenges).toHaveLength(1)
|
|
2081
|
+
expect(challenges[0]?.method).toBe('alpha')
|
|
2082
|
+
expect(result.challenge.headers.get(x402_Types.paymentRequiredHeader)).toBeNull()
|
|
2083
|
+
})
|
|
2084
|
+
|
|
1902
2085
|
test('orders compose challenges by Accept-Payment q-value', async () => {
|
|
1903
2086
|
const mppx = Mppx.create({ methods: [alphaMethod, betaMethod], realm, secretKey })
|
|
1904
2087
|
|
|
@@ -2522,6 +2705,66 @@ describe('compose', () => {
|
|
|
2522
2705
|
})
|
|
2523
2706
|
})
|
|
2524
2707
|
|
|
2708
|
+
async function x402PaymentSignature(
|
|
2709
|
+
accepted: x402_Types.PaymentRequirements,
|
|
2710
|
+
resource: x402_Types.ResourceInfo,
|
|
2711
|
+
extensions: x402_Types.Extensions | undefined,
|
|
2712
|
+
): Promise<string> {
|
|
2713
|
+
const authorization = {
|
|
2714
|
+
from: accounts[0].address,
|
|
2715
|
+
nonce: x402_RouteBinding.nonce({
|
|
2716
|
+
accepted,
|
|
2717
|
+
extensions: extensions!,
|
|
2718
|
+
resource,
|
|
2719
|
+
}),
|
|
2720
|
+
to: accepted.payTo as `0x${string}`,
|
|
2721
|
+
validAfter: '0',
|
|
2722
|
+
validBefore: '9999999999',
|
|
2723
|
+
value: accepted.amount,
|
|
2724
|
+
}
|
|
2725
|
+
const signature = await accounts[0].signTypedData({
|
|
2726
|
+
domain: {
|
|
2727
|
+
chainId: Number(accepted.network.slice(x402_Types.evmNetworkPrefix.length)),
|
|
2728
|
+
name: accepted.extra?.name as string,
|
|
2729
|
+
verifyingContract: accepted.asset as `0x${string}`,
|
|
2730
|
+
version: accepted.extra?.version as string,
|
|
2731
|
+
},
|
|
2732
|
+
message: {
|
|
2733
|
+
...authorization,
|
|
2734
|
+
validAfter: BigInt(authorization.validAfter),
|
|
2735
|
+
validBefore: BigInt(authorization.validBefore),
|
|
2736
|
+
value: BigInt(authorization.value),
|
|
2737
|
+
},
|
|
2738
|
+
primaryType: 'TransferWithAuthorization',
|
|
2739
|
+
types: {
|
|
2740
|
+
TransferWithAuthorization: [
|
|
2741
|
+
{ name: 'from', type: 'address' },
|
|
2742
|
+
{ name: 'to', type: 'address' },
|
|
2743
|
+
{ name: 'value', type: 'uint256' },
|
|
2744
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
2745
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
2746
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
2747
|
+
],
|
|
2748
|
+
},
|
|
2749
|
+
})
|
|
2750
|
+
|
|
2751
|
+
return x402_Header.encodePaymentSignature({
|
|
2752
|
+
accepted,
|
|
2753
|
+
...(extensions ? { extensions } : {}),
|
|
2754
|
+
payload: {
|
|
2755
|
+
authorization,
|
|
2756
|
+
signature,
|
|
2757
|
+
},
|
|
2758
|
+
resource,
|
|
2759
|
+
x402Version: 2,
|
|
2760
|
+
})
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
function payerOf(paymentPayload: PaymentPayload): string {
|
|
2764
|
+
if ('authorization' in paymentPayload.payload) return paymentPayload.payload.authorization.from
|
|
2765
|
+
return paymentPayload.payload.permit2Authorization.from
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2525
2768
|
describe('compose: pre-dispatch narrowing edge cases', () => {
|
|
2526
2769
|
const mockCharge = Method.from({
|
|
2527
2770
|
name: 'alpha',
|