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
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
|
|
package/src/server/Transport.ts
CHANGED
|
@@ -26,6 +26,18 @@ export type Transport<
|
|
|
26
26
|
name: string
|
|
27
27
|
/** Captures the transport request into an immutable verification snapshot. */
|
|
28
28
|
captureRequest?: ((input: input) => MaybePromise<Method.CapturedRequest>) | undefined
|
|
29
|
+
/**
|
|
30
|
+
* Rebinds a transport-native credential to the route challenge after request
|
|
31
|
+
* normalization. Transports with non-Payment-auth wire formats can parse their
|
|
32
|
+
* payload early, then attach the canonical mppx challenge here.
|
|
33
|
+
*/
|
|
34
|
+
bindCredential?:
|
|
35
|
+
| ((options: {
|
|
36
|
+
challenge: Challenge.Challenge
|
|
37
|
+
credential: Credential.Credential
|
|
38
|
+
input: input
|
|
39
|
+
}) => MaybePromise<Credential.Credential>)
|
|
40
|
+
| undefined
|
|
29
41
|
/**
|
|
30
42
|
* Extracts credential from the transport input.
|
|
31
43
|
* Returns `null` if no credential was provided, or throws if malformed.
|
|
@@ -196,6 +208,7 @@ export function http(): Http {
|
|
|
196
208
|
|
|
197
209
|
respondReceipt({ receipt, response }) {
|
|
198
210
|
const headers = new Headers(response.headers)
|
|
211
|
+
headers.set('Cache-Control', withPrivateCacheControl(headers.get('Cache-Control')))
|
|
199
212
|
headers.set('Payment-Receipt', Receipt.serialize(receipt))
|
|
200
213
|
return new Response(response.body, {
|
|
201
214
|
status: response.status,
|
|
@@ -206,6 +219,13 @@ export function http(): Http {
|
|
|
206
219
|
})
|
|
207
220
|
}
|
|
208
221
|
|
|
222
|
+
function withPrivateCacheControl(value: string | null): string {
|
|
223
|
+
if (!value) return 'private'
|
|
224
|
+
const directives = value.split(',').map((directive) => directive.trim().toLowerCase())
|
|
225
|
+
if (directives.includes('private')) return value
|
|
226
|
+
return `${value}, private`
|
|
227
|
+
}
|
|
228
|
+
|
|
209
229
|
/**
|
|
210
230
|
* MCP transport for server-side payment handling with raw JSON-RPC.
|
|
211
231
|
*
|
package/src/server/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * as Expires from '../Expires.js'
|
|
2
2
|
export * as Store from '../Store.js'
|
|
3
|
-
export { stripe, tempo } from './Methods.js'
|
|
3
|
+
export { evm, stripe, tempo } from './Methods.js'
|
|
4
4
|
export * as Mppx from './Mppx.js'
|
|
5
5
|
export * as NodeListener from './NodeListener.js'
|
|
6
6
|
export * as Request from './Request.js'
|
|
@@ -4,13 +4,18 @@ import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
|
4
4
|
import * as Http from '~test/Http.js'
|
|
5
5
|
|
|
6
6
|
import type { StripeClient } from '../internal/types.js'
|
|
7
|
+
import type { charge as StripeCharge } from './Charge.js'
|
|
7
8
|
|
|
8
9
|
const realm = 'api.example.com'
|
|
9
10
|
const secretKey = 'test-secret-key'
|
|
10
11
|
|
|
11
12
|
let httpServer: Awaited<ReturnType<typeof Http.createServer>> | undefined
|
|
12
13
|
|
|
13
|
-
afterEach(() =>
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
httpServer?.close()
|
|
16
|
+
httpServer = undefined
|
|
17
|
+
vi.restoreAllMocks()
|
|
18
|
+
})
|
|
14
19
|
|
|
15
20
|
function createMockStripeClient(
|
|
16
21
|
overrides?: Partial<{ status: string; id: string; throws: boolean }>,
|
|
@@ -126,6 +131,215 @@ describe('stripe.charge with client', () => {
|
|
|
126
131
|
expect(params.metadata.mpp_is_mpp).toBe('true')
|
|
127
132
|
})
|
|
128
133
|
|
|
134
|
+
test('behavior: applies Connect settlement parameters in client call', async () => {
|
|
135
|
+
const { client, create } = createMockStripeClient()
|
|
136
|
+
|
|
137
|
+
const server = Mppx.create({
|
|
138
|
+
methods: [
|
|
139
|
+
stripe.charge({
|
|
140
|
+
client,
|
|
141
|
+
connect({ request }) {
|
|
142
|
+
expect(request.amount).toBe('100')
|
|
143
|
+
return {
|
|
144
|
+
applicationFeeAmount: 12,
|
|
145
|
+
onBehalfOf: 'acct_merchant',
|
|
146
|
+
stripeAccount: 'acct_connected',
|
|
147
|
+
transferData: { amount: 88, destination: 'acct_destination' },
|
|
148
|
+
transferGroup: 'order_123',
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
networkId: 'internal',
|
|
152
|
+
paymentMethodTypes: ['card'],
|
|
153
|
+
}),
|
|
154
|
+
],
|
|
155
|
+
realm,
|
|
156
|
+
secretKey,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const handle = server.charge({ amount: '1', currency: 'usd', decimals: 2 })
|
|
160
|
+
const firstResult = await handle(new Request('https://example.com'))
|
|
161
|
+
expect(firstResult.status).toBe(402)
|
|
162
|
+
if (firstResult.status !== 402) throw new Error()
|
|
163
|
+
|
|
164
|
+
const challenge = Challenge.fromResponse(firstResult.challenge)
|
|
165
|
+
expect(challenge.request).not.toHaveProperty('connect')
|
|
166
|
+
expect(challenge.request.methodDetails).not.toHaveProperty('applicationFeeAmount')
|
|
167
|
+
expect(challenge.request.methodDetails).not.toHaveProperty('stripeAccount')
|
|
168
|
+
|
|
169
|
+
const credential = Credential.from({
|
|
170
|
+
challenge,
|
|
171
|
+
payload: { spt: 'spt_test_token' },
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const result = await handle(
|
|
175
|
+
new Request('https://example.com', {
|
|
176
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
expect(result.status).toBe(200)
|
|
181
|
+
const [params, options] = create.mock.calls[0]!
|
|
182
|
+
expect(params).toMatchObject({
|
|
183
|
+
application_fee_amount: 12,
|
|
184
|
+
on_behalf_of: 'acct_merchant',
|
|
185
|
+
transfer_data: { amount: 88, destination: 'acct_destination' },
|
|
186
|
+
transfer_group: 'order_123',
|
|
187
|
+
})
|
|
188
|
+
expect(options).toMatchObject({ stripeAccount: 'acct_connected' })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('behavior: applies Connect settlement parameters in secretKey call', async () => {
|
|
192
|
+
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
193
|
+
new Response(JSON.stringify({ id: 'pi_fetch_123', status: 'succeeded' }), {
|
|
194
|
+
status: 200,
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const server = Mppx.create({
|
|
199
|
+
methods: [
|
|
200
|
+
stripe.charge({
|
|
201
|
+
connect: {
|
|
202
|
+
applicationFeeAmount: 12,
|
|
203
|
+
onBehalfOf: 'acct_merchant',
|
|
204
|
+
stripeAccount: 'acct_connected',
|
|
205
|
+
transferData: { amount: 88, destination: 'acct_destination' },
|
|
206
|
+
transferGroup: 'order_123',
|
|
207
|
+
},
|
|
208
|
+
networkId: 'internal',
|
|
209
|
+
paymentMethodTypes: ['card'],
|
|
210
|
+
secretKey,
|
|
211
|
+
}),
|
|
212
|
+
],
|
|
213
|
+
realm,
|
|
214
|
+
secretKey,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const handle = server.charge({ amount: '1', currency: 'usd', decimals: 2 })
|
|
218
|
+
const firstResult = await handle(new Request('https://example.com'))
|
|
219
|
+
expect(firstResult.status).toBe(402)
|
|
220
|
+
if (firstResult.status !== 402) throw new Error()
|
|
221
|
+
|
|
222
|
+
const credential = Credential.from({
|
|
223
|
+
challenge: Challenge.fromResponse(firstResult.challenge),
|
|
224
|
+
payload: { spt: 'spt_test_token' },
|
|
225
|
+
})
|
|
226
|
+
const result = await handle(
|
|
227
|
+
new Request('https://example.com', {
|
|
228
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
expect(result.status).toBe(200)
|
|
233
|
+
expect(fetchMock).toHaveBeenCalledOnce()
|
|
234
|
+
const [input, init] = fetchMock.mock.calls[0]!
|
|
235
|
+
expect(input).toBe('https://api.stripe.com/v1/payment_intents')
|
|
236
|
+
const headers = new Headers(init?.headers)
|
|
237
|
+
expect(headers.get('Stripe-Account')).toBe('acct_connected')
|
|
238
|
+
const body = init?.body as URLSearchParams
|
|
239
|
+
expect(body.get('application_fee_amount')).toBe('12')
|
|
240
|
+
expect(body.get('on_behalf_of')).toBe('acct_merchant')
|
|
241
|
+
expect(body.get('transfer_data[amount]')).toBe('88')
|
|
242
|
+
expect(body.get('transfer_data[destination]')).toBe('acct_destination')
|
|
243
|
+
expect(body.get('transfer_group')).toBe('order_123')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('error: surfaces Connect PaymentIntent creation failures', async () => {
|
|
247
|
+
const { client } = createMockStripeClient({ throws: true })
|
|
248
|
+
|
|
249
|
+
const server = Mppx.create({
|
|
250
|
+
methods: [
|
|
251
|
+
stripe.charge({
|
|
252
|
+
client,
|
|
253
|
+
connect: { stripeAccount: 'acct_connected' },
|
|
254
|
+
networkId: 'internal',
|
|
255
|
+
paymentMethodTypes: ['card'],
|
|
256
|
+
}),
|
|
257
|
+
],
|
|
258
|
+
realm,
|
|
259
|
+
secretKey,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
httpServer = await Http.createServer(async (req, res) => {
|
|
263
|
+
const result = await Mppx.toNodeListener(
|
|
264
|
+
server.charge({ amount: '1', currency: 'usd', decimals: 2 }),
|
|
265
|
+
)(req, res)
|
|
266
|
+
if (result.status === 402) return
|
|
267
|
+
res.end('OK')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const response = await fetch(httpServer.url)
|
|
271
|
+
const challenge = Challenge.fromResponse(response)
|
|
272
|
+
const credential = Credential.from({
|
|
273
|
+
challenge,
|
|
274
|
+
payload: { spt: 'spt_test_token' },
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const paidResponse = await fetch(httpServer.url, {
|
|
278
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
279
|
+
})
|
|
280
|
+
expect(paidResponse.status).toBe(402)
|
|
281
|
+
const body = (await paidResponse.json()) as { detail: string }
|
|
282
|
+
expect(body.detail).toContain('Stripe PaymentIntent failed')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
const invalidConnectCases: readonly {
|
|
286
|
+
name: string
|
|
287
|
+
connect: StripeCharge.ConnectSettlement
|
|
288
|
+
}[] = [
|
|
289
|
+
{ name: 'empty stripeAccount', connect: { stripeAccount: '' } },
|
|
290
|
+
{ name: 'fee exceeds amount', connect: { applicationFeeAmount: 101 } },
|
|
291
|
+
{ name: 'negative fee', connect: { applicationFeeAmount: -1 } },
|
|
292
|
+
{
|
|
293
|
+
name: 'empty transfer destination',
|
|
294
|
+
connect: { transferData: { destination: '' } },
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: 'missing transfer destination',
|
|
298
|
+
connect: { transferData: {} } as StripeCharge.ConnectSettlement,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'transfer amount exceeds amount',
|
|
302
|
+
connect: { transferData: { amount: 101, destination: 'acct_destination' } },
|
|
303
|
+
},
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
for (const { connect, name } of invalidConnectCases) {
|
|
307
|
+
test(`error: rejects invalid Connect settlement parameters (${name})`, async () => {
|
|
308
|
+
const { client, create } = createMockStripeClient()
|
|
309
|
+
|
|
310
|
+
const server = Mppx.create({
|
|
311
|
+
methods: [
|
|
312
|
+
stripe.charge({
|
|
313
|
+
client,
|
|
314
|
+
connect,
|
|
315
|
+
networkId: 'internal',
|
|
316
|
+
paymentMethodTypes: ['card'],
|
|
317
|
+
}),
|
|
318
|
+
],
|
|
319
|
+
realm,
|
|
320
|
+
secretKey,
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
const handle = server.charge({ amount: '1', currency: 'usd', decimals: 2 })
|
|
324
|
+
const firstResult = await handle(new Request('https://example.com'))
|
|
325
|
+
expect(firstResult.status).toBe(402)
|
|
326
|
+
if (firstResult.status !== 402) throw new Error()
|
|
327
|
+
|
|
328
|
+
const credential = Credential.from({
|
|
329
|
+
challenge: Challenge.fromResponse(firstResult.challenge),
|
|
330
|
+
payload: { spt: 'spt_test_token' },
|
|
331
|
+
})
|
|
332
|
+
const result = await handle(
|
|
333
|
+
new Request('https://example.com', {
|
|
334
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
335
|
+
}),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
expect(result.status).toBe(402)
|
|
339
|
+
expect(create).not.toHaveBeenCalled()
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
129
343
|
test('behavior: rejects when client throws', async () => {
|
|
130
344
|
const { client } = createMockStripeClient({ throws: true })
|
|
131
345
|
|