@parallel-protocol/mpp 0.1.0

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.
@@ -0,0 +1,340 @@
1
+ import { CHAIN_NAMES, CHAINS_STATIC } from '@parallel-protocol/chains';
2
+ import { MppChargeCredentialSchema, MppVerifyResponseSchema, MppReceiptSchema, encodeBase64Url } from '@parallel-protocol/mpp-types';
3
+ import { z } from 'zod';
4
+
5
+ // src/errors.ts
6
+ var MPP_ERROR_CODES = {
7
+ FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
8
+ FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
9
+ INVALID_CREDENTIAL: "INVALID_CREDENTIAL",
10
+ METHOD_MISMATCH: "METHOD_MISMATCH"
11
+ };
12
+ var MppConfigError = class extends Error {
13
+ constructor(message, cause) {
14
+ super(message);
15
+ this.name = "MppConfigError";
16
+ this.cause = cause;
17
+ }
18
+ };
19
+ var MppRuntimeError = class extends Error {
20
+ constructor(code) {
21
+ super(code);
22
+ this.code = code;
23
+ this.name = "MppRuntimeError";
24
+ }
25
+ code;
26
+ };
27
+ function parseDecimalAmount(amount, decimals) {
28
+ const multiplier = 10n ** BigInt(decimals);
29
+ const parts = amount.split(".");
30
+ const whole = parts[0] ?? "0";
31
+ const fraction = (parts[1] ?? "").padEnd(decimals, "0").slice(0, decimals);
32
+ return BigInt(whole) * multiplier + BigInt(fraction || "0");
33
+ }
34
+ function parsePrice(price, decimals = 6) {
35
+ if (typeof price === "bigint") return price;
36
+ const match = price.match(/^(\d+(?:\.\d+)?)$/);
37
+ if (match?.[1]) return parseDecimalAmount(match[1], decimals);
38
+ throw new Error(
39
+ `Invalid price format: "${price}". Expected "0.01" or a bigint.`
40
+ );
41
+ }
42
+ var CHAIN_ID_BY_SLUG = new Map(
43
+ CHAIN_NAMES.map((c) => [c, CHAINS_STATIC[c].viem.id])
44
+ );
45
+ function chainIdFromSlug(network) {
46
+ return CHAIN_ID_BY_SLUG.get(network);
47
+ }
48
+ function isSuccessStatus(status) {
49
+ return status >= 200 && status < 300;
50
+ }
51
+ var SETTLE_FAILURE_BODY = {
52
+ error: "PAYMENT_SETTLEMENT_FAILED",
53
+ message: "Payment could not be confirmed after multiple attempts. Please retry."
54
+ };
55
+ function matchRoute(path, routes) {
56
+ if (routes[path]) return routes[path];
57
+ let bestMatch;
58
+ let bestLength = 0;
59
+ for (const [pattern, config] of Object.entries(routes)) {
60
+ if (path.startsWith(`${pattern}/`) && pattern.length > bestLength) {
61
+ bestMatch = config;
62
+ bestLength = pattern.length;
63
+ }
64
+ }
65
+ return bestMatch;
66
+ }
67
+ var DEFAULT_TIMEOUT_SECONDS = 300;
68
+ function challengeId() {
69
+ return globalThis.crypto.randomUUID();
70
+ }
71
+ function buildChargeRequest(route, chainId) {
72
+ return {
73
+ amount: parsePrice(route.price, route.decimals).toString(),
74
+ currency: route.currency,
75
+ recipient: route.payTo,
76
+ ...route.description ? { description: route.description } : {},
77
+ methodDetails: { chainId }
78
+ };
79
+ }
80
+ function buildChargeRequestB64(route) {
81
+ const chainId = chainIdFromSlug(route.network);
82
+ if (chainId === void 0) {
83
+ throw new MppConfigError(
84
+ `Unknown network "${route.network}" \u2014 not in the @parallel-protocol/chains catalogue`
85
+ );
86
+ }
87
+ return encodeBase64Url(buildChargeRequest(route, chainId));
88
+ }
89
+ function buildChallenge(route, request = buildChargeRequestB64(route)) {
90
+ const ttl = (route.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
91
+ return {
92
+ id: challengeId(),
93
+ ...route.realm ? { realm: route.realm } : {},
94
+ method: route.method ?? "evm",
95
+ intent: "charge",
96
+ request,
97
+ expires: new Date(Date.now() + ttl).toISOString(),
98
+ ...route.description ? { description: route.description } : {}
99
+ };
100
+ }
101
+ function quote(value) {
102
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
103
+ }
104
+ function serializeChallenge(challenge) {
105
+ const params = [
106
+ ["realm", challenge.realm],
107
+ ["id", challenge.id],
108
+ ["method", challenge.method],
109
+ ["intent", challenge.intent],
110
+ ["request", challenge.request],
111
+ ["digest", challenge.digest],
112
+ ["expires", challenge.expires],
113
+ ["description", challenge.description],
114
+ ["opaque", challenge.opaque]
115
+ ];
116
+ const serialized = params.filter((p) => p[1] !== void 0).map(([k, v]) => `${k}=${quote(v)}`).join(", ");
117
+ return `Payment ${serialized}`;
118
+ }
119
+ function parseAuthorizationHeader(header) {
120
+ if (!header) return null;
121
+ const match = header.match(/^Payment\s+(.+)$/i);
122
+ if (!match?.[1]) return null;
123
+ let decoded;
124
+ try {
125
+ decoded = JSON.parse(
126
+ Buffer.from(match[1].trim(), "base64url").toString("utf8")
127
+ );
128
+ } catch {
129
+ return null;
130
+ }
131
+ const parsed = MppChargeCredentialSchema.safeParse(decoded);
132
+ return parsed.success ? parsed.data : null;
133
+ }
134
+ var FACILITATOR_TIMEOUT_MS = 1e4;
135
+ var MppFacilitatorClient = class {
136
+ url;
137
+ headers;
138
+ constructor(config) {
139
+ this.url = config.url;
140
+ this.headers = {
141
+ "Content-Type": "application/json",
142
+ ...config.apiKey ? { "X-API-Key": config.apiKey } : {}
143
+ };
144
+ }
145
+ /** Verify a credential (phase 1, no on-chain tx). Throws {@link MppRuntimeError} on failure. */
146
+ async verify(credential) {
147
+ let json;
148
+ try {
149
+ const response = await fetch(`${this.url}/mpp/verify`, {
150
+ method: "POST",
151
+ headers: this.headers,
152
+ body: JSON.stringify({ credential }),
153
+ signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
154
+ });
155
+ if (!response.ok) {
156
+ const errorBody = await response.json().catch(() => null);
157
+ const code = errorBody?.error ?? MPP_ERROR_CODES.FACILITATOR_UNAVAILABLE;
158
+ throw new MppRuntimeError(code);
159
+ }
160
+ json = await response.json();
161
+ } catch (e) {
162
+ if (e instanceof MppRuntimeError) throw e;
163
+ throw new MppRuntimeError(MPP_ERROR_CODES.FACILITATOR_UNAVAILABLE);
164
+ }
165
+ try {
166
+ MppVerifyResponseSchema.parse(json);
167
+ } catch {
168
+ throw new MppRuntimeError(MPP_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
169
+ }
170
+ }
171
+ /** Settle a credential (phase 2, on-chain). Returns the receipt or a failure. */
172
+ async settle(credential) {
173
+ let json;
174
+ try {
175
+ const response = await fetch(`${this.url}/mpp/settle`, {
176
+ method: "POST",
177
+ headers: this.headers,
178
+ body: JSON.stringify({ credential }),
179
+ signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
180
+ });
181
+ if (!response.ok) {
182
+ const errorBody = await response.json().catch(() => null);
183
+ return {
184
+ success: false,
185
+ error: {
186
+ code: errorBody?.error ?? "SETTLEMENT_FAILED",
187
+ message: errorBody?.message ?? "Settlement failed"
188
+ }
189
+ };
190
+ }
191
+ json = await response.json();
192
+ } catch (e) {
193
+ if (e instanceof MppRuntimeError) throw e;
194
+ throw new MppRuntimeError(MPP_ERROR_CODES.FACILITATOR_UNAVAILABLE);
195
+ }
196
+ const parsed = MppReceiptSchema.safeParse(json);
197
+ if (!parsed.success) {
198
+ throw new MppRuntimeError(MPP_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
199
+ }
200
+ return { success: true, receipt: parsed.data };
201
+ }
202
+ };
203
+ var ethereumAddressSchema = z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid Ethereum address");
204
+ var priceSchema = z.union([
205
+ z.bigint().positive("Price must be greater than 0"),
206
+ z.string().regex(/^\d+(?:\.\d+)?$/, 'Price must be in format "0.01"').refine((price) => Number(price) > 0, "Price must be greater than 0")
207
+ ]);
208
+ var facilitatorConfigSchema = z.object({
209
+ url: z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
210
+ apiKey: z.string().optional()
211
+ });
212
+ var routeConfigSchema = z.object({
213
+ price: priceSchema,
214
+ decimals: z.number().int().nonnegative().optional(),
215
+ network: z.string().min(1, "Network is required"),
216
+ payTo: ethereumAddressSchema,
217
+ currency: ethereumAddressSchema,
218
+ method: z.enum(["evm", "parallel"]).optional(),
219
+ maxTimeoutSeconds: z.number().int().positive().optional(),
220
+ description: z.string().optional(),
221
+ realm: z.string().optional()
222
+ });
223
+ var mppMiddlewareConfigSchema = z.object({
224
+ facilitator: facilitatorConfigSchema,
225
+ routes: z.record(z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
226
+ });
227
+
228
+ // src/middleware.ts
229
+ function challengeResponse(route, request, error) {
230
+ const challenge = buildChallenge(route, request);
231
+ return {
232
+ status: 402,
233
+ headers: {
234
+ "Content-Type": "application/json",
235
+ "WWW-Authenticate": serializeChallenge(challenge),
236
+ "access-control-expose-headers": "WWW-Authenticate"
237
+ },
238
+ body: {
239
+ ...error ? { error } : {},
240
+ challenge
241
+ }
242
+ };
243
+ }
244
+ function createMppPaymentGate(config) {
245
+ try {
246
+ mppMiddlewareConfigSchema.parse(config);
247
+ } catch (cause) {
248
+ throw new MppConfigError(
249
+ "Invalid MPP middleware config \u2014 check your routes, facilitator URL, payTo and currency addresses",
250
+ cause
251
+ );
252
+ }
253
+ const client = new MppFacilitatorClient(config.facilitator);
254
+ const requestByRoute = new Map(
255
+ Object.values(config.routes).map((route) => [
256
+ route,
257
+ buildChargeRequestB64(route)
258
+ ])
259
+ );
260
+ return async (adapter) => {
261
+ if (adapter.getMethod().toUpperCase() === "OPTIONS")
262
+ return { type: "pass" };
263
+ const route = matchRoute(adapter.getPath(), config.routes);
264
+ if (!route) return { type: "pass" };
265
+ const request = requestByRoute.get(route);
266
+ const credential = parseAuthorizationHeader(
267
+ adapter.getHeader("authorization")
268
+ );
269
+ if (!credential) {
270
+ return { type: "error", result: challengeResponse(route, request) };
271
+ }
272
+ if (credential.challenge.method !== (route.method ?? "evm")) {
273
+ return {
274
+ type: "error",
275
+ result: challengeResponse(
276
+ route,
277
+ request,
278
+ MPP_ERROR_CODES.METHOD_MISMATCH
279
+ )
280
+ };
281
+ }
282
+ try {
283
+ await client.verify(credential);
284
+ } catch (e) {
285
+ const code = e instanceof MppRuntimeError ? e.code : MPP_ERROR_CODES.FACILITATOR_UNAVAILABLE;
286
+ return {
287
+ type: "error",
288
+ result: challengeResponse(route, request, code)
289
+ };
290
+ }
291
+ return {
292
+ type: "verified",
293
+ settle: () => client.settle(credential)
294
+ };
295
+ };
296
+ }
297
+ function createMppPaymentMiddleware(config) {
298
+ const gate = createMppPaymentGate(config);
299
+ return async (adapter) => {
300
+ const gateResult = await gate(adapter);
301
+ if (gateResult.type === "pass") return { pass: true };
302
+ if (gateResult.type === "error") return gateResult.result;
303
+ let handlerResult;
304
+ try {
305
+ handlerResult = await adapter.runHandler();
306
+ } catch {
307
+ return { pass: true, handlerRan: true };
308
+ }
309
+ if (isSuccessStatus(handlerResult.status)) {
310
+ let settled = false;
311
+ try {
312
+ const settleResponse = await gateResult.settle();
313
+ if (settleResponse.success) {
314
+ handlerResult.setResponseHeader(
315
+ "Payment-Receipt",
316
+ encodeBase64Url(settleResponse.receipt)
317
+ );
318
+ handlerResult.setResponseHeader(
319
+ "access-control-expose-headers",
320
+ "Payment-Receipt"
321
+ );
322
+ settled = true;
323
+ }
324
+ } catch {
325
+ }
326
+ if (!settled) {
327
+ handlerResult.discardResponse();
328
+ return {
329
+ status: 402,
330
+ headers: { "Content-Type": "application/json" },
331
+ body: SETTLE_FAILURE_BODY
332
+ };
333
+ }
334
+ }
335
+ handlerResult.sendResponse();
336
+ return { pass: true, handlerRan: true };
337
+ };
338
+ }
339
+
340
+ export { MPP_ERROR_CODES, MppConfigError, MppFacilitatorClient, MppRuntimeError, SETTLE_FAILURE_BODY, buildChallenge, chainIdFromSlug, createMppPaymentGate, createMppPaymentMiddleware, isSuccessStatus, matchRoute, parseAuthorizationHeader, parsePrice, serializeChallenge };
@@ -0,0 +1,355 @@
1
+ 'use strict';
2
+
3
+ var chains = require('@parallel-protocol/chains');
4
+ var mppTypes = require('@parallel-protocol/mpp-types');
5
+ var zod = require('zod');
6
+
7
+ // src/errors.ts
8
+ var MPP_ERROR_CODES = {
9
+ FACILITATOR_UNAVAILABLE: "FACILITATOR_UNAVAILABLE",
10
+ FACILITATOR_INVALID_RESPONSE: "FACILITATOR_INVALID_RESPONSE",
11
+ INVALID_CREDENTIAL: "INVALID_CREDENTIAL",
12
+ METHOD_MISMATCH: "METHOD_MISMATCH"
13
+ };
14
+ var MppConfigError = class extends Error {
15
+ constructor(message, cause) {
16
+ super(message);
17
+ this.name = "MppConfigError";
18
+ this.cause = cause;
19
+ }
20
+ };
21
+ var MppRuntimeError = class extends Error {
22
+ constructor(code) {
23
+ super(code);
24
+ this.code = code;
25
+ this.name = "MppRuntimeError";
26
+ }
27
+ code;
28
+ };
29
+ function parseDecimalAmount(amount, decimals) {
30
+ const multiplier = 10n ** BigInt(decimals);
31
+ const parts = amount.split(".");
32
+ const whole = parts[0] ?? "0";
33
+ const fraction = (parts[1] ?? "").padEnd(decimals, "0").slice(0, decimals);
34
+ return BigInt(whole) * multiplier + BigInt(fraction || "0");
35
+ }
36
+ function parsePrice(price, decimals = 6) {
37
+ if (typeof price === "bigint") return price;
38
+ const match = price.match(/^(\d+(?:\.\d+)?)$/);
39
+ if (match?.[1]) return parseDecimalAmount(match[1], decimals);
40
+ throw new Error(
41
+ `Invalid price format: "${price}". Expected "0.01" or a bigint.`
42
+ );
43
+ }
44
+ var CHAIN_ID_BY_SLUG = new Map(
45
+ chains.CHAIN_NAMES.map((c) => [c, chains.CHAINS_STATIC[c].viem.id])
46
+ );
47
+ function chainIdFromSlug(network) {
48
+ return CHAIN_ID_BY_SLUG.get(network);
49
+ }
50
+ function isSuccessStatus(status) {
51
+ return status >= 200 && status < 300;
52
+ }
53
+ var SETTLE_FAILURE_BODY = {
54
+ error: "PAYMENT_SETTLEMENT_FAILED",
55
+ message: "Payment could not be confirmed after multiple attempts. Please retry."
56
+ };
57
+ function matchRoute(path, routes) {
58
+ if (routes[path]) return routes[path];
59
+ let bestMatch;
60
+ let bestLength = 0;
61
+ for (const [pattern, config] of Object.entries(routes)) {
62
+ if (path.startsWith(`${pattern}/`) && pattern.length > bestLength) {
63
+ bestMatch = config;
64
+ bestLength = pattern.length;
65
+ }
66
+ }
67
+ return bestMatch;
68
+ }
69
+ var DEFAULT_TIMEOUT_SECONDS = 300;
70
+ function challengeId() {
71
+ return globalThis.crypto.randomUUID();
72
+ }
73
+ function buildChargeRequest(route, chainId) {
74
+ return {
75
+ amount: parsePrice(route.price, route.decimals).toString(),
76
+ currency: route.currency,
77
+ recipient: route.payTo,
78
+ ...route.description ? { description: route.description } : {},
79
+ methodDetails: { chainId }
80
+ };
81
+ }
82
+ function buildChargeRequestB64(route) {
83
+ const chainId = chainIdFromSlug(route.network);
84
+ if (chainId === void 0) {
85
+ throw new MppConfigError(
86
+ `Unknown network "${route.network}" \u2014 not in the @parallel-protocol/chains catalogue`
87
+ );
88
+ }
89
+ return mppTypes.encodeBase64Url(buildChargeRequest(route, chainId));
90
+ }
91
+ function buildChallenge(route, request = buildChargeRequestB64(route)) {
92
+ const ttl = (route.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) * 1e3;
93
+ return {
94
+ id: challengeId(),
95
+ ...route.realm ? { realm: route.realm } : {},
96
+ method: route.method ?? "evm",
97
+ intent: "charge",
98
+ request,
99
+ expires: new Date(Date.now() + ttl).toISOString(),
100
+ ...route.description ? { description: route.description } : {}
101
+ };
102
+ }
103
+ function quote(value) {
104
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
105
+ }
106
+ function serializeChallenge(challenge) {
107
+ const params = [
108
+ ["realm", challenge.realm],
109
+ ["id", challenge.id],
110
+ ["method", challenge.method],
111
+ ["intent", challenge.intent],
112
+ ["request", challenge.request],
113
+ ["digest", challenge.digest],
114
+ ["expires", challenge.expires],
115
+ ["description", challenge.description],
116
+ ["opaque", challenge.opaque]
117
+ ];
118
+ const serialized = params.filter((p) => p[1] !== void 0).map(([k, v]) => `${k}=${quote(v)}`).join(", ");
119
+ return `Payment ${serialized}`;
120
+ }
121
+ function parseAuthorizationHeader(header) {
122
+ if (!header) return null;
123
+ const match = header.match(/^Payment\s+(.+)$/i);
124
+ if (!match?.[1]) return null;
125
+ let decoded;
126
+ try {
127
+ decoded = JSON.parse(
128
+ Buffer.from(match[1].trim(), "base64url").toString("utf8")
129
+ );
130
+ } catch {
131
+ return null;
132
+ }
133
+ const parsed = mppTypes.MppChargeCredentialSchema.safeParse(decoded);
134
+ return parsed.success ? parsed.data : null;
135
+ }
136
+ var FACILITATOR_TIMEOUT_MS = 1e4;
137
+ var MppFacilitatorClient = class {
138
+ url;
139
+ headers;
140
+ constructor(config) {
141
+ this.url = config.url;
142
+ this.headers = {
143
+ "Content-Type": "application/json",
144
+ ...config.apiKey ? { "X-API-Key": config.apiKey } : {}
145
+ };
146
+ }
147
+ /** Verify a credential (phase 1, no on-chain tx). Throws {@link MppRuntimeError} on failure. */
148
+ async verify(credential) {
149
+ let json;
150
+ try {
151
+ const response = await fetch(`${this.url}/mpp/verify`, {
152
+ method: "POST",
153
+ headers: this.headers,
154
+ body: JSON.stringify({ credential }),
155
+ signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
156
+ });
157
+ if (!response.ok) {
158
+ const errorBody = await response.json().catch(() => null);
159
+ const code = errorBody?.error ?? MPP_ERROR_CODES.FACILITATOR_UNAVAILABLE;
160
+ throw new MppRuntimeError(code);
161
+ }
162
+ json = await response.json();
163
+ } catch (e) {
164
+ if (e instanceof MppRuntimeError) throw e;
165
+ throw new MppRuntimeError(MPP_ERROR_CODES.FACILITATOR_UNAVAILABLE);
166
+ }
167
+ try {
168
+ mppTypes.MppVerifyResponseSchema.parse(json);
169
+ } catch {
170
+ throw new MppRuntimeError(MPP_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
171
+ }
172
+ }
173
+ /** Settle a credential (phase 2, on-chain). Returns the receipt or a failure. */
174
+ async settle(credential) {
175
+ let json;
176
+ try {
177
+ const response = await fetch(`${this.url}/mpp/settle`, {
178
+ method: "POST",
179
+ headers: this.headers,
180
+ body: JSON.stringify({ credential }),
181
+ signal: AbortSignal.timeout(FACILITATOR_TIMEOUT_MS)
182
+ });
183
+ if (!response.ok) {
184
+ const errorBody = await response.json().catch(() => null);
185
+ return {
186
+ success: false,
187
+ error: {
188
+ code: errorBody?.error ?? "SETTLEMENT_FAILED",
189
+ message: errorBody?.message ?? "Settlement failed"
190
+ }
191
+ };
192
+ }
193
+ json = await response.json();
194
+ } catch (e) {
195
+ if (e instanceof MppRuntimeError) throw e;
196
+ throw new MppRuntimeError(MPP_ERROR_CODES.FACILITATOR_UNAVAILABLE);
197
+ }
198
+ const parsed = mppTypes.MppReceiptSchema.safeParse(json);
199
+ if (!parsed.success) {
200
+ throw new MppRuntimeError(MPP_ERROR_CODES.FACILITATOR_INVALID_RESPONSE);
201
+ }
202
+ return { success: true, receipt: parsed.data };
203
+ }
204
+ };
205
+ var ethereumAddressSchema = zod.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "Invalid Ethereum address");
206
+ var priceSchema = zod.z.union([
207
+ zod.z.bigint().positive("Price must be greater than 0"),
208
+ zod.z.string().regex(/^\d+(?:\.\d+)?$/, 'Price must be in format "0.01"').refine((price) => Number(price) > 0, "Price must be greater than 0")
209
+ ]);
210
+ var facilitatorConfigSchema = zod.z.object({
211
+ url: zod.z.string().refine((v) => URL.canParse(v), "Invalid facilitator URL"),
212
+ apiKey: zod.z.string().optional()
213
+ });
214
+ var routeConfigSchema = zod.z.object({
215
+ price: priceSchema,
216
+ decimals: zod.z.number().int().nonnegative().optional(),
217
+ network: zod.z.string().min(1, "Network is required"),
218
+ payTo: ethereumAddressSchema,
219
+ currency: ethereumAddressSchema,
220
+ method: zod.z.enum(["evm", "parallel"]).optional(),
221
+ maxTimeoutSeconds: zod.z.number().int().positive().optional(),
222
+ description: zod.z.string().optional(),
223
+ realm: zod.z.string().optional()
224
+ });
225
+ var mppMiddlewareConfigSchema = zod.z.object({
226
+ facilitator: facilitatorConfigSchema,
227
+ routes: zod.z.record(zod.z.string(), routeConfigSchema).refine((r) => Object.keys(r).length > 0, "At least one route is required")
228
+ });
229
+
230
+ // src/middleware.ts
231
+ function challengeResponse(route, request, error) {
232
+ const challenge = buildChallenge(route, request);
233
+ return {
234
+ status: 402,
235
+ headers: {
236
+ "Content-Type": "application/json",
237
+ "WWW-Authenticate": serializeChallenge(challenge),
238
+ "access-control-expose-headers": "WWW-Authenticate"
239
+ },
240
+ body: {
241
+ ...error ? { error } : {},
242
+ challenge
243
+ }
244
+ };
245
+ }
246
+ function createMppPaymentGate(config) {
247
+ try {
248
+ mppMiddlewareConfigSchema.parse(config);
249
+ } catch (cause) {
250
+ throw new MppConfigError(
251
+ "Invalid MPP middleware config \u2014 check your routes, facilitator URL, payTo and currency addresses",
252
+ cause
253
+ );
254
+ }
255
+ const client = new MppFacilitatorClient(config.facilitator);
256
+ const requestByRoute = new Map(
257
+ Object.values(config.routes).map((route) => [
258
+ route,
259
+ buildChargeRequestB64(route)
260
+ ])
261
+ );
262
+ return async (adapter) => {
263
+ if (adapter.getMethod().toUpperCase() === "OPTIONS")
264
+ return { type: "pass" };
265
+ const route = matchRoute(adapter.getPath(), config.routes);
266
+ if (!route) return { type: "pass" };
267
+ const request = requestByRoute.get(route);
268
+ const credential = parseAuthorizationHeader(
269
+ adapter.getHeader("authorization")
270
+ );
271
+ if (!credential) {
272
+ return { type: "error", result: challengeResponse(route, request) };
273
+ }
274
+ if (credential.challenge.method !== (route.method ?? "evm")) {
275
+ return {
276
+ type: "error",
277
+ result: challengeResponse(
278
+ route,
279
+ request,
280
+ MPP_ERROR_CODES.METHOD_MISMATCH
281
+ )
282
+ };
283
+ }
284
+ try {
285
+ await client.verify(credential);
286
+ } catch (e) {
287
+ const code = e instanceof MppRuntimeError ? e.code : MPP_ERROR_CODES.FACILITATOR_UNAVAILABLE;
288
+ return {
289
+ type: "error",
290
+ result: challengeResponse(route, request, code)
291
+ };
292
+ }
293
+ return {
294
+ type: "verified",
295
+ settle: () => client.settle(credential)
296
+ };
297
+ };
298
+ }
299
+ function createMppPaymentMiddleware(config) {
300
+ const gate = createMppPaymentGate(config);
301
+ return async (adapter) => {
302
+ const gateResult = await gate(adapter);
303
+ if (gateResult.type === "pass") return { pass: true };
304
+ if (gateResult.type === "error") return gateResult.result;
305
+ let handlerResult;
306
+ try {
307
+ handlerResult = await adapter.runHandler();
308
+ } catch {
309
+ return { pass: true, handlerRan: true };
310
+ }
311
+ if (isSuccessStatus(handlerResult.status)) {
312
+ let settled = false;
313
+ try {
314
+ const settleResponse = await gateResult.settle();
315
+ if (settleResponse.success) {
316
+ handlerResult.setResponseHeader(
317
+ "Payment-Receipt",
318
+ mppTypes.encodeBase64Url(settleResponse.receipt)
319
+ );
320
+ handlerResult.setResponseHeader(
321
+ "access-control-expose-headers",
322
+ "Payment-Receipt"
323
+ );
324
+ settled = true;
325
+ }
326
+ } catch {
327
+ }
328
+ if (!settled) {
329
+ handlerResult.discardResponse();
330
+ return {
331
+ status: 402,
332
+ headers: { "Content-Type": "application/json" },
333
+ body: SETTLE_FAILURE_BODY
334
+ };
335
+ }
336
+ }
337
+ handlerResult.sendResponse();
338
+ return { pass: true, handlerRan: true };
339
+ };
340
+ }
341
+
342
+ exports.MPP_ERROR_CODES = MPP_ERROR_CODES;
343
+ exports.MppConfigError = MppConfigError;
344
+ exports.MppFacilitatorClient = MppFacilitatorClient;
345
+ exports.MppRuntimeError = MppRuntimeError;
346
+ exports.SETTLE_FAILURE_BODY = SETTLE_FAILURE_BODY;
347
+ exports.buildChallenge = buildChallenge;
348
+ exports.chainIdFromSlug = chainIdFromSlug;
349
+ exports.createMppPaymentGate = createMppPaymentGate;
350
+ exports.createMppPaymentMiddleware = createMppPaymentMiddleware;
351
+ exports.isSuccessStatus = isSuccessStatus;
352
+ exports.matchRoute = matchRoute;
353
+ exports.parseAuthorizationHeader = parseAuthorizationHeader;
354
+ exports.parsePrice = parsePrice;
355
+ exports.serializeChallenge = serializeChallenge;