@solvapay/server 1.0.0-preview.9 → 1.0.1-preview.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.
- package/LICENSE.md +21 -0
- package/README.md +87 -46
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/edge.d.ts +2598 -329
- package/dist/edge.js +1006 -296
- package/dist/index.cjs +3601 -2757
- package/dist/index.d.cts +2740 -382
- package/dist/index.d.ts +2740 -382
- package/dist/index.js +1063 -295
- package/dist/webapi-K5XBCEO6.js +3775 -0
- package/package.json +18 -13
- package/dist/chunk-R5U7XKVJ.js +0 -16
- package/dist/esm-5GYCIXIY.js +0 -3475
package/dist/index.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
__require
|
|
3
|
-
} from "./chunk-R5U7XKVJ.js";
|
|
1
|
+
import "./chunk-MLKGABMK.js";
|
|
4
2
|
|
|
5
3
|
// src/index.ts
|
|
6
4
|
import crypto from "crypto";
|
|
7
|
-
import { SolvaPayError as
|
|
5
|
+
import { SolvaPayError as SolvaPayError5 } from "@solvapay/core";
|
|
8
6
|
|
|
9
7
|
// src/client.ts
|
|
10
8
|
import { SolvaPayError } from "@solvapay/core";
|
|
11
9
|
function createSolvaPayClient(opts) {
|
|
12
|
-
const base = opts.apiBaseUrl ?? "https://api
|
|
10
|
+
const base = opts.apiBaseUrl ?? "https://api.solvapay.com";
|
|
13
11
|
if (!opts.apiKey) throw new SolvaPayError("Missing apiKey");
|
|
14
12
|
const headers = {
|
|
15
13
|
"Content-Type": "application/json",
|
|
16
|
-
|
|
14
|
+
Authorization: `Bearer ${opts.apiKey}`
|
|
17
15
|
};
|
|
18
16
|
const debug = process.env.SOLVAPAY_DEBUG === "true";
|
|
19
17
|
const log = (...args) => {
|
|
@@ -41,10 +39,12 @@ function createSolvaPayClient(opts) {
|
|
|
41
39
|
// POST: /v1/sdk/usages
|
|
42
40
|
async trackUsage(params) {
|
|
43
41
|
const url = `${base}/v1/sdk/usages`;
|
|
42
|
+
const { customerRef, ...rest } = params;
|
|
43
|
+
const body = { ...rest, customerId: customerRef };
|
|
44
44
|
const res = await fetch(url, {
|
|
45
45
|
method: "POST",
|
|
46
46
|
headers,
|
|
47
|
-
body: JSON.stringify(
|
|
47
|
+
body: JSON.stringify(body)
|
|
48
48
|
});
|
|
49
49
|
if (!res.ok) {
|
|
50
50
|
const error = await res.text();
|
|
@@ -68,9 +68,22 @@ function createSolvaPayClient(opts) {
|
|
|
68
68
|
const result = await res.json();
|
|
69
69
|
return result;
|
|
70
70
|
},
|
|
71
|
-
// GET: /v1/sdk/customers/{reference}
|
|
71
|
+
// GET: /v1/sdk/customers/{reference} or /v1/sdk/customers?externalRef={externalRef}|email={email}
|
|
72
72
|
async getCustomer(params) {
|
|
73
|
-
|
|
73
|
+
let url;
|
|
74
|
+
let isByExternalRef = false;
|
|
75
|
+
let isByEmail = false;
|
|
76
|
+
if (params.externalRef) {
|
|
77
|
+
url = `${base}/v1/sdk/customers?externalRef=${encodeURIComponent(params.externalRef)}`;
|
|
78
|
+
isByExternalRef = true;
|
|
79
|
+
} else if (params.email) {
|
|
80
|
+
url = `${base}/v1/sdk/customers?email=${encodeURIComponent(params.email)}`;
|
|
81
|
+
isByEmail = true;
|
|
82
|
+
} else if (params.customerRef) {
|
|
83
|
+
url = `${base}/v1/sdk/customers/${params.customerRef}`;
|
|
84
|
+
} else {
|
|
85
|
+
throw new SolvaPayError("One of customerRef, externalRef, or email must be provided");
|
|
86
|
+
}
|
|
74
87
|
const res = await fetch(url, {
|
|
75
88
|
method: "GET",
|
|
76
89
|
headers
|
|
@@ -81,40 +94,28 @@ function createSolvaPayClient(opts) {
|
|
|
81
94
|
throw new SolvaPayError(`Get customer failed (${res.status}): ${error}`);
|
|
82
95
|
}
|
|
83
96
|
const result = await res.json();
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
async getCustomerByExternalRef(params) {
|
|
94
|
-
const url = `${base}/v1/sdk/customers?externalRef=${encodeURIComponent(params.externalRef)}`;
|
|
95
|
-
const res = await fetch(url, {
|
|
96
|
-
method: "GET",
|
|
97
|
-
headers
|
|
98
|
-
});
|
|
99
|
-
if (!res.ok) {
|
|
100
|
-
const error = await res.text();
|
|
101
|
-
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
102
|
-
throw new SolvaPayError(`Get customer by externalRef failed (${res.status}): ${error}`);
|
|
97
|
+
let customer = result;
|
|
98
|
+
if (isByExternalRef || isByEmail) {
|
|
99
|
+
const directCustomer = result && typeof result === "object" && (result.reference || result.customerRef || result.externalRef) ? result : void 0;
|
|
100
|
+
const wrappedCustomer = result && typeof result === "object" && result.customer ? result.customer : void 0;
|
|
101
|
+
const customers = Array.isArray(result) ? result : result && typeof result === "object" && Array.isArray(result.customers) ? result.customers : [];
|
|
102
|
+
customer = directCustomer || wrappedCustomer || customers[0];
|
|
103
|
+
if (!customer) {
|
|
104
|
+
throw new SolvaPayError(`No customer found with externalRef: ${params.externalRef}`);
|
|
105
|
+
}
|
|
103
106
|
}
|
|
104
|
-
const result = await res.json();
|
|
105
|
-
const customer = Array.isArray(result) ? result[0] : result;
|
|
106
107
|
return {
|
|
107
108
|
customerRef: customer.reference || customer.customerRef,
|
|
108
109
|
email: customer.email,
|
|
109
110
|
name: customer.name,
|
|
110
111
|
externalRef: customer.externalRef,
|
|
111
|
-
|
|
112
|
+
purchases: customer.purchases || []
|
|
112
113
|
};
|
|
113
114
|
},
|
|
114
|
-
//
|
|
115
|
-
// GET: /v1/sdk/
|
|
116
|
-
async
|
|
117
|
-
const url = `${base}/v1/sdk/
|
|
115
|
+
// Product management methods (primarily for integration tests)
|
|
116
|
+
// GET: /v1/sdk/products
|
|
117
|
+
async listProducts() {
|
|
118
|
+
const url = `${base}/v1/sdk/products`;
|
|
118
119
|
const res = await fetch(url, {
|
|
119
120
|
method: "GET",
|
|
120
121
|
headers
|
|
@@ -122,18 +123,18 @@ function createSolvaPayClient(opts) {
|
|
|
122
123
|
if (!res.ok) {
|
|
123
124
|
const error = await res.text();
|
|
124
125
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
125
|
-
throw new SolvaPayError(`List
|
|
126
|
+
throw new SolvaPayError(`List products failed (${res.status}): ${error}`);
|
|
126
127
|
}
|
|
127
128
|
const result = await res.json();
|
|
128
|
-
const
|
|
129
|
-
return
|
|
130
|
-
...
|
|
131
|
-
...
|
|
129
|
+
const products = Array.isArray(result) ? result : result.products || [];
|
|
130
|
+
return products.map((product) => ({
|
|
131
|
+
...product,
|
|
132
|
+
...product.data || {}
|
|
132
133
|
}));
|
|
133
134
|
},
|
|
134
|
-
// POST: /v1/sdk/
|
|
135
|
-
async
|
|
136
|
-
const url = `${base}/v1/sdk/
|
|
135
|
+
// POST: /v1/sdk/products
|
|
136
|
+
async createProduct(params) {
|
|
137
|
+
const url = `${base}/v1/sdk/products`;
|
|
137
138
|
const res = await fetch(url, {
|
|
138
139
|
method: "POST",
|
|
139
140
|
headers,
|
|
@@ -142,14 +143,29 @@ function createSolvaPayClient(opts) {
|
|
|
142
143
|
if (!res.ok) {
|
|
143
144
|
const error = await res.text();
|
|
144
145
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
145
|
-
throw new SolvaPayError(`Create
|
|
146
|
+
throw new SolvaPayError(`Create product failed (${res.status}): ${error}`);
|
|
146
147
|
}
|
|
147
148
|
const result = await res.json();
|
|
148
149
|
return result;
|
|
149
150
|
},
|
|
150
|
-
//
|
|
151
|
-
async
|
|
152
|
-
const url = `${base}/v1/sdk/
|
|
151
|
+
// POST: /v1/sdk/products/mcp/bootstrap
|
|
152
|
+
async bootstrapMcpProduct(params) {
|
|
153
|
+
const url = `${base}/v1/sdk/products/mcp/bootstrap`;
|
|
154
|
+
const res = await fetch(url, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers,
|
|
157
|
+
body: JSON.stringify(params)
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const error = await res.text();
|
|
161
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
162
|
+
throw new SolvaPayError(`Bootstrap MCP product failed (${res.status}): ${error}`);
|
|
163
|
+
}
|
|
164
|
+
return await res.json();
|
|
165
|
+
},
|
|
166
|
+
// DELETE: /v1/sdk/products/{productRef}
|
|
167
|
+
async deleteProduct(productRef) {
|
|
168
|
+
const url = `${base}/v1/sdk/products/${productRef}`;
|
|
153
169
|
const res = await fetch(url, {
|
|
154
170
|
method: "DELETE",
|
|
155
171
|
headers
|
|
@@ -157,12 +173,27 @@ function createSolvaPayClient(opts) {
|
|
|
157
173
|
if (!res.ok && res.status !== 404) {
|
|
158
174
|
const error = await res.text();
|
|
159
175
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
160
|
-
throw new SolvaPayError(`Delete
|
|
176
|
+
throw new SolvaPayError(`Delete product failed (${res.status}): ${error}`);
|
|
161
177
|
}
|
|
162
178
|
},
|
|
163
|
-
//
|
|
164
|
-
async
|
|
165
|
-
const url = `${base}/v1/sdk/
|
|
179
|
+
// POST: /v1/sdk/products/{productRef}/clone
|
|
180
|
+
async cloneProduct(productRef, overrides) {
|
|
181
|
+
const url = `${base}/v1/sdk/products/${productRef}/clone`;
|
|
182
|
+
const res = await fetch(url, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers,
|
|
185
|
+
body: JSON.stringify(overrides || {})
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
const error = await res.text();
|
|
189
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
190
|
+
throw new SolvaPayError(`Clone product failed (${res.status}): ${error}`);
|
|
191
|
+
}
|
|
192
|
+
return await res.json();
|
|
193
|
+
},
|
|
194
|
+
// GET: /v1/sdk/products/{productRef}/plans
|
|
195
|
+
async listPlans(productRef) {
|
|
196
|
+
const url = `${base}/v1/sdk/products/${productRef}/plans`;
|
|
166
197
|
const res = await fetch(url, {
|
|
167
198
|
method: "GET",
|
|
168
199
|
headers
|
|
@@ -175,20 +206,20 @@ function createSolvaPayClient(opts) {
|
|
|
175
206
|
const result = await res.json();
|
|
176
207
|
const plans = Array.isArray(result) ? result : result.plans || [];
|
|
177
208
|
return plans.map((plan) => {
|
|
178
|
-
const
|
|
209
|
+
const data = plan.data || {};
|
|
210
|
+
const price = plan.price ?? data.price;
|
|
179
211
|
const unwrapped = {
|
|
180
|
-
...
|
|
212
|
+
...data,
|
|
181
213
|
...plan,
|
|
182
|
-
// Explicitly preserve price field to ensure it's not lost
|
|
183
214
|
...price !== void 0 && { price }
|
|
184
215
|
};
|
|
185
216
|
delete unwrapped.data;
|
|
186
217
|
return unwrapped;
|
|
187
218
|
});
|
|
188
219
|
},
|
|
189
|
-
// POST: /v1/sdk/
|
|
220
|
+
// POST: /v1/sdk/products/{productRef}/plans
|
|
190
221
|
async createPlan(params) {
|
|
191
|
-
const url = `${base}/v1/sdk/
|
|
222
|
+
const url = `${base}/v1/sdk/products/${params.productRef}/plans`;
|
|
192
223
|
const res = await fetch(url, {
|
|
193
224
|
method: "POST",
|
|
194
225
|
headers,
|
|
@@ -202,9 +233,24 @@ function createSolvaPayClient(opts) {
|
|
|
202
233
|
const result = await res.json();
|
|
203
234
|
return result;
|
|
204
235
|
},
|
|
205
|
-
//
|
|
206
|
-
async
|
|
207
|
-
const url = `${base}/v1/sdk/
|
|
236
|
+
// PUT: /v1/sdk/products/{productRef}/plans/{planRef}
|
|
237
|
+
async updatePlan(productRef, planRef, params) {
|
|
238
|
+
const url = `${base}/v1/sdk/products/${productRef}/plans/${planRef}`;
|
|
239
|
+
const res = await fetch(url, {
|
|
240
|
+
method: "PUT",
|
|
241
|
+
headers,
|
|
242
|
+
body: JSON.stringify(params)
|
|
243
|
+
});
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
const error = await res.text();
|
|
246
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
247
|
+
throw new SolvaPayError(`Update plan failed (${res.status}): ${error}`);
|
|
248
|
+
}
|
|
249
|
+
return await res.json();
|
|
250
|
+
},
|
|
251
|
+
// DELETE: /v1/sdk/products/{productRef}/plans/{planRef}
|
|
252
|
+
async deletePlan(productRef, planRef) {
|
|
253
|
+
const url = `${base}/v1/sdk/products/${productRef}/plans/${planRef}`;
|
|
208
254
|
const res = await fetch(url, {
|
|
209
255
|
method: "DELETE",
|
|
210
256
|
headers
|
|
@@ -226,7 +272,7 @@ function createSolvaPayClient(opts) {
|
|
|
226
272
|
"Idempotency-Key": idempotencyKey
|
|
227
273
|
},
|
|
228
274
|
body: JSON.stringify({
|
|
229
|
-
|
|
275
|
+
productRef: params.productRef,
|
|
230
276
|
planRef: params.planRef,
|
|
231
277
|
customerReference: params.customerRef
|
|
232
278
|
})
|
|
@@ -240,7 +286,7 @@ function createSolvaPayClient(opts) {
|
|
|
240
286
|
return result;
|
|
241
287
|
},
|
|
242
288
|
// POST: /v1/sdk/payment-intents/{paymentIntentId}/process
|
|
243
|
-
async
|
|
289
|
+
async processPaymentIntent(params) {
|
|
244
290
|
const url = `${base}/v1/sdk/payment-intents/${params.paymentIntentId}/process`;
|
|
245
291
|
const res = await fetch(url, {
|
|
246
292
|
method: "POST",
|
|
@@ -249,7 +295,7 @@ function createSolvaPayClient(opts) {
|
|
|
249
295
|
"Content-Type": "application/json"
|
|
250
296
|
},
|
|
251
297
|
body: JSON.stringify({
|
|
252
|
-
|
|
298
|
+
productRef: params.productRef,
|
|
253
299
|
customerRef: params.customerRef,
|
|
254
300
|
planRef: params.planRef
|
|
255
301
|
})
|
|
@@ -262,9 +308,9 @@ function createSolvaPayClient(opts) {
|
|
|
262
308
|
const result = await res.json();
|
|
263
309
|
return result;
|
|
264
310
|
},
|
|
265
|
-
// POST: /v1/sdk/
|
|
266
|
-
async
|
|
267
|
-
const url = `${base}/v1/sdk/
|
|
311
|
+
// POST: /v1/sdk/purchases/{purchaseRef}/cancel
|
|
312
|
+
async cancelPurchase(params) {
|
|
313
|
+
const url = `${base}/v1/sdk/purchases/${params.purchaseRef}/cancel`;
|
|
268
314
|
const requestOptions = {
|
|
269
315
|
method: "POST",
|
|
270
316
|
headers
|
|
@@ -277,12 +323,14 @@ function createSolvaPayClient(opts) {
|
|
|
277
323
|
const error = await res.text();
|
|
278
324
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
279
325
|
if (res.status === 404) {
|
|
280
|
-
throw new SolvaPayError(`
|
|
326
|
+
throw new SolvaPayError(`Purchase not found: ${error}`);
|
|
281
327
|
}
|
|
282
328
|
if (res.status === 400) {
|
|
283
|
-
throw new SolvaPayError(
|
|
329
|
+
throw new SolvaPayError(
|
|
330
|
+
`Purchase cannot be cancelled or does not belong to provider: ${error}`
|
|
331
|
+
);
|
|
284
332
|
}
|
|
285
|
-
throw new SolvaPayError(`Cancel
|
|
333
|
+
throw new SolvaPayError(`Cancel purchase failed (${res.status}): ${error}`);
|
|
286
334
|
}
|
|
287
335
|
const responseText = await res.text();
|
|
288
336
|
let responseData;
|
|
@@ -290,26 +338,43 @@ function createSolvaPayClient(opts) {
|
|
|
290
338
|
responseData = JSON.parse(responseText);
|
|
291
339
|
} catch (parseError) {
|
|
292
340
|
log(`\u274C Failed to parse response as JSON: ${parseError}`);
|
|
293
|
-
throw new SolvaPayError(
|
|
341
|
+
throw new SolvaPayError(
|
|
342
|
+
`Invalid JSON response from cancel purchase endpoint: ${responseText.substring(0, 200)}`
|
|
343
|
+
);
|
|
294
344
|
}
|
|
295
345
|
if (!responseData || typeof responseData !== "object") {
|
|
296
346
|
log(`\u274C Invalid response structure: ${JSON.stringify(responseData)}`);
|
|
297
|
-
throw new SolvaPayError(`Invalid response structure from cancel
|
|
347
|
+
throw new SolvaPayError(`Invalid response structure from cancel purchase endpoint`);
|
|
298
348
|
}
|
|
299
349
|
let result;
|
|
300
|
-
if (responseData.
|
|
301
|
-
result = responseData.
|
|
350
|
+
if (responseData.purchase && typeof responseData.purchase === "object") {
|
|
351
|
+
result = responseData.purchase;
|
|
302
352
|
} else if (responseData.reference) {
|
|
303
353
|
result = responseData;
|
|
304
354
|
} else {
|
|
305
|
-
result = responseData.
|
|
355
|
+
result = responseData.purchase || responseData;
|
|
306
356
|
}
|
|
307
357
|
if (!result || typeof result !== "object") {
|
|
308
|
-
log(`\u274C Invalid
|
|
309
|
-
throw new SolvaPayError(`Invalid
|
|
358
|
+
log(`\u274C Invalid purchase data in response. Full response:`, responseData);
|
|
359
|
+
throw new SolvaPayError(`Invalid purchase data in cancel purchase response`);
|
|
310
360
|
}
|
|
311
361
|
return result;
|
|
312
362
|
},
|
|
363
|
+
// POST: /v1/sdk/user-info
|
|
364
|
+
async getUserInfo(params) {
|
|
365
|
+
const url = `${base}/v1/sdk/user-info`;
|
|
366
|
+
const res = await fetch(url, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers,
|
|
369
|
+
body: JSON.stringify(params)
|
|
370
|
+
});
|
|
371
|
+
if (!res.ok) {
|
|
372
|
+
const error = await res.text();
|
|
373
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
374
|
+
throw new SolvaPayError(`Get user info failed (${res.status}): ${error}`);
|
|
375
|
+
}
|
|
376
|
+
return await res.json();
|
|
377
|
+
},
|
|
313
378
|
// POST: /v1/sdk/checkout-sessions
|
|
314
379
|
async createCheckoutSession(params) {
|
|
315
380
|
const url = `${base}/v1/sdk/checkout-sessions`;
|
|
@@ -392,34 +457,35 @@ function sleep(ms) {
|
|
|
392
457
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
393
458
|
}
|
|
394
459
|
function createRequestDeduplicator(options = {}) {
|
|
395
|
-
const {
|
|
396
|
-
cacheTTL = 2e3,
|
|
397
|
-
maxCacheSize = 1e3,
|
|
398
|
-
cacheErrors = true
|
|
399
|
-
} = options;
|
|
460
|
+
const { cacheTTL = 2e3, maxCacheSize = 1e3, cacheErrors = true } = options;
|
|
400
461
|
const inFlightRequests = /* @__PURE__ */ new Map();
|
|
401
462
|
const resultCache = /* @__PURE__ */ new Map();
|
|
402
|
-
let
|
|
463
|
+
let _cleanupInterval = null;
|
|
403
464
|
if (cacheTTL > 0) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
465
|
+
_cleanupInterval = setInterval(
|
|
466
|
+
() => {
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
const entriesToDelete = [];
|
|
469
|
+
for (const [key, cached] of resultCache.entries()) {
|
|
470
|
+
if (now - cached.timestamp >= cacheTTL) {
|
|
471
|
+
entriesToDelete.push(key);
|
|
472
|
+
}
|
|
410
473
|
}
|
|
411
|
-
|
|
412
|
-
for (const key of entriesToDelete) {
|
|
413
|
-
resultCache.delete(key);
|
|
414
|
-
}
|
|
415
|
-
if (resultCache.size > maxCacheSize) {
|
|
416
|
-
const sortedEntries = Array.from(resultCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
417
|
-
const toRemove = sortedEntries.slice(0, resultCache.size - maxCacheSize);
|
|
418
|
-
for (const [key] of toRemove) {
|
|
474
|
+
for (const key of entriesToDelete) {
|
|
419
475
|
resultCache.delete(key);
|
|
420
476
|
}
|
|
421
|
-
|
|
422
|
-
|
|
477
|
+
if (resultCache.size > maxCacheSize) {
|
|
478
|
+
const sortedEntries = Array.from(resultCache.entries()).sort(
|
|
479
|
+
(a, b) => a[1].timestamp - b[1].timestamp
|
|
480
|
+
);
|
|
481
|
+
const toRemove = sortedEntries.slice(0, resultCache.size - maxCacheSize);
|
|
482
|
+
for (const [key] of toRemove) {
|
|
483
|
+
resultCache.delete(key);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
Math.min(cacheTTL, 1e3)
|
|
488
|
+
);
|
|
423
489
|
}
|
|
424
490
|
const deduplicate = async (key, fn) => {
|
|
425
491
|
if (cacheTTL > 0) {
|
|
@@ -483,6 +549,12 @@ function createRequestDeduplicator(options = {}) {
|
|
|
483
549
|
|
|
484
550
|
// src/paywall.ts
|
|
485
551
|
var PaywallError = class extends Error {
|
|
552
|
+
/**
|
|
553
|
+
* Creates a new PaywallError instance.
|
|
554
|
+
*
|
|
555
|
+
* @param message - Error message
|
|
556
|
+
* @param structuredContent - Structured content with checkout URLs and metadata
|
|
557
|
+
*/
|
|
486
558
|
constructor(message, structuredContent) {
|
|
487
559
|
super(message);
|
|
488
560
|
this.structuredContent = structuredContent;
|
|
@@ -490,8 +562,8 @@ var PaywallError = class extends Error {
|
|
|
490
562
|
}
|
|
491
563
|
};
|
|
492
564
|
var sharedCustomerLookupDeduplicator = createRequestDeduplicator({
|
|
493
|
-
cacheTTL:
|
|
494
|
-
// Cache results for
|
|
565
|
+
cacheTTL: 6e4,
|
|
566
|
+
// Cache results for 60 seconds (reduces API calls significantly)
|
|
495
567
|
maxCacheSize: 1e3,
|
|
496
568
|
// Maximum cache entries
|
|
497
569
|
cacheErrors: false
|
|
@@ -501,26 +573,20 @@ var SolvaPayPaywall = class {
|
|
|
501
573
|
constructor(apiClient, options = {}) {
|
|
502
574
|
this.apiClient = apiClient;
|
|
503
575
|
this.debug = options.debug ?? process.env.SOLVAPAY_DEBUG === "true";
|
|
576
|
+
this.limitsCacheTTL = options.limitsCacheTTL ?? 1e4;
|
|
504
577
|
}
|
|
505
578
|
customerCreationAttempts = /* @__PURE__ */ new Set();
|
|
506
579
|
customerRefMapping = /* @__PURE__ */ new Map();
|
|
507
|
-
// input ref -> backend ref
|
|
508
580
|
debug;
|
|
581
|
+
limitsCache = /* @__PURE__ */ new Map();
|
|
582
|
+
limitsCacheTTL;
|
|
509
583
|
log(...args) {
|
|
510
584
|
if (this.debug) {
|
|
511
585
|
console.log(...args);
|
|
512
586
|
}
|
|
513
587
|
}
|
|
514
|
-
|
|
515
|
-
return metadata.
|
|
516
|
-
}
|
|
517
|
-
getPackageJsonName() {
|
|
518
|
-
try {
|
|
519
|
-
const pkg = __require(process.cwd() + "/package.json");
|
|
520
|
-
return pkg.name;
|
|
521
|
-
} catch {
|
|
522
|
-
return void 0;
|
|
523
|
-
}
|
|
588
|
+
resolveProduct(metadata) {
|
|
589
|
+
return metadata.product || process.env.SOLVAPAY_PRODUCT || "default-product";
|
|
524
590
|
}
|
|
525
591
|
generateRequestId() {
|
|
526
592
|
const timestamp = Date.now();
|
|
@@ -530,40 +596,102 @@ var SolvaPayPaywall = class {
|
|
|
530
596
|
/**
|
|
531
597
|
* Core protection method - works for both MCP and HTTP
|
|
532
598
|
*/
|
|
599
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
533
600
|
async protect(handler, metadata = {}, getCustomerRef) {
|
|
534
|
-
const
|
|
535
|
-
const
|
|
601
|
+
const product = this.resolveProduct(metadata);
|
|
602
|
+
const configuredPlanRef = metadata.plan?.trim();
|
|
603
|
+
const usagePlanRef = configuredPlanRef || "unspecified";
|
|
604
|
+
const usageType = metadata.usageType || "requests";
|
|
536
605
|
return async (args) => {
|
|
537
606
|
const startTime = Date.now();
|
|
538
607
|
const requestId = this.generateRequestId();
|
|
539
608
|
const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
|
|
540
|
-
|
|
609
|
+
let backendCustomerRef;
|
|
610
|
+
if (inputCustomerRef.startsWith("cus_")) {
|
|
611
|
+
backendCustomerRef = inputCustomerRef;
|
|
612
|
+
} else {
|
|
613
|
+
backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
|
|
614
|
+
}
|
|
615
|
+
let resolvedMeterName;
|
|
541
616
|
try {
|
|
542
|
-
const
|
|
543
|
-
this.
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (
|
|
617
|
+
const limitsCacheKey = `${backendCustomerRef}:${product}:${configuredPlanRef || ""}:${usageType}`;
|
|
618
|
+
const cachedLimits = this.limitsCache.get(limitsCacheKey);
|
|
619
|
+
const now = Date.now();
|
|
620
|
+
let withinLimits;
|
|
621
|
+
let remaining;
|
|
622
|
+
let checkoutUrl;
|
|
623
|
+
const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
|
|
624
|
+
if (hasFreshCachedLimits) {
|
|
625
|
+
checkoutUrl = cachedLimits.checkoutUrl;
|
|
626
|
+
resolvedMeterName = cachedLimits.meterName;
|
|
627
|
+
if (cachedLimits.remaining > 0) {
|
|
628
|
+
cachedLimits.remaining--;
|
|
629
|
+
if (cachedLimits.remaining <= 0) {
|
|
630
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
631
|
+
}
|
|
632
|
+
withinLimits = true;
|
|
633
|
+
remaining = cachedLimits.remaining;
|
|
634
|
+
} else {
|
|
635
|
+
withinLimits = false;
|
|
636
|
+
remaining = 0;
|
|
637
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
if (cachedLimits) {
|
|
641
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
642
|
+
}
|
|
643
|
+
const limitsCheck = await this.apiClient.checkLimits({
|
|
644
|
+
customerRef: backendCustomerRef,
|
|
645
|
+
productRef: product,
|
|
646
|
+
...configuredPlanRef ? { planRef: configuredPlanRef } : {},
|
|
647
|
+
meterName: usageType
|
|
648
|
+
});
|
|
649
|
+
withinLimits = limitsCheck.withinLimits;
|
|
650
|
+
remaining = limitsCheck.remaining;
|
|
651
|
+
checkoutUrl = limitsCheck.checkoutUrl;
|
|
652
|
+
resolvedMeterName = limitsCheck.meterName;
|
|
653
|
+
const consumedAllowance = withinLimits && remaining > 0;
|
|
654
|
+
if (consumedAllowance) {
|
|
655
|
+
remaining = Math.max(0, remaining - 1);
|
|
656
|
+
}
|
|
657
|
+
if (consumedAllowance) {
|
|
658
|
+
this.limitsCache.set(limitsCacheKey, {
|
|
659
|
+
remaining,
|
|
660
|
+
checkoutUrl,
|
|
661
|
+
meterName: resolvedMeterName,
|
|
662
|
+
timestamp: now
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!withinLimits) {
|
|
550
667
|
const latencyMs2 = Date.now() - startTime;
|
|
551
|
-
this.
|
|
552
|
-
|
|
668
|
+
this.trackUsage(
|
|
669
|
+
backendCustomerRef,
|
|
670
|
+
product,
|
|
671
|
+
usagePlanRef,
|
|
672
|
+
resolvedMeterName || usageType,
|
|
673
|
+
"paywall",
|
|
674
|
+
requestId,
|
|
675
|
+
latencyMs2
|
|
676
|
+
);
|
|
553
677
|
throw new PaywallError("Payment required", {
|
|
554
678
|
kind: "payment_required",
|
|
555
|
-
|
|
556
|
-
checkoutUrl:
|
|
557
|
-
message: `
|
|
679
|
+
product,
|
|
680
|
+
checkoutUrl: checkoutUrl || "",
|
|
681
|
+
message: `Purchase required. Remaining: ${remaining}`
|
|
558
682
|
});
|
|
559
683
|
}
|
|
560
|
-
this.log(`\u26A1 Executing handler: ${toolName}`);
|
|
561
684
|
const result = await handler(args);
|
|
562
|
-
this.log(`\u2713 Handler completed successfully`);
|
|
563
685
|
const latencyMs = Date.now() - startTime;
|
|
564
|
-
this.
|
|
565
|
-
|
|
566
|
-
|
|
686
|
+
this.trackUsage(
|
|
687
|
+
backendCustomerRef,
|
|
688
|
+
product,
|
|
689
|
+
usagePlanRef,
|
|
690
|
+
resolvedMeterName || usageType,
|
|
691
|
+
"success",
|
|
692
|
+
requestId,
|
|
693
|
+
latencyMs
|
|
694
|
+
);
|
|
567
695
|
return result;
|
|
568
696
|
} catch (error) {
|
|
569
697
|
if (error instanceof Error) {
|
|
@@ -572,10 +700,18 @@ var SolvaPayPaywall = class {
|
|
|
572
700
|
} else {
|
|
573
701
|
this.log(`\u274C Error in paywall:`, error);
|
|
574
702
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
703
|
+
if (!(error instanceof PaywallError)) {
|
|
704
|
+
const latencyMs = Date.now() - startTime;
|
|
705
|
+
this.trackUsage(
|
|
706
|
+
backendCustomerRef,
|
|
707
|
+
product,
|
|
708
|
+
usagePlanRef,
|
|
709
|
+
resolvedMeterName || usageType,
|
|
710
|
+
"fail",
|
|
711
|
+
requestId,
|
|
712
|
+
latencyMs
|
|
713
|
+
);
|
|
714
|
+
}
|
|
579
715
|
throw error;
|
|
580
716
|
}
|
|
581
717
|
};
|
|
@@ -585,10 +721,11 @@ var SolvaPayPaywall = class {
|
|
|
585
721
|
* This is a public helper for testing, pre-creating customers, and internal use.
|
|
586
722
|
* Only attempts creation once per customer (idempotent).
|
|
587
723
|
* Returns the backend customer reference to use in API calls.
|
|
588
|
-
*
|
|
589
|
-
* @param customerRef - The customer reference (e.g., Supabase user ID)
|
|
724
|
+
*
|
|
725
|
+
* @param customerRef - The customer reference used as a cache key (e.g., Supabase user ID)
|
|
590
726
|
* @param externalRef - Optional external reference for backend lookup (e.g., Supabase user ID)
|
|
591
|
-
* If provided, will lookup existing customer by externalRef before creating new one
|
|
727
|
+
* If provided, will lookup existing customer by externalRef before creating new one.
|
|
728
|
+
* The externalRef is stored on the SolvaPay backend for customer lookup.
|
|
592
729
|
* @param options - Optional customer details (email, name) for customer creation
|
|
593
730
|
*/
|
|
594
731
|
async ensureCustomer(customerRef, externalRef, options) {
|
|
@@ -598,97 +735,148 @@ var SolvaPayPaywall = class {
|
|
|
598
735
|
if (customerRef === "anonymous") {
|
|
599
736
|
return customerRef;
|
|
600
737
|
}
|
|
738
|
+
if (customerRef.startsWith("cus_")) {
|
|
739
|
+
return customerRef;
|
|
740
|
+
}
|
|
601
741
|
const cacheKey = externalRef || customerRef;
|
|
602
742
|
if (this.customerRefMapping.has(customerRef)) {
|
|
603
743
|
const cached = this.customerRefMapping.get(customerRef);
|
|
604
|
-
this.log(`\u2705 [PER-INSTANCE CACHE HIT] Using cached customer lookup: ${customerRef} -> ${cached}`);
|
|
605
744
|
return cached;
|
|
606
745
|
}
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
this.
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
this.log(`\u2705 Found existing customer by externalRef: ${externalRef} -> ${ref}`);
|
|
618
|
-
this.customerRefMapping.set(customerRef, ref);
|
|
619
|
-
this.customerCreationAttempts.add(customerRef);
|
|
620
|
-
if (externalRef !== customerRef) {
|
|
621
|
-
this.customerCreationAttempts.add(externalRef);
|
|
622
|
-
}
|
|
623
|
-
return ref;
|
|
624
|
-
}
|
|
625
|
-
} catch (error) {
|
|
626
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
627
|
-
if (errorMessage.includes("404") || errorMessage.includes("not found")) {
|
|
628
|
-
this.log(`\u{1F50D} Customer not found by externalRef, will create new: ${externalRef}`);
|
|
629
|
-
} else {
|
|
630
|
-
this.log(`\u26A0\uFE0F Error looking up customer by externalRef: ${errorMessage}`);
|
|
746
|
+
const backendRef = await sharedCustomerLookupDeduplicator.deduplicate(cacheKey, async () => {
|
|
747
|
+
if (externalRef) {
|
|
748
|
+
try {
|
|
749
|
+
const existingCustomer = await this.apiClient.getCustomer({ externalRef });
|
|
750
|
+
if (existingCustomer && existingCustomer.customerRef) {
|
|
751
|
+
const ref = existingCustomer.customerRef;
|
|
752
|
+
this.customerRefMapping.set(customerRef, ref);
|
|
753
|
+
this.customerCreationAttempts.add(customerRef);
|
|
754
|
+
if (externalRef !== customerRef) {
|
|
755
|
+
this.customerCreationAttempts.add(externalRef);
|
|
631
756
|
}
|
|
757
|
+
return ref;
|
|
758
|
+
}
|
|
759
|
+
} catch (error) {
|
|
760
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
761
|
+
if (!errorMessage.includes("404") && !errorMessage.includes("not found")) {
|
|
762
|
+
this.log(`\u26A0\uFE0F Error looking up customer by externalRef: ${errorMessage}`);
|
|
632
763
|
}
|
|
633
764
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
765
|
+
}
|
|
766
|
+
if (this.customerCreationAttempts.has(customerRef) || externalRef && this.customerCreationAttempts.has(externalRef)) {
|
|
767
|
+
const mappedRef = this.customerRefMapping.get(customerRef);
|
|
768
|
+
return mappedRef || customerRef;
|
|
769
|
+
}
|
|
770
|
+
if (!this.apiClient.createCustomer) {
|
|
771
|
+
console.warn(
|
|
772
|
+
`\u26A0\uFE0F Cannot auto-create customer ${customerRef}: createCustomer method not available on API client`
|
|
773
|
+
);
|
|
774
|
+
return customerRef;
|
|
775
|
+
}
|
|
776
|
+
this.customerCreationAttempts.add(customerRef);
|
|
777
|
+
try {
|
|
778
|
+
const createParams = {
|
|
779
|
+
email: options?.email || `${customerRef}-${Date.now()}@auto-created.local`
|
|
780
|
+
};
|
|
781
|
+
if (options?.name) {
|
|
782
|
+
createParams.name = options.name;
|
|
637
783
|
}
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
return customerRef;
|
|
784
|
+
if (externalRef) {
|
|
785
|
+
createParams.externalRef = externalRef;
|
|
641
786
|
}
|
|
642
|
-
this.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
787
|
+
const result = await this.apiClient.createCustomer(createParams);
|
|
788
|
+
const resultObj = result;
|
|
789
|
+
const ref = resultObj.customerRef || resultObj.reference || customerRef;
|
|
790
|
+
this.customerRefMapping.set(customerRef, ref);
|
|
791
|
+
return ref;
|
|
792
|
+
} catch (error) {
|
|
793
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
794
|
+
if (errorMessage.includes("409") || errorMessage.includes("already exists")) {
|
|
649
795
|
if (externalRef) {
|
|
650
|
-
|
|
796
|
+
try {
|
|
797
|
+
const searchResult = await this.apiClient.getCustomer({ externalRef });
|
|
798
|
+
if (searchResult && searchResult.customerRef) {
|
|
799
|
+
this.customerRefMapping.set(customerRef, searchResult.customerRef);
|
|
800
|
+
return searchResult.customerRef;
|
|
801
|
+
}
|
|
802
|
+
} catch (lookupError) {
|
|
803
|
+
this.log(`\u26A0\uFE0F Failed to lookup existing customer by externalRef after 409:`, lookupError instanceof Error ? lookupError.message : lookupError);
|
|
804
|
+
}
|
|
651
805
|
}
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
806
|
+
const isEmailConflict = errorMessage.includes("email") || errorMessage.includes("identifier email");
|
|
807
|
+
if (externalRef && isEmailConflict && options?.email) {
|
|
808
|
+
try {
|
|
809
|
+
const byEmail = await this.apiClient.getCustomer({ email: options.email });
|
|
810
|
+
if (byEmail && byEmail.customerRef) {
|
|
811
|
+
this.customerRefMapping.set(customerRef, byEmail.customerRef);
|
|
812
|
+
this.log(
|
|
813
|
+
`\u26A0\uFE0F Resolved customer ${customerRef} by email after conflict; using existing customer ${byEmail.customerRef}`
|
|
814
|
+
);
|
|
815
|
+
return byEmail.customerRef;
|
|
816
|
+
}
|
|
817
|
+
} catch (emailLookupError) {
|
|
818
|
+
this.log(
|
|
819
|
+
`\u26A0\uFE0F Email lookup failed after customer conflict for ${customerRef}:`,
|
|
820
|
+
emailLookupError instanceof Error ? emailLookupError.message : emailLookupError
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
const retryParams = {
|
|
825
|
+
email: `${customerRef}-${Date.now()}@auto-created.local`,
|
|
826
|
+
externalRef
|
|
827
|
+
};
|
|
828
|
+
if (options?.name) {
|
|
829
|
+
retryParams.name = options.name;
|
|
830
|
+
}
|
|
831
|
+
const retryResult = await this.apiClient.createCustomer(retryParams);
|
|
832
|
+
const retryObj = retryResult;
|
|
833
|
+
const retryRef = retryObj.customerRef || retryObj.reference || customerRef;
|
|
834
|
+
this.customerRefMapping.set(customerRef, retryRef);
|
|
835
|
+
this.log(
|
|
836
|
+
`\u26A0\uFE0F Retried customer creation for ${customerRef} with generated email after email conflict`
|
|
837
|
+
);
|
|
838
|
+
return retryRef;
|
|
839
|
+
} catch (retryError) {
|
|
840
|
+
this.log(
|
|
841
|
+
`\u26A0\uFE0F Retry create customer with generated email failed for ${customerRef}:`,
|
|
842
|
+
retryError instanceof Error ? retryError.message : retryError
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const unresolvedMessage = errorMessage || "Customer already exists but could not be resolved";
|
|
847
|
+
throw new Error(
|
|
848
|
+
`Failed to resolve existing customer for ${customerRef} after conflict: ${unresolvedMessage}. Ensure the existing customer is linked to this externalRef.`
|
|
849
|
+
);
|
|
666
850
|
}
|
|
851
|
+
this.log(
|
|
852
|
+
`\u274C Failed to auto-create customer ${customerRef}:`,
|
|
853
|
+
error instanceof Error ? error.message : error
|
|
854
|
+
);
|
|
855
|
+
throw error;
|
|
667
856
|
}
|
|
668
|
-
);
|
|
857
|
+
});
|
|
669
858
|
if (backendRef !== customerRef) {
|
|
670
859
|
this.customerRefMapping.set(customerRef, backendRef);
|
|
671
860
|
}
|
|
672
861
|
return backendRef;
|
|
673
862
|
}
|
|
674
|
-
async trackUsage(customerRef,
|
|
863
|
+
async trackUsage(customerRef, _productRef, _planRef, action, outcome, requestId, actionDuration) {
|
|
675
864
|
await withRetry(
|
|
676
865
|
() => this.apiClient.trackUsage({
|
|
677
866
|
customerRef,
|
|
678
|
-
|
|
679
|
-
|
|
867
|
+
actionType: "api_call",
|
|
868
|
+
units: 1,
|
|
680
869
|
outcome,
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
870
|
+
productReference: _productRef,
|
|
871
|
+
duration: actionDuration,
|
|
872
|
+
metadata: { action: action || "api_requests", requestId },
|
|
684
873
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
685
874
|
}),
|
|
686
875
|
{
|
|
687
876
|
maxRetries: 2,
|
|
688
877
|
initialDelay: 500,
|
|
689
878
|
shouldRetry: (error) => error.message.includes("Customer not found"),
|
|
690
|
-
|
|
691
|
-
onRetry: (error, attempt) => {
|
|
879
|
+
onRetry: (_error, attempt) => {
|
|
692
880
|
console.warn(`\u26A0\uFE0F Customer not found (attempt ${attempt + 1}/3), retrying in 500ms...`);
|
|
693
881
|
}
|
|
694
882
|
}
|
|
@@ -707,9 +895,6 @@ var AdapterUtils = class {
|
|
|
707
895
|
if (!customerRef || customerRef === "anonymous") {
|
|
708
896
|
return "anonymous";
|
|
709
897
|
}
|
|
710
|
-
if (!customerRef.startsWith("customer_") && !customerRef.startsWith("demo_")) {
|
|
711
|
-
return `customer_${customerRef.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
712
|
-
}
|
|
713
898
|
return customerRef;
|
|
714
899
|
}
|
|
715
900
|
/**
|
|
@@ -717,7 +902,7 @@ var AdapterUtils = class {
|
|
|
717
902
|
*/
|
|
718
903
|
static async extractFromJWT(token, options) {
|
|
719
904
|
try {
|
|
720
|
-
const { jwtVerify } = await import("./
|
|
905
|
+
const { jwtVerify } = await import("./webapi-K5XBCEO6.js");
|
|
721
906
|
const jwtSecret = new TextEncoder().encode(
|
|
722
907
|
options?.secret || process.env.OAUTH_JWKS_SECRET || "test-jwt-secret"
|
|
723
908
|
);
|
|
@@ -726,19 +911,25 @@ var AdapterUtils = class {
|
|
|
726
911
|
audience: options?.audience || process.env.OAUTH_CLIENT_ID || "test-client-id"
|
|
727
912
|
});
|
|
728
913
|
return payload.sub || null;
|
|
729
|
-
} catch
|
|
914
|
+
} catch {
|
|
730
915
|
return null;
|
|
731
916
|
}
|
|
732
917
|
}
|
|
733
918
|
};
|
|
734
919
|
async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
|
|
920
|
+
const backendRefCache = /* @__PURE__ */ new Map();
|
|
921
|
+
const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
|
|
922
|
+
const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
|
|
735
923
|
return async (context) => {
|
|
736
924
|
try {
|
|
737
925
|
const args = await adapter.extractArgs(context);
|
|
738
926
|
const customerRef = await adapter.getCustomerRef(context);
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
927
|
+
let backendRef = backendRefCache.get(customerRef);
|
|
928
|
+
if (!backendRef) {
|
|
929
|
+
backendRef = await paywall.ensureCustomer(customerRef, customerRef);
|
|
930
|
+
backendRefCache.set(customerRef, backendRef);
|
|
931
|
+
}
|
|
932
|
+
args.auth = { customer_ref: backendRef };
|
|
742
933
|
const result = await protectedHandler(args);
|
|
743
934
|
return adapter.formatResponse(result, context);
|
|
744
935
|
} catch (error) {
|
|
@@ -796,7 +987,7 @@ var HttpAdapter = class {
|
|
|
796
987
|
const errorResponse2 = {
|
|
797
988
|
success: false,
|
|
798
989
|
error: "Payment required",
|
|
799
|
-
|
|
990
|
+
product: error.structuredContent.product,
|
|
800
991
|
checkoutUrl: error.structuredContent.checkoutUrl,
|
|
801
992
|
message: error.structuredContent.message
|
|
802
993
|
};
|
|
@@ -840,7 +1031,7 @@ var NextAdapter = class {
|
|
|
840
1031
|
if (request.method !== "GET" && request.headers.get("content-type")?.includes("application/json")) {
|
|
841
1032
|
body = await request.json();
|
|
842
1033
|
}
|
|
843
|
-
} catch
|
|
1034
|
+
} catch {
|
|
844
1035
|
}
|
|
845
1036
|
let routeParams = {};
|
|
846
1037
|
if (context?.params) {
|
|
@@ -869,6 +1060,10 @@ var NextAdapter = class {
|
|
|
869
1060
|
return AdapterUtils.ensureCustomerRef(jwtSub);
|
|
870
1061
|
}
|
|
871
1062
|
}
|
|
1063
|
+
const userId = request.headers.get("x-user-id");
|
|
1064
|
+
if (userId) {
|
|
1065
|
+
return AdapterUtils.ensureCustomerRef(userId);
|
|
1066
|
+
}
|
|
872
1067
|
const headerRef = request.headers.get("x-customer-ref");
|
|
873
1068
|
if (headerRef) {
|
|
874
1069
|
return AdapterUtils.ensureCustomerRef(headerRef);
|
|
@@ -884,24 +1079,30 @@ var NextAdapter = class {
|
|
|
884
1079
|
}
|
|
885
1080
|
formatError(error, _context) {
|
|
886
1081
|
if (error instanceof PaywallError) {
|
|
887
|
-
return new Response(
|
|
1082
|
+
return new Response(
|
|
1083
|
+
JSON.stringify({
|
|
1084
|
+
success: false,
|
|
1085
|
+
error: "Payment required",
|
|
1086
|
+
product: error.structuredContent.product,
|
|
1087
|
+
checkoutUrl: error.structuredContent.checkoutUrl,
|
|
1088
|
+
message: error.structuredContent.message
|
|
1089
|
+
}),
|
|
1090
|
+
{
|
|
1091
|
+
status: 402,
|
|
1092
|
+
headers: { "Content-Type": "application/json" }
|
|
1093
|
+
}
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
return new Response(
|
|
1097
|
+
JSON.stringify({
|
|
888
1098
|
success: false,
|
|
889
|
-
error: "
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}), {
|
|
894
|
-
status: 402,
|
|
1099
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
1100
|
+
}),
|
|
1101
|
+
{
|
|
1102
|
+
status: 500,
|
|
895
1103
|
headers: { "Content-Type": "application/json" }
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
return new Response(JSON.stringify({
|
|
899
|
-
success: false,
|
|
900
|
-
error: error instanceof Error ? error.message : "Internal server error"
|
|
901
|
-
}), {
|
|
902
|
-
status: 500,
|
|
903
|
-
headers: { "Content-Type": "application/json" }
|
|
904
|
-
});
|
|
1104
|
+
}
|
|
1105
|
+
);
|
|
905
1106
|
}
|
|
906
1107
|
};
|
|
907
1108
|
|
|
@@ -918,43 +1119,58 @@ var McpAdapter = class {
|
|
|
918
1119
|
const ref = await this.options.getCustomerRef(args);
|
|
919
1120
|
return AdapterUtils.ensureCustomerRef(ref);
|
|
920
1121
|
}
|
|
921
|
-
const
|
|
1122
|
+
const auth = args?.auth;
|
|
1123
|
+
const customerRef = auth?.customer_ref || "anonymous";
|
|
922
1124
|
return AdapterUtils.ensureCustomerRef(customerRef);
|
|
923
1125
|
}
|
|
924
1126
|
formatResponse(result, _context) {
|
|
925
1127
|
const transformed = this.options.transformResponse ? this.options.transformResponse(result) : result;
|
|
926
1128
|
return {
|
|
927
|
-
content: [
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1129
|
+
content: [
|
|
1130
|
+
{
|
|
1131
|
+
type: "text",
|
|
1132
|
+
text: JSON.stringify(transformed, null, 2)
|
|
1133
|
+
}
|
|
1134
|
+
]
|
|
931
1135
|
};
|
|
932
1136
|
}
|
|
933
1137
|
formatError(error, _context) {
|
|
934
1138
|
if (error instanceof PaywallError) {
|
|
935
1139
|
return {
|
|
936
|
-
content: [
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1140
|
+
content: [
|
|
1141
|
+
{
|
|
1142
|
+
type: "text",
|
|
1143
|
+
text: JSON.stringify(
|
|
1144
|
+
{
|
|
1145
|
+
success: false,
|
|
1146
|
+
error: "Payment required",
|
|
1147
|
+
product: error.structuredContent.product,
|
|
1148
|
+
checkoutUrl: error.structuredContent.checkoutUrl,
|
|
1149
|
+
message: error.structuredContent.message
|
|
1150
|
+
},
|
|
1151
|
+
null,
|
|
1152
|
+
2
|
|
1153
|
+
)
|
|
1154
|
+
}
|
|
1155
|
+
],
|
|
946
1156
|
isError: true,
|
|
947
1157
|
structuredContent: error.structuredContent
|
|
948
1158
|
};
|
|
949
1159
|
}
|
|
950
1160
|
return {
|
|
951
|
-
content: [
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1161
|
+
content: [
|
|
1162
|
+
{
|
|
1163
|
+
type: "text",
|
|
1164
|
+
text: JSON.stringify(
|
|
1165
|
+
{
|
|
1166
|
+
success: false,
|
|
1167
|
+
error: error instanceof Error ? error.message : "Unknown error occurred"
|
|
1168
|
+
},
|
|
1169
|
+
null,
|
|
1170
|
+
2
|
|
1171
|
+
)
|
|
1172
|
+
}
|
|
1173
|
+
],
|
|
958
1174
|
isError: true
|
|
959
1175
|
};
|
|
960
1176
|
}
|
|
@@ -962,6 +1178,141 @@ var McpAdapter = class {
|
|
|
962
1178
|
|
|
963
1179
|
// src/factory.ts
|
|
964
1180
|
import { SolvaPayError as SolvaPayError2, getSolvaPayConfig } from "@solvapay/core";
|
|
1181
|
+
|
|
1182
|
+
// src/virtual-tools.ts
|
|
1183
|
+
var TOOL_GET_USER_INFO = {
|
|
1184
|
+
name: "get_user_info",
|
|
1185
|
+
description: "Get information about the current user and their purchase status for this MCP server. Returns user profile (reference, name, email) and active purchase details including product name, type, dates, and usage limit if applicable.",
|
|
1186
|
+
inputSchema: {
|
|
1187
|
+
type: "object",
|
|
1188
|
+
properties: {},
|
|
1189
|
+
required: []
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
var TOOL_UPGRADE = {
|
|
1193
|
+
name: "upgrade",
|
|
1194
|
+
description: "Get available pricing options and checkout URLs for upgrading. Returns a list of available pricing options with their details (price, features) and checkout URLs. Users can click on a checkout URL to purchase. If a specific planRef is provided, returns only the checkout URL for that pricing option.",
|
|
1195
|
+
inputSchema: {
|
|
1196
|
+
type: "object",
|
|
1197
|
+
properties: {
|
|
1198
|
+
planRef: {
|
|
1199
|
+
type: "string",
|
|
1200
|
+
description: 'Optional pricing reference (e.g., "pln_abc123") to get a checkout URL for a specific option. If not provided, returns all available pricing options with their checkout URLs.'
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
required: []
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
var TOOL_MANAGE_ACCOUNT = {
|
|
1207
|
+
name: "manage_account",
|
|
1208
|
+
description: "Get a URL to the customer portal where users can view and manage their account. The portal shows current account status, billing history, and allows subscription changes. Returns a secure, time-limited URL that the user can click to access their account management page.",
|
|
1209
|
+
inputSchema: {
|
|
1210
|
+
type: "object",
|
|
1211
|
+
properties: {},
|
|
1212
|
+
required: []
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
var VIRTUAL_TOOL_DEFINITIONS = [TOOL_GET_USER_INFO, TOOL_UPGRADE, TOOL_MANAGE_ACCOUNT];
|
|
1216
|
+
function mcpTextResult(text) {
|
|
1217
|
+
return { content: [{ type: "text", text }] };
|
|
1218
|
+
}
|
|
1219
|
+
function mcpErrorResult(message) {
|
|
1220
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
1221
|
+
}
|
|
1222
|
+
function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
|
|
1223
|
+
return async (args) => {
|
|
1224
|
+
const customerRef = getCustomerRef(args);
|
|
1225
|
+
try {
|
|
1226
|
+
if (!apiClient.getUserInfo) {
|
|
1227
|
+
return mcpErrorResult("getUserInfo is not available on this API client");
|
|
1228
|
+
}
|
|
1229
|
+
const userInfo = await apiClient.getUserInfo({ customerRef, productRef });
|
|
1230
|
+
return mcpTextResult(JSON.stringify(userInfo, null, 2));
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
return mcpErrorResult(
|
|
1233
|
+
`Failed to retrieve user information: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function createUpgradeHandler(apiClient, productRef, getCustomerRef) {
|
|
1239
|
+
return async (args) => {
|
|
1240
|
+
const customerRef = getCustomerRef(args);
|
|
1241
|
+
const planRef = typeof args.planRef === "string" ? args.planRef : void 0;
|
|
1242
|
+
try {
|
|
1243
|
+
const result = await apiClient.createCheckoutSession({
|
|
1244
|
+
customerReference: customerRef,
|
|
1245
|
+
productRef,
|
|
1246
|
+
...planRef && { planRef }
|
|
1247
|
+
});
|
|
1248
|
+
const checkoutUrl = result.checkoutUrl;
|
|
1249
|
+
if (planRef) {
|
|
1250
|
+
const responseText2 = `## Upgrade
|
|
1251
|
+
|
|
1252
|
+
**[Click here to upgrade \u2192](${checkoutUrl})**
|
|
1253
|
+
|
|
1254
|
+
After completing the checkout, your purchase will be activated immediately.`;
|
|
1255
|
+
return mcpTextResult(responseText2);
|
|
1256
|
+
}
|
|
1257
|
+
const responseText = `## Upgrade Your Subscription
|
|
1258
|
+
|
|
1259
|
+
**[Click here to view pricing options and upgrade \u2192](${checkoutUrl})**
|
|
1260
|
+
|
|
1261
|
+
You'll be able to compare options and select the one that's right for you.`;
|
|
1262
|
+
return mcpTextResult(responseText);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
return mcpErrorResult(
|
|
1265
|
+
`Failed to create checkout session: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function createManageAccountHandler(apiClient, productRef, getCustomerRef) {
|
|
1271
|
+
return async (args) => {
|
|
1272
|
+
const customerRef = getCustomerRef(args);
|
|
1273
|
+
try {
|
|
1274
|
+
const session = await apiClient.createCustomerSession({ customerRef, productRef });
|
|
1275
|
+
const portalUrl = session.customerUrl;
|
|
1276
|
+
const responseText = `## Manage Your Account
|
|
1277
|
+
|
|
1278
|
+
Access your account management portal to:
|
|
1279
|
+
- View your current account status
|
|
1280
|
+
- See billing history and invoices
|
|
1281
|
+
- Update payment methods
|
|
1282
|
+
- Cancel or modify your subscription
|
|
1283
|
+
|
|
1284
|
+
**[Open Account Portal \u2192](${portalUrl})**
|
|
1285
|
+
|
|
1286
|
+
This link is secure and will expire after a short period.`;
|
|
1287
|
+
return mcpTextResult(responseText);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
return mcpErrorResult(
|
|
1290
|
+
`Failed to create customer portal session: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function createVirtualTools(apiClient, options) {
|
|
1296
|
+
const { product, getCustomerRef, exclude = [] } = options;
|
|
1297
|
+
const excludeSet = new Set(exclude);
|
|
1298
|
+
const allTools = [
|
|
1299
|
+
{
|
|
1300
|
+
...TOOL_GET_USER_INFO,
|
|
1301
|
+
handler: createGetUserInfoHandler(apiClient, product, getCustomerRef)
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
...TOOL_UPGRADE,
|
|
1305
|
+
handler: createUpgradeHandler(apiClient, product, getCustomerRef)
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
...TOOL_MANAGE_ACCOUNT,
|
|
1309
|
+
handler: createManageAccountHandler(apiClient, product, getCustomerRef)
|
|
1310
|
+
}
|
|
1311
|
+
];
|
|
1312
|
+
return allTools.filter((t) => !excludeSet.has(t.name));
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/factory.ts
|
|
965
1316
|
function createSolvaPay(config) {
|
|
966
1317
|
let resolvedConfig;
|
|
967
1318
|
if (!config) {
|
|
@@ -978,7 +1329,8 @@ function createSolvaPay(config) {
|
|
|
978
1329
|
apiBaseUrl: resolvedConfig.apiBaseUrl
|
|
979
1330
|
});
|
|
980
1331
|
const paywall = new SolvaPayPaywall(apiClient, {
|
|
981
|
-
debug: process.env.SOLVAPAY_DEBUG !== "false"
|
|
1332
|
+
debug: process.env.SOLVAPAY_DEBUG !== "false",
|
|
1333
|
+
limitsCacheTTL: resolvedConfig.limitsCacheTTL
|
|
982
1334
|
});
|
|
983
1335
|
return {
|
|
984
1336
|
// Direct access to API client for advanced operations
|
|
@@ -993,11 +1345,11 @@ function createSolvaPay(config) {
|
|
|
993
1345
|
}
|
|
994
1346
|
return apiClient.createPaymentIntent(params);
|
|
995
1347
|
},
|
|
996
|
-
|
|
997
|
-
if (!apiClient.
|
|
998
|
-
throw new SolvaPayError2("
|
|
1348
|
+
processPaymentIntent(params) {
|
|
1349
|
+
if (!apiClient.processPaymentIntent) {
|
|
1350
|
+
throw new SolvaPayError2("processPaymentIntent is not available on this API client");
|
|
999
1351
|
}
|
|
1000
|
-
return apiClient.
|
|
1352
|
+
return apiClient.processPaymentIntent(params);
|
|
1001
1353
|
},
|
|
1002
1354
|
checkLimits(params) {
|
|
1003
1355
|
return apiClient.checkLimits(params);
|
|
@@ -1012,91 +1364,507 @@ function createSolvaPay(config) {
|
|
|
1012
1364
|
return apiClient.createCustomer(params);
|
|
1013
1365
|
},
|
|
1014
1366
|
getCustomer(params) {
|
|
1015
|
-
if (!apiClient.getCustomer) {
|
|
1016
|
-
throw new SolvaPayError2("getCustomer is not available on this API client");
|
|
1017
|
-
}
|
|
1018
1367
|
return apiClient.getCustomer(params);
|
|
1019
1368
|
},
|
|
1020
1369
|
createCheckoutSession(params) {
|
|
1021
|
-
return apiClient.createCheckoutSession(
|
|
1370
|
+
return apiClient.createCheckoutSession({
|
|
1371
|
+
customerReference: params.customerRef,
|
|
1372
|
+
productRef: params.productRef,
|
|
1373
|
+
planRef: params.planRef
|
|
1374
|
+
});
|
|
1022
1375
|
},
|
|
1023
1376
|
createCustomerSession(params) {
|
|
1024
1377
|
return apiClient.createCustomerSession(params);
|
|
1025
1378
|
},
|
|
1379
|
+
bootstrapMcpProduct(params) {
|
|
1380
|
+
if (!apiClient.bootstrapMcpProduct) {
|
|
1381
|
+
throw new SolvaPayError2("bootstrapMcpProduct is not available on this API client");
|
|
1382
|
+
}
|
|
1383
|
+
return apiClient.bootstrapMcpProduct(params);
|
|
1384
|
+
},
|
|
1385
|
+
getVirtualTools(options) {
|
|
1386
|
+
return createVirtualTools(apiClient, options);
|
|
1387
|
+
},
|
|
1026
1388
|
// Payable API for framework-specific handlers
|
|
1027
1389
|
payable(options = {}) {
|
|
1028
|
-
const
|
|
1029
|
-
const plan = options.planRef || options.plan
|
|
1030
|
-
const
|
|
1390
|
+
const product = options.productRef || options.product || process.env.SOLVAPAY_PRODUCT || "default-product";
|
|
1391
|
+
const plan = options.planRef || options.plan;
|
|
1392
|
+
const usageType = options.usageType || "requests";
|
|
1393
|
+
const metadata = { product, plan, usageType };
|
|
1031
1394
|
return {
|
|
1032
|
-
//
|
|
1395
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1033
1396
|
http(businessLogic, adapterOptions) {
|
|
1034
|
-
const adapter = new HttpAdapter(
|
|
1397
|
+
const adapter = new HttpAdapter({
|
|
1398
|
+
...adapterOptions,
|
|
1399
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
1400
|
+
});
|
|
1401
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
1035
1402
|
return async (req, reply) => {
|
|
1036
|
-
const handler = await
|
|
1037
|
-
adapter,
|
|
1038
|
-
paywall,
|
|
1039
|
-
metadata,
|
|
1040
|
-
businessLogic
|
|
1041
|
-
);
|
|
1403
|
+
const handler = await handlerPromise;
|
|
1042
1404
|
return handler([req, reply]);
|
|
1043
1405
|
};
|
|
1044
1406
|
},
|
|
1045
|
-
//
|
|
1407
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1046
1408
|
next(businessLogic, adapterOptions) {
|
|
1047
|
-
const adapter = new NextAdapter(
|
|
1409
|
+
const adapter = new NextAdapter({
|
|
1410
|
+
...adapterOptions,
|
|
1411
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
1412
|
+
});
|
|
1413
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
1048
1414
|
return async (request, context) => {
|
|
1049
|
-
const handler = await
|
|
1050
|
-
adapter,
|
|
1051
|
-
paywall,
|
|
1052
|
-
metadata,
|
|
1053
|
-
businessLogic
|
|
1054
|
-
);
|
|
1415
|
+
const handler = await handlerPromise;
|
|
1055
1416
|
return handler([request, context]);
|
|
1056
1417
|
};
|
|
1057
1418
|
},
|
|
1058
|
-
//
|
|
1419
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1059
1420
|
mcp(businessLogic, adapterOptions) {
|
|
1060
|
-
const adapter = new McpAdapter(
|
|
1421
|
+
const adapter = new McpAdapter({
|
|
1422
|
+
...adapterOptions,
|
|
1423
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
1424
|
+
});
|
|
1425
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
1061
1426
|
return async (args) => {
|
|
1062
|
-
const handler = await
|
|
1063
|
-
adapter,
|
|
1064
|
-
paywall,
|
|
1065
|
-
metadata,
|
|
1066
|
-
businessLogic
|
|
1067
|
-
);
|
|
1427
|
+
const handler = await handlerPromise;
|
|
1068
1428
|
return handler(args);
|
|
1069
1429
|
};
|
|
1070
1430
|
},
|
|
1071
|
-
//
|
|
1431
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1072
1432
|
async function(businessLogic) {
|
|
1073
|
-
const getCustomerRef = (args) =>
|
|
1433
|
+
const getCustomerRef = (args) => {
|
|
1434
|
+
const configuredRef = options.getCustomerRef?.(args);
|
|
1435
|
+
if (typeof configuredRef === "string") {
|
|
1436
|
+
return configuredRef;
|
|
1437
|
+
}
|
|
1438
|
+
return args.auth?.customer_ref || "anonymous";
|
|
1439
|
+
};
|
|
1074
1440
|
return paywall.protect(businessLogic, metadata, getCustomerRef);
|
|
1075
1441
|
}
|
|
1076
1442
|
};
|
|
1077
1443
|
}
|
|
1078
1444
|
};
|
|
1079
1445
|
}
|
|
1080
|
-
|
|
1446
|
+
|
|
1447
|
+
// src/mcp-auth.ts
|
|
1448
|
+
var McpBearerAuthError = class extends Error {
|
|
1449
|
+
constructor(message) {
|
|
1450
|
+
super(message);
|
|
1451
|
+
this.name = "McpBearerAuthError";
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
function base64UrlDecode(input) {
|
|
1455
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1456
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
1457
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
1458
|
+
}
|
|
1459
|
+
function extractBearerToken(authorization) {
|
|
1460
|
+
if (!authorization) return null;
|
|
1461
|
+
if (!authorization.startsWith("Bearer ")) return null;
|
|
1462
|
+
return authorization.slice(7).trim() || null;
|
|
1463
|
+
}
|
|
1464
|
+
function decodeJwtPayload(token) {
|
|
1465
|
+
const parts = token.split(".");
|
|
1466
|
+
if (parts.length < 2) {
|
|
1467
|
+
throw new McpBearerAuthError("Invalid JWT format");
|
|
1468
|
+
}
|
|
1081
1469
|
try {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1470
|
+
const payloadText = base64UrlDecode(parts[1]);
|
|
1471
|
+
const payload = JSON.parse(payloadText);
|
|
1472
|
+
return payload;
|
|
1084
1473
|
} catch {
|
|
1085
|
-
|
|
1474
|
+
throw new McpBearerAuthError("Invalid JWT payload");
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function getCustomerRefFromJwtPayload(payload, options = {}) {
|
|
1478
|
+
const claimPriority = options.claimPriority || ["customerRef", "customer_ref", "sub"];
|
|
1479
|
+
for (const claim of claimPriority) {
|
|
1480
|
+
const value = payload[claim];
|
|
1481
|
+
if (typeof value === "string" && value.trim()) {
|
|
1482
|
+
return value.trim();
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
throw new McpBearerAuthError(
|
|
1486
|
+
`No customer reference claim found (checked: ${claimPriority.join(", ")})`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
function getCustomerRefFromBearerAuthHeader(authorization, options = {}) {
|
|
1490
|
+
const token = extractBearerToken(authorization);
|
|
1491
|
+
if (!token) {
|
|
1492
|
+
throw new McpBearerAuthError("Missing bearer token");
|
|
1493
|
+
}
|
|
1494
|
+
const payload = decodeJwtPayload(token);
|
|
1495
|
+
return getCustomerRefFromJwtPayload(payload, options);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// src/helpers/error.ts
|
|
1499
|
+
import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
|
|
1500
|
+
function isErrorResult(result) {
|
|
1501
|
+
return typeof result === "object" && result !== null && "error" in result && "status" in result;
|
|
1502
|
+
}
|
|
1503
|
+
function handleRouteError(error, operationName, defaultMessage) {
|
|
1504
|
+
console.error(`[${operationName}] Error:`, error);
|
|
1505
|
+
if (error instanceof SolvaPayError3) {
|
|
1506
|
+
const errorMessage2 = error.message;
|
|
1507
|
+
return {
|
|
1508
|
+
error: errorMessage2,
|
|
1509
|
+
status: 500,
|
|
1510
|
+
details: errorMessage2
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1514
|
+
const message = defaultMessage || `${operationName} failed`;
|
|
1515
|
+
return {
|
|
1516
|
+
error: message,
|
|
1517
|
+
status: 500,
|
|
1518
|
+
details: errorMessage
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// src/helpers/auth.ts
|
|
1523
|
+
async function getAuthenticatedUserCore(request, options = {}) {
|
|
1524
|
+
try {
|
|
1525
|
+
const { requireUserId, getUserEmailFromRequest, getUserNameFromRequest } = await import("@solvapay/auth");
|
|
1526
|
+
const userIdOrError = requireUserId(request);
|
|
1527
|
+
if (userIdOrError instanceof Response) {
|
|
1528
|
+
const clonedResponse = userIdOrError.clone();
|
|
1529
|
+
const body = await clonedResponse.json().catch(() => ({ error: "Unauthorized" }));
|
|
1530
|
+
return {
|
|
1531
|
+
error: body.error || "Unauthorized",
|
|
1532
|
+
status: userIdOrError.status,
|
|
1533
|
+
details: body.error || "Unauthorized"
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
const userId = userIdOrError;
|
|
1537
|
+
const email = options.includeEmail !== false ? await getUserEmailFromRequest(request) : null;
|
|
1538
|
+
const name = options.includeName !== false ? await getUserNameFromRequest(request) : null;
|
|
1539
|
+
return {
|
|
1540
|
+
userId,
|
|
1541
|
+
email,
|
|
1542
|
+
name
|
|
1543
|
+
};
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
return handleRouteError(error, "Get authenticated user", "Authentication failed");
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// src/helpers/customer.ts
|
|
1550
|
+
async function syncCustomerCore(request, options = {}) {
|
|
1551
|
+
try {
|
|
1552
|
+
const userResult = await getAuthenticatedUserCore(request, {
|
|
1553
|
+
includeEmail: options.includeEmail,
|
|
1554
|
+
includeName: options.includeName
|
|
1555
|
+
});
|
|
1556
|
+
if (isErrorResult(userResult)) {
|
|
1557
|
+
return userResult;
|
|
1558
|
+
}
|
|
1559
|
+
const { userId, email, name } = userResult;
|
|
1560
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
1561
|
+
const customerRef = await solvaPay.ensureCustomer(userId, userId, {
|
|
1562
|
+
email: email || void 0,
|
|
1563
|
+
name: name || void 0
|
|
1564
|
+
});
|
|
1565
|
+
return customerRef;
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
return handleRouteError(error, "Sync customer", "Failed to sync customer");
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/helpers/payment.ts
|
|
1572
|
+
async function createPaymentIntentCore(request, body, options = {}) {
|
|
1573
|
+
try {
|
|
1574
|
+
if (!body.planRef || !body.productRef) {
|
|
1575
|
+
return {
|
|
1576
|
+
error: "Missing required parameters: planRef and productRef are required",
|
|
1577
|
+
status: 400
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
const customerResult = await syncCustomerCore(request, {
|
|
1581
|
+
solvaPay: options.solvaPay,
|
|
1582
|
+
includeEmail: options.includeEmail,
|
|
1583
|
+
includeName: options.includeName
|
|
1584
|
+
});
|
|
1585
|
+
if (isErrorResult(customerResult)) {
|
|
1586
|
+
return customerResult;
|
|
1587
|
+
}
|
|
1588
|
+
const customerRef = customerResult;
|
|
1589
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
1590
|
+
const paymentIntent = await solvaPay.createPaymentIntent({
|
|
1591
|
+
productRef: body.productRef,
|
|
1592
|
+
planRef: body.planRef,
|
|
1593
|
+
customerRef
|
|
1594
|
+
});
|
|
1595
|
+
return {
|
|
1596
|
+
id: paymentIntent.id,
|
|
1597
|
+
clientSecret: paymentIntent.clientSecret,
|
|
1598
|
+
publishableKey: paymentIntent.publishableKey,
|
|
1599
|
+
accountId: paymentIntent.accountId,
|
|
1600
|
+
customerRef
|
|
1601
|
+
};
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
return handleRouteError(error, "Create payment intent", "Payment intent creation failed");
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
async function processPaymentIntentCore(request, body, options = {}) {
|
|
1607
|
+
try {
|
|
1608
|
+
if (!body.paymentIntentId || !body.productRef) {
|
|
1609
|
+
return {
|
|
1610
|
+
error: "paymentIntentId and productRef are required",
|
|
1611
|
+
status: 400
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
const customerResult = await syncCustomerCore(request, {
|
|
1615
|
+
solvaPay: options.solvaPay
|
|
1616
|
+
});
|
|
1617
|
+
if (isErrorResult(customerResult)) {
|
|
1618
|
+
return customerResult;
|
|
1619
|
+
}
|
|
1620
|
+
const customerRef = customerResult;
|
|
1621
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
1622
|
+
const result = await solvaPay.processPaymentIntent({
|
|
1623
|
+
paymentIntentId: body.paymentIntentId,
|
|
1624
|
+
productRef: body.productRef,
|
|
1625
|
+
customerRef,
|
|
1626
|
+
planRef: body.planRef
|
|
1627
|
+
});
|
|
1628
|
+
return result;
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
return handleRouteError(error, "Process payment intent", "Payment processing failed");
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// src/helpers/checkout.ts
|
|
1635
|
+
async function createCheckoutSessionCore(request, body, options = {}) {
|
|
1636
|
+
try {
|
|
1637
|
+
if (!body.productRef) {
|
|
1638
|
+
return {
|
|
1639
|
+
error: "Missing required parameter: productRef is required",
|
|
1640
|
+
status: 400
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
const customerResult = await syncCustomerCore(request, {
|
|
1644
|
+
solvaPay: options.solvaPay,
|
|
1645
|
+
includeEmail: options.includeEmail,
|
|
1646
|
+
includeName: options.includeName
|
|
1647
|
+
});
|
|
1648
|
+
if (isErrorResult(customerResult)) {
|
|
1649
|
+
return customerResult;
|
|
1650
|
+
}
|
|
1651
|
+
const customerRef = customerResult;
|
|
1652
|
+
let returnUrl = body.returnUrl || options.returnUrl;
|
|
1653
|
+
if (!returnUrl) {
|
|
1654
|
+
try {
|
|
1655
|
+
const url = new URL(request.url);
|
|
1656
|
+
returnUrl = url.origin;
|
|
1657
|
+
} catch {
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
1661
|
+
const session = await solvaPay.createCheckoutSession({
|
|
1662
|
+
productRef: body.productRef,
|
|
1663
|
+
customerRef,
|
|
1664
|
+
planRef: body.planRef || void 0,
|
|
1665
|
+
returnUrl
|
|
1666
|
+
});
|
|
1667
|
+
return {
|
|
1668
|
+
sessionId: session.sessionId,
|
|
1669
|
+
checkoutUrl: session.checkoutUrl
|
|
1670
|
+
};
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
return handleRouteError(error, "Create checkout session", "Checkout session creation failed");
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
async function createCustomerSessionCore(request, options = {}) {
|
|
1676
|
+
try {
|
|
1677
|
+
const customerResult = await syncCustomerCore(request, {
|
|
1678
|
+
solvaPay: options.solvaPay,
|
|
1679
|
+
includeEmail: options.includeEmail,
|
|
1680
|
+
includeName: options.includeName
|
|
1681
|
+
});
|
|
1682
|
+
if (isErrorResult(customerResult)) {
|
|
1683
|
+
return customerResult;
|
|
1684
|
+
}
|
|
1685
|
+
const customerRef = customerResult;
|
|
1686
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
1687
|
+
const session = await solvaPay.createCustomerSession({
|
|
1688
|
+
customerRef
|
|
1689
|
+
});
|
|
1690
|
+
return session;
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
return handleRouteError(error, "Create customer session", "Customer session creation failed");
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// src/helpers/renewal.ts
|
|
1697
|
+
import { SolvaPayError as SolvaPayError4 } from "@solvapay/core";
|
|
1698
|
+
async function cancelPurchaseCore(request, body, options = {}) {
|
|
1699
|
+
try {
|
|
1700
|
+
if (!body.purchaseRef) {
|
|
1701
|
+
return {
|
|
1702
|
+
error: "Missing required parameter: purchaseRef is required",
|
|
1703
|
+
status: 400
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
1707
|
+
if (!solvaPay.apiClient.cancelPurchase) {
|
|
1708
|
+
return {
|
|
1709
|
+
error: "Cancel purchase method not available on SDK client",
|
|
1710
|
+
status: 500
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
let cancelledPurchase = await solvaPay.apiClient.cancelPurchase({
|
|
1714
|
+
purchaseRef: body.purchaseRef,
|
|
1715
|
+
reason: body.reason
|
|
1716
|
+
});
|
|
1717
|
+
if (!cancelledPurchase || typeof cancelledPurchase !== "object") {
|
|
1718
|
+
return {
|
|
1719
|
+
error: "Invalid response from cancel purchase endpoint",
|
|
1720
|
+
status: 500
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
const responseObj = cancelledPurchase;
|
|
1724
|
+
if (responseObj.purchase && typeof responseObj.purchase === "object") {
|
|
1725
|
+
cancelledPurchase = responseObj.purchase;
|
|
1726
|
+
}
|
|
1727
|
+
if (!cancelledPurchase.reference) {
|
|
1728
|
+
return {
|
|
1729
|
+
error: "Cancel purchase response missing required fields",
|
|
1730
|
+
status: 500
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
const isCancelled = cancelledPurchase.status === "cancelled" || cancelledPurchase.cancelledAt;
|
|
1734
|
+
if (!isCancelled) {
|
|
1735
|
+
return {
|
|
1736
|
+
error: `Purchase cancellation failed: backend returned status '${cancelledPurchase.status}' without cancelledAt timestamp`,
|
|
1737
|
+
status: 500
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1741
|
+
return cancelledPurchase;
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
if (error instanceof SolvaPayError4) {
|
|
1744
|
+
const errorMessage = error.message;
|
|
1745
|
+
if (errorMessage.includes("not found")) {
|
|
1746
|
+
return {
|
|
1747
|
+
error: "Purchase not found",
|
|
1748
|
+
status: 404,
|
|
1749
|
+
details: errorMessage
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
if (errorMessage.includes("cannot be cancelled") || errorMessage.includes("does not belong to provider")) {
|
|
1753
|
+
return {
|
|
1754
|
+
error: "Purchase cannot be cancelled or does not belong to provider",
|
|
1755
|
+
status: 400,
|
|
1756
|
+
details: errorMessage
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
error: errorMessage,
|
|
1761
|
+
status: 500,
|
|
1762
|
+
details: errorMessage
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
return handleRouteError(error, "Cancel purchase", "Failed to cancel purchase");
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/helpers/plans.ts
|
|
1770
|
+
import { getSolvaPayConfig as getSolvaPayConfig2 } from "@solvapay/core";
|
|
1771
|
+
async function listPlansCore(request) {
|
|
1772
|
+
try {
|
|
1773
|
+
const url = new URL(request.url);
|
|
1774
|
+
const productRef = url.searchParams.get("productRef");
|
|
1775
|
+
if (!productRef) {
|
|
1776
|
+
return {
|
|
1777
|
+
error: "Missing required parameter: productRef",
|
|
1778
|
+
status: 400
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
const config = getSolvaPayConfig2();
|
|
1782
|
+
const solvapaySecretKey = config.apiKey;
|
|
1783
|
+
const solvapayApiBaseUrl = config.apiBaseUrl;
|
|
1784
|
+
if (!solvapaySecretKey) {
|
|
1785
|
+
return {
|
|
1786
|
+
error: "Server configuration error: SolvaPay secret key not configured",
|
|
1787
|
+
status: 500
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
const apiClient = createSolvaPayClient({
|
|
1791
|
+
apiKey: solvapaySecretKey,
|
|
1792
|
+
apiBaseUrl: solvapayApiBaseUrl
|
|
1793
|
+
});
|
|
1794
|
+
if (!apiClient.listPlans) {
|
|
1795
|
+
return {
|
|
1796
|
+
error: "List plans method not available",
|
|
1797
|
+
status: 500
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
const plans = await apiClient.listPlans(productRef);
|
|
1801
|
+
return {
|
|
1802
|
+
plans: plans || [],
|
|
1803
|
+
productRef
|
|
1804
|
+
};
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
return handleRouteError(error, "List plans", "Failed to fetch plans");
|
|
1086
1807
|
}
|
|
1087
1808
|
}
|
|
1088
1809
|
|
|
1089
1810
|
// src/index.ts
|
|
1090
|
-
function verifyWebhook({
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1811
|
+
function verifyWebhook({
|
|
1812
|
+
body,
|
|
1813
|
+
signature,
|
|
1814
|
+
secret
|
|
1815
|
+
}) {
|
|
1816
|
+
const toleranceSec = 300;
|
|
1817
|
+
if (!signature) throw new SolvaPayError5("Missing webhook signature");
|
|
1818
|
+
const parts = signature.split(",");
|
|
1819
|
+
const tPart = parts.find((p) => p.startsWith("t="));
|
|
1820
|
+
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
1821
|
+
if (!tPart || !v1Part) {
|
|
1822
|
+
throw new SolvaPayError5("Malformed webhook signature");
|
|
1823
|
+
}
|
|
1824
|
+
const timestamp = parseInt(tPart.slice(2), 10);
|
|
1825
|
+
const receivedHmac = v1Part.slice(3);
|
|
1826
|
+
if (Number.isNaN(timestamp) || !receivedHmac) {
|
|
1827
|
+
throw new SolvaPayError5("Malformed webhook signature");
|
|
1828
|
+
}
|
|
1829
|
+
if (toleranceSec > 0) {
|
|
1830
|
+
const age = Math.abs(Math.floor(Date.now() / 1e3) - timestamp);
|
|
1831
|
+
if (age > toleranceSec) {
|
|
1832
|
+
throw new SolvaPayError5("Webhook signature timestamp too old");
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const expectedHmac = crypto.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
|
|
1836
|
+
if (receivedHmac.length !== expectedHmac.length) {
|
|
1837
|
+
throw new SolvaPayError5("Invalid webhook signature");
|
|
1838
|
+
}
|
|
1839
|
+
const ok = crypto.timingSafeEqual(Buffer.from(expectedHmac), Buffer.from(receivedHmac));
|
|
1840
|
+
if (!ok) throw new SolvaPayError5("Invalid webhook signature");
|
|
1841
|
+
try {
|
|
1842
|
+
return JSON.parse(body);
|
|
1843
|
+
} catch {
|
|
1844
|
+
throw new SolvaPayError5("Invalid webhook payload: body is not valid JSON");
|
|
1845
|
+
}
|
|
1095
1846
|
}
|
|
1096
1847
|
export {
|
|
1848
|
+
McpBearerAuthError,
|
|
1097
1849
|
PaywallError,
|
|
1850
|
+
VIRTUAL_TOOL_DEFINITIONS,
|
|
1851
|
+
cancelPurchaseCore,
|
|
1852
|
+
createCheckoutSessionCore,
|
|
1853
|
+
createCustomerSessionCore,
|
|
1854
|
+
createPaymentIntentCore,
|
|
1098
1855
|
createSolvaPay,
|
|
1099
1856
|
createSolvaPayClient,
|
|
1857
|
+
createVirtualTools,
|
|
1858
|
+
decodeJwtPayload,
|
|
1859
|
+
extractBearerToken,
|
|
1860
|
+
getAuthenticatedUserCore,
|
|
1861
|
+
getCustomerRefFromBearerAuthHeader,
|
|
1862
|
+
getCustomerRefFromJwtPayload,
|
|
1863
|
+
handleRouteError,
|
|
1864
|
+
isErrorResult,
|
|
1865
|
+
listPlansCore,
|
|
1866
|
+
processPaymentIntentCore,
|
|
1867
|
+
syncCustomerCore,
|
|
1100
1868
|
verifyWebhook,
|
|
1101
1869
|
withRetry
|
|
1102
1870
|
};
|