mppx 0.6.28 → 0.6.30
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 +23 -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/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 +10 -0
- package/dist/evm/index.d.ts.map +1 -0
- package/dist/evm/index.js +9 -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.map +1 -1
- package/dist/proxy/internal/Headers.js +11 -1
- 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/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.map +1 -1
- package/dist/tempo/server/Charge.js +23 -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 +13 -12
- package/dist/tempo/server/Session.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 +54 -0
- package/src/Challenge.ts +17 -10
- package/src/Method.ts +1 -1
- 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 +114 -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 +14 -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 +20 -0
- package/src/proxy/internal/Headers.ts +12 -1
- 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/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 +69 -0
- package/src/tempo/server/Charge.ts +32 -0
- package/src/tempo/server/Session.test.ts +30 -0
- package/src/tempo/server/Session.ts +15 -16
- 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/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/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
|
@@ -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
|
|
@@ -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',
|
package/src/server/Mppx.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type { MaybePromise } from '../internal/types.js'
|
|
|
11
11
|
import type * as Method from '../Method.js'
|
|
12
12
|
import * as PaymentRequest from '../PaymentRequest.js'
|
|
13
13
|
import type * as Receipt from '../Receipt.js'
|
|
14
|
+
import * as x402_Header from '../x402/Header.js'
|
|
15
|
+
import * as x402_Types from '../x402/Types.js'
|
|
14
16
|
import * as z from '../zod.js'
|
|
15
17
|
import * as Html from './internal/html/config.js'
|
|
16
18
|
import { serviceWorker } from './internal/html/serviceWorker.gen.js'
|
|
@@ -790,7 +792,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
790
792
|
: staticMeta
|
|
791
793
|
|
|
792
794
|
// Extract credential once — getCredential may have side effects (e.g. SSE transports).
|
|
793
|
-
|
|
795
|
+
let [credential, credentialError] = (() => {
|
|
794
796
|
try {
|
|
795
797
|
const credential = transport.getCredential(input) as Credential.Credential | null
|
|
796
798
|
return [credential ? hydrateCredentialMeta(credential) : null, undefined] as const
|
|
@@ -898,6 +900,21 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
898
900
|
if ('response' in routeChallenge) return { challenge: routeChallenge.response, status: 402 }
|
|
899
901
|
const { challenge, parsedRequest, request } = routeChallenge
|
|
900
902
|
|
|
903
|
+
if (credential && transport.bindCredential) {
|
|
904
|
+
try {
|
|
905
|
+
credential = hydrateCredentialMeta(
|
|
906
|
+
(await transport.bindCredential({
|
|
907
|
+
challenge,
|
|
908
|
+
credential,
|
|
909
|
+
input,
|
|
910
|
+
})) as Credential.Credential,
|
|
911
|
+
)
|
|
912
|
+
} catch (e) {
|
|
913
|
+
credential = null
|
|
914
|
+
credentialError = e as Error
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
901
918
|
// Credential was provided but malformed
|
|
902
919
|
if (credentialError) {
|
|
903
920
|
const reason = getSafeCredentialReason(credentialError)
|
|
@@ -2004,6 +2021,29 @@ type ConfiguredHandler = ((input: Request) => Promise<MethodFn.Response<Transpor
|
|
|
2004
2021
|
}
|
|
2005
2022
|
}
|
|
2006
2023
|
|
|
2024
|
+
const paymentAuthChallengeHeader = 'WWW-Authenticate'
|
|
2025
|
+
|
|
2026
|
+
const challengeHeaderMerges = [
|
|
2027
|
+
{
|
|
2028
|
+
name: paymentAuthChallengeHeader,
|
|
2029
|
+
values: (context) =>
|
|
2030
|
+
context.challengeEntries
|
|
2031
|
+
.map((entry) =>
|
|
2032
|
+
(entry.result.challenge as Response).headers.get(paymentAuthChallengeHeader),
|
|
2033
|
+
)
|
|
2034
|
+
.filter((value): value is string => value !== null),
|
|
2035
|
+
merge: (values) => values,
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
name: x402_Types.paymentRequiredHeader,
|
|
2039
|
+
values: (context) =>
|
|
2040
|
+
context.negotiatedChallengeResponses
|
|
2041
|
+
.map((response) => response.headers.get(x402_Types.paymentRequiredHeader))
|
|
2042
|
+
.filter((value): value is string => value !== null),
|
|
2043
|
+
merge: mergeX402PaymentRequiredHeaders,
|
|
2044
|
+
},
|
|
2045
|
+
] satisfies readonly ChallengeHeaderMerge[]
|
|
2046
|
+
|
|
2007
2047
|
/** An entry for `compose()`: a method reference, handler function ref, or string key paired with its options. */
|
|
2008
2048
|
type ComposeEntry<methods extends readonly Method.AnyServer[]> =
|
|
2009
2049
|
| {
|
|
@@ -2172,7 +2212,7 @@ export function compose(
|
|
|
2172
2212
|
if (result?.status !== 402) continue
|
|
2173
2213
|
|
|
2174
2214
|
const response = result.challenge as Response
|
|
2175
|
-
const wwwAuth = response.headers.get(
|
|
2215
|
+
const wwwAuth = response.headers.get(paymentAuthChallengeHeader)
|
|
2176
2216
|
if (!wwwAuth) continue
|
|
2177
2217
|
|
|
2178
2218
|
entries.push({
|
|
@@ -2199,14 +2239,39 @@ export function compose(
|
|
|
2199
2239
|
}
|
|
2200
2240
|
})()
|
|
2201
2241
|
|
|
2202
|
-
|
|
2242
|
+
const challengeResponses = results.flatMap((result) =>
|
|
2243
|
+
result.status === 402 ? [result.challenge as Response] : [],
|
|
2244
|
+
)
|
|
2245
|
+
const unnegotiatedX402Responses =
|
|
2246
|
+
input.headers.has('Accept-Payment') || challengeEntries.length === 0
|
|
2247
|
+
? []
|
|
2248
|
+
: challengeResponses.filter(
|
|
2249
|
+
(response) =>
|
|
2250
|
+
response.headers.has(x402_Types.paymentRequiredHeader) &&
|
|
2251
|
+
!response.headers.has(paymentAuthChallengeHeader),
|
|
2252
|
+
)
|
|
2253
|
+
const negotiatedChallengeResponses =
|
|
2254
|
+
challengeEntries.length > 0
|
|
2255
|
+
? [
|
|
2256
|
+
...challengeEntries.map((entry) => entry.result.challenge as Response),
|
|
2257
|
+
...unnegotiatedX402Responses,
|
|
2258
|
+
]
|
|
2259
|
+
: challengeResponses
|
|
2260
|
+
|
|
2261
|
+
// Merge challenge headers from the negotiated 402 responses.
|
|
2203
2262
|
const mergedHeaders = new Headers()
|
|
2204
2263
|
mergedHeaders.set('Cache-Control', 'no-store')
|
|
2205
2264
|
|
|
2206
|
-
for (const
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
|
|
2265
|
+
for (const header of challengeHeaderMerges) {
|
|
2266
|
+
for (const value of header.merge(
|
|
2267
|
+
header.values({
|
|
2268
|
+
challengeEntries,
|
|
2269
|
+
challengeResponses,
|
|
2270
|
+
negotiatedChallengeResponses,
|
|
2271
|
+
}),
|
|
2272
|
+
)) {
|
|
2273
|
+
mergedHeaders.append(header.name, value)
|
|
2274
|
+
}
|
|
2210
2275
|
}
|
|
2211
2276
|
|
|
2212
2277
|
// Collect html-enabled handlers and their challenges
|
|
@@ -2257,11 +2322,15 @@ export function compose(
|
|
|
2257
2322
|
}
|
|
2258
2323
|
}
|
|
2259
2324
|
|
|
2260
|
-
// Non-HTML fallback:
|
|
2325
|
+
// Non-HTML fallback: prefer the first Payment-auth body, otherwise use
|
|
2326
|
+
// the first transport-specific 402 body.
|
|
2261
2327
|
let body: string | null = null
|
|
2262
|
-
|
|
2328
|
+
const bodyResponses =
|
|
2329
|
+
challengeEntries.length > 0
|
|
2330
|
+
? challengeEntries.map((entry) => entry.result.challenge as Response)
|
|
2331
|
+
: challengeResponses
|
|
2332
|
+
for (const response of bodyResponses) {
|
|
2263
2333
|
if (!body) {
|
|
2264
|
-
const response = entry.result.challenge as Response
|
|
2265
2334
|
const contentType = response.headers.get('Content-Type')
|
|
2266
2335
|
if (contentType) mergedHeaders.set('Content-Type', contentType)
|
|
2267
2336
|
body = await response.text()
|
|
@@ -2276,6 +2345,50 @@ export function compose(
|
|
|
2276
2345
|
}
|
|
2277
2346
|
}
|
|
2278
2347
|
|
|
2348
|
+
type ChallengeHeaderMerge = {
|
|
2349
|
+
name: string
|
|
2350
|
+
values(context: {
|
|
2351
|
+
challengeEntries: readonly {
|
|
2352
|
+
handler: ConfiguredHandler
|
|
2353
|
+
challenge: Challenge.Challenge
|
|
2354
|
+
result: Extract<MethodFn.Response<Transport.Http>, { status: 402 }>
|
|
2355
|
+
}[]
|
|
2356
|
+
challengeResponses: readonly Response[]
|
|
2357
|
+
negotiatedChallengeResponses: readonly Response[]
|
|
2358
|
+
}): readonly string[]
|
|
2359
|
+
merge(values: readonly string[]): readonly string[]
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
function mergeX402PaymentRequiredHeaders(values: readonly string[]): readonly string[] {
|
|
2363
|
+
if (values.length === 0) return []
|
|
2364
|
+
const [first, ...rest] = values.map((value) => x402_Header.decodePaymentRequired(value))
|
|
2365
|
+
if (!first) throw new Error('Expected at least one x402 payment-required header.')
|
|
2366
|
+
const incompatible = rest.some(
|
|
2367
|
+
(value) =>
|
|
2368
|
+
!isDeepStrictEqual(value.resource, first.resource) ||
|
|
2369
|
+
!isDeepStrictEqual(value.extensions, first.extensions),
|
|
2370
|
+
)
|
|
2371
|
+
if (incompatible)
|
|
2372
|
+
return [
|
|
2373
|
+
x402_Header.encodePaymentRequired({
|
|
2374
|
+
...first,
|
|
2375
|
+
error: [first.error, 'Cannot merge x402 payment requirements with different resources.']
|
|
2376
|
+
.filter((value): value is string => value !== undefined && value.length > 0)
|
|
2377
|
+
.join('; '),
|
|
2378
|
+
}),
|
|
2379
|
+
]
|
|
2380
|
+
const error = [first.error, ...rest.map((value) => value.error)]
|
|
2381
|
+
.filter((value): value is string => value !== undefined && value.length > 0)
|
|
2382
|
+
.join('; ')
|
|
2383
|
+
return [
|
|
2384
|
+
x402_Header.encodePaymentRequired({
|
|
2385
|
+
...first,
|
|
2386
|
+
accepts: [first.accepts, ...rest.map((value) => value.accepts)].flat(),
|
|
2387
|
+
...(error ? { error } : {}),
|
|
2388
|
+
}),
|
|
2389
|
+
]
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2279
2392
|
/**
|
|
2280
2393
|
* Wraps a payment handler to create a Node.js HTTP listener.
|
|
2281
2394
|
*
|
|
@@ -2316,7 +2429,7 @@ export function toNodeListener(
|
|
|
2316
2429
|
}
|
|
2317
2430
|
|
|
2318
2431
|
const wrapped = result.withReceipt(new globalThis.Response()) as globalThis.Response
|
|
2319
|
-
|
|
2432
|
+
for (const [name, value] of wrapped.headers) res.setHeader(name, value)
|
|
2320
2433
|
}
|
|
2321
2434
|
|
|
2322
2435
|
return result
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
3
3
|
|
|
4
|
-
import { NodeListener, Request } from 'mppx/server'
|
|
4
|
+
import { Mppx, NodeListener, Request } from 'mppx/server'
|
|
5
5
|
import { afterEach, describe, expect, test } from 'vp/test'
|
|
6
6
|
import * as Http from '~test/Http.js'
|
|
7
7
|
|
|
@@ -185,6 +185,33 @@ describe('toNodeListener', () => {
|
|
|
185
185
|
expect(await response.json()).toEqual({ path: '/hello' })
|
|
186
186
|
})
|
|
187
187
|
|
|
188
|
+
test('copies all receipt response headers for successful payment handlers', async () => {
|
|
189
|
+
const handler = Mppx.toNodeListener(async () => ({
|
|
190
|
+
status: 200,
|
|
191
|
+
withReceipt(response?: Response) {
|
|
192
|
+
if (!response) {
|
|
193
|
+
const error = new Error('withReceipt() requires a response argument')
|
|
194
|
+
error.name = 'MissingReceiptResponseError'
|
|
195
|
+
throw error
|
|
196
|
+
}
|
|
197
|
+
const headers = new Headers(response?.headers)
|
|
198
|
+
headers.set('Payment-Receipt', 'receipt')
|
|
199
|
+
headers.set('PAYMENT-RESPONSE', 'x402-response')
|
|
200
|
+
return new Response(response?.body, { headers })
|
|
201
|
+
},
|
|
202
|
+
}))
|
|
203
|
+
|
|
204
|
+
server = await Http.createServer(async (req, res) => {
|
|
205
|
+
await handler(req, res)
|
|
206
|
+
res.end('ok')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const response = await fetch(server.url)
|
|
210
|
+
expect(response.headers.get('Payment-Receipt')).toBe('receipt')
|
|
211
|
+
expect(response.headers.get('PAYMENT-RESPONSE')).toBe('x402-response')
|
|
212
|
+
expect(await response.text()).toBe('ok')
|
|
213
|
+
})
|
|
214
|
+
|
|
188
215
|
test('forwards request method', async () => {
|
|
189
216
|
const handler = Request.toNodeListener(async (request) => {
|
|
190
217
|
return Response.json({ method: request.method })
|
|
@@ -408,6 +408,7 @@ describe('http', () => {
|
|
|
408
408
|
}).toMatchInlineSnapshot(`
|
|
409
409
|
{
|
|
410
410
|
"headers": {
|
|
411
|
+
"cache-control": "private",
|
|
411
412
|
"content-type": "text/plain;charset=UTF-8",
|
|
412
413
|
"payment-receipt": "eyJtZXRob2QiOiJ0ZW1wbyIsInJlZmVyZW5jZSI6IjB4dHhoYXNoIiwic3RhdHVzIjoic3VjY2VzcyIsInRpbWVzdGFtcCI6IjIwMjUtMDEtMDFUMDA6MDA6MDAuMDAwWiJ9",
|
|
413
414
|
},
|
|
@@ -415,6 +416,24 @@ describe('http', () => {
|
|
|
415
416
|
}
|
|
416
417
|
`)
|
|
417
418
|
})
|
|
419
|
+
|
|
420
|
+
test('preserves existing cache directives while marking receipts private', () => {
|
|
421
|
+
const transport = Transport.http()
|
|
422
|
+
const originalResponse = new Response('OK', {
|
|
423
|
+
status: 200,
|
|
424
|
+
headers: { 'Cache-Control': 'max-age=60' },
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const response = transport.respondReceipt({
|
|
428
|
+
credential,
|
|
429
|
+
input: new Request('https://example.com'),
|
|
430
|
+
receipt,
|
|
431
|
+
response: originalResponse,
|
|
432
|
+
challengeId: challenge.id,
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
expect(response.headers.get('Cache-Control')).toBe('max-age=60, private')
|
|
436
|
+
})
|
|
418
437
|
})
|
|
419
438
|
})
|
|
420
439
|
|