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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Bytes, Hash } from 'ox'
|
|
2
|
+
|
|
3
|
+
import * as PaymentRequest from '../../PaymentRequest.js'
|
|
4
|
+
import type * as Types from '../Types.js'
|
|
5
|
+
|
|
6
|
+
/** Computes the route-bound EIP-3009 nonce for an x402 exact payment. */
|
|
7
|
+
export function nonce(parameters: {
|
|
8
|
+
accepted: Types.PaymentRequirements
|
|
9
|
+
extensions: Types.Extensions
|
|
10
|
+
resource: Types.ResourceInfo
|
|
11
|
+
}): `0x${string}` {
|
|
12
|
+
const input = [
|
|
13
|
+
PaymentRequest.serialize(parameters.accepted),
|
|
14
|
+
PaymentRequest.serialize(parameters.resource),
|
|
15
|
+
PaymentRequest.serialize(parameters.extensions),
|
|
16
|
+
].join('|')
|
|
17
|
+
return Hash.sha256(Bytes.fromString(input), { as: 'Hex' }) as `0x${string}`
|
|
18
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
2
|
+
|
|
3
|
+
import { Bytes, Hash } from 'ox'
|
|
4
|
+
import { getAddress } from 'viem'
|
|
5
|
+
|
|
6
|
+
import * as BodyDigest from '../../BodyDigest.js'
|
|
7
|
+
import * as Challenge from '../../Challenge.js'
|
|
8
|
+
import type * as Credential from '../../Credential.js'
|
|
9
|
+
import * as Credential_ from '../../Credential.js'
|
|
10
|
+
import { VerificationFailedError } from '../../Errors.js'
|
|
11
|
+
import * as Types from '../../evm/Types.js'
|
|
12
|
+
import * as PaymentRequest from '../../PaymentRequest.js'
|
|
13
|
+
import * as Scope from '../../server/internal/scope.js'
|
|
14
|
+
import * as ServerTransport from '../../server/Transport.js'
|
|
15
|
+
import * as x402_Header from '../Header.js'
|
|
16
|
+
import * as x402_RouteBinding from '../internal/RouteBinding.js'
|
|
17
|
+
import * as x402_Types from '../Types.js'
|
|
18
|
+
import * as x402_Facilitator from './Facilitator.js'
|
|
19
|
+
|
|
20
|
+
const pendingX402Credential = Symbol('mppx.evm.pendingX402Credential')
|
|
21
|
+
const x402Credential = Symbol('mppx.evm.x402Credential')
|
|
22
|
+
const mppxExtensionKey = 'mppx'
|
|
23
|
+
const mppxRouteBindingSchema = {
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
properties: {
|
|
26
|
+
[Scope.reservedMetaKey]: { type: 'string' },
|
|
27
|
+
digest: { type: 'string' },
|
|
28
|
+
method: { type: 'string' },
|
|
29
|
+
nonce: { type: 'string' },
|
|
30
|
+
opaque: { type: 'string' },
|
|
31
|
+
},
|
|
32
|
+
required: ['method'],
|
|
33
|
+
type: 'object',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Options = {
|
|
37
|
+
/** Facilitator client or base URL used for x402-compatible settlement. */
|
|
38
|
+
facilitator?: string | x402_Types.Facilitator | undefined
|
|
39
|
+
/** Fetch implementation used for facilitator RPCs. */
|
|
40
|
+
fetch?: typeof globalThis.fetch | undefined
|
|
41
|
+
/** Maximum time in seconds allowed for x402-compatible payment completion. @default 300 */
|
|
42
|
+
maxTimeoutSeconds?: number | undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ResolvedOptions = {
|
|
46
|
+
authorization: Types.AuthorizationConfig
|
|
47
|
+
facilitator?: x402_Types.Facilitator | undefined
|
|
48
|
+
maxTimeoutSeconds: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type Path = {
|
|
52
|
+
bindCredential: NonNullable<ServerTransport.Http['bindCredential']>
|
|
53
|
+
getCredential: ServerTransport.Http['getCredential']
|
|
54
|
+
respondChallenge: (
|
|
55
|
+
options: Parameters<ServerTransport.Http['respondChallenge']>[0],
|
|
56
|
+
response?: Response | undefined,
|
|
57
|
+
) => Response | Promise<Response>
|
|
58
|
+
respondReceipt: (
|
|
59
|
+
options: Parameters<ServerTransport.Http['respondReceipt']>[0],
|
|
60
|
+
response: Response,
|
|
61
|
+
) => Response
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Resolves optional x402 compatibility options for an EVM charge. */
|
|
65
|
+
export function resolveOptions(parameters: {
|
|
66
|
+
authorization: Types.AuthorizationConfig
|
|
67
|
+
options?: Options | undefined
|
|
68
|
+
}): ResolvedOptions {
|
|
69
|
+
return {
|
|
70
|
+
authorization: parameters.authorization,
|
|
71
|
+
...(parameters.options?.facilitator
|
|
72
|
+
? {
|
|
73
|
+
facilitator: x402_Facilitator.resolve(
|
|
74
|
+
parameters.options.facilitator,
|
|
75
|
+
'EVM authorization x402 requires `facilitator`.',
|
|
76
|
+
{ fetch: parameters.options.fetch },
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
: {}),
|
|
80
|
+
maxTimeoutSeconds: parameters.options?.maxTimeoutSeconds ?? 300,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Creates the x402 wire path for an EVM charge method. */
|
|
85
|
+
export function createPath(config: ResolvedOptions): Path {
|
|
86
|
+
return {
|
|
87
|
+
getCredential(request) {
|
|
88
|
+
const paymentSignature = request.headers.get(x402_Types.paymentSignatureHeader)
|
|
89
|
+
if (!paymentSignature) return null
|
|
90
|
+
const paymentPayload = x402_Header.decodePaymentSignature(paymentSignature)
|
|
91
|
+
|
|
92
|
+
return markPendingCredential(
|
|
93
|
+
Credential_.from({
|
|
94
|
+
challenge: pendingChallenge(paymentPayload),
|
|
95
|
+
payload: paymentPayload,
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async bindCredential({ challenge, credential, input }) {
|
|
101
|
+
const paymentPayload = parsePaymentPayload(credential.payload)
|
|
102
|
+
if (!paymentPayload) return credential
|
|
103
|
+
if (!isPendingCredential(credential)) return credential
|
|
104
|
+
await assertBodyDigest(challenge, input)
|
|
105
|
+
|
|
106
|
+
const request = challenge.request as Types.ChargeRequest
|
|
107
|
+
const paymentRequirements = toPaymentRequirements(request, config)
|
|
108
|
+
if (!isDeepStrictEqual(paymentPayload.accepted, paymentRequirements))
|
|
109
|
+
throw new VerificationFailedError({
|
|
110
|
+
reason: 'x402 payment payload does not match route requirements',
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const expectedResource = { url: input.url }
|
|
114
|
+
if (!isDeepStrictEqual(paymentPayload.resource, expectedResource))
|
|
115
|
+
throw new VerificationFailedError({
|
|
116
|
+
reason: 'x402 payment payload resource does not match route resource',
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const expectedExtensions = routeExtensions(challenge, input)
|
|
120
|
+
if (!containsExtensions(paymentPayload.extensions, expectedExtensions))
|
|
121
|
+
throw new VerificationFailedError({
|
|
122
|
+
reason: 'x402 payment payload extensions do not match route binding',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const payload = payloadToAuthorization(paymentPayload)
|
|
126
|
+
const expectedNonce = x402_RouteBinding.nonce({
|
|
127
|
+
accepted: paymentRequirements,
|
|
128
|
+
extensions: paymentPayload.extensions!,
|
|
129
|
+
resource: expectedResource,
|
|
130
|
+
})
|
|
131
|
+
if (payload.nonce !== expectedNonce)
|
|
132
|
+
throw new VerificationFailedError({
|
|
133
|
+
reason: 'x402 authorization nonce does not match route binding',
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return markCredential(
|
|
137
|
+
Credential_.from({
|
|
138
|
+
challenge,
|
|
139
|
+
payload,
|
|
140
|
+
source: Types.toSource({
|
|
141
|
+
address: getAddress(payload.from),
|
|
142
|
+
chainId: request.methodDetails.chainId,
|
|
143
|
+
}),
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
respondChallenge(options, response) {
|
|
149
|
+
if (!response) throw new Error('x402 path requires a base challenge response.')
|
|
150
|
+
if (options.input.body !== null && options.challenge.digest === undefined) return response
|
|
151
|
+
const headers = new Headers(response.headers)
|
|
152
|
+
const request = options.challenge.request as Types.ChargeRequest
|
|
153
|
+
headers.set(
|
|
154
|
+
x402_Types.paymentRequiredHeader,
|
|
155
|
+
x402_Header.encodePaymentRequired({
|
|
156
|
+
accepts: [toPaymentRequirements(request, config)],
|
|
157
|
+
error:
|
|
158
|
+
options.error?.message ?? `${x402_Types.paymentSignatureHeader} header is required`,
|
|
159
|
+
extensions: routeExtensions(options.challenge, options.input),
|
|
160
|
+
resource: { url: options.input.url },
|
|
161
|
+
x402Version: 2,
|
|
162
|
+
}),
|
|
163
|
+
)
|
|
164
|
+
return new Response(response.body, {
|
|
165
|
+
headers,
|
|
166
|
+
status: response.status,
|
|
167
|
+
statusText: response.statusText,
|
|
168
|
+
})
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
respondReceipt(options, response) {
|
|
172
|
+
if (!options.input.headers.has(x402_Types.paymentSignatureHeader)) return response
|
|
173
|
+
|
|
174
|
+
const payload = Types.AuthorizationPayloadSchema.parse(options.credential.payload)
|
|
175
|
+
const request = options.credential.challenge.request as Types.ChargeRequest
|
|
176
|
+
const headers = new Headers(response.headers)
|
|
177
|
+
headers.set(
|
|
178
|
+
x402_Types.paymentResponseHeader,
|
|
179
|
+
x402_Header.encodePaymentResponse({
|
|
180
|
+
network: Types.networkOf(request.methodDetails.chainId),
|
|
181
|
+
payer: payload.from,
|
|
182
|
+
success: true,
|
|
183
|
+
transaction: options.receipt.reference,
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
return new Response(response.body, {
|
|
187
|
+
headers,
|
|
188
|
+
status: response.status,
|
|
189
|
+
statusText: response.statusText,
|
|
190
|
+
})
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Settles a verified EVM authorization through an x402 facilitator. */
|
|
196
|
+
export function settleWithFacilitator(parameters: ResolvedOptions): SettleWithFacilitator {
|
|
197
|
+
const { facilitator, maxTimeoutSeconds } = parameters
|
|
198
|
+
if (!facilitator) throw new Error('EVM authorization x402 requires `facilitator`.')
|
|
199
|
+
|
|
200
|
+
return async ({ payload, request }) => {
|
|
201
|
+
const paymentRequirements = toPaymentRequirements(request, {
|
|
202
|
+
...parameters,
|
|
203
|
+
maxTimeoutSeconds,
|
|
204
|
+
})
|
|
205
|
+
const paymentPayload: x402_Types.PaymentPayload = {
|
|
206
|
+
accepted: paymentRequirements,
|
|
207
|
+
payload: {
|
|
208
|
+
authorization: {
|
|
209
|
+
from: payload.from,
|
|
210
|
+
nonce: payload.nonce,
|
|
211
|
+
to: payload.to,
|
|
212
|
+
validAfter: payload.validAfter,
|
|
213
|
+
validBefore: payload.validBefore,
|
|
214
|
+
value: payload.value,
|
|
215
|
+
},
|
|
216
|
+
signature: payload.signature,
|
|
217
|
+
},
|
|
218
|
+
x402Version: 2,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const verified = await facilitator.verify(paymentPayload, paymentRequirements)
|
|
222
|
+
if (!verified.isValid)
|
|
223
|
+
throw new VerificationFailedError({
|
|
224
|
+
reason:
|
|
225
|
+
verified.invalidMessage ?? verified.invalidReason ?? 'EVM facilitator verify failed',
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const settled = await facilitator.settle(paymentPayload, paymentRequirements)
|
|
229
|
+
if (!settled.success)
|
|
230
|
+
throw new VerificationFailedError({
|
|
231
|
+
reason: settled.errorMessage ?? settled.errorReason ?? 'EVM facilitator settlement failed',
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
reference: settled.transaction,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export type SettleWithFacilitator = (parameters: {
|
|
241
|
+
credential: Credential.Credential<Types.AuthorizationPayload>
|
|
242
|
+
payload: Types.AuthorizationPayload
|
|
243
|
+
request: Types.ChargeRequest
|
|
244
|
+
source: ReturnType<typeof Types.toSource>
|
|
245
|
+
}) => Promise<{
|
|
246
|
+
reference: string
|
|
247
|
+
timestamp?: string | undefined
|
|
248
|
+
}>
|
|
249
|
+
|
|
250
|
+
/** Returns whether a credential was converted from an x402 payment payload. */
|
|
251
|
+
export function isCredential(credential: Credential.Credential): boolean {
|
|
252
|
+
return (credential as { [x402Credential]?: true })[x402Credential] === true
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Returns whether a credential was parsed from the x402 payment header. */
|
|
256
|
+
export function isPendingCredential(credential: Credential.Credential): boolean {
|
|
257
|
+
return (credential as { [pendingX402Credential]?: true })[pendingX402Credential] === true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Converts a native EVM charge request to x402 exact payment requirements. */
|
|
261
|
+
export function toPaymentRequirements(
|
|
262
|
+
request: Types.ChargeRequest,
|
|
263
|
+
config: Pick<ResolvedOptions, 'authorization' | 'maxTimeoutSeconds'>,
|
|
264
|
+
): x402_Types.PaymentRequirements {
|
|
265
|
+
return {
|
|
266
|
+
amount: request.amount,
|
|
267
|
+
asset: request.currency,
|
|
268
|
+
extra: {
|
|
269
|
+
assetTransferMethod: Types.eip3009,
|
|
270
|
+
name: config.authorization.name,
|
|
271
|
+
version: config.authorization.version,
|
|
272
|
+
},
|
|
273
|
+
maxTimeoutSeconds: config.maxTimeoutSeconds,
|
|
274
|
+
network: Types.networkOf(request.methodDetails.chainId),
|
|
275
|
+
payTo: request.recipient,
|
|
276
|
+
scheme: 'exact',
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function parsePaymentPayload(payload: unknown): x402_Types.PaymentPayload | undefined {
|
|
281
|
+
const parsed = x402_Types.PaymentPayloadSchema.safeParse(payload)
|
|
282
|
+
return parsed.success ? parsed.data : undefined
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Converts an x402 EIP-3009 payment payload to the native EVM authorization payload. */
|
|
286
|
+
export function payloadToAuthorization(
|
|
287
|
+
paymentPayload: x402_Types.PaymentPayload,
|
|
288
|
+
): Types.AuthorizationPayload {
|
|
289
|
+
if (!('authorization' in paymentPayload.payload))
|
|
290
|
+
throw new VerificationFailedError({
|
|
291
|
+
reason: 'EVM charge only supports x402 EIP-3009 authorization payloads',
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
return Types.AuthorizationPayloadSchema.parse({
|
|
295
|
+
...paymentPayload.payload.authorization,
|
|
296
|
+
signature: paymentPayload.payload.signature,
|
|
297
|
+
type: 'authorization',
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function pendingChallenge(paymentPayload: x402_Types.PaymentPayload) {
|
|
302
|
+
// The route challenge is built after request normalization in bindCredential().
|
|
303
|
+
// Until then, this deterministic local ID only carries the x402 payload through
|
|
304
|
+
// the standard credential pipeline; it is never HMAC-verified.
|
|
305
|
+
return Challenge.from({
|
|
306
|
+
id: pendingChallengeId(paymentPayload),
|
|
307
|
+
intent: Types.chargeIntent,
|
|
308
|
+
method: Types.paymentMethod,
|
|
309
|
+
realm: 'x402',
|
|
310
|
+
request: paymentPayload.accepted,
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function pendingChallengeId(paymentPayload: x402_Types.PaymentPayload): string {
|
|
315
|
+
const hash = Hash.sha256(Bytes.fromString(JSON.stringify(paymentPayload)), { as: 'Hex' })
|
|
316
|
+
return `${x402_Types.syntheticChallengeIdPrefix}${hash}`
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function routeExtensions(challenge: Challenge.Challenge, input: Request): x402_Types.Extensions {
|
|
320
|
+
const binding: Record<string, unknown> = { method: input.method }
|
|
321
|
+
const scope = Scope.read(challenge.meta)
|
|
322
|
+
if (scope !== undefined) binding[Scope.reservedMetaKey] = scope
|
|
323
|
+
if (challenge.digest !== undefined) binding.digest = challenge.digest
|
|
324
|
+
const opaque =
|
|
325
|
+
challenge.opaque ?? (challenge.meta ? PaymentRequest.serialize(challenge.meta) : undefined)
|
|
326
|
+
if (opaque !== undefined) binding.opaque = opaque
|
|
327
|
+
return {
|
|
328
|
+
[mppxExtensionKey]: {
|
|
329
|
+
info: binding,
|
|
330
|
+
schema: mppxRouteBindingSchema,
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function containsExtensions(
|
|
336
|
+
actual: x402_Types.Extensions | undefined,
|
|
337
|
+
expected: x402_Types.Extensions,
|
|
338
|
+
): boolean {
|
|
339
|
+
if (!actual) return false
|
|
340
|
+
return Object.entries(expected).every(([key, expectedExtension]) => {
|
|
341
|
+
const actualExtension = actual[key]
|
|
342
|
+
return (
|
|
343
|
+
actualExtension !== undefined &&
|
|
344
|
+
isDeepStrictEqual(actualExtension.schema, expectedExtension.schema) &&
|
|
345
|
+
isDeepStrictEqual(stripClientNonce(actualExtension.info), expectedExtension.info)
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function stripClientNonce(info: Record<string, unknown>): Record<string, unknown> {
|
|
351
|
+
const { nonce, ...rest } = info
|
|
352
|
+
if (nonce !== undefined && typeof nonce !== 'string') return info
|
|
353
|
+
return rest
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function assertBodyDigest(challenge: Challenge.Challenge, input: Request): Promise<void> {
|
|
357
|
+
if (input.body === null) return
|
|
358
|
+
if (challenge.digest === undefined)
|
|
359
|
+
throw new VerificationFailedError({
|
|
360
|
+
reason: 'x402 payment requires a body digest for body-bearing requests',
|
|
361
|
+
})
|
|
362
|
+
let body: string
|
|
363
|
+
try {
|
|
364
|
+
body = await input.clone().text()
|
|
365
|
+
} catch {
|
|
366
|
+
throw new VerificationFailedError({
|
|
367
|
+
reason: 'x402 payment cannot bind streaming request body',
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
if (!BodyDigest.verify(challenge.digest as BodyDigest.BodyDigest, body))
|
|
371
|
+
throw new VerificationFailedError({
|
|
372
|
+
reason: 'x402 payment body digest mismatch',
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function markPendingCredential<const credential extends Credential.Credential>(
|
|
377
|
+
credential: credential,
|
|
378
|
+
): credential {
|
|
379
|
+
Object.defineProperty(credential, pendingX402Credential, {
|
|
380
|
+
enumerable: true,
|
|
381
|
+
value: true,
|
|
382
|
+
})
|
|
383
|
+
return credential
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function markCredential<const credential extends Credential.Credential>(
|
|
387
|
+
credential: credential,
|
|
388
|
+
): credential {
|
|
389
|
+
Object.defineProperty(credential, x402Credential, {
|
|
390
|
+
enumerable: true,
|
|
391
|
+
value: true,
|
|
392
|
+
})
|
|
393
|
+
return credential
|
|
394
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
2
|
+
|
|
3
|
+
import type * as Types from '../Types.js'
|
|
4
|
+
import * as Facilitator from './Facilitator.js'
|
|
5
|
+
|
|
6
|
+
const paymentRequirements = {
|
|
7
|
+
amount: '10000',
|
|
8
|
+
asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
9
|
+
extra: {
|
|
10
|
+
assetTransferMethod: 'eip3009',
|
|
11
|
+
name: 'USDC',
|
|
12
|
+
version: '2',
|
|
13
|
+
},
|
|
14
|
+
maxTimeoutSeconds: 60,
|
|
15
|
+
network: 'eip155:84532',
|
|
16
|
+
payTo: '0x209693Bc6afc0C5328bA36FaF03C514EF312287C',
|
|
17
|
+
scheme: 'exact',
|
|
18
|
+
} satisfies Types.PaymentRequirements
|
|
19
|
+
|
|
20
|
+
const paymentPayload = {
|
|
21
|
+
accepted: paymentRequirements,
|
|
22
|
+
payload: {
|
|
23
|
+
authorization: {
|
|
24
|
+
from: '0x857b06519E91e3A54538791bDbb0E22373e36b66',
|
|
25
|
+
nonce: '0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480',
|
|
26
|
+
to: paymentRequirements.payTo,
|
|
27
|
+
validAfter: '1740672089',
|
|
28
|
+
validBefore: '1740672154',
|
|
29
|
+
value: paymentRequirements.amount,
|
|
30
|
+
},
|
|
31
|
+
signature:
|
|
32
|
+
'0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c',
|
|
33
|
+
},
|
|
34
|
+
x402Version: 2,
|
|
35
|
+
} satisfies Types.PaymentPayload
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.unstubAllGlobals()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('x402 facilitator http client', () => {
|
|
42
|
+
test('sends v2 verify envelopes', async () => {
|
|
43
|
+
const calls: { init?: RequestInit | undefined; input: RequestInfo | URL }[] = []
|
|
44
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
45
|
+
calls.push({ init, input })
|
|
46
|
+
return new Response(JSON.stringify({ isValid: true }), {
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
51
|
+
|
|
52
|
+
const facilitator = Facilitator.http('https://facilitator.example')
|
|
53
|
+
await facilitator.verify(paymentPayload, paymentRequirements)
|
|
54
|
+
|
|
55
|
+
expect(fetchMock).toHaveBeenCalledOnce()
|
|
56
|
+
const { init } = calls[0]!
|
|
57
|
+
expect(JSON.parse(init!.body as string)).toEqual({
|
|
58
|
+
paymentPayload,
|
|
59
|
+
paymentRequirements,
|
|
60
|
+
x402Version: 2,
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('sends v2 settle envelopes', async () => {
|
|
65
|
+
const calls: { init?: RequestInit | undefined; input: RequestInfo | URL }[] = []
|
|
66
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
67
|
+
calls.push({ init, input })
|
|
68
|
+
return new Response(
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
network: paymentRequirements.network,
|
|
71
|
+
success: true,
|
|
72
|
+
transaction: `0x${'1'.repeat(64)}`,
|
|
73
|
+
}),
|
|
74
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
78
|
+
|
|
79
|
+
const facilitator = Facilitator.http('https://facilitator.example/')
|
|
80
|
+
await facilitator.settle(paymentPayload, paymentRequirements)
|
|
81
|
+
|
|
82
|
+
expect(fetchMock).toHaveBeenCalledOnce()
|
|
83
|
+
expect(calls[0]!.input).toBe('https://facilitator.example/settle')
|
|
84
|
+
const { init } = calls[0]!
|
|
85
|
+
expect(JSON.parse(init!.body as string)).toEqual({
|
|
86
|
+
paymentPayload,
|
|
87
|
+
paymentRequirements,
|
|
88
|
+
x402Version: 2,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('unwraps mppx fetch wrappers', async () => {
|
|
93
|
+
const rawFetch = vi.fn(async () => {
|
|
94
|
+
return new Response(JSON.stringify({ isValid: true }), {
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
const wrappedFetch = vi.fn(async () => {
|
|
99
|
+
throw new Error('wrapped fetch should not be called')
|
|
100
|
+
}) as unknown as typeof globalThis.fetch & {
|
|
101
|
+
[key: symbol]: typeof globalThis.fetch
|
|
102
|
+
}
|
|
103
|
+
wrappedFetch[Symbol.for('mppx.fetch.wrapper')] = rawFetch as typeof globalThis.fetch
|
|
104
|
+
|
|
105
|
+
const facilitator = Facilitator.http('https://facilitator.example', { fetch: wrappedFetch })
|
|
106
|
+
await facilitator.verify(paymentPayload, paymentRequirements)
|
|
107
|
+
|
|
108
|
+
expect(wrappedFetch).not.toHaveBeenCalled()
|
|
109
|
+
expect(rawFetch).toHaveBeenCalledOnce()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as Types from '../Types.js'
|
|
2
|
+
|
|
3
|
+
const mppxFetchWrapper = Symbol.for('mppx.fetch.wrapper')
|
|
4
|
+
|
|
5
|
+
type WrappedFetch = typeof globalThis.fetch & {
|
|
6
|
+
[mppxFetchWrapper]?: typeof globalThis.fetch
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type HttpOptions = {
|
|
10
|
+
/** Fetch implementation used for facilitator RPCs. */
|
|
11
|
+
fetch?: typeof globalThis.fetch | undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Resolves an x402 facilitator URL or client into a facilitator client. */
|
|
15
|
+
export function resolve(
|
|
16
|
+
facilitator: string | Types.Facilitator,
|
|
17
|
+
errorMessage = 'x402 exact requires `facilitator`.',
|
|
18
|
+
options?: HttpOptions | undefined,
|
|
19
|
+
): Types.Facilitator {
|
|
20
|
+
if (typeof facilitator === 'object' && facilitator !== null) return facilitator
|
|
21
|
+
if (typeof facilitator === 'string') return http(facilitator, options)
|
|
22
|
+
throw new Error(errorMessage)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Creates an x402 facilitator client from an HTTP base URL. */
|
|
26
|
+
export function http(url: string, options?: HttpOptions | undefined): Types.Facilitator {
|
|
27
|
+
const base = url.replace(/\/$/, '')
|
|
28
|
+
const fetch = unwrapFetch(options?.fetch ?? globalThis.fetch)
|
|
29
|
+
return {
|
|
30
|
+
async verify(paymentPayload, paymentRequirements) {
|
|
31
|
+
const response = await fetch(`${base}/verify`, {
|
|
32
|
+
body: JSON.stringify({ paymentPayload, paymentRequirements, x402Version: 2 }),
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
method: 'POST',
|
|
35
|
+
})
|
|
36
|
+
return Types.VerifyResponseSchema.parse(await response.json())
|
|
37
|
+
},
|
|
38
|
+
async settle(paymentPayload, paymentRequirements) {
|
|
39
|
+
const response = await fetch(`${base}/settle`, {
|
|
40
|
+
body: JSON.stringify({ paymentPayload, paymentRequirements, x402Version: 2 }),
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
method: 'POST',
|
|
43
|
+
})
|
|
44
|
+
return Types.SettleResponseSchema.parse(await response.json())
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Returns the underlying raw fetch implementation when given an mppx wrapper. */
|
|
50
|
+
export function unwrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch {
|
|
51
|
+
let current = fetch as WrappedFetch
|
|
52
|
+
while (current[mppxFetchWrapper]) {
|
|
53
|
+
current = current[mppxFetchWrapper] as WrappedFetch
|
|
54
|
+
}
|
|
55
|
+
return current as typeof globalThis.fetch
|
|
56
|
+
}
|