mppx 0.17.0 → 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.
Files changed (83) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/BodyDigest.js +13 -1
  3. package/dist/Method.d.ts +5 -0
  4. package/dist/Store.d.ts +225 -0
  5. package/dist/Store.js +358 -0
  6. package/dist/cli/cli.js +136 -135
  7. package/dist/cli/plugins/index.js +12 -11
  8. package/dist/cli/sessions/Manager.js +12 -1
  9. package/dist/cli/sessions/store.js +7 -1
  10. package/dist/cli/utils.d.ts +2 -0
  11. package/dist/cli/utils.js +6 -0
  12. package/dist/cli/validate/helpers.d.ts +1 -1
  13. package/dist/cli/validate/helpers.js +14 -2
  14. package/dist/client/Mppx.d.ts +1 -1
  15. package/dist/client/Mppx.js +18 -6
  16. package/dist/client/internal/Fetch.d.ts +3 -1
  17. package/dist/client/internal/Fetch.js +68 -3
  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/identity/Client.js +7 -1
  25. package/dist/internal/AcceptPayment.d.ts +12 -2
  26. package/dist/internal/AcceptPayment.js +12 -3
  27. package/dist/internal/sse.d.ts +21 -0
  28. package/dist/internal/sse.js +65 -0
  29. package/dist/internal/version.d.ts +2 -2
  30. package/dist/internal/version.js +1 -1
  31. package/dist/mcp/client/McpClient.d.ts +2 -2
  32. package/dist/mcp/client/McpClient.js +6 -3
  33. package/dist/proxy/Proxy.d.ts +17 -0
  34. package/dist/proxy/Proxy.js +116 -3
  35. package/dist/proxy/internal/Replay.d.ts +105 -0
  36. package/dist/proxy/internal/Replay.js +257 -0
  37. package/dist/server/Mppx.d.ts +2 -0
  38. package/dist/server/Mppx.js +7 -5
  39. package/dist/server/Transport.js +1 -1
  40. package/dist/server/internal/html/config.d.ts +7 -1
  41. package/dist/server/internal/html/config.js +4 -0
  42. package/dist/stripe/internal/types.d.ts +82 -6
  43. package/dist/stripe/server/Charge.js +1 -2
  44. package/dist/stripe/server/internal/html.gen.js +1 -1
  45. package/dist/tempo/client/Charge.d.ts +9 -0
  46. package/dist/tempo/client/Charge.js +13 -9
  47. package/dist/tempo/client/Methods.d.ts +2 -0
  48. package/dist/tempo/internal/fee-payer.d.ts +85 -10
  49. package/dist/tempo/internal/fee-payer.js +183 -19
  50. package/dist/tempo/legacy/session/Chain.js +2 -0
  51. package/dist/tempo/server/Charge.js +6 -3
  52. package/dist/tempo/server/Subscription.js +1 -0
  53. package/dist/tempo/server/internal/html.gen.js +1 -1
  54. package/dist/tempo/server/internal/transport.d.ts +6 -0
  55. package/dist/tempo/server/internal/transport.js +21 -21
  56. package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
  57. package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
  58. package/dist/tempo/session/client/ChannelStore.js +5 -0
  59. package/dist/tempo/session/client/CredentialState.js +2 -0
  60. package/dist/tempo/session/client/Session.d.ts +37 -2
  61. package/dist/tempo/session/client/Session.js +111 -7
  62. package/dist/tempo/session/client/SessionManager.d.ts +2 -1
  63. package/dist/tempo/session/client/SessionManager.js +60 -9
  64. package/dist/tempo/session/client/Transports.d.ts +4 -0
  65. package/dist/tempo/session/client/Transports.js +35 -3
  66. package/dist/tempo/session/precompile/Chain.js +1 -0
  67. package/dist/tempo/session/precompile/Voucher.js +20 -7
  68. package/dist/tempo/session/server/ChannelStore.d.ts +9 -0
  69. package/dist/tempo/session/server/ChannelStore.js +8 -1
  70. package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
  71. package/dist/tempo/session/server/MeteredStream.js +6 -1
  72. package/dist/tempo/session/server/Session.d.ts +2 -1
  73. package/dist/tempo/session/server/Session.js +2 -1
  74. package/dist/tempo/session/server/Settlement.d.ts +4 -2
  75. package/dist/tempo/session/server/Settlement.js +2 -2
  76. package/dist/tempo/session/server/Sse.d.ts +59 -0
  77. package/dist/tempo/session/server/Sse.js +175 -10
  78. package/dist/tempo/session/server/Ws.js +29 -12
  79. package/dist/zod.d.ts +2 -1
  80. package/dist/zod.js +1 -1
  81. package/package.json +1 -3
  82. package/dist/stripe/server/internal/html/types.d.ts +0 -2
  83. package/dist/stripe/server/internal/html/types.js +0 -2
