mppx 0.14.1 → 0.16.0
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 +42 -0
- package/README.md +1 -1
- package/dist/Expires.js +1 -1
- package/dist/Store.d.ts +2 -2
- package/dist/Store.js +13 -3
- package/dist/cli/cli.js +137 -131
- package/dist/cli/plugins/index.js +11 -11
- package/dist/cli/validate/payment.d.ts +12 -0
- package/dist/cli/validate/payment.js +113 -14
- package/dist/client/Mppx.d.ts +13 -3
- package/dist/client/Mppx.js +47 -22
- package/dist/client/internal/Fetch.d.ts +11 -0
- package/dist/client/internal/Fetch.js +46 -8
- package/dist/client/internal/ProofRealm.d.ts +21 -0
- package/dist/client/internal/ProofRealm.js +55 -0
- package/dist/{attestation → extensions/identity}/Constants.d.ts +2 -2
- package/dist/{attestation → extensions/identity}/Constants.js +2 -2
- package/dist/extensions/identity/Types.d.ts +21 -0
- package/dist/extensions/identity/index.d.ts +7 -0
- package/dist/extensions/identity/index.js +6 -0
- package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.d.ts +3 -3
- package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.js +12 -10
- package/dist/extensions/identity/internal/SigningContext.d.ts +6 -0
- package/dist/{attestation → extensions/identity}/internal/SigningContext.js +3 -3
- package/dist/extensions/identity/internal/Verification.d.ts +9 -0
- package/dist/extensions/identity/internal/Verification.js +10 -0
- package/dist/{tap → extensions/identity/tap}/Constants.js +1 -1
- package/dist/extensions/identity/tap/client/index.d.ts +32 -0
- package/dist/{tap → extensions/identity/tap}/client/index.js +13 -9
- package/dist/extensions/identity/tap/server/index.d.ts +27 -0
- package/dist/{tap → extensions/identity/tap}/server/index.js +12 -4
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.js +1 -1
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.d.ts +6 -5
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.js +5 -5
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.d.ts +6 -5
- package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.js +6 -4
- package/dist/identity/Client.d.ts +34 -0
- package/dist/identity/Client.js +133 -0
- package/dist/identity/Server.d.ts +46 -0
- package/dist/identity/Server.js +145 -0
- package/dist/identity/Types.d.ts +90 -0
- package/dist/identity/Types.js +2 -0
- package/dist/{attestation → identity}/index.d.ts +2 -2
- package/dist/{attestation → identity}/index.js +1 -2
- package/dist/identity/internal/SigningContext.d.ts +6 -0
- package/dist/identity/internal/SigningContext.js +11 -0
- package/dist/integrations/x402/Evm.d.ts +2 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.d.ts +5 -0
- package/dist/mcp/client/McpClient.js +2 -0
- package/dist/middlewares/express.js +18 -6
- package/dist/proxy/Proxy.js +10 -5
- package/dist/proxy/Service.d.ts +42 -4
- package/dist/proxy/Service.js +21 -5
- package/dist/server/Mppx.d.ts +32 -8
- package/dist/server/Mppx.js +144 -54
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/Methods.d.ts +5 -0
- package/dist/tempo/Methods.js +1 -0
- package/dist/tempo/client/Subscription.d.ts +5 -0
- package/dist/tempo/internal/charge.js +2 -1
- package/dist/tempo/internal/fee-payer.d.ts +0 -2
- package/dist/tempo/internal/fee-payer.js +0 -4
- package/dist/tempo/server/Charge.js +4 -0
- package/dist/tempo/server/Methods.d.ts +26 -0
- package/dist/tempo/server/Subscription.d.ts +5 -0
- package/dist/tempo/server/Subscription.js +8 -5
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/session/client/Runtime.d.ts +2 -0
- package/dist/tempo/session/client/Session.js +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +6 -0
- package/dist/tempo/session/client/SessionManager.js +14 -7
- package/dist/tempo/session/client/Transports.d.ts +12 -3
- package/dist/tempo/session/client/Transports.js +37 -11
- package/dist/tempo/session/precompile/Chain.js +0 -3
- package/dist/tempo/session/server/Ws.d.ts +5 -2
- package/dist/tempo/session/server/Ws.js +11 -2
- package/dist/tempo/subscription/Store.d.ts +7 -1
- package/dist/tempo/subscription/Store.js +66 -13
- package/dist/tempo/subscription/Types.d.ts +2 -0
- package/package.json +15 -11
- package/dist/attestation/Client.d.ts +0 -17
- package/dist/attestation/Client.js +0 -28
- package/dist/attestation/Server.d.ts +0 -4
- package/dist/attestation/Server.js +0 -8
- package/dist/attestation/Types.d.ts +0 -76
- package/dist/attestation/internal/SigningContext.d.ts +0 -4
- package/dist/tap/client/index.d.ts +0 -26
- package/dist/tap/server/index.d.ts +0 -20
- /package/dist/{attestation → extensions/identity}/Types.js +0 -0
- /package/dist/{tap → extensions/identity/tap}/Constants.d.ts +0 -0
- /package/dist/{tap → extensions/identity/tap}/Types.d.ts +0 -0
- /package/dist/{tap → extensions/identity/tap}/Types.js +0 -0
- /package/dist/{tap → extensions/identity/tap}/index.d.ts +0 -0
- /package/dist/{tap → extensions/identity/tap}/index.js +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.js +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.js +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.js +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.d.ts +0 -0
- /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.js +0 -0
package/dist/proxy/Service.js
CHANGED
|
@@ -86,16 +86,32 @@ export function toLlmsTxt(services, options) {
|
|
|
86
86
|
}
|
|
87
87
|
/** Extracts per-endpoint options from an endpoint definition. */
|
|
88
88
|
export function getOptions(endpoint) {
|
|
89
|
-
if (typeof endpoint === 'object' && endpoint !== null
|
|
89
|
+
if (typeof endpoint === 'object' && endpoint !== null)
|
|
90
90
|
return endpoint.options;
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns the payment handler of an endpoint, or `undefined` for a route declared free (`true` or
|
|
95
|
+
* `{ free: true }`). Throws for anything else, such as `{ pay: undefined }`, so a missing handler
|
|
96
|
+
* fails closed instead of proxying for free with the service's credentials.
|
|
97
|
+
*/
|
|
98
|
+
export function getHandler(endpoint) {
|
|
99
|
+
if (typeof endpoint === 'function')
|
|
100
|
+
return endpoint;
|
|
101
|
+
if (endpoint === true)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (typeof endpoint === 'object' && endpoint !== null) {
|
|
104
|
+
if (typeof endpoint.pay === 'function')
|
|
105
|
+
return endpoint.pay;
|
|
106
|
+
if (endpoint.free === true && endpoint.pay === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
throw new Error('Invalid proxy endpoint: expected a payment handler, `true` or `{ free: true }`.');
|
|
110
|
+
}
|
|
93
111
|
/** Derives discovery payment metadata from an explicitly priced endpoint handler. */
|
|
94
112
|
export function paymentOf(endpoint) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const handler = typeof endpoint === 'function' ? endpoint : endpoint.pay;
|
|
98
|
-
if (!('_internal' in handler))
|
|
113
|
+
const handler = getHandler(endpoint);
|
|
114
|
+
if (!handler || !('_internal' in handler))
|
|
99
115
|
return null;
|
|
100
116
|
const metadata = handler._internal;
|
|
101
117
|
if (!metadata)
|
package/dist/server/Mppx.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import type * as Attestation from '../attestation/Types.js';
|
|
3
2
|
import * as Challenge from '../Challenge.js';
|
|
4
3
|
import * as Credential from '../Credential.js';
|
|
5
4
|
import * as Errors from '../Errors.js';
|
|
5
|
+
import type * as Identity from '../identity/Types.js';
|
|
6
6
|
import type { DeepReadonly, MaybePromise } from '../internal/types.js';
|
|
7
7
|
import * as Method from '../Method.js';
|
|
8
8
|
import type * as Receipt from '../Receipt.js';
|
|
@@ -212,6 +212,7 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
|
|
|
212
212
|
* can be parsed; strings that cannot be deserialized have no challenge
|
|
213
213
|
* context to report.
|
|
214
214
|
*
|
|
215
|
+
* Requires a configured realm or trusted route context.
|
|
215
216
|
* @deprecated Use `broadcastCredential()` instead. This legacy name maps
|
|
216
217
|
* to the same mutating operation and does not provide validation-only
|
|
217
218
|
* semantics.
|
|
@@ -226,6 +227,7 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
|
|
|
226
227
|
verifyCredential(credential: string | Credential.Credential, options?: VerifyCredentialOptions | undefined): Promise<Receipt.Receipt>;
|
|
227
228
|
/**
|
|
228
229
|
* Validate a credential without consuming or broadcasting it.
|
|
230
|
+
* Requires a configured/explicit realm or a trusted captured request URL.
|
|
229
231
|
*
|
|
230
232
|
* This is an advisory pre-check. Methods that support it must not write
|
|
231
233
|
* replay state, sign fee-payer transactions, broadcast, or otherwise
|
|
@@ -234,6 +236,7 @@ export type Mppx<methods extends Methods = Methods, transport extends Transport.
|
|
|
234
236
|
validateCredential(credential: string | Credential.Credential, options?: VerifyCredentialOptions | undefined): Promise<Method.Validation>;
|
|
235
237
|
/**
|
|
236
238
|
* Re-validates and broadcasts a credential, returning a receipt.
|
|
239
|
+
* Requires a configured/explicit realm or a trusted captured request URL.
|
|
237
240
|
*
|
|
238
241
|
* `verifyCredential()` is retained as a backwards-compatible alias for
|
|
239
242
|
* this mutating path.
|
|
@@ -274,8 +277,25 @@ type IntentHandlers<methods extends readonly Method.AnyServer[], transport exten
|
|
|
274
277
|
intent: intent;
|
|
275
278
|
}>, transport>;
|
|
276
279
|
};
|
|
280
|
+
/**
|
|
281
|
+
* Route identity option. HTTP routes accept identity verifiers, all of which must be valid;
|
|
282
|
+
* use `Identity.anyOf` to accept one of several protocols. Other transports reject it.
|
|
283
|
+
*/
|
|
284
|
+
type RouteIdentity<transport extends Transport.AnyTransport> = Transport.InputOf<transport> extends Request ? Transport.ChallengeOutputOf<transport> extends Response ? {
|
|
285
|
+
identity?: readonly Identity.Verifier[] | undefined;
|
|
286
|
+
} : {
|
|
287
|
+
identity?: never;
|
|
288
|
+
} : {
|
|
289
|
+
identity?: never;
|
|
290
|
+
};
|
|
277
291
|
/** Handler for an intent with multiple methods - takes the intersection of all methods' options. */
|
|
278
|
-
type MultiMethodFn<methods extends Method.AnyServer, transport extends Transport.AnyTransport> = (options:
|
|
292
|
+
type MultiMethodFn<methods extends Method.AnyServer, transport extends Transport.AnyTransport> = (options: MultiMethodOptions<methods> & RouteIdentity<transport>) => (input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>;
|
|
293
|
+
/**
|
|
294
|
+
* Intersected options of every method for an intent. When a method's options can't be inferred,
|
|
295
|
+
* the intersection is `unknown`; fall back to any object (interfaces included) so `RouteIdentity`
|
|
296
|
+
* doesn't turn it into an object that rejects every option but `identity`.
|
|
297
|
+
*/
|
|
298
|
+
type MultiMethodOptions<methods extends Method.AnyServer> = UnionToIntersection<MultiMethodFnDistribute<methods>> extends infer options ? unknown extends options ? Record<string, any> : options : never;
|
|
279
299
|
type MultiMethodFnDistribute<M> = M extends Method.Server<infer method, infer defaults, any, any, any> ? MethodFn.Options<method, NonNullable<defaults>> : never;
|
|
280
300
|
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
281
301
|
/** Nested handlers: `mppx.tempo.charge(...)`, grouped by method name then intent. */
|
|
@@ -331,8 +351,6 @@ type ChallengeFn<method extends Method.Method, defaults extends Record<string, u
|
|
|
331
351
|
export declare function create<const methods extends Methods, const transport extends Transport.AnyTransport = Transport.Http>(config: create.Config<methods, transport>): Mppx<methods, transport>;
|
|
332
352
|
export declare namespace create {
|
|
333
353
|
type Config<methods extends Methods = Methods, transport extends Transport.AnyTransport = Transport.Http> = {
|
|
334
|
-
/** Request-attestation verifiers required before handling an HTTP payment request. */
|
|
335
|
-
attestation?: transport extends Transport.Http ? Attestation.VerifierMap | undefined : never;
|
|
336
354
|
/** Array of configured methods. @example [tempo()] */
|
|
337
355
|
methods: methods;
|
|
338
356
|
/** Uses `Payment-Authorization` for Payment credentials so `Authorization` remains available for application authentication. */
|
|
@@ -354,7 +372,7 @@ export declare class MissingReceiptResponseError extends Error {
|
|
|
354
372
|
}
|
|
355
373
|
/** Returns true when an error is the typed `withReceipt()` no-response sentinel. */
|
|
356
374
|
export declare function isMissingReceiptResponseError(error: unknown): error is MissingReceiptResponseError;
|
|
357
|
-
export type MethodFn<method extends Method.Method, transport extends Transport.AnyTransport, defaults extends Record<string, unknown>> = (options: MethodFn.Options<method, defaults>) => (input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>;
|
|
375
|
+
export type MethodFn<method extends Method.Method, transport extends Transport.AnyTransport, defaults extends Record<string, unknown>> = (options: MethodFn.Options<method, defaults> & RouteIdentity<transport>) => (input: Transport.InputOf<transport>) => Promise<MethodFn.Response<transport>>;
|
|
358
376
|
/** @internal */
|
|
359
377
|
export type AnyMethodFn = (options: any) => (input: any) => Promise<any>;
|
|
360
378
|
/** @internal */
|
|
@@ -374,6 +392,10 @@ declare namespace MethodFn {
|
|
|
374
392
|
status: 402;
|
|
375
393
|
} | {
|
|
376
394
|
status: 200;
|
|
395
|
+
/** Canonical challenge resolved from the selected route's terms, when available. */
|
|
396
|
+
challenge?: Challenge.Challenge | undefined;
|
|
397
|
+
/** Verified identity values for the route's verifiers. */
|
|
398
|
+
identity?: Identity.Results | undefined;
|
|
377
399
|
withReceipt: Transport.WithReceipt<transport>;
|
|
378
400
|
};
|
|
379
401
|
}
|
|
@@ -390,6 +412,8 @@ type ConfiguredHandler<transport extends Transport.AnyTransport = Transport.Http
|
|
|
390
412
|
scope?: string | undefined;
|
|
391
413
|
_canonicalRequest: Record<string, unknown>;
|
|
392
414
|
_stableBinding?: Method.StableBindingFn<Method.Method> | undefined;
|
|
415
|
+
/** Whether the offer requires payment-stage identity. */
|
|
416
|
+
_paymentIdentity?: boolean | undefined;
|
|
393
417
|
_transport: Transport.AnyTransport;
|
|
394
418
|
};
|
|
395
419
|
};
|
|
@@ -404,19 +428,19 @@ type ComposedHandler<transport extends Transport.AnyTransport = Transport.Http>
|
|
|
404
428
|
type ComposeEntry<methods extends readonly Method.AnyServer[]> = {
|
|
405
429
|
[i in keyof methods]: readonly [
|
|
406
430
|
methods[i],
|
|
407
|
-
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
|
|
431
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
|
|
408
432
|
];
|
|
409
433
|
}[number] | {
|
|
410
434
|
[i in keyof methods]: readonly [
|
|
411
435
|
`${methods[i]['name']}/${methods[i]['intent']}`,
|
|
412
|
-
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
|
|
436
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
|
|
413
437
|
];
|
|
414
438
|
}[number] | {
|
|
415
439
|
[i in keyof methods]: readonly [
|
|
416
440
|
MethodFn<methods[i], any, any> & {
|
|
417
441
|
_method: methods[i];
|
|
418
442
|
},
|
|
419
|
-
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
|
|
443
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
|
|
420
444
|
];
|
|
421
445
|
}[number];
|
|
422
446
|
export declare function compose(...args: readonly unknown[]): ComposedHandler;
|
package/dist/server/Mppx.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
import * as AttestationServer from '../attestation/Server.js';
|
|
3
2
|
import * as Challenge from '../Challenge.js';
|
|
4
3
|
import * as Constants from '../Constants.js';
|
|
5
4
|
import * as Credential from '../Credential.js';
|
|
6
5
|
import * as Errors from '../Errors.js';
|
|
7
6
|
import * as Expires from '../Expires.js';
|
|
7
|
+
import * as IdentityServer from '../identity/Server.js';
|
|
8
8
|
import * as AcceptPayment from '../internal/AcceptPayment.js';
|
|
9
9
|
import * as Env from '../internal/env.js';
|
|
10
10
|
import { warnOnce } from '../internal/warnOnce.js';
|
|
@@ -59,17 +59,19 @@ export const reservedMppxKeys = new Set(reservedMppxKeyValues);
|
|
|
59
59
|
* ```
|
|
60
60
|
*/
|
|
61
61
|
export function create(config) {
|
|
62
|
-
const {
|
|
62
|
+
const { requiresAuth, realm = Env.get('realm'), selectOffers, secretKey = Env.get('secretKey'), transport = Transport.http({ requiresAuth }), } = config;
|
|
63
63
|
const credentialHeader = requiresAuth ? Constants.Headers.paymentAuthorization : undefined;
|
|
64
|
+
if ('attestation' in config)
|
|
65
|
+
throw new TypeError('Mppx server `attestation` was replaced by per-route `identity`: pass `identity: [verifier]` to each route.');
|
|
66
|
+
// Identity is configured per route; a top-level option would otherwise be ignored (fail open).
|
|
67
|
+
if ('identity' in config)
|
|
68
|
+
throw new TypeError('Mppx server identity is configured per route: pass `identity: [verifier]` to each route.');
|
|
64
69
|
if (!secretKey) {
|
|
65
70
|
throw new Error('Missing secret key. Set the MPP_SECRET_KEY environment variable or pass `secretKey` to Mppx.create().');
|
|
66
71
|
}
|
|
67
72
|
SecretKey.assert(secretKey);
|
|
68
73
|
const methods = config.methods.flat();
|
|
69
74
|
const serverEvents = createServerEventDispatcher();
|
|
70
|
-
const verifyAttestation = attestation
|
|
71
|
-
? createAttestationGuard(attestation)
|
|
72
|
-
: undefined;
|
|
73
75
|
const handlers = {};
|
|
74
76
|
const methodHandlers = new Map();
|
|
75
77
|
const intentCount = {};
|
|
@@ -87,7 +89,6 @@ export function create(config) {
|
|
|
87
89
|
assertNoReservedMppxKeys(methods);
|
|
88
90
|
for (const mi of methods) {
|
|
89
91
|
const fn = createMethodFn({
|
|
90
|
-
...(verifyAttestation && { attest: verifyAttestation }),
|
|
91
92
|
authorize: mi.authorize,
|
|
92
93
|
credentialHeader,
|
|
93
94
|
defaults: mi.defaults,
|
|
@@ -250,9 +251,13 @@ export function create(config) {
|
|
|
250
251
|
options?.meta !== undefined ||
|
|
251
252
|
options?.realm !== undefined ||
|
|
252
253
|
options?.request !== undefined;
|
|
253
|
-
const expectedRealm = options?.realm ??
|
|
254
|
-
|
|
255
|
-
(
|
|
254
|
+
const expectedRealm = options?.realm ?? realm;
|
|
255
|
+
if ((expectedRealm === undefined && options?.capturedRequest === undefined) ||
|
|
256
|
+
(expectedRealm !== undefined && credential.challenge.realm !== expectedRealm))
|
|
257
|
+
await fail(new Errors.InvalidChallengeError({
|
|
258
|
+
id: credential.challenge.id,
|
|
259
|
+
reason: 'standalone verification requires a matching configured realm or trusted route context',
|
|
260
|
+
}), { credential: parsedCredential });
|
|
256
261
|
let parsedRequest = credential.challenge.request;
|
|
257
262
|
let request;
|
|
258
263
|
try {
|
|
@@ -432,8 +437,19 @@ export function create(config) {
|
|
|
432
437
|
}
|
|
433
438
|
// biome-ignore lint/correctness/noUnusedVariables: _
|
|
434
439
|
function createMethodFn(parameters) {
|
|
435
|
-
const {
|
|
436
|
-
return (
|
|
440
|
+
const { authorize, credentialHeader, defaults, events, method, preflight, realm, respond, secretKey, broadcast, stableBinding, transport, validate, verify, } = parameters;
|
|
441
|
+
return (routeOptions) => {
|
|
442
|
+
const { identity = [], ...options } = routeOptions;
|
|
443
|
+
const requestVerifiers = identity.filter((verifier) => IdentityServer.stageOf(verifier) === 'request');
|
|
444
|
+
const paymentVerifiers = identity.filter((verifier) => IdentityServer.stageOf(verifier) === 'payment');
|
|
445
|
+
// Preflight hooks (e.g. session bootstrap) issue and settle their own challenges, which
|
|
446
|
+
// payment-stage verifiers cannot see; reject the combination instead of skipping identity.
|
|
447
|
+
if (paymentVerifiers.length > 0 && preflight)
|
|
448
|
+
throw new TypeError(`Payment-stage identity is not supported on ${method.name}/${method.intent} routes with a preflight hook.`);
|
|
449
|
+
// The HTML checkout cannot answer identity challenges, and it pays before resubmitting, so
|
|
450
|
+
// a browser would pay and then be rejected.
|
|
451
|
+
if (paymentVerifiers.length > 0 && method.html)
|
|
452
|
+
throw new TypeError(`Payment-stage identity is not supported on ${method.name}/${method.intent} routes with the HTML checkout.`);
|
|
437
453
|
const { description, meta, scope, ...rest } = options;
|
|
438
454
|
const staticMeta = Scope.merge({ meta, scope });
|
|
439
455
|
const internal = {
|
|
@@ -451,20 +467,26 @@ function createMethodFn(parameters) {
|
|
|
451
467
|
]),
|
|
452
468
|
],
|
|
453
469
|
_canonicalRequest: PaymentRequest.fromMethod(method, { ...defaults, ...rest }),
|
|
470
|
+
_paymentIdentity: paymentVerifiers.length > 0,
|
|
454
471
|
_stableBinding: stableBinding,
|
|
455
472
|
_transport: transport,
|
|
456
473
|
};
|
|
457
474
|
Object.defineProperty(internal, '_method', { value: method });
|
|
458
475
|
const handler = async (input) => {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
476
|
+
const identityValues = new Map();
|
|
477
|
+
if (identity.length > 0) {
|
|
478
|
+
if (!(input instanceof globalThis.Request))
|
|
479
|
+
throw new TypeError('Route identity requires the HTTP transport.');
|
|
480
|
+
const outcome = await IdentityServer.verify(input, requestVerifiers);
|
|
481
|
+
if (outcome.status !== 'valid') {
|
|
482
|
+
// Returned on the non-200 branch so callers never run paid work: like an early
|
|
483
|
+
// rejection, `challenge` holds the identity response (401/403) to send as is.
|
|
484
|
+
const rejection = IdentityServer.toResponse(outcome);
|
|
485
|
+
identityRejections.add(rejection);
|
|
486
|
+
return { challenge: rejection, status: 402 };
|
|
487
|
+
}
|
|
488
|
+
for (const [verifier, value] of outcome.values)
|
|
489
|
+
identityValues.set(verifier, value);
|
|
468
490
|
}
|
|
469
491
|
if (method.html && isServiceWorkerRequest(input))
|
|
470
492
|
return {
|
|
@@ -503,6 +525,7 @@ function createMethodFn(parameters) {
|
|
|
503
525
|
return { challenge: response, status: 402 };
|
|
504
526
|
return {
|
|
505
527
|
status: 200,
|
|
528
|
+
identity: IdentityServer.results(identityValues),
|
|
506
529
|
withReceipt() {
|
|
507
530
|
return response;
|
|
508
531
|
},
|
|
@@ -510,12 +533,35 @@ function createMethodFn(parameters) {
|
|
|
510
533
|
}
|
|
511
534
|
}
|
|
512
535
|
const emitChallenge = async (parameters) => {
|
|
513
|
-
|
|
536
|
+
let response = await transport.respondChallenge({
|
|
514
537
|
challenge: parameters.challenge,
|
|
515
538
|
input,
|
|
516
539
|
...(parameters.error && { error: parameters.error }),
|
|
517
540
|
...(parameters.html && { html: parameters.html }),
|
|
518
541
|
});
|
|
542
|
+
// Payment-stage verifiers add their own challenge headers to the 402.
|
|
543
|
+
if (paymentVerifiers.length > 0 &&
|
|
544
|
+
input instanceof globalThis.Request &&
|
|
545
|
+
response instanceof globalThis.Response) {
|
|
546
|
+
const outcome = await IdentityServer.verify(input, paymentVerifiers, {
|
|
547
|
+
challenge: parameters.challenge,
|
|
548
|
+
});
|
|
549
|
+
const identityHeaders = new Headers();
|
|
550
|
+
if (outcome.status !== 'valid')
|
|
551
|
+
for (const [name, value] of outcome.headers)
|
|
552
|
+
identityHeaders.append(name, value);
|
|
553
|
+
if ([...identityHeaders].length > 0) {
|
|
554
|
+
const headers = new Headers(response.headers);
|
|
555
|
+
for (const [name, value] of identityHeaders)
|
|
556
|
+
headers.append(name, value);
|
|
557
|
+
response = new globalThis.Response(response.body, {
|
|
558
|
+
headers,
|
|
559
|
+
status: response.status,
|
|
560
|
+
statusText: response.statusText,
|
|
561
|
+
});
|
|
562
|
+
identityChallenges.set(response, identityHeaders);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
519
565
|
if (isIssuedChallengeResponse(response))
|
|
520
566
|
await events.emit('challenge.created', createChallengeContext({
|
|
521
567
|
capturedRequest,
|
|
@@ -632,6 +678,8 @@ function createMethodFn(parameters) {
|
|
|
632
678
|
const { challengeId = challenge.id, credentialForReceipt = { challenge, payload: {} }, envelopeForReceipt, managementResponse, } = options;
|
|
633
679
|
const result = {
|
|
634
680
|
status: 200,
|
|
681
|
+
challenge,
|
|
682
|
+
identity: IdentityServer.results(identityValues),
|
|
635
683
|
withReceipt(response) {
|
|
636
684
|
if (managementResponse) {
|
|
637
685
|
return transport.respondReceipt({
|
|
@@ -661,7 +709,17 @@ function createMethodFn(parameters) {
|
|
|
661
709
|
};
|
|
662
710
|
// No credential provided—issue challenge
|
|
663
711
|
if (!credential) {
|
|
664
|
-
|
|
712
|
+
// Authorization without a payment credential still requires payment-stage identity;
|
|
713
|
+
// a verifier that needs the credential reports missing, so the route issues its 402.
|
|
714
|
+
const authorizeIdentity = authorize && input instanceof globalThis.Request && paymentVerifiers.length > 0
|
|
715
|
+
? await IdentityServer.verify(input, paymentVerifiers, { challenge })
|
|
716
|
+
: undefined;
|
|
717
|
+
if (authorize &&
|
|
718
|
+
input instanceof globalThis.Request &&
|
|
719
|
+
(!authorizeIdentity || authorizeIdentity.status === 'valid')) {
|
|
720
|
+
if (authorizeIdentity?.status === 'valid')
|
|
721
|
+
for (const [verifier, value] of authorizeIdentity.values)
|
|
722
|
+
identityValues.set(verifier, value);
|
|
665
723
|
try {
|
|
666
724
|
const authorized = await authorize({
|
|
667
725
|
challenge,
|
|
@@ -834,6 +892,33 @@ function createMethodFn(parameters) {
|
|
|
834
892
|
credential: parsedCredential,
|
|
835
893
|
request: parsedRequest,
|
|
836
894
|
});
|
|
895
|
+
// Payment-stage identity is checked against the answered challenge before settlement.
|
|
896
|
+
if (paymentVerifiers.length > 0) {
|
|
897
|
+
const outcome = await IdentityServer.verify(input, paymentVerifiers, {
|
|
898
|
+
challenge: credential.challenge,
|
|
899
|
+
credential: parsedCredential,
|
|
900
|
+
});
|
|
901
|
+
if (outcome.status !== 'valid') {
|
|
902
|
+
const error = new Errors.VerificationFailedError({ reason: outcome.reason });
|
|
903
|
+
await emitPaymentFailed({
|
|
904
|
+
challenge,
|
|
905
|
+
credential: parsedCredential,
|
|
906
|
+
error,
|
|
907
|
+
request: parsedRequest,
|
|
908
|
+
retryChallenge: challenge,
|
|
909
|
+
submittedChallenge: credential.challenge,
|
|
910
|
+
});
|
|
911
|
+
// The client may already have pushed this payment, so a fresh 402 would invite
|
|
912
|
+
// it to pay again. Reject without a challenge, like a replayed credential.
|
|
913
|
+
// Returned on the non-200 branch so callers never run paid work: like an early
|
|
914
|
+
// rejection, `challenge` holds the identity response (401/403) to send as is.
|
|
915
|
+
const rejection = IdentityServer.toResponse(outcome);
|
|
916
|
+
identityRejections.add(rejection);
|
|
917
|
+
return { challenge: rejection, status: 402 };
|
|
918
|
+
}
|
|
919
|
+
for (const [verifier, value] of outcome.values)
|
|
920
|
+
identityValues.set(verifier, value);
|
|
921
|
+
}
|
|
837
922
|
// User-provided verification (e.g., check signature, submit tx, verify payment).
|
|
838
923
|
// If verification fails, re-issue the challenge so the client can retry.
|
|
839
924
|
let receiptData;
|
|
@@ -917,7 +1002,7 @@ function createMethodFn(parameters) {
|
|
|
917
1002
|
function createChallengeFn(parameters) {
|
|
918
1003
|
const { credentialHeader, defaults, method, realm, secretKey } = parameters;
|
|
919
1004
|
return async (options) => {
|
|
920
|
-
const { description, meta, scope, ...rest } = options;
|
|
1005
|
+
const { description, identity: _, meta, scope, ...rest } = options;
|
|
921
1006
|
const effectiveMeta = Scope.merge({ meta, scope });
|
|
922
1007
|
const expires = 'expires' in options
|
|
923
1008
|
? normalizeExpires(options.expires)
|
|
@@ -936,33 +1021,6 @@ function createChallengeFn(parameters) {
|
|
|
936
1021
|
}).then((resolved) => resolved.challenge);
|
|
937
1022
|
};
|
|
938
1023
|
}
|
|
939
|
-
function createAttestationGuard(verifiers) {
|
|
940
|
-
const entries = Object.entries(verifiers);
|
|
941
|
-
if (entries.length === 0)
|
|
942
|
-
throw new TypeError('Mppx server attestation must configure at least one verifier.');
|
|
943
|
-
const cache = new WeakMap();
|
|
944
|
-
return (request) => {
|
|
945
|
-
const cached = cache.get(request);
|
|
946
|
-
if (cached)
|
|
947
|
-
return cached;
|
|
948
|
-
const verification = AttestationServer.verify(request, verifiers).then((outcomes) => {
|
|
949
|
-
for (const [name, outcome] of Object.entries(outcomes))
|
|
950
|
-
if (outcome.status === 'invalid')
|
|
951
|
-
return new globalThis.Response(`Request attestation "${name}" is invalid: ${outcome.reason}`, { status: 401 });
|
|
952
|
-
for (const [name, outcome] of Object.entries(outcomes)) {
|
|
953
|
-
if (outcome.status === 'verified')
|
|
954
|
-
continue;
|
|
955
|
-
const reason = outcome.status === 'unverified'
|
|
956
|
-
? `Request attestation "${name}" could not be verified: ${outcome.reason}`
|
|
957
|
-
: `Request attestation "${name}" is required.`;
|
|
958
|
-
return new globalThis.Response(reason, { status: 403 });
|
|
959
|
-
}
|
|
960
|
-
return undefined;
|
|
961
|
-
});
|
|
962
|
-
cache.set(request, verification);
|
|
963
|
-
return verification;
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
1024
|
/** Dispatches instance events and scopes success hooks to the supplied configured method. */
|
|
967
1025
|
function createServerEventDispatcher() {
|
|
968
1026
|
const handlers = {
|
|
@@ -1526,6 +1584,16 @@ function withParsedCredentialPayload(credential, payload) {
|
|
|
1526
1584
|
};
|
|
1527
1585
|
}
|
|
1528
1586
|
const paymentAuthChallengeHeader = Constants.Headers.wwwAuthenticate;
|
|
1587
|
+
/** Payment-stage identity headers added to issued 402 responses, keyed by response. @internal */
|
|
1588
|
+
const identityChallenges = new WeakMap();
|
|
1589
|
+
/** Identity rejections (401/403) returned on a route's non-success branch. @internal */
|
|
1590
|
+
const identityRejections = new WeakSet();
|
|
1591
|
+
/** Records a merged 402's identity challenge so an outer composer can forward it. @internal */
|
|
1592
|
+
function withIdentityChallenge(response, headers) {
|
|
1593
|
+
if (headers)
|
|
1594
|
+
identityChallenges.set(response, headers);
|
|
1595
|
+
return response;
|
|
1596
|
+
}
|
|
1529
1597
|
const challengeHeaderMerges = [
|
|
1530
1598
|
{
|
|
1531
1599
|
name: paymentAuthChallengeHeader,
|
|
@@ -1571,6 +1639,9 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1571
1639
|
});
|
|
1572
1640
|
if (handlers.length === 0)
|
|
1573
1641
|
throw new Error('compose() requires at least one handler');
|
|
1642
|
+
// A 402 carries one identity challenge, bound to one payment challenge.
|
|
1643
|
+
if (handlers.flatMap(getConfiguredOffers).filter((offer) => offer._paymentIdentity).length > 1)
|
|
1644
|
+
throw new TypeError('A composed route can include at most one offer with payment-stage identity.');
|
|
1574
1645
|
const offerSelector = selectOffers;
|
|
1575
1646
|
const offerEntries = handlers.map((handler) => {
|
|
1576
1647
|
const internal = handler._internal;
|
|
@@ -1679,21 +1750,30 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1679
1750
|
return transportCredentialHandler(input);
|
|
1680
1751
|
// No credential — evaluate handlers sequentially so authorize()/renewal hooks
|
|
1681
1752
|
// can safely claim the request without racing each other.
|
|
1753
|
+
// An identity rejection from one offer does not hide the others: it is returned only
|
|
1754
|
+
// when no offer issued a payment challenge.
|
|
1682
1755
|
const results = [];
|
|
1683
1756
|
for (const handler of selectedHandlers) {
|
|
1684
1757
|
const result = transportResults.get(handler) ?? (await handler(input));
|
|
1685
1758
|
if (result.status === 200)
|
|
1686
1759
|
return result;
|
|
1687
|
-
|
|
1760
|
+
const rejected = identityRejections.has(result.challenge);
|
|
1761
|
+
if (result.challenge.status !== 402 && !rejected)
|
|
1688
1762
|
return result;
|
|
1689
1763
|
results.push(result);
|
|
1690
1764
|
}
|
|
1765
|
+
const offers = results.filter((result) => result.status === 402 && !identityRejections.has(result.challenge));
|
|
1766
|
+
if (offers.length === 0 && results[0])
|
|
1767
|
+
return results[0];
|
|
1691
1768
|
const challengeEntries = (() => {
|
|
1692
1769
|
const entries = [];
|
|
1693
1770
|
for (let i = 0; i < selectedHandlers.length; i++) {
|
|
1694
1771
|
const result = results[i];
|
|
1695
1772
|
if (result?.status !== 402)
|
|
1696
1773
|
continue;
|
|
1774
|
+
// Identity rejections may carry non-Payment challenges (e.g. Bearer); they are not offers.
|
|
1775
|
+
if (identityRejections.has(result.challenge))
|
|
1776
|
+
continue;
|
|
1697
1777
|
const response = result.challenge;
|
|
1698
1778
|
const wwwAuth = response.headers.get(paymentAuthChallengeHeader);
|
|
1699
1779
|
if (!wwwAuth)
|
|
@@ -1718,7 +1798,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1718
1798
|
return entries;
|
|
1719
1799
|
}
|
|
1720
1800
|
})();
|
|
1721
|
-
const challengeResponses =
|
|
1801
|
+
const challengeResponses = offers.map((result) => result.challenge);
|
|
1722
1802
|
const unnegotiatedX402Responses = input.headers.has(Constants.Headers.acceptPayment) || challengeEntries.length === 0
|
|
1723
1803
|
? []
|
|
1724
1804
|
: challengeResponses.filter((response) => response.headers.has(x402_Types.paymentRequiredHeader) &&
|
|
@@ -1729,6 +1809,11 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1729
1809
|
...unnegotiatedX402Responses,
|
|
1730
1810
|
]
|
|
1731
1811
|
: challengeResponses;
|
|
1812
|
+
// At most one offer carries an identity challenge (enforced when composing); forward it
|
|
1813
|
+
// only if its payment challenge is still advertised after negotiation.
|
|
1814
|
+
const identityHeaders = negotiatedChallengeResponses
|
|
1815
|
+
.map((response) => identityChallenges.get(response))
|
|
1816
|
+
.find((headers) => headers !== undefined);
|
|
1732
1817
|
// Merge challenge headers from the negotiated 402 responses.
|
|
1733
1818
|
const mergedHeaders = new Headers();
|
|
1734
1819
|
mergedHeaders.set('Cache-Control', 'no-store');
|
|
@@ -1741,6 +1826,11 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1741
1826
|
mergedHeaders.append(header.name, value);
|
|
1742
1827
|
}
|
|
1743
1828
|
}
|
|
1829
|
+
// Identity WWW-Authenticate values already arrive in the merged offer value above.
|
|
1830
|
+
if (identityHeaders)
|
|
1831
|
+
for (const [name, value] of identityHeaders)
|
|
1832
|
+
if (name !== paymentAuthChallengeHeader.toLowerCase())
|
|
1833
|
+
mergedHeaders.append(name, value);
|
|
1744
1834
|
// Collect html-enabled handlers and their challenges
|
|
1745
1835
|
const htmlEntries = challengeEntries.filter((entry) => entry.handler._internal?.html);
|
|
1746
1836
|
const wantsHtml = input.headers.get('Accept')?.includes('text/html');
|
|
@@ -1777,7 +1867,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1777
1867
|
});
|
|
1778
1868
|
return {
|
|
1779
1869
|
status: 402,
|
|
1780
|
-
challenge: new Response(body, { status: 402, headers: mergedHeaders }),
|
|
1870
|
+
challenge: withIdentityChallenge(new Response(body, { status: 402, headers: mergedHeaders }), identityHeaders),
|
|
1781
1871
|
};
|
|
1782
1872
|
}
|
|
1783
1873
|
// Non-HTML fallback: prefer the first Payment-auth body, otherwise use
|
|
@@ -1797,7 +1887,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1797
1887
|
}
|
|
1798
1888
|
return {
|
|
1799
1889
|
status: 402,
|
|
1800
|
-
challenge: new Response(body, { status: 402, headers: mergedHeaders }),
|
|
1890
|
+
challenge: withIdentityChallenge(new Response(body, { status: 402, headers: mergedHeaders }), identityHeaders),
|
|
1801
1891
|
};
|
|
1802
1892
|
};
|
|
1803
1893
|
const offers = handlers.flatMap(getConfiguredOffers);
|