mppx 0.12.0 → 0.13.2

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 (110) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/Method.d.ts +7 -1
  3. package/dist/Method.js +7 -2
  4. package/dist/Receipt.d.ts +2 -0
  5. package/dist/Receipt.js +2 -0
  6. package/dist/cli/cli.d.ts +8 -0
  7. package/dist/cli/cli.js +134 -136
  8. package/dist/cli/plugins/index.js +12 -14
  9. package/dist/cli/plugins/tempo.js +9 -2
  10. package/dist/cli/sessions/request.d.ts +1 -1
  11. package/dist/cli/sessions/request.js +6 -8
  12. package/dist/cli/utils.d.ts +11 -1
  13. package/dist/cli/utils.js +15 -2
  14. package/dist/cli/validate/challenge.d.ts +2 -0
  15. package/dist/cli/validate/challenge.js +19 -4
  16. package/dist/cli/validate/discovery.d.ts +1 -0
  17. package/dist/cli/validate/discovery.js +10 -1
  18. package/dist/cli/validate/payment.js +34 -12
  19. package/dist/client/Methods.d.ts +0 -2
  20. package/dist/client/Methods.js +0 -2
  21. package/dist/client/Mppx.js +1 -0
  22. package/dist/client/index.d.ts +1 -1
  23. package/dist/client/index.js +1 -1
  24. package/dist/client/internal/Fetch.d.ts +2 -0
  25. package/dist/client/internal/Fetch.js +22 -7
  26. package/dist/client/internal/MethodChallenge.d.ts +1 -0
  27. package/dist/evm/Types.js +1 -1
  28. package/dist/evm/server/Charge.js +5 -0
  29. package/dist/integrations/x402/Evm.js +4 -3
  30. package/dist/internal/HeaderCodec.d.ts +2 -2
  31. package/dist/internal/version.d.ts +2 -2
  32. package/dist/internal/version.js +1 -1
  33. package/dist/mcp/client/McpClient.js +4 -7
  34. package/dist/middlewares/elysia.d.ts +11 -8
  35. package/dist/middlewares/elysia.js +67 -14
  36. package/dist/middlewares/express.js +13 -6
  37. package/dist/middlewares/hono.js +4 -4
  38. package/dist/middlewares/nextjs.js +9 -5
  39. package/dist/proxy/internal/Headers.js +65 -0
  40. package/dist/proxy/services/stripe.js +1 -0
  41. package/dist/server/Mppx.d.ts +14 -2
  42. package/dist/server/Mppx.js +85 -44
  43. package/dist/server/NodeListener.js +71 -6
  44. package/dist/server/Request.d.ts +11 -1
  45. package/dist/server/Request.js +132 -13
  46. package/dist/server/Transport.d.ts +14 -1
  47. package/dist/server/Transport.js +1 -0
  48. package/dist/stripe/server/Methods.d.ts +10 -3
  49. package/dist/stripe/server/Methods.js +17 -2
  50. package/dist/stripe/server/internal/html.gen.js +1 -1
  51. package/dist/tempo/Methods.d.ts +1 -0
  52. package/dist/tempo/Methods.js +1 -0
  53. package/dist/tempo/client/Charge.js +17 -13
  54. package/dist/tempo/client/Methods.d.ts +1 -5
  55. package/dist/tempo/client/Methods.js +10 -5
  56. package/dist/tempo/client/ResolveAccount.d.ts +3 -0
  57. package/dist/tempo/client/index.d.ts +0 -3
  58. package/dist/tempo/client/index.js +0 -2
  59. package/dist/tempo/internal/fee-payer.d.ts +6 -3
  60. package/dist/tempo/internal/fee-payer.js +23 -6
  61. package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
  62. package/dist/tempo/internal/machine-token-charge.js +2 -1
  63. package/dist/tempo/legacy/index.d.ts +1 -3
  64. package/dist/tempo/legacy/index.js +1 -3
  65. package/dist/tempo/server/Charge.js +87 -53
  66. package/dist/tempo/server/Methods.d.ts +55 -10
  67. package/dist/tempo/server/Subscription.d.ts +1 -1
  68. package/dist/tempo/server/Subscription.js +31 -4
  69. package/dist/tempo/server/internal/html.gen.js +1 -1
  70. package/dist/tempo/server/internal/transport.js +28 -11
  71. package/dist/tempo/session/client/ChannelOps.js +5 -0
  72. package/dist/tempo/session/client/Session.d.ts +1 -0
  73. package/dist/tempo/session/client/Session.js +7 -4
  74. package/dist/tempo/session/client/SessionManager.js +3 -0
  75. package/dist/tempo/session/client/Transports.d.ts +8 -1
  76. package/dist/tempo/session/client/Transports.js +12 -3
  77. package/dist/tempo/session/precompile/Chain.js +16 -1
  78. package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
  79. package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
  80. package/dist/tempo/session/precompile/Voucher.js +48 -0
  81. package/dist/tempo/session/server/ChannelStore.d.ts +20 -2
  82. package/dist/tempo/session/server/ChannelStore.js +25 -4
  83. package/dist/tempo/session/server/CredentialVerification.d.ts +4 -1
  84. package/dist/tempo/session/server/CredentialVerification.js +34 -6
  85. package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
  86. package/dist/tempo/session/server/MeteredStream.js +85 -29
  87. package/dist/tempo/session/server/Session.d.ts +2 -1
  88. package/dist/tempo/session/server/Session.js +2 -1
  89. package/dist/tempo/session/server/Settlement.d.ts +21 -0
  90. package/dist/tempo/session/server/Settlement.js +92 -17
  91. package/dist/tempo/session/server/Sse.d.ts +3 -1
  92. package/dist/tempo/session/server/Sse.js +105 -32
  93. package/dist/tempo/session/server/Transports.d.ts +24 -19
  94. package/dist/tempo/session/server/Transports.js +169 -58
  95. package/dist/tempo/session/server/Ws.d.ts +5 -1
  96. package/dist/tempo/session/server/Ws.js +75 -20
  97. package/dist/tempo/subscription/Store.d.ts +2 -0
  98. package/dist/tempo/subscription/Store.js +3 -0
  99. package/dist/validation/core.js +2 -1
  100. package/dist/x402/server/EvmCharge.d.ts +3 -1
  101. package/dist/x402/server/EvmCharge.js +14 -1
  102. package/package.json +6 -6
  103. package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
  104. package/dist/tempo/legacy/client/ChannelOps.js +0 -168
  105. package/dist/tempo/legacy/client/Session.d.ts +0 -185
  106. package/dist/tempo/legacy/client/Session.js +0 -279
  107. package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
  108. package/dist/tempo/legacy/client/SessionManager.js +0 -706
  109. package/dist/tempo/legacy/client/index.d.ts +0 -7
  110. package/dist/tempo/legacy/client/index.js +0 -5