@@ -1,4 +1,10 @@
1
1
  import * as x402_ChallengeBrand from '../x402/internal/ChallengeBrand.js';
2
+ /**
3
+ * Selection context for flows that receive the credential context only after an offer is chosen,
4
+ * such as `preparePayment()`. Methods may accept offers they would otherwise skip by policy and
5
+ * enforce that policy when the credential is created.
6
+ */
7
+ export const deferredContext = Symbol('mppx.deferredContext');
2
8
  /** Builds the typed key tree used by callback-style preference config. */
3
9
  export function buildKeys(methods) {
4
10
  const keys = {};
@@ -70,8 +76,11 @@ export function selectChallenge(challenges, methods, preferences) {
70
76
  method: candidate.method,
71
77
  };
72
78
  }
73
- /** Returns supported challenge candidates ordered by client payment preferences. */
74
- export function selectChallengeCandidates(challenges, methods, preferences) {
79
+ /**
80
+ * Returns supported challenge candidates ordered by client payment preferences.
81
+ * `context` is the caller's credential context, when known before selection.
82
+ */
83
+ export function selectChallengeCandidates(challenges, methods, preferences, context) {
75
84
  const methodsByKey = new Map();
76
85
  for (const method of methods) {
77
86
  const key = keyOf(method);
@@ -90,7 +99,7 @@ export function selectChallengeCandidates(challenges, methods, preferences) {
90
99
  if (!match || match.q <= 0)
91
100
  return [];
92
101
  return matchingMethods
93
- .filter((method) => method.canHandleChallenge?.({ challenge }) ?? true)
102
+ .filter((method) => method.canHandleChallenge?.(context === undefined ? { challenge } : { challenge, context }) ?? true)
94
103
  .map((method) => ({
95
104
  challenge,
96
105
  index,
@@ -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.0";
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.0";
4
+ export declare const sdkIdentifier = "mppx/0.17.3";
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,6 +1,6 @@
1
1
  // Generated by scripts/sync-version.ts.
2
2
  /** Current mppx package version. */
3
- export const version = '0.17.0';
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 proofs are refused unless their realm is listed here.
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 ? undefined : (onPaymentRequired ?? config.onPaymentRequired),
63
+ onPaymentRequired: onPaymentRequired === null
64
+ ? undefined
65
+ : (onPaymentRequired ?? config.onPaymentRequired),
63
66
  requestOptions: Object.keys(requestOptions).length
64
67
  ? requestOptions
65
68
  : undefined,
@@ -100,7 +103,7 @@ function createPaymentAwareCallTool(callTool, config) {
100
103
  const paymentPreferences = AcceptPayment.resolve(methods, config.paymentPreferences);
101
104
  const retryWithPayment = async (params, call, paymentRequired, cause, attemptsRemaining = maxPaymentAttempts) => {
102
105
  const challenges = paymentRequired.challenges;
103
- const candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, paymentPreferences.entries);
106
+ const candidates = AcceptPayment.selectChallengeCandidates(challenges, methods, paymentPreferences.entries, call.context);
104
107
  const orderedCandidates = config.orderChallenges
105
108
  ? await config.orderChallenges(candidates)
106
109
  : candidates;
@@ -1,5 +1,8 @@
1
1
  import type * as http from 'node:http';
2
+ import * as Replay from './internal/Replay.js';
2
3
  import * as Service from './Service.js';
4
+ /** Options for {@link create.Config.replay}. */
5
+ export type ReplayOptions = Replay.Options;
3
6
  /** A paid API proxy that gates upstream services behind the mppx 402 protocol. */
4
7
  export type Proxy = {
5
8
  /** Fetch API handler. Works with Bun, Deno, Next.js, Hono, Elysia, SvelteKit, etc. */
@@ -46,8 +49,22 @@ export declare namespace create {
46
49
  docs?: Service.Docs | undefined;
47
50
  /** Custom `fetch` implementation. Defaults to `globalThis.fetch`. */
48
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;
49
54
  /** Default handler for failed upstream attempts. Services may override it. */
50
55
  onUpstreamError?: Service.UpstreamErrorHandler | undefined;
56
+ /**
57
+ * Lets a payer collect a paid response after losing the connection.
58
+ *
59
+ * The paid upstream request runs detached from the client (its response must
60
+ * start within `ttl`), and its outcome is recorded under a key derived from the
61
+ * credential and the request's method, target and body. A retry with the
62
+ * same credential and request never pays again: it gets the stored
63
+ * response, `202` while the delivery runs, or, after a failed delivery, a
64
+ * new upstream attempt up to `maxAttempts`. Streamed (`text/event-stream`)
65
+ * and oversized responses are delivered but not stored. Off by default.
66
+ */
67
+ replay?: ReplayOptions | undefined;
51
68
  /** Services to proxy. Each service is mounted at `/{serviceId}/`. */
52
69
  services: Service.Service[];
53
70
  /** Human-readable title for the proxy shown in `llms.txt`. */
@@ -4,6 +4,7 @@ import * as Scope from '../server/internal/scope.js';
4
4
  import * as Mppx from '../server/Mppx.js';
5
5
  import * as Request from '../server/Request.js';
6
6
  import * as Headers from './internal/Headers.js';
7
+ import * as Replay from './internal/Replay.js';
7
8
  import * as Route from './internal/Route.js';
8
9
  import * as Service from './Service.js';
9
10
  /**
@@ -33,7 +34,11 @@ import * as Service from './Service.js';
33
34
  * ```
34
35
  */
35
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');
36
40
  const fetchImpl = config.fetch ?? globalThis.fetch;
41
+ const replay = config.replay ? Replay.resolve(config.replay) : undefined;
37
42
  const services = new Map(config.services.map((s) => {
38
43
  const proxy = createFetchProxy(s.baseUrl, { fetch: fetchImpl });
39
44
  return [s.id, { service: s, proxy }];
@@ -98,8 +103,22 @@ export function create(config) {
98
103
  if (!matched)
99
104
  return new Response('Not Found', { status: 404 });
100
105
  const endpoint = matched.value;
101
- const ctx = { request, service, upstreamPath };
102
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 };
103
122
  if (!handler)
104
123
  return proxyUpstream({
105
124
  request,
@@ -108,13 +127,60 @@ export function create(config) {
108
127
  proxy,
109
128
  onUpstreamError: service.onUpstreamError ?? config.onUpstreamError,
110
129
  });
130
+ // With replay, a paid request whose credential already paid for this exact
131
+ // request collects its recorded delivery instead of paying again.
132
+ let paidRequest = request;
133
+ let replayBody;
134
+ let replayKey;
135
+ // Routes with identity verifiers keep paying normally: a replay would skip them.
136
+ const credential = replay && !fallbackMatch && !requiresIdentity(handler) ? paymentCredential : undefined;
137
+ if (replay && credential) {
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 });
142
+ if (!body)
143
+ return new Response('Payload Too Large', { status: 413 });
144
+ replayBody = body;
145
+ paidRequest = Replay.withBody(request, body);
146
+ replayKey = Replay.key({
147
+ body,
148
+ credential,
149
+ method: request.method,
150
+ target: url.origin + url.pathname + url.search,
151
+ });
152
+ const found = await Replay.lookup(replay, replayKey);
153
+ if (found.type === 'respond')
154
+ return found.response;
155
+ if (found.type === 'retry') {
156
+ const options = Service.getOptions(endpoint);
157
+ return Replay.deliver({
158
+ applyReceipt: (response) => Replay.withReceiptHeader(response, found.receipt),
159
+ attempts: found.attempts,
160
+ replay,
161
+ replayKey,
162
+ requestHeaders: found.requestHeaders,
163
+ run: (signal) => {
164
+ // Send the headers of the request that was paid for, not the retry's.
165
+ const upstream = Replay.withBody(paidRequest, body, signal, found.requestHeaders);
166
+ return proxyUpstream({
167
+ request: upstream,
168
+ service,
169
+ ctx: { ...ctx, request: upstream, ...options },
170
+ proxy,
171
+ onUpstreamError: service.onUpstreamError ?? config.onUpstreamError,
172
+ });
173
+ },
174
+ });
175
+ }
176
+ }
111
177
  const scope = getConfiguredScope(handler) ??
112
178
  deriveRouteScope({
113
179
  basePath: config.basePath,
114
180
  routeKey: matched.key,
115
181
  serviceId,
116
182
  });
117
- const result = await handler(getConfiguredScope(handler) ? request : Scope.attach(request, scope));
183
+ const result = await handler(getConfiguredScope(handler) ? paidRequest : Scope.attach(paidRequest, scope));
118
184
  if (result.status === 402)
119
185
  return result.challenge;
120
186
  const managementResponse = (() => {
@@ -131,7 +197,34 @@ export function create(config) {
131
197
  return managementResponse;
132
198
  if (fallbackMatch)
133
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
+ }
134
207
  const options = Service.getOptions(endpoint);
208
+ if (replay && replayKey && replayBody) {
209
+ const body = replayBody;
210
+ return Replay.deliver({
211
+ applyReceipt: (response) => result.withReceipt(response),
212
+ attempts: 1,
213
+ replay,
214
+ replayKey,
215
+ requestHeaders: [...Headers.scrub(paidRequest.headers).entries()],
216
+ run: (signal) => {
217
+ const upstream = Replay.withBody(paidRequest, body, signal);
218
+ return proxyUpstream({
219
+ request: upstream,
220
+ service,
221
+ ctx: { ...ctx, request: upstream, ...options },
222
+ proxy,
223
+ onUpstreamError: service.onUpstreamError ?? config.onUpstreamError,
224
+ });
225
+ },
226
+ });
227
+ }
135
228
  const upstreamRes = await proxyUpstream({
136
229
  request,
137
230
  service,
@@ -143,7 +236,7 @@ export function create(config) {
143
236
  }
144
237
  return {
145
238
  fetch: handle,
146
- listener: Request.toNodeListener(handle),
239
+ listener: Request.toNodeListener(handle, { maxBodySize: maxRequestBodySize }),
147
240
  };
148
241
  }
149
242
  /** @internal */
@@ -284,6 +377,26 @@ function buildDiscoveryRoutes(services) {
284
377
  };
285
378
  }));
286
379
  }
380
+ /** Headers the handler reads Payment credentials from, lowercased (Authorization by default). */
381
+ function credentialHeaders(handler) {
382
+ const internal = ('_internal' in handler ? handler._internal : undefined);
383
+ const headers = internal?.offers
384
+ ? internal.offers.flatMap((offer) => offer.credentialHeaders ?? [])
385
+ : (internal?.credentialHeaders ?? []);
386
+ const names = [...new Set(headers.map((header) => header.toLowerCase()))];
387
+ return names.length > 0 ? names : ['authorization'];
388
+ }
389
+ /** Whether a paid handler, or any offer of a composed one, requires identity verifiers. */
390
+ function requiresIdentity(handler) {
391
+ if (!('_internal' in handler))
392
+ return false;
393
+ const internal = handler._internal;
394
+ if (!internal)
395
+ return false;
396
+ if (internal.offers)
397
+ return internal.offers.some((offer) => offer._identity);
398
+ return internal._identity === true;
399
+ }
287
400
  function getConfiguredScope(handler) {
288
401
  if (!('_internal' in handler))
289
402
  return undefined;
@@ -0,0 +1,105 @@
1
+ import type * as Store from '../../Store.js';
2
+ /** Options for replaying paid proxy deliveries. */
3
+ export type Options = {
4
+ /**
5
+ * Upstream attempts per credential, including the first, before a retry gets `502`.
6
+ * A retry after a failed delivery (an upstream error, a 5xx or a body that broke
7
+ * off) sends the paid request upstream again without charging, so a
8
+ * non-idempotent upstream may run the operation twice; set `1` to only ever
9
+ * replay a recorded response. @default 3
10
+ */
11
+ maxAttempts?: number | undefined;
12
+ /** Largest response body, in bytes, kept for replay. Larger or streamed bodies are delivered but not kept. @default 1_048_576 */
13
+ maxBodySize?: number | undefined;
14
+ /** Largest request body, in bytes, a paid request may carry; larger ones get `413`. @default 1_048_576 */
15
+ maxRequestBodySize?: number | undefined;
16
+ /** `Retry-After` seconds returned while a delivery is still in flight. @default 5 */
17
+ retryAfter?: number | undefined;
18
+ /** Atomic store shared by every proxy instance that may receive a retry. */
19
+ store: Store.AtomicStore;
20
+ /**
21
+ * How long to wait for the upstream to start responding, and how long its outcome is kept after it is
22
+ * recorded, in milliseconds. Expired records are removed when they are next
23
+ * looked up, as with mppx's replay markers; give the backing store its own
24
+ * key expiry to reclaim records nobody retries. @default 900_000
25
+ */
26
+ ttl?: number | undefined;
27
+ /** Keeps background work alive after the response returns, such as Cloudflare's `ctx.waitUntil`. */
28
+ waitUntil?: ((promise: Promise<unknown>) => void) | undefined;
29
+ };
30
+ /** Replay options with defaults applied. */
31
+ export type Resolved = {
32
+ maxAttempts: number;
33
+ maxBodySize: number;
34
+ maxRequestBodySize: number;
35
+ retryAfter: number;
36
+ store: Store.AtomicStore;
37
+ ttl: number;
38
+ waitUntil: ((promise: Promise<unknown>) => void) | undefined;
39
+ };
40
+ /** A delivery record, keyed by credential and request. */
41
+ export type Record = {
42
+ attempts: number;
43
+ expires: number;
44
+ receipt: string | null;
45
+ requestHeaders: [string, string][];
46
+ status: 'pending';
47
+ } | {
48
+ attempts: number;
49
+ expires: number;
50
+ receipt: string | null;
51
+ requestHeaders: [string, string][];
52
+ status: 'failed';
53
+ } | {
54
+ expires: number;
55
+ status: 'delivered';
56
+ } | {
57
+ body: string;
58
+ expires: number;
59
+ headers: [string, string][];
60
+ responseStatus: number;
61
+ status: 'complete';
62
+ statusText: string;
63
+ };
64
+ /** Applies defaults to replay options. */
65
+ export declare function resolve(options: Options): Resolved;
66
+ /** Returns the request's Payment credential header value, if it carries one. */
67
+ export declare function credentialOf(request: Request, headerNames?: readonly string[]): string | undefined;
68
+ /** Reads a request body, or returns null when it is larger than maxRequestBodySize. */
69
+ export declare function readBody(request: Request, maxRequestBodySize: number): Promise<Uint8Array<ArrayBuffer> | null>;
70
+ /** Derives the replay key from the credential and the exact request (origin, path, query, method, body) it paid for. */
71
+ export declare function key(parameters: {
72
+ body: Uint8Array;
73
+ credential: string;
74
+ method: string;
75
+ target: string;
76
+ }): string;
77
+ /** Rebuilds a request around an already-read body, optionally with another abort signal. */
78
+ export declare function withBody(request: Request, body: Uint8Array, signal?: AbortSignal, headers?: HeadersInit): Request;
79
+ /** What a request with a known credential should do. */
80
+ export type Lookup = {
81
+ type: 'none';
82
+ } | {
83
+ response: Response;
84
+ type: 'respond';
85
+ } | {
86
+ attempts: number;
87
+ receipt: string | null;
88
+ requestHeaders: [string, string][];
89
+ type: 'retry';
90
+ };
91
+ /** Looks up a delivery and, for a failed one, claims the next upstream attempt. */
92
+ export declare function lookup(replay: Resolved, replayKey: string): Promise<Lookup>;
93
+ /** Runs a paid upstream delivery detached from the client and records its outcome. */
94
+ export declare function deliver(parameters: {
95
+ applyReceipt: (response: Response) => Response | Promise<Response>;
96
+ attempts: number;
97
+ replay: Resolved;
98
+ replayKey: string;
99
+ /** The upstream headers of the paid request; a retried delivery reuses them. */
100
+ requestHeaders: [string, string][];
101
+ run: (signal: AbortSignal) => Promise<Response>;
102
+ }): Promise<Response>;
103
+ /** Adds a stored receipt to a response built for a retried delivery. */
104
+ export declare function withReceiptHeader(response: Response, receipt: string | null): Response;
105
+ //# sourceMappingURL=Replay.d.ts.map