mppx 0.17.1 → 0.17.3
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/Store.d.ts +225 -0
- package/dist/Store.js +358 -0
- package/dist/cli/cli.js +95 -94
- package/dist/cli/plugins/index.js +12 -11
- package/dist/cli/sessions/Manager.js +12 -1
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +6 -0
- package/dist/client/Mppx.d.ts +1 -1
- package/dist/client/Mppx.js +7 -3
- package/dist/client/internal/Fetch.d.ts +1 -1
- package/dist/client/internal/Fetch.js +9 -0
- package/dist/client/internal/MethodResponse.d.ts +8 -0
- package/dist/client/internal/MethodResponse.js +9 -0
- package/dist/client/internal/ProofRealm.d.ts +3 -3
- package/dist/client/internal/ProofRealm.js +4 -4
- package/dist/client/internal/protocols/Mcp.d.ts +4 -0
- package/dist/client/internal/protocols/Mcp.js +27 -14
- package/dist/identity/Client.js +7 -1
- package/dist/internal/sse.d.ts +21 -0
- package/dist/internal/sse.js +65 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.d.ts +2 -2
- package/dist/mcp/client/McpClient.js +5 -2
- package/dist/proxy/Proxy.d.ts +2 -0
- package/dist/proxy/Proxy.js +31 -6
- package/dist/proxy/internal/Replay.js +1 -1
- package/dist/server/Mppx.js +5 -4
- package/dist/server/Transport.js +1 -1
- package/dist/server/internal/html/config.d.ts +7 -1
- package/dist/server/internal/html/config.js +4 -0
- package/dist/stripe/internal/types.d.ts +82 -6
- package/dist/stripe/server/Charge.js +1 -2
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +85 -10
- package/dist/tempo/internal/fee-payer.js +183 -19
- package/dist/tempo/legacy/session/Chain.js +2 -0
- package/dist/tempo/server/Charge.js +2 -2
- package/dist/tempo/server/Subscription.js +1 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.d.ts +6 -0
- package/dist/tempo/server/internal/transport.js +21 -21
- package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/client/CredentialState.js +2 -0
- package/dist/tempo/session/client/Session.d.ts +11 -1
- package/dist/tempo/session/client/Session.js +39 -6
- package/dist/tempo/session/client/SessionManager.d.ts +1 -1
- package/dist/tempo/session/client/SessionManager.js +8 -4
- package/dist/tempo/session/client/Transports.d.ts +2 -0
- package/dist/tempo/session/client/Transports.js +4 -1
- package/dist/tempo/session/precompile/Chain.js +1 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +9 -0
- package/dist/tempo/session/server/ChannelStore.js +8 -1
- package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
- package/dist/tempo/session/server/MeteredStream.js +6 -1
- package/dist/tempo/session/server/Sse.d.ts +59 -0
- package/dist/tempo/session/server/Sse.js +175 -10
- package/dist/zod.d.ts +2 -1
- package/dist/zod.js +1 -1
- package/package.json +1 -3
- package/dist/stripe/server/internal/html/types.d.ts +0 -2
- package/dist/stripe/server/internal/html/types.js +0 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Challenge from '../../Challenge.js';
|
|
2
|
+
import * as ProofRealm from '../../client/internal/ProofRealm.js';
|
|
2
3
|
import { resolveEscrow } from '../../tempo/session/client/ChannelOps.js';
|
|
3
4
|
import { getSessionManagerInternals } from '../../tempo/session/client/internal/SessionManager.js';
|
|
4
5
|
import { sessionManager } from '../../tempo/session/client/SessionManager.js';
|
|
@@ -25,6 +26,14 @@ function assertCloseChallengeScope(challenge, channel) {
|
|
|
25
26
|
/** Rehydrates durable session context and cooperatively closes it through the manager. */
|
|
26
27
|
export async function closeWithSessionManager(parameters) {
|
|
27
28
|
assertCloseChallengeScope(parameters.challenge, parameters.channel);
|
|
29
|
+
let channel = parameters.channel;
|
|
30
|
+
if (channel.origin === undefined) {
|
|
31
|
+
const url = new URL(parameters.input instanceof Request ? parameters.input.url : parameters.input);
|
|
32
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
33
|
+
throw new Error('Legacy session close requires an HTTP origin');
|
|
34
|
+
ProofRealm.assert(parameters.challenge, url, parameters.manager.trustedTempoProofRealms);
|
|
35
|
+
channel = { ...channel, origin: url.origin };
|
|
36
|
+
}
|
|
28
37
|
const networkFetch = resolveFetch(parameters.fetch);
|
|
29
38
|
let pendingChallenge;
|
|
30
39
|
const validatedFetch = async (input, init) => {
|
|
@@ -48,7 +57,9 @@ export async function closeWithSessionManager(parameters) {
|
|
|
48
57
|
escrow: parameters.channel.escrow,
|
|
49
58
|
fetch: validatedFetch,
|
|
50
59
|
});
|
|
51
|
-
getSessionManagerInternals(manager).rehydrate(parameters);
|
|
60
|
+
getSessionManagerInternals(manager).rehydrate({ ...parameters, channel });
|
|
61
|
+
if (channel !== parameters.channel)
|
|
62
|
+
await parameters.manager.channelStore?.set(channel);
|
|
52
63
|
const receipt = await manager.close();
|
|
53
64
|
if (!receipt)
|
|
54
65
|
throw new Error('Session close response did not include a payment receipt.');
|
package/dist/cli/utils.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export declare function fmtRequestValue(key: string, value: unknown, ctx: {
|
|
|
58
58
|
}): string;
|
|
59
59
|
export declare function decodeMemo(hex: string): string | undefined;
|
|
60
60
|
export declare function fmtChallengeValue(key: string, value: unknown): string;
|
|
61
|
+
/** Formats a problem-details field for display, serializing nested objects and arrays as JSON. */
|
|
62
|
+
export declare function fmtProblemValue(value: unknown): string;
|
|
61
63
|
export declare function link(url: string, text: string): string;
|
|
62
64
|
export declare function parseMethodOpts(raw: string | string[] | undefined): Record<string, string>;
|
|
63
65
|
/** Returns true when the CLI is running inside an agent wrapper that prefers machine-readable output. */
|
package/dist/cli/utils.js
CHANGED
|
@@ -141,6 +141,12 @@ export function fmtChallengeValue(key, value) {
|
|
|
141
141
|
}
|
|
142
142
|
return String(value);
|
|
143
143
|
}
|
|
144
|
+
/** Formats a problem-details field for display, serializing nested objects and arrays as JSON. */
|
|
145
|
+
export function fmtProblemValue(value) {
|
|
146
|
+
if (value !== null && typeof value === 'object')
|
|
147
|
+
return JSON.stringify(value);
|
|
148
|
+
return String(value);
|
|
149
|
+
}
|
|
144
150
|
export function link(url, text) {
|
|
145
151
|
return pc.link(url, text);
|
|
146
152
|
}
|
package/dist/client/Mppx.d.ts
CHANGED
|
@@ -189,7 +189,7 @@ export declare namespace create {
|
|
|
189
189
|
/** Transport to use (defaults to HTTP). */
|
|
190
190
|
transport?: transport | undefined;
|
|
191
191
|
/**
|
|
192
|
-
* Server origins allowed to request zero-amount Tempo proofs for a realm other than their
|
|
192
|
+
* Server origins allowed to request zero-amount Tempo proofs or session payments for a realm other than their
|
|
193
193
|
* hostname, mapped to that realm, such as `{ 'https://api.example.com': 'MPP Payment' }`.
|
|
194
194
|
* With `Transport.mcp()`, which has no origin, only realms listed here are trusted,
|
|
195
195
|
* whatever their origin.
|
package/dist/client/Mppx.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as IdentityClient from '../identity/Client.js';
|
|
|
4
4
|
import * as AcceptPayment from '../internal/AcceptPayment.js';
|
|
5
5
|
import * as Fetch from './internal/Fetch.js';
|
|
6
6
|
import * as MethodChallenge from './internal/MethodChallenge.js';
|
|
7
|
+
import * as MethodResponse from './internal/MethodResponse.js';
|
|
7
8
|
import * as ProofRealm from './internal/ProofRealm.js';
|
|
8
9
|
import * as Transport from './Transport.js';
|
|
9
10
|
const preparedPaymentMethods = new WeakMap();
|
|
@@ -112,7 +113,7 @@ export function create(config) {
|
|
|
112
113
|
try {
|
|
113
114
|
if (selectedChallenge.expires)
|
|
114
115
|
Expires.assert(selectedChallenge.expires, selectedChallenge.id);
|
|
115
|
-
const createCredential = memoizeCreateCredential((overrideContext) => createCredentialForMethod(selectedChallenge, selectedMethod, overrideContext ?? context));
|
|
116
|
+
const createCredential = memoizeCreateCredential((overrideContext) => createCredentialForMethod(selectedChallenge, selectedMethod, overrideContext ?? context, responseUrl?.origin));
|
|
116
117
|
const eventCredential = await events.emit('challenge.received', createChallengeReceivedPayload({
|
|
117
118
|
challenge: selectedChallenge,
|
|
118
119
|
challenges,
|
|
@@ -482,9 +483,12 @@ function resolveChallengePreferences(fallback, override) {
|
|
|
482
483
|
async function resolveChallengeOrder(candidates, orderChallenges) {
|
|
483
484
|
return orderChallenges ? orderChallenges(candidates) : candidates;
|
|
484
485
|
}
|
|
485
|
-
async function createCredentialForMethod(challenge, mi, context) {
|
|
486
|
+
async function createCredentialForMethod(challenge, mi, context, origin) {
|
|
486
487
|
const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined;
|
|
487
|
-
|
|
488
|
+
const parameters = parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge };
|
|
489
|
+
if (origin)
|
|
490
|
+
MethodResponse.attachOrigin(parameters, origin);
|
|
491
|
+
return mi.createCredential(parameters);
|
|
488
492
|
}
|
|
489
493
|
/** Returns the URL of the HTTP server that sent `response`, or `undefined` when it has none. */
|
|
490
494
|
function resolveResponseUrl(response) {
|
|
@@ -116,7 +116,7 @@ export declare namespace from {
|
|
|
116
116
|
/** Transport to use for challenge extraction and credential attachment. */
|
|
117
117
|
transport?: Transport.AnyTransport | undefined;
|
|
118
118
|
/**
|
|
119
|
-
* Server origins allowed to request zero-amount Tempo proofs for a realm other than their
|
|
119
|
+
* Server origins allowed to request zero-amount Tempo proofs or session payments for a realm other than their
|
|
120
120
|
* hostname, mapped to that realm, such as `{ 'https://api.example.com': 'MPP Payment' }`.
|
|
121
121
|
*/
|
|
122
122
|
trustedTempoProofRealms?: ProofRealm.TrustedRealms | undefined;
|
|
@@ -8,6 +8,7 @@ import * as Transport from '../Transport.js';
|
|
|
8
8
|
import * as MethodChallenge from './MethodChallenge.js';
|
|
9
9
|
import * as MethodResponse from './MethodResponse.js';
|
|
10
10
|
import * as ProofRealm from './ProofRealm.js';
|
|
11
|
+
import { adapterOwnsPayment } from './protocols/Mcp.js';
|
|
11
12
|
// We tag wrappers with a global symbol so we can recognize wrappers created by mppx,
|
|
12
13
|
// even across multiple module instances/bundles. This lets restore() avoid clobbering
|
|
13
14
|
// an unrelated fetch installed by user code or another library.
|
|
@@ -129,6 +130,8 @@ export function from(config) {
|
|
|
129
130
|
const transportRequest = withCapturedBody(initialRequest.init, await capturedBody);
|
|
130
131
|
if (init?.[skipPayment])
|
|
131
132
|
return response;
|
|
133
|
+
if (adapterOwnsPayment(transportRequest))
|
|
134
|
+
return response;
|
|
132
135
|
if (!(await transport.isPaymentRequired(response, transportRequest)))
|
|
133
136
|
return response;
|
|
134
137
|
// Only extract context for payment handling after confirming 402.
|
|
@@ -194,6 +197,7 @@ export function from(config) {
|
|
|
194
197
|
if (!MethodChallenge.has(selected.method))
|
|
195
198
|
return resolveCredential(selectedChallenge, selected.method, credentialContext);
|
|
196
199
|
const attempt = {
|
|
200
|
+
origin: resolveRequestUrl(paymentInput).origin,
|
|
197
201
|
prepare: () => MethodChallenge.handle(selected.method, {
|
|
198
202
|
challenge: selectedChallenge,
|
|
199
203
|
context: credentialContext,
|
|
@@ -211,6 +215,7 @@ export function from(config) {
|
|
|
211
215
|
.prepare()
|
|
212
216
|
.then(() => resolveCredential(selectedChallenge, selected.method, credentialContext, attempt));
|
|
213
217
|
preparedCredential = {
|
|
218
|
+
context: credentialContext,
|
|
214
219
|
attempt,
|
|
215
220
|
key: overrideContext === undefined ? key : undefined,
|
|
216
221
|
method: selected.method,
|
|
@@ -239,6 +244,7 @@ export function from(config) {
|
|
|
239
244
|
await settleAttempt(prepared, { status: 'rejected' });
|
|
240
245
|
preparedCredential = undefined;
|
|
241
246
|
}
|
|
247
|
+
const effectiveContext = preparedCredential ? preparedCredential.context : context;
|
|
242
248
|
validateCredentialHeaderValue(credential);
|
|
243
249
|
await events.emit('credential.created', createCredentialCreatedPayload({
|
|
244
250
|
challenge: selectedChallenge,
|
|
@@ -322,6 +328,7 @@ export function from(config) {
|
|
|
322
328
|
if (!response.ok)
|
|
323
329
|
return response;
|
|
324
330
|
return MethodResponse.handle(selected.method, {
|
|
331
|
+
context: effectiveContext,
|
|
325
332
|
challenge: selectedChallenge,
|
|
326
333
|
credential,
|
|
327
334
|
fetch: baseFetch,
|
|
@@ -753,6 +760,8 @@ export function validateCredentialHeaderValue(credential) {
|
|
|
753
760
|
async function resolveCredential(challenge, mi, context, attempt) {
|
|
754
761
|
const parsedContext = mi.context && context !== undefined ? mi.context.parse(context) : undefined;
|
|
755
762
|
const parameters = parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge };
|
|
763
|
+
if (attempt?.origin)
|
|
764
|
+
MethodResponse.attachOrigin(parameters, attempt.origin);
|
|
756
765
|
if (attempt)
|
|
757
766
|
MethodResponse.attachAttempt(mi, parameters, attempt);
|
|
758
767
|
return mi.createCredential(parameters);
|
|
@@ -7,6 +7,8 @@ export type AttemptOutcome = {
|
|
|
7
7
|
status: 'accepted' | 'pending' | 'rejected';
|
|
8
8
|
};
|
|
9
9
|
export type Attempt = {
|
|
10
|
+
/** Authorized HTTP origin, checked before any credential is signed. */
|
|
11
|
+
origin?: string | undefined;
|
|
10
12
|
/** Re-runs method preparation when serialized state changed before signing. */
|
|
11
13
|
prepare: () => Promise<void>;
|
|
12
14
|
/** Settles state created for one credential attempt. */
|
|
@@ -14,6 +16,8 @@ export type Attempt = {
|
|
|
14
16
|
};
|
|
15
17
|
/** Inputs available when a client method handles a successful paid response. */
|
|
16
18
|
export type HandlerParameters = {
|
|
19
|
+
/** Original request policy for automatic follow-up payments. */
|
|
20
|
+
context?: unknown;
|
|
17
21
|
challenge: Challenge.Challenge;
|
|
18
22
|
credential: string;
|
|
19
23
|
fetch: typeof globalThis.fetch;
|
|
@@ -41,4 +45,8 @@ export declare function handle(method: Method.AnyClient, parameters: HandlerPara
|
|
|
41
45
|
export declare function registerTransactionHash<const method extends Method.AnyClient>(method: method, derive: TransactionHash): method;
|
|
42
46
|
/** Returns the credential's on-chain hash when its method can derive it. */
|
|
43
47
|
export declare function getTransactionHash(method: Method.AnyClient, credential: string): string | undefined;
|
|
48
|
+
/** Associates an explicitly verified HTTP origin with credential creation. */
|
|
49
|
+
export declare function attachOrigin(parameters: object, origin: string): void;
|
|
50
|
+
/** Returns the authorized origin for direct or fetch-owned credential creation. */
|
|
51
|
+
export declare function getOrigin(parameters: object): string | undefined;
|
|
44
52
|
//# sourceMappingURL=MethodResponse.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const handlers = new WeakMap();
|
|
2
2
|
const attempts = new WeakMap();
|
|
3
|
+
const origins = new WeakMap();
|
|
3
4
|
const transactionHashes = new WeakMap();
|
|
4
5
|
/** Registers an internal response adapter without changing the public method shape. */
|
|
5
6
|
export function register(method, handler) {
|
|
@@ -38,4 +39,12 @@ export function getTransactionHash(method, credential) {
|
|
|
38
39
|
return undefined;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
/** Associates an explicitly verified HTTP origin with credential creation. */
|
|
43
|
+
export function attachOrigin(parameters, origin) {
|
|
44
|
+
origins.set(parameters, origin);
|
|
45
|
+
}
|
|
46
|
+
/** Returns the authorized origin for direct or fetch-owned credential creation. */
|
|
47
|
+
export function getOrigin(parameters) {
|
|
48
|
+
return origins.get(parameters) ?? attempts.get(parameters)?.origin;
|
|
49
|
+
}
|
|
41
50
|
//# sourceMappingURL=MethodResponse.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type * as Challenge from '../../Challenge.js';
|
|
2
2
|
/**
|
|
3
|
-
* Pins that let a server origin request zero-amount Tempo proofs for a realm other than its
|
|
3
|
+
* Pins that let a server origin request zero-amount Tempo proofs or session payments for a realm other than its
|
|
4
4
|
* hostname, keyed by origin (`scheme://host[:port]`), such as
|
|
5
5
|
* `{ 'https://api.example.com': 'MPP Payment' }`.
|
|
6
6
|
*/
|
|
7
7
|
export type TrustedRealms = Readonly<Record<string, string>>;
|
|
8
8
|
/**
|
|
9
|
-
* Refuses a
|
|
9
|
+
* Refuses a Tempo proof or session payment whose realm the HTTP server that sent the Challenge does not
|
|
10
10
|
* own. A proof authenticates the payer at the Challenge realm, so a foreign origin could otherwise
|
|
11
11
|
* relay another server's Challenge and replay the signed proof there. The server owns the realm
|
|
12
12
|
* when it is the request hostname, or when `trustedRealms` pins it to the request origin. Pass
|
|
@@ -14,7 +14,7 @@ export type TrustedRealms = Readonly<Record<string, string>>;
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function assert(challenge: Challenge.Challenge, url: URL | (() => URL), trustedRealms: TrustedRealms | undefined): void;
|
|
16
16
|
/**
|
|
17
|
-
* Refuses a
|
|
17
|
+
* Refuses a Tempo proof or session payment on a transport without an origin, such as MCP, unless
|
|
18
18
|
* `trustedRealms` lists its realm.
|
|
19
19
|
*/
|
|
20
20
|
export declare function assertWithoutOrigin(challenge: Challenge.Challenge, trustedRealms: readonly string[] | undefined): void;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Refuses a
|
|
2
|
+
* Refuses a Tempo proof or session payment whose realm the HTTP server that sent the Challenge does not
|
|
3
3
|
* own. A proof authenticates the payer at the Challenge realm, so a foreign origin could otherwise
|
|
4
4
|
* relay another server's Challenge and replay the signed proof there. The server owns the realm
|
|
5
5
|
* when it is the request hostname, or when `trustedRealms` pins it to the request origin. Pass
|
|
6
6
|
* `url` as a function to resolve it only for proof Challenges.
|
|
7
7
|
*/
|
|
8
8
|
export function assert(challenge, url, trustedRealms) {
|
|
9
|
-
if (!isProof(challenge))
|
|
9
|
+
if (!isProof(challenge) && !(challenge.method === 'tempo' && challenge.intent === 'session'))
|
|
10
10
|
return;
|
|
11
11
|
url = typeof url === 'function' ? url() : url;
|
|
12
12
|
const { realm } = challenge;
|
|
@@ -19,11 +19,11 @@ export function assert(challenge, url, trustedRealms) {
|
|
|
19
19
|
throw new Error(`Refusing to sign Tempo proof for realm ${JSON.stringify(realm)} from ${url.origin}. Pin the realm to this origin with \`trustedTempoProofRealms\` if this server is expected to issue it.`);
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Refuses a
|
|
22
|
+
* Refuses a Tempo proof or session payment on a transport without an origin, such as MCP, unless
|
|
23
23
|
* `trustedRealms` lists its realm.
|
|
24
24
|
*/
|
|
25
25
|
export function assertWithoutOrigin(challenge, trustedRealms) {
|
|
26
|
-
if (!isProof(challenge))
|
|
26
|
+
if (!isProof(challenge) && !(challenge.method === 'tempo' && challenge.intent === 'session'))
|
|
27
27
|
return;
|
|
28
28
|
if (trustedRealms?.includes(challenge.realm))
|
|
29
29
|
return;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import * as Mcp from '../../../Mcp.js';
|
|
2
2
|
import type { Protocol } from './Protocol.js';
|
|
3
|
+
/** Metadata that delegates all payment decisions to the MCP client adapter. */
|
|
4
|
+
export declare const adapterPaymentMetaKey = "mppx/payment-client";
|
|
5
|
+
/** Whether the caller's MCP adapter owns approval and payment retry for this request. */
|
|
6
|
+
export declare function adapterOwnsPayment(request?: RequestInit): boolean;
|
|
3
7
|
type CorePaymentRequiredData = NonNullable<Mcp.ErrorObject['data']>;
|
|
4
8
|
export type PaymentRequiredData = Pick<CorePaymentRequiredData, 'challenges'> & Partial<Pick<CorePaymentRequiredData, 'httpStatus' | 'problem'>>;
|
|
5
9
|
/** Extracts validated payment-required data from MCP errors or tool result metadata. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createParser } from 'eventsource-parser';
|
|
2
1
|
import * as Challenge from '../../../Challenge.js';
|
|
3
2
|
import * as Credential from '../../../Credential.js';
|
|
3
|
+
import { createParser } from '../../../internal/sse.js';
|
|
4
4
|
import * as Mcp from '../../../Mcp.js';
|
|
5
5
|
import { paymentRequiredStatus } from './Shared.js';
|
|
6
6
|
/** Returns the JSON-RPC request id — only requests can receive a `-32042` response. */
|
|
@@ -18,6 +18,22 @@ function jsonRpcRequestId(body) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/** Metadata that delegates all payment decisions to the MCP client adapter. */
|
|
22
|
+
export const adapterPaymentMetaKey = 'mppx/payment-client';
|
|
23
|
+
/** Whether the caller's MCP adapter owns approval and payment retry for this request. */
|
|
24
|
+
export function adapterOwnsPayment(request) {
|
|
25
|
+
if (typeof request?.body !== 'string')
|
|
26
|
+
return false;
|
|
27
|
+
try {
|
|
28
|
+
const message = JSON.parse(request.body);
|
|
29
|
+
return (message?.jsonrpc === '2.0' &&
|
|
30
|
+
message?.method === 'tools/call' &&
|
|
31
|
+
message.params?._meta?.[adapterPaymentMetaKey] === true);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
21
37
|
// Bound automatic inspection; the caller retains the original response stream.
|
|
22
38
|
const maxInspectionBytes = 1024 * 1024;
|
|
23
39
|
const responseCache = new WeakMap();
|
|
@@ -99,18 +115,16 @@ async function parseSseJsonRpcResponse(response) {
|
|
|
99
115
|
let received = 0;
|
|
100
116
|
let dataEventSeen = false;
|
|
101
117
|
let message;
|
|
102
|
-
const parser = createParser({
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
catch { }
|
|
113
|
-
},
|
|
118
|
+
const parser = createParser((event) => {
|
|
119
|
+
if (dataEventSeen || !event.data)
|
|
120
|
+
return;
|
|
121
|
+
dataEventSeen = true;
|
|
122
|
+
if (event.event !== 'message')
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
message = parseMessage(JSON.parse(event.data));
|
|
126
|
+
}
|
|
127
|
+
catch { }
|
|
114
128
|
});
|
|
115
129
|
try {
|
|
116
130
|
for (;;) {
|
|
@@ -119,7 +133,6 @@ async function parseSseJsonRpcResponse(response) {
|
|
|
119
133
|
const { done, value } = await reader.read();
|
|
120
134
|
if (done) {
|
|
121
135
|
parser.feed(decoder.decode());
|
|
122
|
-
parser.reset({ consume: true });
|
|
123
136
|
return message;
|
|
124
137
|
}
|
|
125
138
|
received += value.byteLength;
|
package/dist/identity/Client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Challenge from '../Challenge.js';
|
|
2
2
|
import * as Credential from '../Credential.js';
|
|
3
3
|
import * as SigningContext from './internal/SigningContext.js';
|
|
4
|
+
const skipPayment = Symbol.for('mppx.fetch.skipPayment');
|
|
4
5
|
/** x402 credential headers (v2 and v1); a request carrying one is a paid attempt. */
|
|
5
6
|
const x402CredentialHeaders = ['payment-signature', 'x-payment'];
|
|
6
7
|
/**
|
|
@@ -99,7 +100,12 @@ export function wrapFetch(fetch, client, context) {
|
|
|
99
100
|
// A retry may answer a refreshed challenge: bind proofs to the challenge its credential
|
|
100
101
|
// carries, and let provers prepare for a refreshed 402 of the same method and intent.
|
|
101
102
|
const challenge = credentialChallenge(request.headers) ?? context?.challenge;
|
|
102
|
-
const { response } = await client.send(
|
|
103
|
+
const { response } = await client.send((input, options) => fetch(input, {
|
|
104
|
+
...options,
|
|
105
|
+
...(init?.[skipPayment]
|
|
106
|
+
? { [skipPayment]: true }
|
|
107
|
+
: {}),
|
|
108
|
+
}), request, { ...context, challenge });
|
|
103
109
|
if (response.status === 402 && challenge)
|
|
104
110
|
// A 402 without a parseable Payment challenge is returned as is: the caller decides.
|
|
105
111
|
for (const next of challengesOf(response))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** An event dispatched from a `text/event-stream` body. */
|
|
2
|
+
export type Event = {
|
|
3
|
+
/** The event's data: its `data` field values joined by `\n`. */
|
|
4
|
+
data: string;
|
|
5
|
+
/** The event's type: its last `event` field value, or `'message'` when it has none. */
|
|
6
|
+
event: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Creates an incremental parser for a `text/event-stream` body, as the WHATWG HTML
|
|
10
|
+
* spec interprets one. Feed it decoded text chunks split anywhere; it calls `onEvent`
|
|
11
|
+
* for each event a blank line ends. Lines end in `\n`, `\r\n` or `\r`; comments (`:`),
|
|
12
|
+
* `id`, `retry` and unknown fields are ignored, and an event that never ends is not
|
|
13
|
+
* dispatched.
|
|
14
|
+
*
|
|
15
|
+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
|
16
|
+
*/
|
|
17
|
+
export declare function createParser(onEvent: (event: Event) => void): {
|
|
18
|
+
/** Parses the next chunk of the stream. */
|
|
19
|
+
feed(chunk: string): void;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an incremental parser for a `text/event-stream` body, as the WHATWG HTML
|
|
3
|
+
* spec interprets one. Feed it decoded text chunks split anywhere; it calls `onEvent`
|
|
4
|
+
* for each event a blank line ends. Lines end in `\n`, `\r\n` or `\r`; comments (`:`),
|
|
5
|
+
* `id`, `retry` and unknown fields are ignored, and an event that never ends is not
|
|
6
|
+
* dispatched.
|
|
7
|
+
*
|
|
8
|
+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
|
9
|
+
*/
|
|
10
|
+
export function createParser(onEvent) {
|
|
11
|
+
let buffer = '';
|
|
12
|
+
let started = false;
|
|
13
|
+
let skipLineFeed = false;
|
|
14
|
+
let data;
|
|
15
|
+
let event = '';
|
|
16
|
+
function parseLine(line) {
|
|
17
|
+
if (line === '') {
|
|
18
|
+
if (data !== undefined)
|
|
19
|
+
onEvent({ data, event: event || 'message' });
|
|
20
|
+
data = undefined;
|
|
21
|
+
event = '';
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (line.startsWith(':'))
|
|
25
|
+
return;
|
|
26
|
+
const colon = line.indexOf(':');
|
|
27
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
28
|
+
let value = colon === -1 ? '' : line.slice(colon + 1);
|
|
29
|
+
if (value.startsWith(' '))
|
|
30
|
+
value = value.slice(1);
|
|
31
|
+
if (field === 'data')
|
|
32
|
+
data = data === undefined ? value : `${data}\n${value}`;
|
|
33
|
+
else if (field === 'event')
|
|
34
|
+
event = value;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
/** Parses the next chunk of the stream. */
|
|
38
|
+
feed(chunk) {
|
|
39
|
+
if (!started && chunk) {
|
|
40
|
+
started = true;
|
|
41
|
+
if (chunk.startsWith('\uFEFF'))
|
|
42
|
+
chunk = chunk.slice(1);
|
|
43
|
+
}
|
|
44
|
+
// A `\r` ending the last chunk may be the first half of a `\r\n`.
|
|
45
|
+
if (skipLineFeed && chunk) {
|
|
46
|
+
skipLineFeed = false;
|
|
47
|
+
if (chunk.startsWith('\n'))
|
|
48
|
+
chunk = chunk.slice(1);
|
|
49
|
+
}
|
|
50
|
+
buffer += chunk;
|
|
51
|
+
if (!/[\r\n]/.test(chunk))
|
|
52
|
+
return;
|
|
53
|
+
let start = 0;
|
|
54
|
+
const terminator = /\r\n?|\n/g;
|
|
55
|
+
for (let match = terminator.exec(buffer); match; match = terminator.exec(buffer)) {
|
|
56
|
+
parseLine(buffer.slice(start, match.index));
|
|
57
|
+
start = terminator.lastIndex;
|
|
58
|
+
if (match[0] === '\r' && start === buffer.length)
|
|
59
|
+
skipLineFeed = true;
|
|
60
|
+
}
|
|
61
|
+
buffer = buffer.slice(start);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Current mppx package version. */
|
|
2
|
-
export declare const version = "0.17.
|
|
2
|
+
export declare const version = "0.17.3";
|
|
3
3
|
/** Canonical SDK identifier for this mppx release. */
|
|
4
|
-
export declare const sdkIdentifier = "mppx/0.17.
|
|
4
|
+
export declare const sdkIdentifier = "mppx/0.17.3";
|
|
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.17.
|
|
3
|
+
export const version = '0.17.3';
|
|
4
4
|
/** Canonical SDK identifier for this mppx release. */
|
|
5
5
|
export const sdkIdentifier = `mppx/${version}`;
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
|
@@ -70,8 +70,8 @@ export declare namespace wrap {
|
|
|
70
70
|
/** Array of methods to use. Accepts individual clients or tuples (e.g. from `tempo()`). */
|
|
71
71
|
methods: methods;
|
|
72
72
|
/**
|
|
73
|
-
* Realms allowed to request zero-amount Tempo proofs. An MCP server has no origin that could
|
|
74
|
-
* own a realm, so these
|
|
73
|
+
* Realms allowed to request zero-amount Tempo proofs or session payments. An MCP server has no origin that could
|
|
74
|
+
* own a realm, so these signatures are refused unless their realm is listed here.
|
|
75
75
|
*/
|
|
76
76
|
trustedTempoProofRealms?: readonly string[] | undefined;
|
|
77
77
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Challenge from '../../Challenge.js';
|
|
2
2
|
import * as ProofRealm from '../../client/internal/ProofRealm.js';
|
|
3
|
+
import { adapterPaymentMetaKey } from '../../client/internal/protocols/Mcp.js';
|
|
3
4
|
import * as Credential from '../../Credential.js';
|
|
4
5
|
import * as Expires from '../../Expires.js';
|
|
5
6
|
import * as AcceptPayment from '../../internal/AcceptPayment.js';
|
|
@@ -57,9 +58,11 @@ export function wrap(client, config) {
|
|
|
57
58
|
enumerable: false,
|
|
58
59
|
value: (params, resultSchema, options) => {
|
|
59
60
|
const { context, onPaymentRequired, ...requestOptions } = options ?? {};
|
|
60
|
-
return callTool(params, {
|
|
61
|
+
return callTool({ ...params, _meta: { ...params._meta, [adapterPaymentMetaKey]: true } }, {
|
|
61
62
|
context,
|
|
62
|
-
onPaymentRequired: onPaymentRequired === null
|
|
63
|
+
onPaymentRequired: onPaymentRequired === null
|
|
64
|
+
? undefined
|
|
65
|
+
: (onPaymentRequired ?? config.onPaymentRequired),
|
|
63
66
|
requestOptions: Object.keys(requestOptions).length
|
|
64
67
|
? requestOptions
|
|
65
68
|
: undefined,
|
package/dist/proxy/Proxy.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export declare namespace create {
|
|
|
49
49
|
docs?: Service.Docs | undefined;
|
|
50
50
|
/** Custom `fetch` implementation. Defaults to `globalThis.fetch`. */
|
|
51
51
|
fetch?: typeof globalThis.fetch | undefined;
|
|
52
|
+
/** Maximum request body bytes on every route, including free routes. Defaults to 10 MiB. */
|
|
53
|
+
maxRequestBodySize?: number | undefined;
|
|
52
54
|
/** Default handler for failed upstream attempts. Services may override it. */
|
|
53
55
|
onUpstreamError?: Service.UpstreamErrorHandler | undefined;
|
|
54
56
|
/**
|
package/dist/proxy/Proxy.js
CHANGED
|
@@ -34,6 +34,9 @@ import * as Service from './Service.js';
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
export function create(config) {
|
|
37
|
+
const maxRequestBodySize = config.maxRequestBodySize ?? 10 * 1024 * 1024;
|
|
38
|
+
if (!Number.isSafeInteger(maxRequestBodySize) || maxRequestBodySize < 0)
|
|
39
|
+
throw new RangeError('maxRequestBodySize must be a non-negative safe integer');
|
|
37
40
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
38
41
|
const replay = config.replay ? Replay.resolve(config.replay) : undefined;
|
|
39
42
|
const services = new Map(config.services.map((s) => {
|
|
@@ -100,8 +103,22 @@ export function create(config) {
|
|
|
100
103
|
if (!matched)
|
|
101
104
|
return new Response('Not Found', { status: 404 });
|
|
102
105
|
const endpoint = matched.value;
|
|
103
|
-
const ctx = { request, service, upstreamPath };
|
|
104
106
|
const handler = Service.getHandler(endpoint);
|
|
107
|
+
const declaredLength = Number(request.headers.get('content-length'));
|
|
108
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxRequestBodySize)
|
|
109
|
+
return new Response('Payload Too Large', { status: 413 });
|
|
110
|
+
const paymentCredential = handler
|
|
111
|
+
? Replay.credentialOf(request, credentialHeaders(handler))
|
|
112
|
+
: undefined;
|
|
113
|
+
let boundedBody;
|
|
114
|
+
if ((!handler || paymentCredential) && request.body) {
|
|
115
|
+
const body = await Replay.readBody(request, maxRequestBodySize);
|
|
116
|
+
if (!body)
|
|
117
|
+
return new Response('Payload Too Large', { status: 413 });
|
|
118
|
+
boundedBody = body;
|
|
119
|
+
request = Replay.withBody(request, body);
|
|
120
|
+
}
|
|
121
|
+
const ctx = { request, service, upstreamPath };
|
|
105
122
|
if (!handler)
|
|
106
123
|
return proxyUpstream({
|
|
107
124
|
request,
|
|
@@ -116,11 +133,12 @@ export function create(config) {
|
|
|
116
133
|
let replayBody;
|
|
117
134
|
let replayKey;
|
|
118
135
|
// Routes with identity verifiers keep paying normally: a replay would skip them.
|
|
119
|
-
const credential = replay && !fallbackMatch && !requiresIdentity(handler)
|
|
120
|
-
? Replay.credentialOf(request, credentialHeaders(handler))
|
|
121
|
-
: undefined;
|
|
136
|
+
const credential = replay && !fallbackMatch && !requiresIdentity(handler) ? paymentCredential : undefined;
|
|
122
137
|
if (replay && credential) {
|
|
123
|
-
const body =
|
|
138
|
+
const body = boundedBody ??
|
|
139
|
+
(await Replay.readBody(request, Math.min(maxRequestBodySize, replay.maxRequestBodySize)));
|
|
140
|
+
if (body && body.length > replay.maxRequestBodySize)
|
|
141
|
+
return new Response('Payload Too Large', { status: 413 });
|
|
124
142
|
if (!body)
|
|
125
143
|
return new Response('Payload Too Large', { status: 413 });
|
|
126
144
|
replayBody = body;
|
|
@@ -179,6 +197,13 @@ export function create(config) {
|
|
|
179
197
|
return managementResponse;
|
|
180
198
|
if (fallbackMatch)
|
|
181
199
|
return new Response('Method Not Allowed', { status: 405 });
|
|
200
|
+
if (!boundedBody && !replayBody && request.body) {
|
|
201
|
+
const body = await Replay.readBody(request, maxRequestBodySize);
|
|
202
|
+
if (!body)
|
|
203
|
+
return result.withReceipt(new Response('Payload Too Large', { status: 413 }));
|
|
204
|
+
request = Replay.withBody(request, body);
|
|
205
|
+
ctx.request = request;
|
|
206
|
+
}
|
|
182
207
|
const options = Service.getOptions(endpoint);
|
|
183
208
|
if (replay && replayKey && replayBody) {
|
|
184
209
|
const body = replayBody;
|
|
@@ -211,7 +236,7 @@ export function create(config) {
|
|
|
211
236
|
}
|
|
212
237
|
return {
|
|
213
238
|
fetch: handle,
|
|
214
|
-
listener: Request.toNodeListener(handle),
|
|
239
|
+
listener: Request.toNodeListener(handle, { maxBodySize: maxRequestBodySize }),
|
|
215
240
|
};
|
|
216
241
|
}
|
|
217
242
|
/** @internal */
|
package/dist/server/Mppx.js
CHANGED
|
@@ -1876,11 +1876,12 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
mergedHeaders.set('Content-Type', 'text/html; charset=utf-8');
|
|
1878
1878
|
const firstData = Object.values(dataMap)[0];
|
|
1879
|
+
const entries = await Promise.all(htmlEntries.map(async (entry) => ({
|
|
1880
|
+
challenge: entry.challenge,
|
|
1881
|
+
content: await Html.resolveContent(entry.handler._internal.html.content),
|
|
1882
|
+
})));
|
|
1879
1883
|
const body = Html.render({
|
|
1880
|
-
entries
|
|
1881
|
-
challenge: entry.challenge,
|
|
1882
|
-
content: entry.handler._internal.html.content,
|
|
1883
|
-
})),
|
|
1884
|
+
entries,
|
|
1884
1885
|
dataMap,
|
|
1885
1886
|
formattedAmount: firstData.formattedAmount,
|
|
1886
1887
|
panels: true,
|
package/dist/server/Transport.js
CHANGED
|
@@ -98,7 +98,7 @@ export function http(options = {}) {
|
|
|
98
98
|
},
|
|
99
99
|
};
|
|
100
100
|
return Html.render({
|
|
101
|
-
entries: [{ challenge, content: options.html.content }],
|
|
101
|
+
entries: [{ challenge, content: await Html.resolveContent(options.html.content) }],
|
|
102
102
|
dataMap,
|
|
103
103
|
formattedAmount: amount,
|
|
104
104
|
text,
|