@@ -4,6 +4,10 @@
4
4
  * Useful when bridging Fetch API handlers with Node.js HTTP servers.
5
5
  */
6
6
  export async function sendResponse(res, response) {
7
+ if (isResponseClosed(res)) {
8
+ await response.body?.cancel('response connection closed').catch(() => { });
9
+ return;
10
+ }
7
11
  const headers = {};
8
12
  for (const [key, value] of response.headers) {
9
13
  if (key in headers) {
@@ -23,21 +27,82 @@ export async function sendResponse(res, response) {
23
27
  res.writeHead(response.status, headers);
24
28
  if (response.body != null && res.req?.method !== 'HEAD') {
25
29
  const reader = response.body.getReader();
30
+ let connectionClosed = false;
31
+ const onClose = () => {
32
+ connectionClosed = true;
33
+ void reader.cancel('response connection closed').catch(() => { });
34
+ };
35
+ res.once('close', onClose);
36
+ if (isResponseClosed(res))
37
+ onClose();
26
38
  try {
39
+ if (res.destroyed || res.writableEnded) {
40
+ await reader.cancel('response connection closed');
41
+ return;
42
+ }
27
43
  while (true) {
28
44
  const { done, value } = await reader.read();
29
- if (done)
45
+ if (done || connectionClosed)
30
46
  break;
31
- if (res.write(value) === false)
32
- await new Promise((resolve) => {
33
- res.once('drain', resolve);
34
- });
47
+ if (res.write(value) === false) {
48
+ try {
49
+ if (!(await waitForDrain(res))) {
50
+ await reader.cancel('response connection closed');
51
+ return;
52
+ }
53
+ }
54
+ catch (error) {
55
+ await reader.cancel(error).catch(() => { });
56
+ throw error;
57
+ }
58
+ }
35
59
  }
36
60
  }
37
61
  finally {
62
+ res.removeListener('close', onClose);
38
63
  reader.releaseLock();
39
64
  }
40
65
  }
41
- res.end();
66
+ if (!isResponseClosed(res))
67
+ res.end();
68
+ }
69
+ /** Returns whether the HTTP/1 response or underlying HTTP/2 stream has closed. */
70
+ function isResponseClosed(res) {
71
+ const stream = 'stream' in res ? res.stream : undefined;
72
+ return Boolean(res.destroyed || res.writableEnded || stream?.destroyed || stream?.closed);
73
+ }
74
+ /**
75
+ * Waits for write backpressure to clear.
76
+ *
77
+ * Returns `true` on drain, `false` when the response closes, and rejects on a
78
+ * write-side error. Event listeners are always removed before settlement.
79
+ */
80
+ function waitForDrain(res) {
81
+ if (isResponseClosed(res))
82
+ return Promise.resolve(false);
83
+ return new Promise((resolve, reject) => {
84
+ const cleanup = () => {
85
+ res.removeListener('drain', onDrain);
86
+ res.removeListener('close', onClose);
87
+ res.removeListener('error', onError);
88
+ };
89
+ const onDrain = () => {
90
+ cleanup();
91
+ resolve(true);
92
+ };
93
+ const onClose = () => {
94
+ cleanup();
95
+ resolve(false);
96
+ };
97
+ const onError = (error) => {
98
+ cleanup();
99
+ reject(error);
100
+ };
101
+ res.once('drain', onDrain);
102
+ res.once('close', onClose);
103
+ res.once('error', onError);
104
+ if (isResponseClosed(res))
105
+ onClose();
106
+ });
42
107
  }
43
108
  //# sourceMappingURL=NodeListener.js.map
@@ -2,9 +2,17 @@ import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http
2
2
  export type FetchHandler = (request: Request) => Promise<Response> | Response;
3
3
  export type RequestListenerOptions = {
4
4
  host?: string | undefined;
5
+ /** Maximum accepted request body size in bytes. @default 10485760 (10 MiB) */
6
+ maxBodySize?: number | undefined;
5
7
  onError?: ((error: unknown) => void | Response | Promise<void | Response>) | undefined;
6
8
  protocol?: string | undefined;
7
9
  };
10
+ /** Options used only while converting a Node request to a Fetch request. */
11
+ export type NodeConversionOptions = Omit<RequestListenerOptions, 'onError'>;
12
+ /** Raised when a Node request body exceeds the configured byte limit. */
13
+ export declare class RequestBodyTooLargeError extends Error {
14
+ readonly name = "RequestBodyTooLargeError";
15
+ }
8
16
  /**
9
17
  * Converts a Fetch API handler into a Node.js HTTP request listener.
10
18
  *
@@ -20,5 +28,7 @@ export declare function toNodeListener(handler: FetchHandler, options?: RequestL
20
28
  * @param res - The Node.js ServerResponse (used for abort signal lifecycle).
21
29
  * @returns A Fetch API Request.
22
30
  */
23
- export declare function fromNodeListener(req: IncomingMessage, res: ServerResponse, options?: RequestListenerOptions | undefined): Request;
31
+ export declare function fromNodeListener(req: IncomingMessage, res: ServerResponse, options?: NodeConversionOptions | undefined): Request;
32
+ /** Waits for a Node-backed request body to finish and enforces its size limit. */
33
+ export declare function waitForBody(request: Request): Promise<void>;
24
34
  //# sourceMappingURL=Request.d.ts.map
@@ -1,4 +1,10 @@
1
1
  import * as NodeListener from './NodeListener.js';
2
+ const defaultMaxBodySize = 10 * 1024 * 1024;
3
+ const bodyCompletions = new WeakMap();
4
+ /** Raised when a Node request body exceeds the configured byte limit. */
5
+ export class RequestBodyTooLargeError extends Error {
6
+ name = 'RequestBodyTooLargeError';
7
+ }
2
8
  /**
3
9
  * Converts a Fetch API handler into a Node.js HTTP request listener.
4
10
  *
@@ -9,6 +15,11 @@ import * as NodeListener from './NodeListener.js';
9
15
  export function toNodeListener(handler, options) {
10
16
  const onError = options?.onError ??
11
17
  ((error) => {
18
+ if (error instanceof RequestBodyTooLargeError)
19
+ return new Response('Payload Too Large', {
20
+ status: 413,
21
+ headers: { 'Content-Type': 'text/plain' },
22
+ });
12
23
  console.error(error);
13
24
  return new Response('Internal Server Error', {
14
25
  status: 500,
@@ -20,6 +31,7 @@ export function toNodeListener(handler, options) {
20
31
  try {
21
32
  const request = fromNodeListener(req, res, options);
22
33
  response = await handler(request);
34
+ await waitForBody(request);
23
35
  }
24
36
  catch (error) {
25
37
  try {
@@ -70,20 +82,127 @@ export function fromNodeListener(req, res, options) {
70
82
  headers,
71
83
  signal: controller.signal,
72
84
  };
73
- if (method !== 'GET' && method !== 'HEAD' && hasBody(headers)) {
74
- init.body = new ReadableStream({
75
- start(c) {
76
- req.on('data', (chunk) => {
77
- c.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
78
- });
79
- req.on('end', () => {
80
- c.close();
81
- });
82
- },
83
- });
84
- init.duplex = 'half';
85
+ const maxBodySize = options?.maxBodySize ?? defaultMaxBodySize;
86
+ validateBodySize(req, maxBodySize);
87
+ let bodyCompletion;
88
+ let body;
89
+ if (hasBody(headers)) {
90
+ const monitored = createBodyStream(req, maxBodySize);
91
+ body = monitored.body;
92
+ if (method !== 'GET' && method !== 'HEAD') {
93
+ init.body = body;
94
+ init.duplex = 'half';
95
+ bodyCompletion = monitored.completion;
96
+ }
97
+ else {
98
+ void consumeBody(body).catch(() => { });
99
+ bodyCompletion = monitored.completion;
100
+ }
101
+ }
102
+ let request;
103
+ try {
104
+ request = new Request(url, init);
105
+ }
106
+ catch (error) {
107
+ void body?.cancel(error).catch(() => { });
108
+ throw error;
109
+ }
110
+ if (bodyCompletion)
111
+ bodyCompletions.set(request, bodyCompletion);
112
+ return request;
113
+ }
114
+ /** Waits for a Node-backed request body to finish and enforces its size limit. */
115
+ export async function waitForBody(request) {
116
+ const completion = bodyCompletions.get(request);
117
+ if (!completion)
118
+ return;
119
+ if (request.body && !request.bodyUsed && !request.body.locked)
120
+ await consumeBody(request.body);
121
+ const error = await completion;
122
+ if (error !== undefined)
123
+ throw error;
124
+ }
125
+ async function consumeBody(body) {
126
+ const reader = body.getReader();
127
+ try {
128
+ while (!(await reader.read()).done) { }
129
+ }
130
+ finally {
131
+ reader.releaseLock();
132
+ }
133
+ }
134
+ function createBodyStream(req, maxBodySize) {
135
+ let received = 0;
136
+ let closed = false;
137
+ let cleanup = () => { };
138
+ let resolveCompletion;
139
+ const completion = new Promise((resolve) => {
140
+ resolveCompletion = resolve;
141
+ });
142
+ const body = new ReadableStream({
143
+ start(controller) {
144
+ const finish = (action, error) => {
145
+ if (closed)
146
+ return;
147
+ closed = true;
148
+ cleanup();
149
+ action();
150
+ resolveCompletion(error);
151
+ };
152
+ const onData = (value) => {
153
+ const chunk = typeof value === 'string' ? Buffer.from(value) : value;
154
+ received += chunk.byteLength;
155
+ if (received > maxBodySize) {
156
+ const error = new RequestBodyTooLargeError(`Request body exceeds ${maxBodySize} bytes`);
157
+ finish(() => controller.error(error), error);
158
+ req.resume();
159
+ return;
160
+ }
161
+ controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
162
+ if ((controller.desiredSize ?? 0) <= 0)
163
+ req.pause();
164
+ };
165
+ const onEnd = () => finish(() => controller.close());
166
+ const onError = (error) => finish(() => controller.error(error), error);
167
+ const onAborted = () => {
168
+ const error = new Error('request aborted');
169
+ finish(() => controller.error(error), error);
170
+ };
171
+ cleanup = () => {
172
+ req.removeListener('data', onData);
173
+ req.removeListener('end', onEnd);
174
+ req.removeListener('error', onError);
175
+ req.removeListener('aborted', onAborted);
176
+ };
177
+ req.pause();
178
+ req.on('data', onData);
179
+ req.once('end', onEnd);
180
+ req.once('error', onError);
181
+ req.once('aborted', onAborted);
182
+ },
183
+ pull() {
184
+ if (!closed)
185
+ req.resume();
186
+ },
187
+ cancel(reason) {
188
+ if (closed)
189
+ return;
190
+ closed = true;
191
+ cleanup();
192
+ req.destroy(reason instanceof Error ? reason : undefined);
193
+ resolveCompletion(reason);
194
+ },
195
+ });
196
+ return { body, completion };
197
+ }
198
+ function validateBodySize(req, maxBodySize) {
199
+ if (!Number.isSafeInteger(maxBodySize) || maxBodySize < 0)
200
+ throw new TypeError('maxBodySize must be a non-negative safe integer');
201
+ const contentLength = Number(req.headers['content-length']);
202
+ if (Number.isFinite(contentLength) && contentLength > maxBodySize) {
203
+ req.resume();
204
+ throw new RequestBodyTooLargeError(`Request body exceeds ${maxBodySize} bytes`);
85
205
  }
86
- return new Request(url, init);
87
206
  }
88
207
  function hasBody(headers) {
89
208
  const contentLength = headers.get('content-length');
@@ -16,6 +16,10 @@ export { type McpSdk, mcpSdk } from '../mcp/server/Transport.js';
16
16
  export type Transport<in out input = unknown, in out challengeOutput = unknown, in receiptResponse = challengeOutput, out receiptOutput = receiptResponse> = {
17
17
  /** Transport name for identification. */
18
18
  name: string;
19
+ /** Whether receipt wrapping accepts route-produced async iterables. */
20
+ supportsStreamingReceipts?: boolean | undefined;
21
+ /** HTTP header names from which this transport can read credentials. */
22
+ credentialHeaders?: readonly string[] | undefined;
19
23
  /** Captures the transport request into an immutable verification snapshot. */
20
24
  captureRequest?: ((input: input) => MaybePromise<Method.CapturedRequest>) | undefined;
21
25
  /**
@@ -33,6 +37,11 @@ export type Transport<in out input = unknown, in out challengeOutput = unknown,
33
37
  * Returns `null` if no credential was provided, or throws if malformed.
34
38
  */
35
39
  getCredential: (input: input) => Credential.Credential | null;
40
+ /** Returns whether a transport-native credential targets a configured request. */
41
+ matchCredential?: ((options: {
42
+ input: input;
43
+ request: Record<string, unknown>;
44
+ }) => MaybePromise<boolean>) | undefined;
36
45
  /** Creates a transport response for a payment challenge. */
37
46
  respondChallenge: (options: {
38
47
  challenge: Challenge.Challenge;
@@ -53,7 +62,11 @@ export type Transport<in out input = unknown, in out challengeOutput = unknown,
53
62
  export type AnyTransport = Transport<any, any, any, any>;
54
63
  export type Http = Transport<Request, Response>;
55
64
  export type Mcp = Transport<core_Mcp.JsonRpcRequest, core_Mcp.Response>;
56
- export type Sse<stream = any> = Transport<Request, Response, Response | AsyncIterable<string> | ((stream: stream) => AsyncIterable<string>), Response>;
65
+ /**
66
+ * SSE receipt wrapping is asynchronous so accounting completes before even a
67
+ * bodyless response can be returned to the application.
68
+ */
69
+ export type Sse<stream = any> = Transport<Request, Response, Response | AsyncIterable<string> | ((stream: stream) => AsyncIterable<string>), Promise<Response>>;
57
70
  /** Extracts the input type from a transport. */
58
71
  export type InputOf<transport extends AnyTransport = AnyTransport> = transport extends Transport<infer input, any, any> ? input : never;
59
72
  /** Extracts the challenge output type from a transport. */
@@ -39,6 +39,7 @@ export function http(options = {}) {
39
39
  : Constants.Headers.authorization;
40
40
  return from({
41
41
  name: 'http',
42
+ credentialHeaders: [credentialHeader],
42
43
  captureRequest(request) {
43
44
  return {
44
45
  hasBody: request.body !== null,
@@ -1,5 +1,6 @@
1
1
  import { charge as evmCharge } from '../../evm/server/Charge.js';
2
2
  import type * as Method from '../../Method.js';
3
+ import type * as Store from '../../Store.js';
3
4
  import { charge as tempoCharge } from '../../tempo/server/Charge.js';
4
5
  import { session as tempoSession } from '../../tempo/session/server/Session.js';
5
6
  import * as PaymentIntent from '../internal/payment-intent.js';
@@ -19,6 +20,10 @@ export declare namespace stripe {
19
20
  networkId: string;
20
21
  livemode: boolean;
21
22
  hostedFeePayer?: boolean | undefined;
23
+ /**
24
+ * Atomic replay store shared by every Stripe-backed Tempo server instance.
25
+ */
26
+ store: Store.AtomicStore;
22
27
  connect?: ConnectConfig;
23
28
  depositAddresses?: Partial<Record<Network, string>> | ((network: Network) => Promise<string>);
24
29
  metadata?: Record<string, string> | undefined;
@@ -40,7 +45,7 @@ type CustomRailNetwork = Exclude<stripe.Network, 'tempo' | 'base'>;
40
45
  type AdditionalConfig = {
41
46
  base?: BaseConfig;
42
47
  tempo?: {
43
- session: Omit<tempoSession.Parameters, 'currency' | 'recipient'>;
48
+ session: Omit<tempoSession.Parameters, 'currency' | 'recipient' | 'store'>;
44
49
  };
45
50
  } & {
46
51
  [K in CustomRailNetwork]?: CustomRailFactory;
@@ -75,10 +80,10 @@ interface StripeMachinePayments<P extends stripe.Parameters = stripe.Parameters>
75
80
  charge: (params: {
76
81
  recipient: stripe.DepositAddress<'tempo'>;
77
82
  metadata?: Record<string, string>;
78
- } & Partial<Omit<Parameters<typeof tempoCharge>[0], 'currency' | 'recipient'>>) => TempoServer;
83
+ } & Partial<Omit<Parameters<typeof tempoCharge>[0], 'currency' | 'recipient' | 'store' | 'storeKeyPrefix'>>) => TempoServer;
79
84
  session: (params: {
80
85
  recipient: stripe.DepositAddress<'tempo'>;
81
- } & Omit<tempoSession.Parameters, 'currency' | 'recipient'>) => TempoSessionServer;
86
+ } & Omit<tempoSession.Parameters, 'currency' | 'recipient' | 'store'>) => TempoSessionServer;
82
87
  };
83
88
  base: {
84
89
  charge: (params: {
@@ -106,6 +111,7 @@ declare class DefaultMethodsBuilder implements PromiseLike<DefaultMethods> {
106
111
  * client: stripeClient,
107
112
  * networkId: process.env.STRIPE_PROFILE_ID!,
108
113
  * livemode: !process.env.STRIPE_SECRET_KEY!.includes('_test_'),
114
+ * store,
109
115
  * })
110
116
  *
111
117
  * // Async: deposit addresses resolved from Stripe API
@@ -123,6 +129,7 @@ declare class DefaultMethodsBuilder implements PromiseLike<DefaultMethods> {
123
129
  * networkId: process.env.STRIPE_PROFILE_ID!,
124
130
  * livemode: !process.env.STRIPE_SECRET_KEY!.includes('_test_'),
125
131
  * depositAddresses: { tempo: process.env.TEMPO_DEPOSIT_ADDRESS! },
132
+ * store,
126
133
  * })
127
134
  *
128
135
  * const mppx = Mppx.create({
@@ -37,6 +37,7 @@ class DefaultMethodsBuilder {
37
37
  * client: stripeClient,
38
38
  * networkId: process.env.STRIPE_PROFILE_ID!,
39
39
  * livemode: !process.env.STRIPE_SECRET_KEY!.includes('_test_'),
40
+ * store,
40
41
  * })
41
42
  *
42
43
  * // Async: deposit addresses resolved from Stripe API
@@ -54,6 +55,7 @@ class DefaultMethodsBuilder {
54
55
  * networkId: process.env.STRIPE_PROFILE_ID!,
55
56
  * livemode: !process.env.STRIPE_SECRET_KEY!.includes('_test_'),
56
57
  * depositAddresses: { tempo: process.env.TEMPO_DEPOSIT_ADDRESS! },
58
+ * store,
57
59
  * })
58
60
  *
59
61
  * const mppx = Mppx.create({
@@ -80,11 +82,13 @@ class DefaultMethodsBuilder {
80
82
  * retries the recording once without them. SPT payments do not use this fallback.
81
83
  */
82
84
  export function stripe(parameters) {
83
- const { client, networkId, livemode, hostedFeePayer, connect, depositAddresses, metadata } = parameters;
85
+ const { client, networkId, livemode, hostedFeePayer, connect, depositAddresses, metadata, store, } = parameters;
84
86
  if (!client.rawRequest)
85
87
  throw new Error('stripe.create() requires a Stripe SDK client with rawRequest() (v15+)');
86
88
  if (hostedFeePayer && connect)
87
89
  throw new Error('Stripe hosted fee payer does not support Connect account routing.');
90
+ if (typeof store?.update !== 'function')
91
+ throw new Error('stripe.create() requires a shared atomic `store` for replay protection.');
88
92
  const tempoCurrency = (livemode ? tempoDefaults.tokens.usdc : tempoDefaults.tokens.pathUsd);
89
93
  const hostedTempoFeePayer = hostedFeePayer && livemode ? HostedFeePayer.create(client) : undefined;
90
94
  const tempoPaymentHandler = createPaymentSuccessHandler(client, 'tempo', connect, metadata);
@@ -104,7 +108,13 @@ export function stripe(parameters) {
104
108
  });
105
109
  }
106
110
  function makeTempoCharge(params) {
107
- const { recipient, metadata: callMetadata, ...rest } = params;
111
+ const supplied = params;
112
+ const suppliedStore = supplied.store;
113
+ if (suppliedStore !== undefined && suppliedStore !== store)
114
+ throw new Error('stripe.create() Tempo charges must use the factory-level shared atomic `store`.');
115
+ if (supplied.storeKeyPrefix !== undefined)
116
+ throw new Error('stripe.create() Tempo charges cannot override the shared store namespace.');
117
+ const { recipient, metadata: callMetadata, store: _, storeKeyPrefix: __, ...rest } = params;
108
118
  const handler = callMetadata
109
119
  ? createPaymentSuccessHandler(client, 'tempo', connect, { ...metadata, ...callMetadata })
110
120
  : tempoPaymentHandler;
@@ -116,9 +126,13 @@ export function stripe(parameters) {
116
126
  canOffer: cryptoCanOffer,
117
127
  onPaymentSuccess: handler,
118
128
  ...rest,
129
+ store,
119
130
  }));
120
131
  }
121
132
  function makeTempoSession(params) {
133
+ const suppliedStore = params.store;
134
+ if (suppliedStore !== undefined && suppliedStore !== store)
135
+ throw new Error('stripe.create() Tempo sessions must use the factory-level shared atomic `store`.');
122
136
  const { recipient, onSessionSettlement, ...rest } = params;
123
137
  return tempoSession({
124
138
  currency: tempoCurrency,
@@ -126,6 +140,7 @@ export function stripe(parameters) {
126
140
  ...(!livemode && { chainId: tempoDefaults.chainId.testnet }),
127
141
  ...(hostedTempoFeePayer && { feePayer: hostedTempoFeePayer }),
128
142
  ...rest,
143
+ store,
129
144
  async onSessionSettlement(context) {
130
145
  await tempoPaymentHandler({
131
146
  intent: 'session',