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