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
|
@@ -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, context))
|
|
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.retry)
|
|
73
|
+
continue;
|
|
74
|
+
const copy = response.clone();
|
|
75
|
+
try {
|
|
76
|
+
await prover.retry(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,46 @@
|
|
|
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
|
+
/** Builds the rejection response for a failed request-stage outcome. @internal */
|
|
39
|
+
export declare function toResponse(outcome: Exclude<Outcome, {
|
|
40
|
+
status: 'valid';
|
|
41
|
+
}>): Response;
|
|
42
|
+
/** Creates a results lookup from verified values. @internal */
|
|
43
|
+
export declare function results(values?: ReadonlyMap<Types.Verifier, unknown>): Types.Results;
|
|
44
|
+
/** Returns the stage a verifier runs at. @internal */
|
|
45
|
+
export declare function stageOf(verifier: Types.Verifier): Types.Stage;
|
|
46
|
+
//# sourceMappingURL=Server.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
/** Builds the rejection response for a failed request-stage outcome. @internal */
|
|
94
|
+
export function toResponse(outcome) {
|
|
95
|
+
const headers = new Headers(outcome.headers);
|
|
96
|
+
headers.set('Cache-Control', 'no-store');
|
|
97
|
+
return new Response(outcome.reason, { headers, status: outcome.code });
|
|
98
|
+
}
|
|
99
|
+
/** Creates a results lookup from verified values. @internal */
|
|
100
|
+
export function results(values = new Map()) {
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
get(verifier) {
|
|
103
|
+
return values.get(verifier);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Returns the stage a verifier runs at. @internal */
|
|
108
|
+
export function stageOf(verifier) {
|
|
109
|
+
const stage = verifier.stage ?? 'request';
|
|
110
|
+
// An unknown stage would leave the verifier unchecked, so it is rejected (fail closed).
|
|
111
|
+
if (stage !== 'request' && stage !== 'payment')
|
|
112
|
+
throw new TypeError(`Unknown identity verifier stage "${String(stage)}".`);
|
|
113
|
+
return stage;
|
|
114
|
+
}
|
|
115
|
+
const cache = new WeakMap();
|
|
116
|
+
function run(verifier, request, context) {
|
|
117
|
+
let byVerifier = cache.get(request);
|
|
118
|
+
if (!byVerifier)
|
|
119
|
+
cache.set(request, (byVerifier = new Map()));
|
|
120
|
+
let byContext = byVerifier.get(verifier);
|
|
121
|
+
if (!byContext)
|
|
122
|
+
byVerifier.set(verifier, (byContext = new Map()));
|
|
123
|
+
// Decisions can depend on the credential, so each credential object gets its own entry.
|
|
124
|
+
const key = context.challenge?.id ?? '';
|
|
125
|
+
let byCredential = byContext.get(key);
|
|
126
|
+
if (!byCredential)
|
|
127
|
+
byContext.set(key, (byCredential = new Map()));
|
|
128
|
+
let outcome = byCredential.get(context.credential);
|
|
129
|
+
if (!outcome) {
|
|
130
|
+
outcome = Promise.resolve(verifier.verify(request, context));
|
|
131
|
+
byCredential.set(context.credential, outcome);
|
|
132
|
+
}
|
|
133
|
+
return outcome;
|
|
134
|
+
}
|
|
135
|
+
function mergeHeaders(outcomes) {
|
|
136
|
+
const merged = new Headers();
|
|
137
|
+
for (const outcome of outcomes) {
|
|
138
|
+
if (outcome.status === 'valid' || !outcome.headers)
|
|
139
|
+
continue;
|
|
140
|
+
for (const [name, value] of new Headers(outcome.headers))
|
|
141
|
+
merged.append(name, value);
|
|
142
|
+
}
|
|
143
|
+
return merged;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=Server.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
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 run when a
|
|
9
|
+
* challenge is issued (to add their own challenge headers to the 402) and
|
|
10
|
+
* again on the paid 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 challenge being issued, or the challenge and credential being
|
|
18
|
+
* answered on the paid retry.
|
|
19
|
+
*/
|
|
20
|
+
export type Context = {
|
|
21
|
+
/** The MPP challenge being issued or answered. */
|
|
22
|
+
challenge?: Challenge.Challenge | undefined;
|
|
23
|
+
/** The payment credential submitted with the request. */
|
|
24
|
+
credential?: Credential.Credential | undefined;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The outcome of one identity verifier.
|
|
28
|
+
*
|
|
29
|
+
* `missing` means the identity proof was not presented; its `headers` (for
|
|
30
|
+
* example a protocol challenge) are returned to the client. `invalid` means a
|
|
31
|
+
* proof was presented but must be rejected. `valid` carries the verified value.
|
|
32
|
+
*/
|
|
33
|
+
export type Verification<value = unknown> = {
|
|
34
|
+
status: 'missing';
|
|
35
|
+
/** HTTP status for a request-stage rejection. @default 403 */
|
|
36
|
+
code?: number | undefined;
|
|
37
|
+
/** Headers returned to the client, such as a protocol challenge. */
|
|
38
|
+
headers?: HeadersInit | undefined;
|
|
39
|
+
/** Human-readable reason. */
|
|
40
|
+
reason?: string | undefined;
|
|
41
|
+
} | {
|
|
42
|
+
status: 'invalid';
|
|
43
|
+
/** HTTP status for a request-stage rejection. @default 401 */
|
|
44
|
+
code?: number | undefined;
|
|
45
|
+
/** Headers returned to the client, such as a protocol error object. */
|
|
46
|
+
headers?: HeadersInit | undefined;
|
|
47
|
+
/** Human-readable reason. */
|
|
48
|
+
reason: string;
|
|
49
|
+
} | {
|
|
50
|
+
status: 'valid';
|
|
51
|
+
value: value;
|
|
52
|
+
};
|
|
53
|
+
/** Verifies one identity protocol on the server. */
|
|
54
|
+
export type Verifier<value = unknown> = {
|
|
55
|
+
/** When the verifier runs. @default 'request' */
|
|
56
|
+
stage?: Stage | undefined;
|
|
57
|
+
/** Returns the protocol's verification outcome for the request. */
|
|
58
|
+
verify: (request: Request, context: Context) => MaybePromise<Verification<value>>;
|
|
59
|
+
};
|
|
60
|
+
/** Verified identity values, looked up by the verifier that produced them. */
|
|
61
|
+
export type Results = {
|
|
62
|
+
/** Returns the verified value for a verifier, or `undefined` if it did not run or match. */
|
|
63
|
+
get<value>(verifier: Verifier<value>): value | undefined;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Values shared by every prover contributing to one HTTP attempt.
|
|
67
|
+
*
|
|
68
|
+
* Every retry gets a new context, so single-use nonces are never replayed.
|
|
69
|
+
*/
|
|
70
|
+
export type ProveContext = Context & {
|
|
71
|
+
/** Unix timestamp, in seconds, at which the attempt was created. */
|
|
72
|
+
readonly created: number;
|
|
73
|
+
/** Base64url-encoded random value identifying the attempt. */
|
|
74
|
+
readonly nonce: string;
|
|
75
|
+
};
|
|
76
|
+
/** Adds one identity protocol to outbound requests on the client. */
|
|
77
|
+
export type Prover = {
|
|
78
|
+
/** Origins the prover may run for, such as `['https://shop.example']`, or `'*'` for any origin. */
|
|
79
|
+
scope: readonly string[] | '*';
|
|
80
|
+
/** Returns a new request carrying the protocol's identity material. */
|
|
81
|
+
prove: (request: Request, context: ProveContext) => MaybePromise<Request>;
|
|
82
|
+
/**
|
|
83
|
+
* Inspects a rejected or 402 response and prepares a proof for the next attempt.
|
|
84
|
+
*
|
|
85
|
+
* Return `true` to resend a rejected (non-402) request once. On a 402 the
|
|
86
|
+
* context carries the selected challenge and the payment retry always follows.
|
|
87
|
+
*/
|
|
88
|
+
retry?: ((response: Response, context: Context) => MaybePromise<boolean | undefined>) | undefined;
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=Types.d.ts.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 './
|
|
5
|
-
export * from './Types.js';
|
|
5
|
+
export type * from './Types.js';
|
|
6
6
|
//# sourceMappingURL=index.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.
|
|
2
|
+
export declare const version = "0.16.0";
|
|
3
3
|
/** Canonical SDK identifier for this mppx release. */
|
|
4
|
-
export declare const sdkIdentifier = "mppx/0.
|
|
4
|
+
export declare const sdkIdentifier = "mppx/0.16.0";
|
|
5
5
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/internal/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.ts.
|
|
2
2
|
/** Current mppx package version. */
|
|
3
|
-
export const version = '0.
|
|
3
|
+
export const version = '0.16.0';
|
|
4
4
|
/** Canonical SDK identifier for this mppx release. */
|
|
5
5
|
export const sdkIdentifier = `mppx/${version}`;
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
|
@@ -69,6 +69,11 @@ export declare namespace wrap {
|
|
|
69
69
|
paymentPreferences?: AcceptPayment.Config<FlattenMethods<methods>> | undefined;
|
|
70
70
|
/** Array of methods to use. Accepts individual clients or tuples (e.g. from `tempo()`). */
|
|
71
71
|
methods: methods;
|
|
72
|
+
/**
|
|
73
|
+
* Realms allowed to request zero-amount Tempo proofs. An MCP server has no origin that could
|
|
74
|
+
* own a realm, so these proofs are refused unless their realm is listed here.
|
|
75
|
+
*/
|
|
76
|
+
trustedTempoProofRealms?: readonly string[] | undefined;
|
|
72
77
|
};
|
|
73
78
|
type McpClient<client extends Pick<Client, 'callTool'> = Pick<Client, 'callTool'>, methods extends Methods = DefaultMethods> = Omit<client, 'callTool'> & {
|
|
74
79
|
/** Call a tool with automatic payment handling. Preserves the MCP SDK signature. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Challenge from '../../Challenge.js';
|
|
2
|
+
import * as ProofRealm from '../../client/internal/ProofRealm.js';
|
|
2
3
|
import * as Credential from '../../Credential.js';
|
|
3
4
|
import * as Expires from '../../Expires.js';
|
|
4
5
|
import * as AcceptPayment from '../../internal/AcceptPayment.js';
|
|
@@ -111,6 +112,7 @@ function createPaymentAwareCallTool(callTool, config) {
|
|
|
111
112
|
}
|
|
112
113
|
if (selected.challenge.expires)
|
|
113
114
|
Expires.assert(selected.challenge.expires, selected.challenge.id);
|
|
115
|
+
ProofRealm.assertWithoutOrigin(selected.challenge, config.trustedTempoProofRealms);
|
|
114
116
|
if (call.onPaymentRequired) {
|
|
115
117
|
const approved = await call.onPaymentRequired(selected.challenge);
|
|
116
118
|
if (!approved)
|
package/dist/proxy/Proxy.js
CHANGED
|
@@ -91,22 +91,23 @@ export function create(config) {
|
|
|
91
91
|
// so same-path paid routes can coexist without sharing credentials.
|
|
92
92
|
Route.matchPath(service.routes, upstreamPath,
|
|
93
93
|
// skip free routes (e.g. `'GET /foo/bar': true`)
|
|
94
|
-
(endpoint) => endpoint !==
|
|
94
|
+
(endpoint) => Service.getHandler(endpoint) !== undefined &&
|
|
95
|
+
matchesPaymentBinding(endpoint, fallbackBinding))
|
|
95
96
|
: null;
|
|
96
97
|
const matched = exactMatch ?? fallbackMatch;
|
|
97
98
|
if (!matched)
|
|
98
99
|
return new Response('Not Found', { status: 404 });
|
|
99
100
|
const endpoint = matched.value;
|
|
100
101
|
const ctx = { request, service, upstreamPath };
|
|
101
|
-
|
|
102
|
+
const handler = Service.getHandler(endpoint);
|
|
103
|
+
if (!handler)
|
|
102
104
|
return proxyUpstream({
|
|
103
105
|
request,
|
|
104
106
|
service,
|
|
105
|
-
ctx,
|
|
107
|
+
ctx: { ...ctx, ...Service.getOptions(endpoint) },
|
|
106
108
|
proxy,
|
|
107
109
|
onUpstreamError: service.onUpstreamError ?? config.onUpstreamError,
|
|
108
110
|
});
|
|
109
|
-
const handler = typeof endpoint === 'function' ? endpoint : endpoint.pay;
|
|
110
111
|
const scope = getConfiguredScope(handler) ??
|
|
111
112
|
deriveRouteScope({
|
|
112
113
|
basePath: config.basePath,
|
|
@@ -276,6 +277,10 @@ function buildDiscoveryRoutes(services) {
|
|
|
276
277
|
method: hasMethod ? tokens[0] : 'GET',
|
|
277
278
|
path: `/${service.id}${path}`,
|
|
278
279
|
payment: endpoint ? Service.paymentOf(endpoint) : null,
|
|
280
|
+
...(typeof endpoint === 'object' && endpoint?.requestBody
|
|
281
|
+
? { requestBody: endpoint.requestBody }
|
|
282
|
+
: {}),
|
|
283
|
+
...(typeof endpoint === 'object' && endpoint?.summary ? { summary: endpoint.summary } : {}),
|
|
279
284
|
};
|
|
280
285
|
}));
|
|
281
286
|
}
|
|
@@ -328,7 +333,7 @@ function getPaymentBinding(request) {
|
|
|
328
333
|
return null;
|
|
329
334
|
}
|
|
330
335
|
function matchesPaymentBinding(endpoint, binding) {
|
|
331
|
-
if (endpoint
|
|
336
|
+
if (!Service.getHandler(endpoint))
|
|
332
337
|
return false;
|
|
333
338
|
if (!binding)
|
|
334
339
|
return true;
|
package/dist/proxy/Service.d.ts
CHANGED
|
@@ -30,13 +30,45 @@ export type Docs = {
|
|
|
30
30
|
* An endpoint definition.
|
|
31
31
|
*
|
|
32
32
|
* - `IntentHandler` — payment required, calls the handler to issue a 402 challenge or verify payment.
|
|
33
|
-
* - `{ pay, options }`
|
|
33
|
+
* - `EndpointConfig` — `{ pay, options, summary, requestBody }` (paid) or `{ free: true, options, summary, requestBody }` (free).
|
|
34
34
|
* - `true` — free passthrough, no payment required, rewriteRequest is applied.
|
|
35
35
|
*/
|
|
36
|
-
export type Endpoint = IntentHandler |
|
|
36
|
+
export type Endpoint = IntentHandler | EndpointConfig | true;
|
|
37
|
+
/**
|
|
38
|
+
* An endpoint definition with per-endpoint config overrides and discovery metadata.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* routes: {
|
|
43
|
+
* 'POST /v1/search': {
|
|
44
|
+
* pay: mppx.charge({ amount: '0.01' }),
|
|
45
|
+
* summary: 'Search the web',
|
|
46
|
+
* requestBody: {
|
|
47
|
+
* content: { 'application/json': { schema: { type: 'object' } } },
|
|
48
|
+
* },
|
|
49
|
+
* },
|
|
50
|
+
* 'GET /v1/status': { free: true, summary: 'Service status' },
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export type EndpointConfig = EndpointMetadata & ({
|
|
55
|
+
/** Payment handler. */
|
|
37
56
|
pay: IntentHandler;
|
|
38
|
-
|
|
39
|
-
} |
|
|
57
|
+
free?: undefined;
|
|
58
|
+
} | {
|
|
59
|
+
/** Free passthrough route, declared explicitly so a missing handler never means free. */
|
|
60
|
+
free: true;
|
|
61
|
+
pay?: undefined;
|
|
62
|
+
});
|
|
63
|
+
/** Per-endpoint config and discovery metadata shared by paid and free endpoint objects. */
|
|
64
|
+
export type EndpointMetadata = {
|
|
65
|
+
/** Per-endpoint config overrides passed to request hooks via `ctx`. */
|
|
66
|
+
options?: EndpointOptions | undefined;
|
|
67
|
+
/** OpenAPI request body object emitted in `/openapi.json`. */
|
|
68
|
+
requestBody?: Record<string, unknown> | undefined;
|
|
69
|
+
/** Short summary emitted in `/openapi.json`. */
|
|
70
|
+
summary?: string | undefined;
|
|
71
|
+
};
|
|
40
72
|
/** Map of `"METHOD /pattern"` keys to endpoint definitions. */
|
|
41
73
|
export type EndpointMap<routes extends string = string> = Partial<Record<routes, Endpoint>> & Record<string & {}, Endpoint>;
|
|
42
74
|
/** Per-endpoint configuration overrides (e.g. `{ apiKey: 'sk-...' }`). */
|
|
@@ -149,6 +181,12 @@ export declare function toLlmsTxt(services: Service[], options?: {
|
|
|
149
181
|
}): string;
|
|
150
182
|
/** Extracts per-endpoint options from an endpoint definition. */
|
|
151
183
|
export declare function getOptions(endpoint: Endpoint): EndpointOptions | undefined;
|
|
184
|
+
/**
|
|
185
|
+
* Returns the payment handler of an endpoint, or `undefined` for a route declared free (`true` or
|
|
186
|
+
* `{ free: true }`). Throws for anything else, such as `{ pay: undefined }`, so a missing handler
|
|
187
|
+
* fails closed instead of proxying for free with the service's credentials.
|
|
188
|
+
*/
|
|
189
|
+
export declare function getHandler(endpoint: Endpoint): IntentHandler | undefined;
|
|
152
190
|
/** Derives discovery payment metadata from an explicitly priced endpoint handler. */
|
|
153
191
|
export declare function paymentOf(endpoint: Endpoint): Record<string, unknown> | null;
|
|
154
192
|
//# sourceMappingURL=Service.d.ts.map
|
package/dist/proxy/Service.js
CHANGED
|
@@ -86,16 +86,32 @@ export function toLlmsTxt(services, options) {
|
|
|
86
86
|
}
|
|
87
87
|
/** Extracts per-endpoint options from an endpoint definition. */
|
|
88
88
|
export function getOptions(endpoint) {
|
|
89
|
-
if (typeof endpoint === 'object' && endpoint !== null
|
|
89
|
+
if (typeof endpoint === 'object' && endpoint !== null)
|
|
90
90
|
return endpoint.options;
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns the payment handler of an endpoint, or `undefined` for a route declared free (`true` or
|
|
95
|
+
* `{ free: true }`). Throws for anything else, such as `{ pay: undefined }`, so a missing handler
|
|
96
|
+
* fails closed instead of proxying for free with the service's credentials.
|
|
97
|
+
*/
|
|
98
|
+
export function getHandler(endpoint) {
|
|
99
|
+
if (typeof endpoint === 'function')
|
|
100
|
+
return endpoint;
|
|
101
|
+
if (endpoint === true)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (typeof endpoint === 'object' && endpoint !== null) {
|
|
104
|
+
if (typeof endpoint.pay === 'function')
|
|
105
|
+
return endpoint.pay;
|
|
106
|
+
if (endpoint.free === true && endpoint.pay === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
throw new Error('Invalid proxy endpoint: expected a payment handler, `true` or `{ free: true }`.');
|
|
110
|
+
}
|
|
93
111
|
/** Derives discovery payment metadata from an explicitly priced endpoint handler. */
|
|
94
112
|
export function paymentOf(endpoint) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const handler = typeof endpoint === 'function' ? endpoint : endpoint.pay;
|
|
98
|
-
if (!('_internal' in handler))
|
|
113
|
+
const handler = getHandler(endpoint);
|
|
114
|
+
if (!handler || !('_internal' in handler))
|
|
99
115
|
return null;
|
|
100
116
|
const metadata = handler._internal;
|
|
101
117
|
if (!metadata)
|