mppx 0.15.0 → 0.17.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 +47 -0
- package/dist/Challenge.d.ts +6 -0
- package/dist/Challenge.js +12 -0
- package/dist/Method.d.ts +2 -0
- package/dist/cli/cli.js +143 -131
- package/dist/cli/plugins/index.js +11 -11
- package/dist/cli/validate/payment.d.ts +12 -0
- package/dist/cli/validate/payment.js +137 -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/client/internal/protocols/Mpp.js +4 -1
- 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 +59 -0
- package/dist/identity/Server.js +194 -0
- package/dist/identity/Types.d.ts +112 -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 +164 -54
- package/dist/server/internal/challengeMeta.d.ts +6 -0
- package/dist/server/internal/challengeMeta.js +17 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/client/Charge.d.ts +11 -0
- package/dist/tempo/client/Charge.js +30 -1
- package/dist/tempo/internal/charge.js +2 -1
- package/dist/tempo/internal/defaults.d.ts +9 -0
- package/dist/tempo/internal/defaults.js +9 -0
- package/dist/tempo/internal/fee-payer.d.ts +0 -2
- package/dist/tempo/internal/fee-payer.js +2 -6
- package/dist/tempo/server/Charge.js +12 -1
- 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.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 ? paymentVerifiers : undefined,
|
|
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 {
|
|
@@ -482,6 +500,12 @@ function createMethodFn(parameters) {
|
|
|
482
500
|
const effectiveMeta = scope === undefined && input instanceof globalThis.Request
|
|
483
501
|
? Scope.merge({ meta: staticMeta, scope: Scope.get(input) })
|
|
484
502
|
: staticMeta;
|
|
503
|
+
// Every offer of one 402 shares the identity nonce (composed routes pass each offer the
|
|
504
|
+
// same request), so a payment-stage proof is bound to the 402, whichever offer is paid.
|
|
505
|
+
const identityNonce = paymentVerifiers.length > 0 && input instanceof globalThis.Request
|
|
506
|
+
? IdentityServer.nonceFor(input, ChallengeMeta.createNonce)
|
|
507
|
+
: undefined;
|
|
508
|
+
const challengeMeta = ChallengeMeta.withIdentity(effectiveMeta, identityNonce);
|
|
485
509
|
// Extract credential once — getCredential may have side effects (e.g. SSE transports).
|
|
486
510
|
let [credential, credentialError] = (() => {
|
|
487
511
|
try {
|
|
@@ -507,6 +531,7 @@ function createMethodFn(parameters) {
|
|
|
507
531
|
return { challenge: response, status: 402 };
|
|
508
532
|
return {
|
|
509
533
|
status: 200,
|
|
534
|
+
identity: IdentityServer.results(identityValues),
|
|
510
535
|
withReceipt() {
|
|
511
536
|
return response;
|
|
512
537
|
},
|
|
@@ -514,12 +539,31 @@ function createMethodFn(parameters) {
|
|
|
514
539
|
}
|
|
515
540
|
}
|
|
516
541
|
const emitChallenge = async (parameters) => {
|
|
517
|
-
|
|
542
|
+
let response = await transport.respondChallenge({
|
|
518
543
|
challenge: parameters.challenge,
|
|
519
544
|
input,
|
|
520
545
|
...(parameters.error && { error: parameters.error }),
|
|
521
546
|
...(parameters.html && { html: parameters.html }),
|
|
522
547
|
});
|
|
548
|
+
// Payment-stage verifiers add their own challenge headers to the 402.
|
|
549
|
+
if (identityNonce !== undefined &&
|
|
550
|
+
input instanceof globalThis.Request &&
|
|
551
|
+
response instanceof globalThis.Response) {
|
|
552
|
+
const identityHeaders = await IdentityServer.challenge(input, paymentVerifiers, identityNonce);
|
|
553
|
+
if ([...identityHeaders].length > 0) {
|
|
554
|
+
const paymentChallenge = response.headers.get(paymentAuthChallengeHeader);
|
|
555
|
+
const headers = new Headers(response.headers);
|
|
556
|
+
for (const [name, value] of identityHeaders)
|
|
557
|
+
headers.append(name, value);
|
|
558
|
+
response = new globalThis.Response(response.body, {
|
|
559
|
+
headers,
|
|
560
|
+
status: response.status,
|
|
561
|
+
statusText: response.statusText,
|
|
562
|
+
});
|
|
563
|
+
identityChallenges.set(response, identityHeaders);
|
|
564
|
+
paymentChallenges.set(response, paymentChallenge);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
523
567
|
if (isIssuedChallengeResponse(response))
|
|
524
568
|
await events.emit('challenge.created', createChallengeContext({
|
|
525
569
|
capturedRequest,
|
|
@@ -552,7 +596,7 @@ function createMethodFn(parameters) {
|
|
|
552
596
|
description,
|
|
553
597
|
expires,
|
|
554
598
|
header: credentialHeader,
|
|
555
|
-
meta:
|
|
599
|
+
meta: challengeMeta,
|
|
556
600
|
method,
|
|
557
601
|
realm,
|
|
558
602
|
request: parameters.request,
|
|
@@ -567,7 +611,7 @@ function createMethodFn(parameters) {
|
|
|
567
611
|
description,
|
|
568
612
|
expires,
|
|
569
613
|
header: credentialHeader,
|
|
570
|
-
meta:
|
|
614
|
+
meta: challengeMeta,
|
|
571
615
|
method,
|
|
572
616
|
realm,
|
|
573
617
|
routeRequest: rest,
|
|
@@ -637,6 +681,7 @@ function createMethodFn(parameters) {
|
|
|
637
681
|
const result = {
|
|
638
682
|
status: 200,
|
|
639
683
|
challenge,
|
|
684
|
+
identity: IdentityServer.results(identityValues),
|
|
640
685
|
withReceipt(response) {
|
|
641
686
|
if (managementResponse) {
|
|
642
687
|
return transport.respondReceipt({
|
|
@@ -666,7 +711,17 @@ function createMethodFn(parameters) {
|
|
|
666
711
|
};
|
|
667
712
|
// No credential provided—issue challenge
|
|
668
713
|
if (!credential) {
|
|
669
|
-
|
|
714
|
+
// Authorization without a payment credential still requires payment-stage identity;
|
|
715
|
+
// a verifier that needs the credential reports missing, so the route issues its 402.
|
|
716
|
+
const authorizeIdentity = authorize && input instanceof globalThis.Request && paymentVerifiers.length > 0
|
|
717
|
+
? await IdentityServer.verify(input, paymentVerifiers, { challenge })
|
|
718
|
+
: undefined;
|
|
719
|
+
if (authorize &&
|
|
720
|
+
input instanceof globalThis.Request &&
|
|
721
|
+
(!authorizeIdentity || authorizeIdentity.status === 'valid')) {
|
|
722
|
+
if (authorizeIdentity?.status === 'valid')
|
|
723
|
+
for (const [verifier, value] of authorizeIdentity.values)
|
|
724
|
+
identityValues.set(verifier, value);
|
|
670
725
|
try {
|
|
671
726
|
const authorized = await authorize({
|
|
672
727
|
challenge,
|
|
@@ -839,6 +894,34 @@ function createMethodFn(parameters) {
|
|
|
839
894
|
credential: parsedCredential,
|
|
840
895
|
request: parsedRequest,
|
|
841
896
|
});
|
|
897
|
+
// Payment-stage identity is checked against the answered challenge before settlement.
|
|
898
|
+
if (paymentVerifiers.length > 0) {
|
|
899
|
+
const outcome = await IdentityServer.verify(input, paymentVerifiers, {
|
|
900
|
+
challenge: credential.challenge,
|
|
901
|
+
credential: parsedCredential,
|
|
902
|
+
nonce: ChallengeMeta.identity(credential.challenge.meta),
|
|
903
|
+
});
|
|
904
|
+
if (outcome.status !== 'valid') {
|
|
905
|
+
const error = new Errors.VerificationFailedError({ reason: outcome.reason });
|
|
906
|
+
await emitPaymentFailed({
|
|
907
|
+
challenge,
|
|
908
|
+
credential: parsedCredential,
|
|
909
|
+
error,
|
|
910
|
+
request: parsedRequest,
|
|
911
|
+
retryChallenge: challenge,
|
|
912
|
+
submittedChallenge: credential.challenge,
|
|
913
|
+
});
|
|
914
|
+
// The client may already have pushed this payment, so a fresh 402 would invite
|
|
915
|
+
// it to pay again. Reject without a challenge, like a replayed credential.
|
|
916
|
+
// Returned on the non-200 branch so callers never run paid work: like an early
|
|
917
|
+
// rejection, `challenge` holds the identity response (401/403) to send as is.
|
|
918
|
+
const rejection = IdentityServer.toResponse(outcome);
|
|
919
|
+
identityRejections.add(rejection);
|
|
920
|
+
return { challenge: rejection, status: 402 };
|
|
921
|
+
}
|
|
922
|
+
for (const [verifier, value] of outcome.values)
|
|
923
|
+
identityValues.set(verifier, value);
|
|
924
|
+
}
|
|
842
925
|
// User-provided verification (e.g., check signature, submit tx, verify payment).
|
|
843
926
|
// If verification fails, re-issue the challenge so the client can retry.
|
|
844
927
|
let receiptData;
|
|
@@ -922,7 +1005,7 @@ function createMethodFn(parameters) {
|
|
|
922
1005
|
function createChallengeFn(parameters) {
|
|
923
1006
|
const { credentialHeader, defaults, method, realm, secretKey } = parameters;
|
|
924
1007
|
return async (options) => {
|
|
925
|
-
const { description, meta, scope, ...rest } = options;
|
|
1008
|
+
const { description, identity: _, meta, scope, ...rest } = options;
|
|
926
1009
|
const effectiveMeta = Scope.merge({ meta, scope });
|
|
927
1010
|
const expires = 'expires' in options
|
|
928
1011
|
? normalizeExpires(options.expires)
|
|
@@ -941,33 +1024,6 @@ function createChallengeFn(parameters) {
|
|
|
941
1024
|
}).then((resolved) => resolved.challenge);
|
|
942
1025
|
};
|
|
943
1026
|
}
|
|
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
1027
|
/** Dispatches instance events and scopes success hooks to the supplied configured method. */
|
|
972
1028
|
function createServerEventDispatcher() {
|
|
973
1029
|
const handlers = {
|
|
@@ -1295,6 +1351,7 @@ async function resolveRouteChallenge(parameters) {
|
|
|
1295
1351
|
? (await parameters.request({
|
|
1296
1352
|
capturedRequest: parameters.capturedRequest,
|
|
1297
1353
|
credential: parameters.credential,
|
|
1354
|
+
expires: parameters.credential?.challenge.expires ?? parameters.expires,
|
|
1298
1355
|
request: merged,
|
|
1299
1356
|
}))
|
|
1300
1357
|
: merged;
|
|
@@ -1531,11 +1588,34 @@ function withParsedCredentialPayload(credential, payload) {
|
|
|
1531
1588
|
};
|
|
1532
1589
|
}
|
|
1533
1590
|
const paymentAuthChallengeHeader = Constants.Headers.wwwAuthenticate;
|
|
1591
|
+
/** Payment-stage identity headers added to issued 402 responses, keyed by response. @internal */
|
|
1592
|
+
const identityChallenges = new WeakMap();
|
|
1593
|
+
/** Identity rejections (401/403) returned on a route's non-success branch. @internal */
|
|
1594
|
+
const identityRejections = new WeakSet();
|
|
1595
|
+
/**
|
|
1596
|
+
* A 402's Payment `WWW-Authenticate` value without its identity challenge, keyed by response.
|
|
1597
|
+
* Composers merge these and append the shared identity challenge once. @internal
|
|
1598
|
+
*/
|
|
1599
|
+
const paymentChallenges = new WeakMap();
|
|
1600
|
+
/** Records a merged 402's identity challenge so an outer composer can forward it. @internal */
|
|
1601
|
+
function withIdentityChallenge(response, headers, paymentChallenge) {
|
|
1602
|
+
if (headers) {
|
|
1603
|
+
identityChallenges.set(response, headers);
|
|
1604
|
+
paymentChallenges.set(response, paymentChallenge);
|
|
1605
|
+
}
|
|
1606
|
+
return response;
|
|
1607
|
+
}
|
|
1534
1608
|
const challengeHeaderMerges = [
|
|
1535
1609
|
{
|
|
1536
1610
|
name: paymentAuthChallengeHeader,
|
|
1537
1611
|
values: (context) => context.challengeEntries
|
|
1538
|
-
.map((entry) =>
|
|
1612
|
+
.map((entry) => {
|
|
1613
|
+
const response = entry.result.challenge;
|
|
1614
|
+
// Identity challenges are appended once by the composer, not once per offer.
|
|
1615
|
+
return paymentChallenges.has(response)
|
|
1616
|
+
? paymentChallenges.get(response)
|
|
1617
|
+
: response.headers.get(paymentAuthChallengeHeader);
|
|
1618
|
+
})
|
|
1539
1619
|
.filter((value) => value !== null),
|
|
1540
1620
|
merge: (values) => values,
|
|
1541
1621
|
},
|
|
@@ -1576,6 +1656,13 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1576
1656
|
});
|
|
1577
1657
|
if (handlers.length === 0)
|
|
1578
1658
|
throw new Error('compose() requires at least one handler');
|
|
1659
|
+
// A 402 carries one identity challenge, shared by its offers through the identity nonce, so
|
|
1660
|
+
// every offer that requires payment-stage identity must require the same verifiers.
|
|
1661
|
+
const identityOffers = handlers
|
|
1662
|
+
.flatMap(getConfiguredOffers)
|
|
1663
|
+
.flatMap((offer) => (offer._paymentIdentity ? [offer._paymentIdentity] : []));
|
|
1664
|
+
if (identityOffers.some((verifiers) => !sameVerifiers(verifiers, identityOffers[0])))
|
|
1665
|
+
throw new TypeError('Offers in a composed route must require the same payment-stage identity verifiers.');
|
|
1579
1666
|
const offerSelector = selectOffers;
|
|
1580
1667
|
const offerEntries = handlers.map((handler) => {
|
|
1581
1668
|
const internal = handler._internal;
|
|
@@ -1684,21 +1771,30 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1684
1771
|
return transportCredentialHandler(input);
|
|
1685
1772
|
// No credential — evaluate handlers sequentially so authorize()/renewal hooks
|
|
1686
1773
|
// can safely claim the request without racing each other.
|
|
1774
|
+
// An identity rejection from one offer does not hide the others: it is returned only
|
|
1775
|
+
// when no offer issued a payment challenge.
|
|
1687
1776
|
const results = [];
|
|
1688
1777
|
for (const handler of selectedHandlers) {
|
|
1689
1778
|
const result = transportResults.get(handler) ?? (await handler(input));
|
|
1690
1779
|
if (result.status === 200)
|
|
1691
1780
|
return result;
|
|
1692
|
-
|
|
1781
|
+
const rejected = identityRejections.has(result.challenge);
|
|
1782
|
+
if (result.challenge.status !== 402 && !rejected)
|
|
1693
1783
|
return result;
|
|
1694
1784
|
results.push(result);
|
|
1695
1785
|
}
|
|
1786
|
+
const offers = results.filter((result) => result.status === 402 && !identityRejections.has(result.challenge));
|
|
1787
|
+
if (offers.length === 0 && results[0])
|
|
1788
|
+
return results[0];
|
|
1696
1789
|
const challengeEntries = (() => {
|
|
1697
1790
|
const entries = [];
|
|
1698
1791
|
for (let i = 0; i < selectedHandlers.length; i++) {
|
|
1699
1792
|
const result = results[i];
|
|
1700
1793
|
if (result?.status !== 402)
|
|
1701
1794
|
continue;
|
|
1795
|
+
// Identity rejections may carry non-Payment challenges (e.g. Bearer); they are not offers.
|
|
1796
|
+
if (identityRejections.has(result.challenge))
|
|
1797
|
+
continue;
|
|
1702
1798
|
const response = result.challenge;
|
|
1703
1799
|
const wwwAuth = response.headers.get(paymentAuthChallengeHeader);
|
|
1704
1800
|
if (!wwwAuth)
|
|
@@ -1723,7 +1819,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1723
1819
|
return entries;
|
|
1724
1820
|
}
|
|
1725
1821
|
})();
|
|
1726
|
-
const challengeResponses =
|
|
1822
|
+
const challengeResponses = offers.map((result) => result.challenge);
|
|
1727
1823
|
const unnegotiatedX402Responses = input.headers.has(Constants.Headers.acceptPayment) || challengeEntries.length === 0
|
|
1728
1824
|
? []
|
|
1729
1825
|
: challengeResponses.filter((response) => response.headers.has(x402_Types.paymentRequiredHeader) &&
|
|
@@ -1734,6 +1830,11 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1734
1830
|
...unnegotiatedX402Responses,
|
|
1735
1831
|
]
|
|
1736
1832
|
: challengeResponses;
|
|
1833
|
+
// Offers share one identity challenge (enforced when composing); forward it only if an
|
|
1834
|
+
// offer that carries it is still advertised after negotiation.
|
|
1835
|
+
const identityHeaders = negotiatedChallengeResponses
|
|
1836
|
+
.map((response) => identityChallenges.get(response))
|
|
1837
|
+
.find((headers) => headers !== undefined);
|
|
1737
1838
|
// Merge challenge headers from the negotiated 402 responses.
|
|
1738
1839
|
const mergedHeaders = new Headers();
|
|
1739
1840
|
mergedHeaders.set('Cache-Control', 'no-store');
|
|
@@ -1746,6 +1847,11 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1746
1847
|
mergedHeaders.append(header.name, value);
|
|
1747
1848
|
}
|
|
1748
1849
|
}
|
|
1850
|
+
// The merged offer values above exclude identity challenges; append the shared one once.
|
|
1851
|
+
const paymentChallenge = mergedHeaders.get(paymentAuthChallengeHeader);
|
|
1852
|
+
if (identityHeaders)
|
|
1853
|
+
for (const [name, value] of identityHeaders)
|
|
1854
|
+
mergedHeaders.append(name, value);
|
|
1749
1855
|
// Collect html-enabled handlers and their challenges
|
|
1750
1856
|
const htmlEntries = challengeEntries.filter((entry) => entry.handler._internal?.html);
|
|
1751
1857
|
const wantsHtml = input.headers.get('Accept')?.includes('text/html');
|
|
@@ -1782,7 +1888,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1782
1888
|
});
|
|
1783
1889
|
return {
|
|
1784
1890
|
status: 402,
|
|
1785
|
-
challenge: new Response(body, { status: 402, headers: mergedHeaders }),
|
|
1891
|
+
challenge: withIdentityChallenge(new Response(body, { status: 402, headers: mergedHeaders }), identityHeaders, paymentChallenge),
|
|
1786
1892
|
};
|
|
1787
1893
|
}
|
|
1788
1894
|
// Non-HTML fallback: prefer the first Payment-auth body, otherwise use
|
|
@@ -1802,7 +1908,7 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1802
1908
|
}
|
|
1803
1909
|
return {
|
|
1804
1910
|
status: 402,
|
|
1805
|
-
challenge: new Response(body, { status: 402, headers: mergedHeaders }),
|
|
1911
|
+
challenge: withIdentityChallenge(new Response(body, { status: 402, headers: mergedHeaders }), identityHeaders, paymentChallenge),
|
|
1806
1912
|
};
|
|
1807
1913
|
};
|
|
1808
1914
|
const offers = handlers.flatMap(getConfiguredOffers);
|
|
@@ -1811,6 +1917,10 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1811
1917
|
return composed;
|
|
1812
1918
|
}
|
|
1813
1919
|
/** Returns the flattened configured offers accepted by a handler. */
|
|
1920
|
+
/** Whether two verifier lists hold the same verifiers in the same order. */
|
|
1921
|
+
function sameVerifiers(a, b) {
|
|
1922
|
+
return a.length === b.length && a.every((verifier, i) => verifier === b[i]);
|
|
1923
|
+
}
|
|
1814
1924
|
function getConfiguredOffers(handler) {
|
|
1815
1925
|
const internal = handler._internal;
|
|
1816
1926
|
if (!internal)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Adds fresh issuance randomness while preserving application and route metadata. */
|
|
2
2
|
export declare function withNonce(meta: Record<string, string> | undefined): Record<string, string>;
|
|
3
|
+
/** Returns fresh randomness for an identity nonce. */
|
|
4
|
+
export declare function createNonce(): string;
|
|
5
|
+
/** Adds an identity nonce to challenge metadata. */
|
|
6
|
+
export declare function withIdentity(meta: Record<string, string> | undefined, nonce: string | undefined): Record<string, string> | undefined;
|
|
7
|
+
/** Reads the identity nonce from challenge metadata. */
|
|
8
|
+
export declare function identity(meta: Record<string, string> | undefined): string | undefined;
|
|
3
9
|
/** Removes issuance-only metadata when comparing stable route requirements. */
|
|
4
10
|
export declare function routeMeta(meta: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
5
11
|
//# sourceMappingURL=challengeMeta.d.ts.map
|
|
@@ -6,9 +6,25 @@ export function withNonce(meta) {
|
|
|
6
6
|
// Ox uses runtime-specific secure randomness, including Node without global Web Crypto.
|
|
7
7
|
return { ...meta, [nonceKey]: Secp256k1.randomPrivateKey() };
|
|
8
8
|
}
|
|
9
|
+
/** Reserved metadata key for the HMAC-bound identity nonce shared by every offer in one 402. */
|
|
10
|
+
const identityKey = '_mppx_identity';
|
|
11
|
+
/** Returns fresh randomness for an identity nonce. */
|
|
12
|
+
export function createNonce() {
|
|
13
|
+
return Secp256k1.randomPrivateKey();
|
|
14
|
+
}
|
|
15
|
+
/** Adds an identity nonce to challenge metadata. */
|
|
16
|
+
export function withIdentity(meta, nonce) {
|
|
17
|
+
if (nonce === undefined)
|
|
18
|
+
return meta;
|
|
19
|
+
return { ...meta, [identityKey]: nonce };
|
|
20
|
+
}
|
|
21
|
+
/** Reads the identity nonce from challenge metadata. */
|
|
22
|
+
export function identity(meta) {
|
|
23
|
+
return meta?.[identityKey];
|
|
24
|
+
}
|
|
9
25
|
/** Removes issuance-only metadata when comparing stable route requirements. */
|
|
10
26
|
export function routeMeta(meta) {
|
|
11
|
-
const { [nonceKey]: _, ...rest } = meta ?? {};
|
|
27
|
+
const { [nonceKey]: _, [identityKey]: __, ...rest } = meta ?? {};
|
|
12
28
|
return Object.keys(rest).length ? rest : undefined;
|
|
13
29
|
}
|
|
14
30
|
//# sourceMappingURL=challengeMeta.js.map
|