mppx 0.15.0 → 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 +28 -0
- 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 +10 -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 +1 -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/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 +27 -8
- package/dist/server/Mppx.js +136 -51
- package/dist/stripe/server/internal/html.gen.js +1 -1
- 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/Subscription.js +0 -1
- 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/subscription/Store.js +10 -9
- 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/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';
|
|
@@ -277,8 +277,25 @@ type IntentHandlers<methods extends readonly Method.AnyServer[], transport exten
|
|
|
277
277
|
intent: intent;
|
|
278
278
|
}>, transport>;
|
|
279
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
|
+
};
|
|
280
291
|
/** Handler for an intent with multiple methods - takes the intersection of all methods' options. */
|
|
281
|
-
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;
|
|
282
299
|
type MultiMethodFnDistribute<M> = M extends Method.Server<infer method, infer defaults, any, any, any> ? MethodFn.Options<method, NonNullable<defaults>> : never;
|
|
283
300
|
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
284
301
|
/** Nested handlers: `mppx.tempo.charge(...)`, grouped by method name then intent. */
|
|
@@ -334,8 +351,6 @@ type ChallengeFn<method extends Method.Method, defaults extends Record<string, u
|
|
|
334
351
|
export declare function create<const methods extends Methods, const transport extends Transport.AnyTransport = Transport.Http>(config: create.Config<methods, transport>): Mppx<methods, transport>;
|
|
335
352
|
export declare namespace create {
|
|
336
353
|
type Config<methods extends Methods = Methods, transport extends Transport.AnyTransport = Transport.Http> = {
|
|
337
|
-
/** Request-attestation verifiers required before handling an HTTP payment request. */
|
|
338
|
-
attestation?: transport extends Transport.Http ? Attestation.VerifierMap | undefined : never;
|
|
339
354
|
/** Array of configured methods. @example [tempo()] */
|
|
340
355
|
methods: methods;
|
|
341
356
|
/** Uses `Payment-Authorization` for Payment credentials so `Authorization` remains available for application authentication. */
|
|
@@ -357,7 +372,7 @@ export declare class MissingReceiptResponseError extends Error {
|
|
|
357
372
|
}
|
|
358
373
|
/** Returns true when an error is the typed `withReceipt()` no-response sentinel. */
|
|
359
374
|
export declare function isMissingReceiptResponseError(error: unknown): error is MissingReceiptResponseError;
|
|
360
|
-
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>>;
|
|
361
376
|
/** @internal */
|
|
362
377
|
export type AnyMethodFn = (options: any) => (input: any) => Promise<any>;
|
|
363
378
|
/** @internal */
|
|
@@ -379,6 +394,8 @@ declare namespace MethodFn {
|
|
|
379
394
|
status: 200;
|
|
380
395
|
/** Canonical challenge resolved from the selected route's terms, when available. */
|
|
381
396
|
challenge?: Challenge.Challenge | undefined;
|
|
397
|
+
/** Verified identity values for the route's verifiers. */
|
|
398
|
+
identity?: Identity.Results | undefined;
|
|
382
399
|
withReceipt: Transport.WithReceipt<transport>;
|
|
383
400
|
};
|
|
384
401
|
}
|
|
@@ -395,6 +412,8 @@ type ConfiguredHandler<transport extends Transport.AnyTransport = Transport.Http
|
|
|
395
412
|
scope?: string | undefined;
|
|
396
413
|
_canonicalRequest: Record<string, unknown>;
|
|
397
414
|
_stableBinding?: Method.StableBindingFn<Method.Method> | undefined;
|
|
415
|
+
/** Whether the offer requires payment-stage identity. */
|
|
416
|
+
_paymentIdentity?: boolean | undefined;
|
|
398
417
|
_transport: Transport.AnyTransport;
|
|
399
418
|
};
|
|
400
419
|
};
|
|
@@ -409,19 +428,19 @@ type ComposedHandler<transport extends Transport.AnyTransport = Transport.Http>
|
|
|
409
428
|
type ComposeEntry<methods extends readonly Method.AnyServer[]> = {
|
|
410
429
|
[i in keyof methods]: readonly [
|
|
411
430
|
methods[i],
|
|
412
|
-
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
|
|
431
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
|
|
413
432
|
];
|
|
414
433
|
}[number] | {
|
|
415
434
|
[i in keyof methods]: readonly [
|
|
416
435
|
`${methods[i]['name']}/${methods[i]['intent']}`,
|
|
417
|
-
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
|
|
436
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
|
|
418
437
|
];
|
|
419
438
|
}[number] | {
|
|
420
439
|
[i in keyof methods]: readonly [
|
|
421
440
|
MethodFn<methods[i], any, any> & {
|
|
422
441
|
_method: methods[i];
|
|
423
442
|
},
|
|
424
|
-
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>>
|
|
443
|
+
MethodFn.Options<methods[i], NonNullable<methods[i]['defaults']>> & RouteIdentity<Transport.Http>
|
|
425
444
|
];
|
|
426
445
|
}[number];
|
|
427
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,
|
|
@@ -436,8 +437,19 @@ export function create(config) {
|
|
|
436
437
|
}
|
|
437
438
|
// biome-ignore lint/correctness/noUnusedVariables: _
|
|
438
439
|
function createMethodFn(parameters) {
|
|
439
|
-
const {
|
|
440
|
-
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.`);
|
|
441
453
|
const { description, meta, scope, ...rest } = options;
|
|
442
454
|
const staticMeta = Scope.merge({ meta, scope });
|
|
443
455
|
const internal = {
|
|
@@ -455,20 +467,26 @@ function createMethodFn(parameters) {
|
|
|
455
467
|
]),
|
|
456
468
|
],
|
|
457
469
|
_canonicalRequest: PaymentRequest.fromMethod(method, { ...defaults, ...rest }),
|
|
470
|
+
_paymentIdentity: paymentVerifiers.length > 0,
|
|
458
471
|
_stableBinding: stableBinding,
|
|
459
472
|
_transport: transport,
|
|
460
473
|
};
|
|
461
474
|
Object.defineProperty(internal, '_method', { value: method });
|
|
462
475
|
const handler = async (input) => {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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);
|
|
472
490
|
}
|
|
473
491
|
if (method.html && isServiceWorkerRequest(input))
|
|
474
492
|
return {
|
|
@@ -507,6 +525,7 @@ function createMethodFn(parameters) {
|
|
|
507
525
|
return { challenge: response, status: 402 };
|
|
508
526
|
return {
|
|
509
527
|
status: 200,
|
|
528
|
+
identity: IdentityServer.results(identityValues),
|
|
510
529
|
withReceipt() {
|
|
511
530
|
return response;
|
|
512
531
|
},
|
|
@@ -514,12 +533,35 @@ function createMethodFn(parameters) {
|
|
|
514
533
|
}
|
|
515
534
|
}
|
|
516
535
|
const emitChallenge = async (parameters) => {
|
|
517
|
-
|
|
536
|
+
let response = await transport.respondChallenge({
|
|
518
537
|
challenge: parameters.challenge,
|
|
519
538
|
input,
|
|
520
539
|
...(parameters.error && { error: parameters.error }),
|
|
521
540
|
...(parameters.html && { html: parameters.html }),
|
|
522
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
|
+
}
|
|
523
565
|
if (isIssuedChallengeResponse(response))
|
|
524
566
|
await events.emit('challenge.created', createChallengeContext({
|
|
525
567
|
capturedRequest,
|
|
@@ -637,6 +679,7 @@ function createMethodFn(parameters) {
|
|
|
637
679
|
const result = {
|
|
638
680
|
status: 200,
|
|
639
681
|
challenge,
|
|
682
|
+
identity: IdentityServer.results(identityValues),
|
|
640
683
|
withReceipt(response) {
|
|
641
684
|
if (managementResponse) {
|
|
642
685
|
return transport.respondReceipt({
|
|
@@ -666,7 +709,17 @@ function createMethodFn(parameters) {
|
|
|
666
709
|
};
|
|
667
710
|
// No credential provided—issue challenge
|
|
668
711
|
if (!credential) {
|
|
669
|
-
|
|
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);
|
|
670
723
|
try {
|
|
671
724
|
const authorized = await authorize({
|
|
672
725
|
challenge,
|
|
@@ -839,6 +892,33 @@ function createMethodFn(parameters) {
|
|
|
839
892
|
credential: parsedCredential,
|
|
840
893
|
request: parsedRequest,
|
|
841
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
|
+
}
|
|
842
922
|
// User-provided verification (e.g., check signature, submit tx, verify payment).
|
|
843
923
|
// If verification fails, re-issue the challenge so the client can retry.
|
|
844
924
|
let receiptData;
|
|
@@ -922,7 +1002,7 @@ function createMethodFn(parameters) {
|
|
|
922
1002
|
function createChallengeFn(parameters) {
|
|
923
1003
|
const { credentialHeader, defaults, method, realm, secretKey } = parameters;
|
|
924
1004
|
return async (options) => {
|
|
925
|
-
const { description, meta, scope, ...rest } = options;
|
|
1005
|
+
const { description, identity: _, meta, scope, ...rest } = options;
|
|
926
1006
|
const effectiveMeta = Scope.merge({ meta, scope });
|
|
927
1007
|
const expires = 'expires' in options
|
|
928
1008
|
? normalizeExpires(options.expires)
|
|
@@ -941,33 +1021,6 @@ function createChallengeFn(parameters) {
|
|
|
941
1021
|
}).then((resolved) => resolved.challenge);
|
|
942
1022
|
};
|
|
943
1023
|
}
|
|
944
|
-
function createAttestationGuard(verifiers) {
|
|
945
|
-
const entries = Object.entries(verifiers);
|
|
946
|
-
if (entries.length === 0)
|
|
947
|
-
throw new TypeError('Mppx server attestation must configure at least one verifier.');
|
|
948
|
-
const cache = new WeakMap();
|
|
949
|
-
return (request) => {
|
|
950
|
-
const cached = cache.get(request);
|
|
951
|
-
if (cached)
|
|
952
|
-
return cached;
|
|
953
|
-
const verification = AttestationServer.verify(request, verifiers).then((outcomes) => {
|
|
954
|
-
for (const [name, outcome] of Object.entries(outcomes))
|
|
955
|
-
if (outcome.status === 'invalid')
|
|
956
|
-
return new globalThis.Response(`Request attestation "${name}" is invalid: ${outcome.reason}`, { status: 401 });
|
|
957
|
-
for (const [name, outcome] of Object.entries(outcomes)) {
|
|
958
|
-
if (outcome.status === 'verified')
|
|
959
|
-
continue;
|
|
960
|
-
const reason = outcome.status === 'unverified'
|
|
961
|
-
? `Request attestation "${name}" could not be verified: ${outcome.reason}`
|
|
962
|
-
: `Request attestation "${name}" is required.`;
|
|
963
|
-
return new globalThis.Response(reason, { status: 403 });
|
|
964
|
-
}
|
|
965
|
-
return undefined;
|
|
966
|
-
});
|
|
967
|
-
cache.set(request, verification);
|
|
968
|
-
return verification;
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
1024
|
/** Dispatches instance events and scopes success hooks to the supplied configured method. */
|
|
972
1025
|
function createServerEventDispatcher() {
|
|
973
1026
|
const handlers = {
|
|
@@ -1531,6 +1584,16 @@ function withParsedCredentialPayload(credential, payload) {
|
|
|
1531
1584
|
};
|
|
1532
1585
|
}
|
|
1533
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
|
+
}
|
|
1534
1597
|
const challengeHeaderMerges = [
|
|
1535
1598
|
{
|
|
1536
1599
|
name: paymentAuthChallengeHeader,
|
|
@@ -1576,6 +1639,9 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1576
1639
|
});
|
|
1577
1640
|
if (handlers.length === 0)
|
|
1578
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.');
|
|
1579
1645
|
const offerSelector = selectOffers;
|
|
1580
1646
|
const offerEntries = handlers.map((handler) => {
|
|
1581
1647
|
const internal = handler._internal;
|
|
@@ -1684,21 +1750,30 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1684
1750
|
return transportCredentialHandler(input);
|
|
1685
1751
|
// No credential — evaluate handlers sequentially so authorize()/renewal hooks
|
|
1686
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.
|
|
1687
1755
|
const results = [];
|
|
1688
1756
|
for (const handler of selectedHandlers) {
|
|
1689
1757
|
const result = transportResults.get(handler) ?? (await handler(input));
|
|
1690
1758
|
if (result.status === 200)
|
|
1691
1759
|
return result;
|
|
1692
|
-
|
|
1760
|
+
const rejected = identityRejections.has(result.challenge);
|
|
1761
|
+
if (result.challenge.status !== 402 && !rejected)
|
|
1693
1762
|
return result;
|
|
1694
1763
|
results.push(result);
|
|
1695
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];
|
|
1696
1768
|
const challengeEntries = (() => {
|
|
1697
1769
|
const entries = [];
|
|
1698
1770
|
for (let i = 0; i < selectedHandlers.length; i++) {
|
|
1699
1771
|
const result = results[i];
|
|
1700
1772
|
if (result?.status !== 402)
|
|
1701
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;
|
|
1702
1777
|
const response = result.challenge;
|
|
1703
1778
|
const wwwAuth = response.headers.get(paymentAuthChallengeHeader);
|
|
1704
1779
|
if (!wwwAuth)
|
|
@@ -1723,7 +1798,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1723
1798
|
return entries;
|
|
1724
1799
|
}
|
|
1725
1800
|
})();
|
|
1726
|
-
const challengeResponses =
|
|
1801
|
+
const challengeResponses = offers.map((result) => result.challenge);
|
|
1727
1802
|
const unnegotiatedX402Responses = input.headers.has(Constants.Headers.acceptPayment) || challengeEntries.length === 0
|
|
1728
1803
|
? []
|
|
1729
1804
|
: challengeResponses.filter((response) => response.headers.has(x402_Types.paymentRequiredHeader) &&
|
|
@@ -1734,6 +1809,11 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1734
1809
|
...unnegotiatedX402Responses,
|
|
1735
1810
|
]
|
|
1736
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);
|
|
1737
1817
|
// Merge challenge headers from the negotiated 402 responses.
|
|
1738
1818
|
const mergedHeaders = new Headers();
|
|
1739
1819
|
mergedHeaders.set('Cache-Control', 'no-store');
|
|
@@ -1746,6 +1826,11 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1746
1826
|
mergedHeaders.append(header.name, value);
|
|
1747
1827
|
}
|
|
1748
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);
|
|
1749
1834
|
// Collect html-enabled handlers and their challenges
|
|
1750
1835
|
const htmlEntries = challengeEntries.filter((entry) => entry.handler._internal?.html);
|
|
1751
1836
|
const wantsHtml = input.headers.get('Accept')?.includes('text/html');
|
|
@@ -1782,7 +1867,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1782
1867
|
});
|
|
1783
1868
|
return {
|
|
1784
1869
|
status: 402,
|
|
1785
|
-
challenge: new Response(body, { status: 402, headers: mergedHeaders }),
|
|
1870
|
+
challenge: withIdentityChallenge(new Response(body, { status: 402, headers: mergedHeaders }), identityHeaders),
|
|
1786
1871
|
};
|
|
1787
1872
|
}
|
|
1788
1873
|
// Non-HTML fallback: prefer the first Payment-auth body, otherwise use
|
|
@@ -1802,7 +1887,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1802
1887
|
}
|
|
1803
1888
|
return {
|
|
1804
1889
|
status: 402,
|
|
1805
|
-
challenge: new Response(body, { status: 402, headers: mergedHeaders }),
|
|
1890
|
+
challenge: withIdentityChallenge(new Response(body, { status: 402, headers: mergedHeaders }), identityHeaders),
|
|
1806
1891
|
};
|
|
1807
1892
|
};
|
|
1808
1893
|
const offers = handlers.flatMap(getConfiguredOffers);
|