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.
Files changed (101) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/Challenge.d.ts +6 -0
  3. package/dist/Challenge.js +12 -0
  4. package/dist/Method.d.ts +2 -0
  5. package/dist/cli/cli.js +143 -131
  6. package/dist/cli/plugins/index.js +11 -11
  7. package/dist/cli/validate/payment.d.ts +12 -0
  8. package/dist/cli/validate/payment.js +137 -14
  9. package/dist/client/Mppx.d.ts +13 -3
  10. package/dist/client/Mppx.js +47 -22
  11. package/dist/client/internal/Fetch.d.ts +11 -0
  12. package/dist/client/internal/Fetch.js +46 -8
  13. package/dist/client/internal/ProofRealm.d.ts +21 -0
  14. package/dist/client/internal/ProofRealm.js +55 -0
  15. package/dist/client/internal/protocols/Mpp.js +4 -1
  16. package/dist/{attestation → extensions/identity}/Constants.d.ts +2 -2
  17. package/dist/{attestation → extensions/identity}/Constants.js +2 -2
  18. package/dist/extensions/identity/Types.d.ts +21 -0
  19. package/dist/extensions/identity/index.d.ts +7 -0
  20. package/dist/extensions/identity/index.js +6 -0
  21. package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.d.ts +3 -3
  22. package/dist/{attestation → extensions/identity}/internal/HttpMessageSignature.js +10 -10
  23. package/dist/extensions/identity/internal/SigningContext.d.ts +6 -0
  24. package/dist/{attestation → extensions/identity}/internal/SigningContext.js +3 -3
  25. package/dist/extensions/identity/internal/Verification.d.ts +9 -0
  26. package/dist/extensions/identity/internal/Verification.js +10 -0
  27. package/dist/{tap → extensions/identity/tap}/Constants.js +1 -1
  28. package/dist/extensions/identity/tap/client/index.d.ts +32 -0
  29. package/dist/{tap → extensions/identity/tap}/client/index.js +13 -9
  30. package/dist/extensions/identity/tap/server/index.d.ts +27 -0
  31. package/dist/{tap → extensions/identity/tap}/server/index.js +12 -4
  32. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.js +1 -1
  33. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.d.ts +6 -5
  34. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/client/index.js +5 -5
  35. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.d.ts +6 -5
  36. package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/server/index.js +6 -4
  37. package/dist/identity/Client.d.ts +34 -0
  38. package/dist/identity/Client.js +133 -0
  39. package/dist/identity/Server.d.ts +59 -0
  40. package/dist/identity/Server.js +194 -0
  41. package/dist/identity/Types.d.ts +112 -0
  42. package/dist/identity/Types.js +2 -0
  43. package/dist/{attestation → identity}/index.d.ts +2 -2
  44. package/dist/{attestation → identity}/index.js +1 -2
  45. package/dist/identity/internal/SigningContext.d.ts +6 -0
  46. package/dist/identity/internal/SigningContext.js +11 -0
  47. package/dist/integrations/x402/Evm.d.ts +1 -0
  48. package/dist/internal/version.d.ts +2 -2
  49. package/dist/internal/version.js +1 -1
  50. package/dist/mcp/client/McpClient.d.ts +5 -0
  51. package/dist/mcp/client/McpClient.js +2 -0
  52. package/dist/proxy/Proxy.js +10 -5
  53. package/dist/proxy/Service.d.ts +42 -4
  54. package/dist/proxy/Service.js +21 -5
  55. package/dist/server/Mppx.d.ts +27 -8
  56. package/dist/server/Mppx.js +164 -54
  57. package/dist/server/internal/challengeMeta.d.ts +6 -0
  58. package/dist/server/internal/challengeMeta.js +17 -1
  59. package/dist/stripe/server/internal/html.gen.js +1 -1
  60. package/dist/tempo/client/Charge.d.ts +11 -0
  61. package/dist/tempo/client/Charge.js +30 -1
  62. package/dist/tempo/internal/charge.js +2 -1
  63. package/dist/tempo/internal/defaults.d.ts +9 -0
  64. package/dist/tempo/internal/defaults.js +9 -0
  65. package/dist/tempo/internal/fee-payer.d.ts +0 -2
  66. package/dist/tempo/internal/fee-payer.js +2 -6
  67. package/dist/tempo/server/Charge.js +12 -1
  68. package/dist/tempo/server/Subscription.js +0 -1
  69. package/dist/tempo/server/internal/html.gen.js +1 -1
  70. package/dist/tempo/session/client/Runtime.d.ts +2 -0
  71. package/dist/tempo/session/client/Session.js +1 -1
  72. package/dist/tempo/session/client/SessionManager.d.ts +6 -0
  73. package/dist/tempo/session/client/SessionManager.js +14 -7
  74. package/dist/tempo/session/client/Transports.d.ts +12 -3
  75. package/dist/tempo/session/client/Transports.js +37 -11
  76. package/dist/tempo/session/precompile/Chain.js +0 -3
  77. package/dist/tempo/subscription/Store.js +10 -9
  78. package/package.json +15 -11
  79. package/dist/attestation/Client.d.ts +0 -17
  80. package/dist/attestation/Client.js +0 -28
  81. package/dist/attestation/Server.d.ts +0 -4
  82. package/dist/attestation/Server.js +0 -8
  83. package/dist/attestation/Types.d.ts +0 -76
  84. package/dist/attestation/internal/SigningContext.d.ts +0 -4
  85. package/dist/tap/client/index.d.ts +0 -26
  86. package/dist/tap/server/index.d.ts +0 -20
  87. /package/dist/{attestation → extensions/identity}/Types.js +0 -0
  88. /package/dist/{tap → extensions/identity/tap}/Constants.d.ts +0 -0
  89. /package/dist/{tap → extensions/identity/tap}/Types.d.ts +0 -0
  90. /package/dist/{tap → extensions/identity/tap}/Types.js +0 -0
  91. /package/dist/{tap → extensions/identity/tap}/index.d.ts +0 -0
  92. /package/dist/{tap → extensions/identity/tap}/index.js +0 -0
  93. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Constants.d.ts +0 -0
  94. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.d.ts +0 -0
  95. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/Types.js +0 -0
  96. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.d.ts +0 -0
  97. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/index.js +0 -0
  98. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.d.ts +0 -0
  99. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/JwkThumbprint.js +0 -0
  100. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.d.ts +0 -0
  101. /package/dist/{web-bot-auth → extensions/identity/web-bot-auth}/internal/SignatureAgent.js +0 -0
