mppx 0.17.1 → 0.17.4
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 +65 -0
- package/dist/Errors.d.ts +94 -0
- package/dist/Errors.js +83 -0
- package/dist/Store.d.ts +413 -0
- package/dist/Store.js +837 -0
- package/dist/cli/cli.js +141 -136
- package/dist/cli/internal.js +28 -6
- package/dist/cli/plugins/index.js +16 -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/cli/validate/challenge.d.ts +4 -0
- package/dist/cli/validate/challenge.js +9 -6
- 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/cloudflare/ChannelStore.d.ts +97 -0
- package/dist/cloudflare/ChannelStore.js +304 -0
- package/dist/cloudflare/Channels.d.ts +92 -0
- package/dist/cloudflare/Channels.js +325 -0
- package/dist/cloudflare/index.d.ts +5 -0
- package/dist/cloudflare/index.js +5 -0
- package/dist/identity/Client.js +7 -1
- package/dist/internal/sse.d.ts +21 -0
- package/dist/internal/sse.js +82 -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/Methods.d.ts +2 -0
- package/dist/stripe/server/Methods.js +2 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/record-payment.d.ts +49 -0
- package/dist/stripe/server/internal/record-payment.js +75 -18
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.js +18 -2
- package/dist/tempo/client/Charge.d.ts +32 -1
- package/dist/tempo/client/Charge.js +201 -3
- package/dist/tempo/client/Methods.d.ts +16 -0
- package/dist/tempo/internal/fee-payer.d.ts +64 -14
- package/dist/tempo/internal/fee-payer.js +231 -20
- package/dist/tempo/internal/zone.d.ts +79 -0
- package/dist/tempo/internal/zone.js +133 -0
- package/dist/tempo/legacy/session/Chain.js +2 -0
- package/dist/tempo/server/Charge.d.ts +7 -3
- package/dist/tempo/server/Charge.js +100 -6
- package/dist/tempo/server/Methods.d.ts +40 -0
- 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 +14 -0
- package/dist/tempo/session/server/ChannelStore.js +10 -3
- package/dist/tempo/session/server/CredentialVerification.js +1 -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/validation/core.js +2 -0
- package/dist/zod.d.ts +2 -1
- package/dist/zod.js +1 -1
- package/package.json +5 -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
|
}
|
|
@@ -15,6 +15,10 @@ export declare function validateChallenge(baseUrl: string, endpoint: EndpointSpe
|
|
|
15
15
|
export declare function validateRealmMatchesHost(challenges: Challenge.Challenge[], serverHost: string, results: CheckResult[]): void;
|
|
16
16
|
export declare function validateErrorHandling(baseUrl: string, endpoint: EndpointSpec, options?: {
|
|
17
17
|
body?: string | undefined;
|
|
18
|
+
/** The Challenge whose Credential header the probe targets. Defaults to `Authorization`. */
|
|
19
|
+
challenge?: Challenge.Challenge | undefined;
|
|
20
|
+
/** Extra `Name: value` headers (`-H`), such as an API key the endpoint requires. */
|
|
21
|
+
extraHeaders?: string[] | undefined;
|
|
18
22
|
query?: string[] | undefined;
|
|
19
23
|
}): Promise<CheckResult[]>;
|
|
20
24
|
//# sourceMappingURL=challenge.d.ts.map
|
|
@@ -289,13 +289,16 @@ function validateAmount(request, tag, results, unitHint) {
|
|
|
289
289
|
export async function validateErrorHandling(baseUrl, endpoint, options) {
|
|
290
290
|
const results = [];
|
|
291
291
|
const url = buildUrl(baseUrl, endpoint, options?.query);
|
|
292
|
-
const fetchHeaders =
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
const fetchHeaders = parseHeaders(options?.extraHeaders);
|
|
293
|
+
const credentialHeader = options?.challenge
|
|
294
|
+
? Challenge.credentialHeader(options.challenge)
|
|
295
|
+
: Constants.Headers.authorization;
|
|
296
|
+
fetchHeaders[credentialHeader.toLowerCase()] = `${Constants.Schemes.payment} dGhpcyBpcyBnYXJiYWdl`;
|
|
295
297
|
let fetchBody;
|
|
296
298
|
if (options?.body) {
|
|
297
299
|
fetchBody = options.body;
|
|
298
|
-
fetchHeaders['content-type']
|
|
300
|
+
if (!fetchHeaders['content-type'])
|
|
301
|
+
fetchHeaders['content-type'] = 'application/json';
|
|
299
302
|
}
|
|
300
303
|
try {
|
|
301
304
|
const response = await fetchWithTimeout(url, {
|
|
@@ -314,10 +317,10 @@ export async function validateErrorHandling(baseUrl, endpoint, options) {
|
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
319
|
else if (response.status >= 500) {
|
|
317
|
-
results.push(fail('Malformed credential returns 402', `Got ${response.status} (server error)`,
|
|
320
|
+
results.push(fail('Malformed credential returns 402', `Got ${response.status} (server error)`, `When the ${credentialHeader} header contains an invalid credential, respond with 402 (not 500). Catch credential validation errors and return a fresh challenge.`));
|
|
318
321
|
}
|
|
319
322
|
else {
|
|
320
|
-
results.push(warn('Malformed credential returns 402', `Got ${response.status}`, `When the
|
|
323
|
+
results.push(warn('Malformed credential returns 402', `Got ${response.status}`, `When the ${credentialHeader} header contains an invalid Payment credential, respond with 402 and a fresh WWW-Authenticate challenge. Returning ${response.status} prevents the client from retrying with a valid payment.`));
|
|
321
324
|
}
|
|
322
325
|
}
|
|
323
326
|
catch (error) {
|
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;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Hex } from 'viem';
|
|
2
|
+
import * as Store from '../Store.js';
|
|
3
|
+
import { type State } from '../tempo/session/server/ChannelStore.js';
|
|
4
|
+
/**
|
|
5
|
+
* Durable Object class that stores Tempo session channels, as {@link Store.durableObject}
|
|
6
|
+
* does, and settles them on-chain without a request in flight.
|
|
7
|
+
*
|
|
8
|
+
* Every channel lives in its own object. After each write of a channel state, the object
|
|
9
|
+
* checks how much a settlement could capture ({@link unsettledAmount}): from
|
|
10
|
+
* `max(10% of deposit, minUnsettledAmount)` it sends `{ channelId }` to the settlement
|
|
11
|
+
* queue at once; below that it sets an alarm that sends it once the channel has had no
|
|
12
|
+
* write for `staleAfterMs`. The Worker's `queue()` handler settles it with
|
|
13
|
+
* {@link settle}. After each send, a watchdog alarm sends it again while the channel
|
|
14
|
+
* stays unsettled (a dropped, dead-lettered or failed job), waiting twice as long each
|
|
15
|
+
* time, up to `maxAttempts` sends; settling more on-chain restarts the count. A close request,
|
|
16
|
+
* or a threshold's worth of new vouchers since the last send, sends it again at once and
|
|
17
|
+
* restarts the count, so a failed job never leaves them waiting on a long watchdog.
|
|
18
|
+
*
|
|
19
|
+
* Export a subclass from your Worker, bind it with a `new_sqlite_classes` migration and
|
|
20
|
+
* bind the queue as `MPPX_SETTLEMENT_QUEUE`, or override
|
|
21
|
+
* {@link ChannelStore.settlement} to change the queue or thresholds.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { DurableObject } from 'cloudflare:workers'
|
|
26
|
+
* import { ChannelStore } from 'mppx/cloudflare'
|
|
27
|
+
*
|
|
28
|
+
* export class MppxChannels extends ChannelStore.rpc(DurableObject) {}
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare class ChannelStore extends Store.DurableObjectStore {
|
|
32
|
+
/**
|
|
33
|
+
* The settlement queue and thresholds. Reads the `MPPX_SETTLEMENT_QUEUE` binding by
|
|
34
|
+
* default; a subclass overrides it to use another binding or other thresholds.
|
|
35
|
+
*/
|
|
36
|
+
protected settlement(): ChannelStore.Settlement;
|
|
37
|
+
/** Schedules settlement of a written channel state, and drops it for a closed or deleted one. */
|
|
38
|
+
protected onWrite(key: string, _previous: unknown, next: unknown, transaction?: Store.DurableObjectStore.Storage | undefined): Promise<void>;
|
|
39
|
+
/** Sends a channel over the threshold or gone stale to the queue, or sends it again from the watchdog while unsettled. */
|
|
40
|
+
alarm(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Sends the channel to the settlement queue and sets the watchdog alarm, or gives up after
|
|
43
|
+
* `maxAttempts` sends. A send that fails leaves the count alone and tries again from an
|
|
44
|
+
* alarm, since the write that scheduled it has already committed.
|
|
45
|
+
*/
|
|
46
|
+
private send;
|
|
47
|
+
}
|
|
48
|
+
export declare namespace ChannelStore {
|
|
49
|
+
/** The settlement queue and when to settle. */
|
|
50
|
+
type Settlement = {
|
|
51
|
+
/** Queue the object sends `{ channelId }` to; the Worker's `queue()` handler settles it. */
|
|
52
|
+
queue: Queue | undefined;
|
|
53
|
+
/** Raw amount a settlement must capture before it is sent at once, if 10% of the deposit is less. @default 100_000n */
|
|
54
|
+
minUnsettledAmount?: bigint | undefined;
|
|
55
|
+
/** Sends a channel below the threshold once it has had no write for this long. @default 300_000 */
|
|
56
|
+
staleAfterMs?: number | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Sends a channel again this long after a send while it stays unsettled, doubling each time.
|
|
59
|
+
* Keep the first resend well inside the escrow's close grace period (15 minutes), after which
|
|
60
|
+
* a payer who requested a close can withdraw what is unsettled; a closing channel is sent
|
|
61
|
+
* again after at most 2 minutes. @default 300_000
|
|
62
|
+
*/
|
|
63
|
+
watchdogMs?: number | undefined;
|
|
64
|
+
/** Sends a channel at most this many times without on-chain progress. @default 6 */
|
|
65
|
+
maxAttempts?: number | undefined;
|
|
66
|
+
/** Tries a send that failed again this much later. @default 60_000 */
|
|
67
|
+
retryMs?: number | undefined;
|
|
68
|
+
};
|
|
69
|
+
/** The subset of a Cloudflare `Queue` binding the object uses. */
|
|
70
|
+
type Queue = {
|
|
71
|
+
send: (body: Message) => Promise<unknown>;
|
|
72
|
+
};
|
|
73
|
+
/** The settlement job the object sends. */
|
|
74
|
+
type Message = {
|
|
75
|
+
channelId: Hex;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Whether a stored value is a precompile-backed session channel state. */
|
|
79
|
+
export declare function isChannelState(value: unknown): value is State;
|
|
80
|
+
/**
|
|
81
|
+
* The raw amount a settlement of the channel would capture now: the largest accepted voucher
|
|
82
|
+
* within `spent` (as mppx's `settle` picks it) above `settledOnChain`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function unsettledAmount(state: State): bigint;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the server's own cooperative close may still land: its marker stands in
|
|
87
|
+
* `closeRequestedAt` and its transaction has not expired. That close captures every voucher a
|
|
88
|
+
* settlement could, so settling meanwhile only spends gas.
|
|
89
|
+
*/
|
|
90
|
+
export declare function pendingClose(state: State, now?: number): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* When the channel needs settling: `threshold` (now) once the unsettled amount reaches
|
|
93
|
+
* `max(10% of deposit, minUnsettledAmount)`, `stale` (once idle) below it, or
|
|
94
|
+
* `undefined` when it is finalized or has nothing to settle.
|
|
95
|
+
*/
|
|
96
|
+
export declare function settlementDue(state: State, settlement?: Pick<ChannelStore.Settlement, 'minUnsettledAmount'>): 'stale' | 'threshold' | undefined;
|
|
97
|
+
//# sourceMappingURL=ChannelStore.d.ts.map
|