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.
Files changed (89) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/Errors.d.ts +94 -0
  3. package/dist/Errors.js +83 -0
  4. package/dist/Store.d.ts +413 -0
  5. package/dist/Store.js +837 -0
  6. package/dist/cli/cli.js +141 -136
  7. package/dist/cli/internal.js +28 -6
  8. package/dist/cli/plugins/index.js +16 -11
  9. package/dist/cli/sessions/Manager.js +12 -1
  10. package/dist/cli/utils.d.ts +2 -0
  11. package/dist/cli/utils.js +6 -0
  12. package/dist/cli/validate/challenge.d.ts +4 -0
  13. package/dist/cli/validate/challenge.js +9 -6
  14. package/dist/client/Mppx.d.ts +1 -1
  15. package/dist/client/Mppx.js +7 -3
  16. package/dist/client/internal/Fetch.d.ts +1 -1
  17. package/dist/client/internal/Fetch.js +9 -0
  18. package/dist/client/internal/MethodResponse.d.ts +8 -0
  19. package/dist/client/internal/MethodResponse.js +9 -0
  20. package/dist/client/internal/ProofRealm.d.ts +3 -3
  21. package/dist/client/internal/ProofRealm.js +4 -4
  22. package/dist/client/internal/protocols/Mcp.d.ts +4 -0
  23. package/dist/client/internal/protocols/Mcp.js +27 -14
  24. package/dist/cloudflare/ChannelStore.d.ts +97 -0
  25. package/dist/cloudflare/ChannelStore.js +304 -0
  26. package/dist/cloudflare/Channels.d.ts +92 -0
  27. package/dist/cloudflare/Channels.js +325 -0
  28. package/dist/cloudflare/index.d.ts +5 -0
  29. package/dist/cloudflare/index.js +5 -0
  30. package/dist/identity/Client.js +7 -1
  31. package/dist/internal/sse.d.ts +21 -0
  32. package/dist/internal/sse.js +82 -0
  33. package/dist/internal/version.d.ts +2 -2
  34. package/dist/internal/version.js +1 -1
  35. package/dist/mcp/client/McpClient.d.ts +2 -2
  36. package/dist/mcp/client/McpClient.js +5 -2
  37. package/dist/proxy/Proxy.d.ts +2 -0
  38. package/dist/proxy/Proxy.js +31 -6
  39. package/dist/proxy/internal/Replay.js +1 -1
  40. package/dist/server/Mppx.js +5 -4
  41. package/dist/server/Transport.js +1 -1
  42. package/dist/server/internal/html/config.d.ts +7 -1
  43. package/dist/server/internal/html/config.js +4 -0
  44. package/dist/stripe/internal/types.d.ts +82 -6
  45. package/dist/stripe/server/Charge.js +1 -2
  46. package/dist/stripe/server/Methods.d.ts +2 -0
  47. package/dist/stripe/server/Methods.js +2 -1
  48. package/dist/stripe/server/internal/html.gen.js +1 -1
  49. package/dist/stripe/server/internal/record-payment.d.ts +49 -0
  50. package/dist/stripe/server/internal/record-payment.js +75 -18
  51. package/dist/tempo/Methods.d.ts +8 -0
  52. package/dist/tempo/Methods.js +18 -2
  53. package/dist/tempo/client/Charge.d.ts +32 -1
  54. package/dist/tempo/client/Charge.js +201 -3
  55. package/dist/tempo/client/Methods.d.ts +16 -0
  56. package/dist/tempo/internal/fee-payer.d.ts +64 -14
  57. package/dist/tempo/internal/fee-payer.js +231 -20
  58. package/dist/tempo/internal/zone.d.ts +79 -0
  59. package/dist/tempo/internal/zone.js +133 -0
  60. package/dist/tempo/legacy/session/Chain.js +2 -0
  61. package/dist/tempo/server/Charge.d.ts +7 -3
  62. package/dist/tempo/server/Charge.js +100 -6
  63. package/dist/tempo/server/Methods.d.ts +40 -0
  64. package/dist/tempo/server/Subscription.js +1 -0
  65. package/dist/tempo/server/internal/html.gen.js +1 -1
  66. package/dist/tempo/server/internal/transport.d.ts +6 -0
  67. package/dist/tempo/server/internal/transport.js +21 -21
  68. package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
  69. package/dist/tempo/session/client/CredentialState.js +2 -0
  70. package/dist/tempo/session/client/Session.d.ts +11 -1
  71. package/dist/tempo/session/client/Session.js +39 -6
  72. package/dist/tempo/session/client/SessionManager.d.ts +1 -1
  73. package/dist/tempo/session/client/SessionManager.js +8 -4
  74. package/dist/tempo/session/client/Transports.d.ts +2 -0
  75. package/dist/tempo/session/client/Transports.js +4 -1
  76. package/dist/tempo/session/precompile/Chain.js +1 -0
  77. package/dist/tempo/session/server/ChannelStore.d.ts +14 -0
  78. package/dist/tempo/session/server/ChannelStore.js +10 -3
  79. package/dist/tempo/session/server/CredentialVerification.js +1 -1
  80. package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
  81. package/dist/tempo/session/server/MeteredStream.js +6 -1
  82. package/dist/tempo/session/server/Sse.d.ts +59 -0
  83. package/dist/tempo/session/server/Sse.js +175 -10
  84. package/dist/validation/core.js +2 -0
  85. package/dist/zod.d.ts +2 -1
  86. package/dist/zod.js +1 -1
  87. package/package.json +5 -3
  88. package/dist/stripe/server/internal/html/types.d.ts +0 -2
  89. 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.');
@@ -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
- [Constants.Headers.authorization]: `${Constants.Schemes.payment} dGhpcyBpcyBnYXJiYWdl`,
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'] = 'application/json';
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)`, 'When the Authorization header contains an invalid credential, respond with 402 (not 500). Catch credential validation errors and return a fresh challenge.'));
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 Authorization 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.`));
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) {
@@ -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.
@@ -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
- return mi.createCredential(parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge });
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 zero-amount Tempo proof whose realm the HTTP server that sent the Challenge does not
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 zero-amount Tempo proof on a transport without an origin, such as MCP, unless
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 zero-amount Tempo proof whose realm the HTTP server that sent the Challenge does not
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 zero-amount Tempo proof on a transport without an origin, such as MCP, unless
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
- onEvent(event) {
104
- if (dataEventSeen || !event.data)
105
- return;
106
- dataEventSeen = true;
107
- if (event.event && event.event !== 'message')
108
- return;
109
- try {
110
- message = parseMessage(JSON.parse(event.data));
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