@@ -1,8 +1,9 @@
1
- import type * as Attestation from '../../attestation/Types.js';
2
- import type * as Store from '../../Store.js';
1
+ import type * as Identity from '../../../../identity/Types.js';
2
+ import type * as Store from '../../../../Store.js';
3
+ import type * as SignatureTypes from '../../Types.js';
3
4
  import type * as Types from '../Types.js';
4
5
  /**
5
- * Creates a verifier for the Web Bot Auth HTTPS-directory profile.
6
+ * Creates a request-stage identity verifier for the Web Bot Auth HTTPS-directory profile.
6
7
  *
7
8
  * It requires an `@authority` and signed `Signature-Agent` directory member;
8
9
  * direct key configuration and other Web Bot Auth discovery types are outside
@@ -10,7 +11,7 @@ import type * as Types from '../Types.js';
10
11
  * automated HTTP client. It must not be used on its own as user or payment
11
12
  * authorization.
12
13
  */
13
- export declare function verifier(config: verifier.Config): Attestation.Verifier<Types.VerifiedRequest>;
14
+ export declare function verifier(config: verifier.Config): Identity.Verifier<Types.VerifiedRequest>;
14
15
  export declare namespace verifier {
15
16
  type Config = {
16
17
  /**
@@ -21,7 +22,7 @@ export declare namespace verifier {
21
22
  * must be extractable so their RFC 7638 thumbprints can be verified.
22
23
  */
23
24
  keyResolver: (parameters: {
24
- algorithm: Attestation.SignatureAlgorithm;
25
+ algorithm: SignatureTypes.SignatureAlgorithm;
25
26
  keyId: string;
26
27
  request: Request;
27
28
  signatureAgent: string;
@@ -1,10 +1,11 @@
1
1
  import { isInnerList, parseDictionary, Token } from 'structured-headers';
2
- import * as HttpMessageSignature from '../../attestation/internal/HttpMessageSignature.js';
2
+ import * as HttpMessageSignature from '../../internal/HttpMessageSignature.js';
3
+ import * as Verification from '../../internal/Verification.js';
3
4
  import { Constants } from '../Constants.js';
4
5
  import * as JwkThumbprint from '../internal/JwkThumbprint.js';
5
6
  import * as SignatureAgent from '../internal/SignatureAgent.js';
6
7
  /**
7
- * Creates a verifier for the Web Bot Auth HTTPS-directory profile.
8
+ * Creates a request-stage identity verifier for the Web Bot Auth HTTPS-directory profile.
8
9
  *
9
10
  * It requires an `@authority` and signed `Signature-Agent` directory member;
10
11
  * direct key configuration and other Web Bot Auth discovery types are outside
@@ -17,6 +18,7 @@ export function verifier(config) {
17
18
  if (!Number.isInteger(maxAge) || maxAge <= 0)
18
19
  throw new RangeError('Web Bot Auth maxAge must be a positive integer.');
19
20
  return {
21
+ stage: 'request',
20
22
  async verify(request) {
21
23
  let signatureAgent;
22
24
  const result = await HttpMessageSignature.verify(request, {
@@ -73,9 +75,9 @@ export function verifier(config) {
73
75
  },
74
76
  });
75
77
  if (result.status !== 'verified')
76
- return result;
78
+ return Verification.fromSignature(result);
77
79
  return {
78
- status: 'verified',
80
+ status: 'valid',
79
81
  value: {
80
82
  keyId: result.input.keyId,
81
83
  nonce: result.input.nonce,
@@ -0,0 +1,34 @@
1
+ import type * as Types from './Types.js';
2
+ /** Sends HTTP attempts with every in-scope prover applied. */
3
+ export type Client = {
4
+ /**
5
+ * Proves and sends one request.
6
+ *
7
+ * Requests that carry proofs do not follow redirects: a redirect rejects
8
+ * (`redirect: 'error'`), so proofs never reach another URL. Requests with no
9
+ * prover in scope are sent unchanged. If a prover asks to retry a rejected
10
+ * (non-402) response, the request is proved again and resent once, except for
11
+ * paid attempts, which are never resent.
12
+ */
13
+ send: (fetch: typeof globalThis.fetch, request: Request, context?: Types.Context) => Promise<{
14
+ request: Request;
15
+ response: Response;
16
+ }>;
17
+ /** Lets in-scope provers prepare proofs for a 402 challenge from `url` before the payment retry. */
18
+ challenge: (response: Response, context: Types.Context, url: string) => Promise<void>;
19
+ };
20
+ /**
21
+ * Creates the client runner for a list of provers.
22
+ *
23
+ * Provers run in list order on every attempt, sharing one attempt context.
24
+ * List request signers last so their signatures cover headers added by
25
+ * earlier provers.
26
+ */
27
+ export declare function from(provers: readonly Types.Prover[]): Client;
28
+ /**
29
+ * Wraps a fetch implementation so every request is proved.
30
+ *
31
+ * Prefer `Mppx.create({ identity })`, which also passes the payment challenge to provers.
32
+ */
33
+ export declare function wrapFetch(fetch: typeof globalThis.fetch, client: Client, context?: Types.Context): typeof globalThis.fetch;
34
+ //# sourceMappingURL=Client.d.ts.map
@@ -0,0 +1,133 @@
1
+ import * as Challenge from '../Challenge.js';
2
+ import * as Credential from '../Credential.js';
3
+ import * as SigningContext from './internal/SigningContext.js';
4
+ /** x402 credential headers (v2 and v1); a request carrying one is a paid attempt. */
5
+ const x402CredentialHeaders = ['payment-signature', 'x-payment'];
6
+ /**
7
+ * Creates the client runner for a list of provers.
8
+ *
9
+ * Provers run in list order on every attempt, sharing one attempt context.
10
+ * List request signers last so their signatures cover headers added by
11
+ * earlier provers.
12
+ */
13
+ export function from(provers) {
14
+ if (provers.length === 0)
15
+ throw new TypeError('Mppx client identity must configure at least one prover.');
16
+ // Scopes are matched exactly: a string scope would fall back to substring matching.
17
+ for (const prover of provers)
18
+ if (prover.scope !== '*' && !Array.isArray(prover.scope))
19
+ throw new TypeError('Identity prover scope must be "*" or an array of origins.');
20
+ const inScope = (url) => {
21
+ const origin = new URL(url).origin;
22
+ return provers.filter((prover) => prover.scope === '*' || prover.scope.includes(origin));
23
+ };
24
+ async function sendOnce(fetch, request, context, resendable) {
25
+ // Keep the original readable only when it may be resent; otherwise a clone would tee
26
+ // (and buffer) a streamed body that is never read again.
27
+ const source = resendable ? request.clone() : request;
28
+ const scoped = inScope(request.url);
29
+ if (scoped.length === 0)
30
+ return { request: source, response: await fetch(source) };
31
+ const attempt = Object.freeze({ ...context, ...SigningContext.create() });
32
+ let sent = new Request(source, {
33
+ redirect: request.redirect === 'follow' ? 'error' : request.redirect,
34
+ });
35
+ for (const prover of scoped)
36
+ sent = await prover.prove(sent, attempt);
37
+ return { request: sent, response: await fetch(sent) };
38
+ }
39
+ return {
40
+ async send(fetch, request, context = {}) {
41
+ // A paid attempt (a challenge in context, or a Payment or x402 credential the caller
42
+ // attached) is never resent: its credential may already have settled, and replaying it
43
+ // could pay again.
44
+ const paid = context.challenge !== undefined ||
45
+ x402CredentialHeaders.some((name) => request.headers.has(name)) ||
46
+ [...request.headers.values()].some((value) => Credential.extractPaymentScheme(value) !== null);
47
+ const resendable = !paid && inScope(request.url).some((prover) => prover.retry);
48
+ const result = await sendOnce(fetch, request, context, resendable);
49
+ const { response } = result;
50
+ if (!resendable || response.status < 400 || response.status === 402)
51
+ return result;
52
+ // Each callback reads its own copy, so the returned response stays readable.
53
+ const copies = [];
54
+ let retry = false;
55
+ for (const prover of inScope(request.url)) {
56
+ if (!prover.retry)
57
+ continue;
58
+ const copy = response.clone();
59
+ copies.push(copy);
60
+ if (await prover.retry(copy))
61
+ retry = true;
62
+ }
63
+ if (!retry) {
64
+ release(copies);
65
+ return result;
66
+ }
67
+ release([response, ...copies]);
68
+ return sendOnce(fetch, request, context, false);
69
+ },
70
+ async challenge(response, context, url) {
71
+ for (const prover of inScope(url)) {
72
+ if (!prover.prepare)
73
+ continue;
74
+ const copy = response.clone();
75
+ try {
76
+ await prover.prepare(copy, context);
77
+ }
78
+ finally {
79
+ release([copy]);
80
+ }
81
+ }
82
+ },
83
+ };
84
+ }
85
+ /** Releases unread response copies without waiting, so they neither buffer the body nor block a resend. */
86
+ function release(responses) {
87
+ for (const unread of responses)
88
+ if (!unread.bodyUsed)
89
+ void unread.body?.cancel().catch(() => { });
90
+ }
91
+ /**
92
+ * Wraps a fetch implementation so every request is proved.
93
+ *
94
+ * Prefer `Mppx.create({ identity })`, which also passes the payment challenge to provers.
95
+ */
96
+ export function wrapFetch(fetch, client, context) {
97
+ return async (input, init) => {
98
+ const request = new Request(input, init);
99
+ // A retry may answer a refreshed challenge: bind proofs to the challenge its credential
100
+ // carries, and let provers prepare for a refreshed 402 of the same method and intent.
101
+ const challenge = credentialChallenge(request.headers) ?? context?.challenge;
102
+ const { response } = await client.send(fetch, request, { ...context, challenge });
103
+ if (response.status === 402 && challenge)
104
+ // A 402 without a parseable Payment challenge is returned as is: the caller decides.
105
+ for (const next of challengesOf(response))
106
+ if (next.method === challenge.method && next.intent === challenge.intent)
107
+ await client.challenge(response, { ...context, challenge: next }, request.url);
108
+ return response;
109
+ };
110
+ }
111
+ /** Returns the Payment challenges of a 402, or none when it carries none that parse. */
112
+ function challengesOf(response) {
113
+ try {
114
+ return Challenge.fromResponseList(response);
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ }
120
+ /** Returns the challenge of a Payment credential in `headers`, if any. */
121
+ function credentialChallenge(headers) {
122
+ for (const [, value] of headers) {
123
+ const payment = Credential.extractPaymentScheme(value);
124
+ if (!payment)
125
+ continue;
126
+ try {
127
+ return Credential.deserialize(payment).challenge;
128
+ }
129
+ catch { }
130
+ }
131
+ return undefined;
132
+ }
133
+ //# sourceMappingURL=Client.js.map
@@ -0,0 +1,59 @@
1
+ import type * as Types from './Types.js';
2
+ /**
3
+ * Accepts the first of several identity protocols that verifies.
4
+ *
5
+ * Every member runs. The result is `valid` if any member verifies, `invalid` if none verifies and
6
+ * any member was presented but failed, and `missing` otherwise, with every
7
+ * member's challenge headers. All members must run at the same stage.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * mppx.charge({ amount: '1', identity: [Identity.anyOf(tap, webBotAuth)] })
12
+ * ```
13
+ */
14
+ export declare function anyOf(...verifiers: readonly [Types.Verifier, Types.Verifier, ...Types.Verifier[]]): Types.Verifier<{
15
+ index: number;
16
+ value: unknown;
17
+ }>;
18
+ /** The combined outcome of a route's verifiers at one stage. @internal */
19
+ export type Outcome = {
20
+ status: 'valid';
21
+ values: ReadonlyMap<Types.Verifier, unknown>;
22
+ } | {
23
+ status: 'missing' | 'invalid';
24
+ code: number;
25
+ headers: Headers;
26
+ reason: string;
27
+ };
28
+ /**
29
+ * Runs every verifier at one stage and combines their outcomes.
30
+ *
31
+ * Every verifier must be valid. Any `invalid` outcome rejects with 401 unless
32
+ * it sets its own code; otherwise any `missing` outcome rejects with 403. The
33
+ * headers of every rejected outcome are merged. Each verifier runs at most once
34
+ * per request and context, so replay-protected nonces are consumed once even
35
+ * when several handlers evaluate the same request.
36
+ */
37
+ export declare function verify(request: Request, verifiers: readonly Types.Verifier[], context?: Types.Context): Promise<Outcome>;
38
+ /**
39
+ * Collects the challenge headers of payment-stage verifiers for one 402.
40
+ *
41
+ * Each verifier's hook runs at most once per request and nonce, so every offer
42
+ * in a composed 402 carries the same challenge. @internal
43
+ */
44
+ export declare function challenge(request: Request, verifiers: readonly Types.Verifier[], nonce: string): Promise<Headers>;
45
+ /**
46
+ * Returns the identity nonce for every 402 issued on one request.
47
+ *
48
+ * Composed routes call each offer with the same request, so all offers share it. @internal
49
+ */
50
+ export declare function nonceFor(request: Request, create: () => string): string;
51
+ /** Builds the rejection response for a failed request-stage outcome. @internal */
52
+ export declare function toResponse(outcome: Exclude<Outcome, {
53
+ status: 'valid';
54
+ }>): Response;
55
+ /** Creates a results lookup from verified values. @internal */
56
+ export declare function results(values?: ReadonlyMap<Types.Verifier, unknown>): Types.Results;
57
+ /** Returns the stage a verifier runs at. @internal */
58
+ export declare function stageOf(verifier: Types.Verifier): Types.Stage;
59
+ //# sourceMappingURL=Server.d.ts.map
@@ -0,0 +1,194 @@
1
+ const members = Symbol('mppx.identity.members');
2
+ /**
3
+ * Accepts the first of several identity protocols that verifies.
4
+ *
5
+ * Every member runs. The result is `valid` if any member verifies, `invalid` if none verifies and
6
+ * any member was presented but failed, and `missing` otherwise, with every
7
+ * member's challenge headers. All members must run at the same stage.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * mppx.charge({ amount: '1', identity: [Identity.anyOf(tap, webBotAuth)] })
12
+ * ```
13
+ */
14
+ export function anyOf(...verifiers) {
15
+ const stage = stageOf(verifiers[0]);
16
+ if (verifiers.some((verifier) => stageOf(verifier) !== stage))
17
+ throw new TypeError('Identity anyOf verifiers must run at the same stage.');
18
+ const verifier = {
19
+ [members]: verifiers,
20
+ stage,
21
+ async verify(request, context) {
22
+ // Every member runs, so each presented proof's replay nonce is consumed even when an
23
+ // earlier member already verified; the first valid member wins.
24
+ const all = [];
25
+ for (const member of verifiers)
26
+ all.push(await run(member, request, context));
27
+ const index = all.findIndex((outcome) => outcome.status === 'valid');
28
+ const matched = all[index];
29
+ if (matched?.status === 'valid')
30
+ return { status: 'valid', value: { index, value: matched.value } };
31
+ const outcomes = all;
32
+ const headers = mergeHeaders(outcomes);
33
+ const invalid = outcomes.find((outcome) => outcome.status === 'invalid');
34
+ if (invalid)
35
+ return { status: 'invalid', code: invalid.code, headers, reason: invalid.reason };
36
+ // Keep the first member's custom status so wrapping a verifier does not change its HTTP behavior.
37
+ const code = outcomes.find((outcome) => outcome.code !== undefined)?.code;
38
+ return {
39
+ status: 'missing',
40
+ ...(code !== undefined && { code }),
41
+ headers,
42
+ reason: 'None of the accepted identity proofs was presented.',
43
+ };
44
+ },
45
+ };
46
+ return verifier;
47
+ }
48
+ /**
49
+ * Runs every verifier at one stage and combines their outcomes.
50
+ *
51
+ * Every verifier must be valid. Any `invalid` outcome rejects with 401 unless
52
+ * it sets its own code; otherwise any `missing` outcome rejects with 403. The
53
+ * headers of every rejected outcome are merged. Each verifier runs at most once
54
+ * per request and context, so replay-protected nonces are consumed once even
55
+ * when several handlers evaluate the same request.
56
+ */
57
+ export async function verify(request, verifiers, context = {}) {
58
+ const values = new Map();
59
+ const rejected = [];
60
+ for (const verifier of verifiers) {
61
+ const outcome = await run(verifier, request, context);
62
+ if (outcome.status !== 'valid') {
63
+ rejected.push(outcome);
64
+ continue;
65
+ }
66
+ // Record the matched member at every anyOf level, so results.get(leaf) works when nested.
67
+ let current = verifier;
68
+ let value = outcome.value;
69
+ for (;;) {
70
+ values.set(current, value);
71
+ const nested = current[members];
72
+ if (!nested)
73
+ break;
74
+ const matched = value;
75
+ current = nested[matched.index];
76
+ value = matched.value;
77
+ }
78
+ }
79
+ if (rejected.length === 0)
80
+ return { status: 'valid', values };
81
+ const headers = mergeHeaders(rejected);
82
+ const invalid = rejected.find((outcome) => outcome.status === 'invalid');
83
+ if (invalid)
84
+ return { status: 'invalid', code: invalid.code ?? 401, headers, reason: invalid.reason };
85
+ const missing = rejected[0];
86
+ return {
87
+ status: 'missing',
88
+ code: missing.code ?? 403,
89
+ headers,
90
+ reason: missing.reason ?? 'Identity verification is required.',
91
+ };
92
+ }
93
+ /**
94
+ * Collects the challenge headers of payment-stage verifiers for one 402.
95
+ *
96
+ * Each verifier's hook runs at most once per request and nonce, so every offer
97
+ * in a composed 402 carries the same challenge. @internal
98
+ */
99
+ export async function challenge(request, verifiers, nonce) {
100
+ const headers = new Headers();
101
+ for (const verifier of verifiers) {
102
+ for (const member of leaves(verifier)) {
103
+ if (!member.challenge)
104
+ continue;
105
+ let byVerifier = challenges.get(request);
106
+ if (!byVerifier)
107
+ challenges.set(request, (byVerifier = new Map()));
108
+ let byNonce = byVerifier.get(member);
109
+ if (!byNonce)
110
+ byVerifier.set(member, (byNonce = new Map()));
111
+ let result = byNonce.get(nonce);
112
+ if (!result) {
113
+ result = Promise.resolve(member.challenge(request, { nonce }));
114
+ byNonce.set(nonce, result);
115
+ }
116
+ const init = await result;
117
+ if (init)
118
+ for (const [name, value] of new Headers(init))
119
+ headers.append(name, value);
120
+ }
121
+ }
122
+ return headers;
123
+ }
124
+ /**
125
+ * Returns the identity nonce for every 402 issued on one request.
126
+ *
127
+ * Composed routes call each offer with the same request, so all offers share it. @internal
128
+ */
129
+ export function nonceFor(request, create) {
130
+ let nonce = nonces.get(request);
131
+ if (!nonce)
132
+ nonces.set(request, (nonce = create()));
133
+ return nonce;
134
+ }
135
+ /** Builds the rejection response for a failed request-stage outcome. @internal */
136
+ export function toResponse(outcome) {
137
+ const headers = new Headers(outcome.headers);
138
+ headers.set('Cache-Control', 'no-store');
139
+ return new Response(outcome.reason, { headers, status: outcome.code });
140
+ }
141
+ /** Creates a results lookup from verified values. @internal */
142
+ export function results(values = new Map()) {
143
+ return Object.freeze({
144
+ get(verifier) {
145
+ return values.get(verifier);
146
+ },
147
+ });
148
+ }
149
+ /** Returns the stage a verifier runs at. @internal */
150
+ export function stageOf(verifier) {
151
+ const stage = verifier.stage ?? 'request';
152
+ // An unknown stage would leave the verifier unchecked, so it is rejected (fail closed).
153
+ if (stage !== 'request' && stage !== 'payment')
154
+ throw new TypeError(`Unknown identity verifier stage "${String(stage)}".`);
155
+ return stage;
156
+ }
157
+ /** Returns a verifier and, through `anyOf`, every member it wraps. */
158
+ function leaves(verifier) {
159
+ const nested = verifier[members];
160
+ return nested ? nested.flatMap(leaves) : [verifier];
161
+ }
162
+ const nonces = new WeakMap();
163
+ const challenges = new WeakMap();
164
+ const cache = new WeakMap();
165
+ function run(verifier, request, context) {
166
+ let byVerifier = cache.get(request);
167
+ if (!byVerifier)
168
+ cache.set(request, (byVerifier = new Map()));
169
+ let byContext = byVerifier.get(verifier);
170
+ if (!byContext)
171
+ byVerifier.set(verifier, (byContext = new Map()));
172
+ // Decisions can depend on the credential, so each credential object gets its own entry.
173
+ const key = `${context.challenge?.id ?? ''}|${context.nonce ?? ''}`;
174
+ let byCredential = byContext.get(key);
175
+ if (!byCredential)
176
+ byContext.set(key, (byCredential = new Map()));
177
+ let outcome = byCredential.get(context.credential);
178
+ if (!outcome) {
179
+ outcome = Promise.resolve(verifier.verify(request, context));
180
+ byCredential.set(context.credential, outcome);
181
+ }
182
+ return outcome;
183
+ }
184
+ function mergeHeaders(outcomes) {
185
+ const merged = new Headers();
186
+ for (const outcome of outcomes) {
187
+ if (outcome.status === 'valid' || !outcome.headers)
188
+ continue;
189
+ for (const [name, value] of new Headers(outcome.headers))
190
+ merged.append(name, value);
191
+ }
192
+ return merged;
193
+ }
194
+ //# sourceMappingURL=Server.js.map
@@ -0,0 +1,112 @@
1
+ import type * as Challenge from '../Challenge.js';
2
+ import type * as Credential from '../Credential.js';
3
+ import type { MaybePromise } from '../internal/types.js';
4
+ /**
5
+ * When a verifier runs on the server.
6
+ *
7
+ * `request` verifiers run before any payment challenge is issued and reject
8
+ * the request with their own status code. `payment` verifiers add their own
9
+ * challenge headers to the 402 (through `challenge`) and run on the paid
10
+ * retry, before the credential is settled.
11
+ */
12
+ export type Stage = 'request' | 'payment';
13
+ /**
14
+ * Payment state passed to verifiers and provers.
15
+ *
16
+ * Request-stage verifiers receive an empty context. Payment-stage verifiers
17
+ * receive the answered challenge, its credential and the 402's identity nonce.
18
+ */
19
+ export type Context = {
20
+ /** The MPP challenge being answered (or, for provers, selected). */
21
+ challenge?: Challenge.Challenge | undefined;
22
+ /** The payment credential submitted with the request. */
23
+ credential?: Credential.Credential | undefined;
24
+ /**
25
+ * The identity nonce of the 402 being answered.
26
+ *
27
+ * Every payment offer in one 402 carries the same HMAC-bound nonce, so a proof
28
+ * bound to it is bound to that 402 whichever offer the client pays.
29
+ */
30
+ nonce?: string | undefined;
31
+ };
32
+ /** Context passed to a payment-stage verifier's `challenge` hook. */
33
+ export type ChallengeContext = {
34
+ /** The identity nonce shared by every payment offer in the 402. Bind the proof request to it. */
35
+ nonce: string;
36
+ };
37
+ /**
38
+ * The outcome of one identity verifier.
39
+ *
40
+ * `missing` means the identity proof was not presented; its `headers` (for
41
+ * example a protocol challenge) are returned to the client. `invalid` means a
42
+ * proof was presented but must be rejected. `valid` carries the verified value.
43
+ */
44
+ export type Verification<value = unknown> = {
45
+ status: 'missing';
46
+ /** HTTP status for a request-stage rejection. @default 403 */
47
+ code?: number | undefined;
48
+ /** Headers returned to the client, such as a protocol challenge. */
49
+ headers?: HeadersInit | undefined;
50
+ /** Human-readable reason. */
51
+ reason?: string | undefined;
52
+ } | {
53
+ status: 'invalid';
54
+ /** HTTP status for a request-stage rejection. @default 401 */
55
+ code?: number | undefined;
56
+ /** Headers returned to the client, such as a protocol error object. */
57
+ headers?: HeadersInit | undefined;
58
+ /** Human-readable reason. */
59
+ reason: string;
60
+ } | {
61
+ status: 'valid';
62
+ value: value;
63
+ };
64
+ /** Verifies one identity protocol on the server. */
65
+ export type Verifier<value = unknown> = {
66
+ /** When the verifier runs. @default 'request' */
67
+ stage?: Stage | undefined;
68
+ /**
69
+ * Returns the protocol's challenge headers to add to a 402, such as a proof request.
70
+ *
71
+ * Payment stage only. Runs once per 402, however many payment offers it carries.
72
+ */
73
+ challenge?: ((request: Request, context: ChallengeContext) => MaybePromise<HeadersInit | undefined>) | undefined;
74
+ /** Returns the protocol's verification outcome for the request. */
75
+ verify: (request: Request, context: Context) => MaybePromise<Verification<value>>;
76
+ };
77
+ /** Verified identity values, looked up by the verifier that produced them. */
78
+ export type Results = {
79
+ /** Returns the verified value for a verifier, or `undefined` if it did not run or match. */
80
+ get<value>(verifier: Verifier<value>): value | undefined;
81
+ };
82
+ /**
83
+ * Values shared by every prover contributing to one HTTP attempt.
84
+ *
85
+ * Every retry gets a new context, so single-use nonces are never replayed.
86
+ */
87
+ export type ProveContext = Context & {
88
+ /** Unix timestamp, in seconds, at which the attempt was created. */
89
+ readonly created: number;
90
+ /** Base64url-encoded random value identifying the attempt. */
91
+ readonly nonce: string;
92
+ };
93
+ /** Adds one identity protocol to outbound requests on the client. */
94
+ export type Prover = {
95
+ /** Origins the prover may run for, such as `['https://shop.example']`, or `'*'` for any origin. */
96
+ scope: readonly string[] | '*';
97
+ /** Returns a new request carrying the protocol's identity material. */
98
+ prove: (request: Request, context: ProveContext) => MaybePromise<Request>;
99
+ /**
100
+ * Prepares a proof from a 402 before the payment retry, such as answering its proof request.
101
+ *
102
+ * The context carries the selected challenge; `prove` receives the same challenge
103
+ * on the paid retry.
104
+ */
105
+ prepare?: ((response: Response, context: Context) => MaybePromise<void>) | undefined;
106
+ /**
107
+ * Inspects a rejected (non-402) response. Return `true` to prove and resend the
108
+ * request once. Paid attempts are never resent.
109
+ */
110
+ retry?: ((response: Response) => MaybePromise<boolean | undefined>) | undefined;
111
+ };
112
+ //# sourceMappingURL=Types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Types.js.map
@@ -1,6 +1,6 @@
1
1
  export * as Client from './Client.js';
2
2
  export * as Server from './Server.js';
3
+ export { anyOf } from './Server.js';
3
4
  export * as Store from '../Store.js';
4
- export * from './Constants.js';
5
- export * from './Types.js';
5
+ export type * from './Types.js';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,5 @@
1
1
  export * as Client from './Client.js';
2
2
  export * as Server from './Server.js';
3
+ export { anyOf } from './Server.js';
3
4
  export * as Store from '../Store.js';
4
- export * from './Constants.js';
5
- export * from './Types.js';
6
5
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ /** Creates the attempt values shared by every prover on one HTTP attempt. */
2
+ export declare function create(): {
3
+ readonly created: number;
4
+ readonly nonce: string;
5
+ };
6
+ //# sourceMappingURL=SigningContext.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { Base64 } from 'ox';
2
+ /** Creates the attempt values shared by every prover on one HTTP attempt. */
3
+ export function create() {
4
+ const bytes = new Uint8Array(64);
5
+ crypto.getRandomValues(bytes);
6
+ return {
7
+ created: Math.floor(Date.now() / 1_000),
8
+ nonce: Base64.fromBytes(bytes, { pad: false, url: true }),
9
+ };
10
+ }
11
+ //# sourceMappingURL=SigningContext.js.map
@@ -21,6 +21,7 @@ export declare function createHandler(parameters: {
21
21
  } | {
22
22
  status: 200;
23
23
  challenge?: import("../../Challenge.js").Challenge | undefined;
24
+ identity?: import("../../identity/Types.js").Results | undefined;
24
25
  withReceipt: import("../../server/Transport.js").WithReceipt<import("../../server/Transport.js").Http>;
25
26
  };
26
27
  skipHandlerResponse: Response | undefined;
@@ -1,5 +1,5 @@
1
1
  /** Current mppx package version. */
2
- export declare const version = "0.15.0";
2
+ export declare const version = "0.17.0";
3
3
  /** Canonical SDK identifier for this mppx release. */
4
- export declare const sdkIdentifier = "mppx/0.15.0";
4
+ export declare const sdkIdentifier = "mppx/0.17.0";
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,6 +1,6 @@
1
1
  // Generated by scripts/sync-version.ts.
2
2
  /** Current mppx package version. */
3
- export const version = '0.15.0';
3
+ export const version = '0.17.0';
4
4
  /** Canonical SDK identifier for this mppx release. */
5
5
  export const sdkIdentifier = `mppx/${version}`;
6
6
  //# sourceMappingURL=version.js.map