@solvapay/server 1.0.0-preview.2 → 1.0.0-preview.21
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 +97 -32
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/edge.d.ts +2723 -287
- package/dist/edge.js +1192 -251
- package/dist/{esm-5GYCIXIY.js → esm-UW7WCMEK.js} +1 -1
- package/dist/index.cjs +1269 -246
- package/dist/index.d.cts +2793 -287
- package/dist/index.d.ts +2793 -287
- package/dist/index.js +1255 -251
- package/package.json +16 -6
- package/dist/chunk-R5U7XKVJ.js +0 -16
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) => {
|
|
@@ -20,15 +18,10 @@ function createSolvaPayClient(opts) {
|
|
|
20
18
|
console.log(...args);
|
|
21
19
|
}
|
|
22
20
|
};
|
|
23
|
-
log(`\u{1F50C} SolvaPay API Client initialized`);
|
|
24
|
-
log(` Backend URL: ${base}`);
|
|
25
|
-
log(` API Key: ${opts.apiKey.substring(0, 10)}...`);
|
|
26
21
|
return {
|
|
27
22
|
// POST: /v1/sdk/limits
|
|
28
23
|
async checkLimits(params) {
|
|
29
24
|
const url = `${base}/v1/sdk/limits`;
|
|
30
|
-
log(`\u{1F4E1} API Request: POST ${url}`);
|
|
31
|
-
log(` Params:`, JSON.stringify(params, null, 2));
|
|
32
25
|
const res = await fetch(url, {
|
|
33
26
|
method: "POST",
|
|
34
27
|
headers,
|
|
@@ -40,37 +33,27 @@ function createSolvaPayClient(opts) {
|
|
|
40
33
|
throw new SolvaPayError(`Check limits failed (${res.status}): ${error}`);
|
|
41
34
|
}
|
|
42
35
|
const result = await res.json();
|
|
43
|
-
log(`\u2705 API Response:`, JSON.stringify(result, null, 2));
|
|
44
|
-
log(`\u{1F50D} DEBUG - checkLimits breakdown:`);
|
|
45
|
-
log(` - withinLimits: ${result.withinLimits}`);
|
|
46
|
-
log(` - remaining: ${result.remaining}`);
|
|
47
|
-
log(` - plan: ${result.plan || "N/A"}`);
|
|
48
|
-
log(` - checkoutUrl: ${result.checkoutUrl || "N/A"}`);
|
|
49
|
-
log(` - Full response keys:`, Object.keys(result));
|
|
50
36
|
return result;
|
|
51
37
|
},
|
|
52
38
|
// POST: /v1/sdk/usages
|
|
53
39
|
async trackUsage(params) {
|
|
54
40
|
const url = `${base}/v1/sdk/usages`;
|
|
55
|
-
|
|
56
|
-
|
|
41
|
+
const { customerRef, ...rest } = params;
|
|
42
|
+
const body = { ...rest, customerId: customerRef };
|
|
57
43
|
const res = await fetch(url, {
|
|
58
44
|
method: "POST",
|
|
59
45
|
headers,
|
|
60
|
-
body: JSON.stringify(
|
|
46
|
+
body: JSON.stringify(body)
|
|
61
47
|
});
|
|
62
48
|
if (!res.ok) {
|
|
63
49
|
const error = await res.text();
|
|
64
50
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
65
51
|
throw new SolvaPayError(`Track usage failed (${res.status}): ${error}`);
|
|
66
52
|
}
|
|
67
|
-
log(`\u2705 Usage tracked successfully`);
|
|
68
53
|
},
|
|
69
54
|
// POST: /v1/sdk/customers
|
|
70
55
|
async createCustomer(params) {
|
|
71
56
|
const url = `${base}/v1/sdk/customers`;
|
|
72
|
-
log(`\u{1F4E1} API Request: POST ${url}`);
|
|
73
|
-
log(` Params:`, JSON.stringify(params, null, 2));
|
|
74
57
|
const res = await fetch(url, {
|
|
75
58
|
method: "POST",
|
|
76
59
|
headers,
|
|
@@ -82,18 +65,24 @@ function createSolvaPayClient(opts) {
|
|
|
82
65
|
throw new SolvaPayError(`Create customer failed (${res.status}): ${error}`);
|
|
83
66
|
}
|
|
84
67
|
const result = await res.json();
|
|
85
|
-
log(`\u2705 API Response:`, JSON.stringify(result, null, 2));
|
|
86
|
-
log(`\u{1F50D} DEBUG - createCustomer response:`);
|
|
87
|
-
log(` - reference/customerRef: ${result.reference || result.customerRef}`);
|
|
88
|
-
log(` - Has plan info: ${result.plan ? "YES" : "NO"}`);
|
|
89
|
-
log(` - Has subscription info: ${result.subscription ? "YES" : "NO"}`);
|
|
90
|
-
log(` - Full response keys:`, Object.keys(result));
|
|
91
68
|
return result;
|
|
92
69
|
},
|
|
93
|
-
// GET: /v1/sdk/customers/{reference}
|
|
70
|
+
// GET: /v1/sdk/customers/{reference} or /v1/sdk/customers?externalRef={externalRef}|email={email}
|
|
94
71
|
async getCustomer(params) {
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
}
|
|
97
86
|
const res = await fetch(url, {
|
|
98
87
|
method: "GET",
|
|
99
88
|
headers
|
|
@@ -104,14 +93,28 @@ function createSolvaPayClient(opts) {
|
|
|
104
93
|
throw new SolvaPayError(`Get customer failed (${res.status}): ${error}`);
|
|
105
94
|
}
|
|
106
95
|
const result = await res.json();
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
customerRef: customer.reference || customer.customerRef,
|
|
108
|
+
email: customer.email,
|
|
109
|
+
name: customer.name,
|
|
110
|
+
externalRef: customer.externalRef,
|
|
111
|
+
purchases: customer.purchases || []
|
|
112
|
+
};
|
|
109
113
|
},
|
|
110
|
-
//
|
|
111
|
-
// GET: /v1/sdk/
|
|
112
|
-
async
|
|
113
|
-
const url = `${base}/v1/sdk/
|
|
114
|
-
log(`\u{1F4E1} API Request: GET ${url}`);
|
|
114
|
+
// Product management methods (primarily for integration tests)
|
|
115
|
+
// GET: /v1/sdk/products
|
|
116
|
+
async listProducts() {
|
|
117
|
+
const url = `${base}/v1/sdk/products`;
|
|
115
118
|
const res = await fetch(url, {
|
|
116
119
|
method: "GET",
|
|
117
120
|
headers
|
|
@@ -119,21 +122,18 @@ function createSolvaPayClient(opts) {
|
|
|
119
122
|
if (!res.ok) {
|
|
120
123
|
const error = await res.text();
|
|
121
124
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
122
|
-
throw new SolvaPayError(`List
|
|
125
|
+
throw new SolvaPayError(`List products failed (${res.status}): ${error}`);
|
|
123
126
|
}
|
|
124
127
|
const result = await res.json();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
...
|
|
129
|
-
...agent.data || {}
|
|
128
|
+
const products = Array.isArray(result) ? result : result.products || [];
|
|
129
|
+
return products.map((product) => ({
|
|
130
|
+
...product,
|
|
131
|
+
...product.data || {}
|
|
130
132
|
}));
|
|
131
133
|
},
|
|
132
|
-
// POST: /v1/sdk/
|
|
133
|
-
async
|
|
134
|
-
const url = `${base}/v1/sdk/
|
|
135
|
-
log(`\u{1F4E1} API Request: POST ${url}`);
|
|
136
|
-
log(` Params:`, JSON.stringify(params, null, 2));
|
|
134
|
+
// POST: /v1/sdk/products
|
|
135
|
+
async createProduct(params) {
|
|
136
|
+
const url = `${base}/v1/sdk/products`;
|
|
137
137
|
const res = await fetch(url, {
|
|
138
138
|
method: "POST",
|
|
139
139
|
headers,
|
|
@@ -142,16 +142,29 @@ function createSolvaPayClient(opts) {
|
|
|
142
142
|
if (!res.ok) {
|
|
143
143
|
const error = await res.text();
|
|
144
144
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
145
|
-
throw new SolvaPayError(`Create
|
|
145
|
+
throw new SolvaPayError(`Create product failed (${res.status}): ${error}`);
|
|
146
146
|
}
|
|
147
147
|
const result = await res.json();
|
|
148
|
-
log(`\u2705 API Response:`, JSON.stringify(result, null, 2));
|
|
149
148
|
return result;
|
|
150
149
|
},
|
|
151
|
-
//
|
|
152
|
-
async
|
|
153
|
-
const url = `${base}/v1/sdk/
|
|
154
|
-
|
|
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}`;
|
|
155
168
|
const res = await fetch(url, {
|
|
156
169
|
method: "DELETE",
|
|
157
170
|
headers
|
|
@@ -159,14 +172,27 @@ function createSolvaPayClient(opts) {
|
|
|
159
172
|
if (!res.ok && res.status !== 404) {
|
|
160
173
|
const error = await res.text();
|
|
161
174
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
162
|
-
throw new SolvaPayError(`Delete
|
|
175
|
+
throw new SolvaPayError(`Delete product failed (${res.status}): ${error}`);
|
|
163
176
|
}
|
|
164
|
-
log(`\u2705 Agent deleted successfully`);
|
|
165
177
|
},
|
|
166
|
-
//
|
|
167
|
-
async
|
|
168
|
-
const url = `${base}/v1/sdk/
|
|
169
|
-
|
|
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}`);
|
|
190
|
+
}
|
|
191
|
+
return await res.json();
|
|
192
|
+
},
|
|
193
|
+
// GET: /v1/sdk/products/{productRef}/plans
|
|
194
|
+
async listPlans(productRef) {
|
|
195
|
+
const url = `${base}/v1/sdk/products/${productRef}/plans`;
|
|
170
196
|
const res = await fetch(url, {
|
|
171
197
|
method: "GET",
|
|
172
198
|
headers
|
|
@@ -177,18 +203,22 @@ function createSolvaPayClient(opts) {
|
|
|
177
203
|
throw new SolvaPayError(`List plans failed (${res.status}): ${error}`);
|
|
178
204
|
}
|
|
179
205
|
const result = await res.json();
|
|
180
|
-
log(`\u2705 API Response:`, JSON.stringify(result, null, 2));
|
|
181
206
|
const plans = Array.isArray(result) ? result : result.plans || [];
|
|
182
|
-
return plans.map((plan) =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
return plans.map((plan) => {
|
|
208
|
+
const data = plan.data || {};
|
|
209
|
+
const price = plan.price ?? data.price;
|
|
210
|
+
const unwrapped = {
|
|
211
|
+
...data,
|
|
212
|
+
...plan,
|
|
213
|
+
...price !== void 0 && { price }
|
|
214
|
+
};
|
|
215
|
+
delete unwrapped.data;
|
|
216
|
+
return unwrapped;
|
|
217
|
+
});
|
|
186
218
|
},
|
|
187
|
-
// POST: /v1/sdk/
|
|
219
|
+
// POST: /v1/sdk/products/{productRef}/plans
|
|
188
220
|
async createPlan(params) {
|
|
189
|
-
const url = `${base}/v1/sdk/
|
|
190
|
-
log(`\u{1F4E1} API Request: POST ${url}`);
|
|
191
|
-
log(` Params:`, JSON.stringify(params, null, 2));
|
|
221
|
+
const url = `${base}/v1/sdk/products/${params.productRef}/plans`;
|
|
192
222
|
const res = await fetch(url, {
|
|
193
223
|
method: "POST",
|
|
194
224
|
headers,
|
|
@@ -200,13 +230,26 @@ function createSolvaPayClient(opts) {
|
|
|
200
230
|
throw new SolvaPayError(`Create plan failed (${res.status}): ${error}`);
|
|
201
231
|
}
|
|
202
232
|
const result = await res.json();
|
|
203
|
-
log(`\u2705 API Response:`, JSON.stringify(result, null, 2));
|
|
204
233
|
return result;
|
|
205
234
|
},
|
|
206
|
-
//
|
|
207
|
-
async
|
|
208
|
-
const url = `${base}/v1/sdk/
|
|
209
|
-
|
|
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}`;
|
|
210
253
|
const res = await fetch(url, {
|
|
211
254
|
method: "DELETE",
|
|
212
255
|
headers
|
|
@@ -216,17 +259,11 @@ function createSolvaPayClient(opts) {
|
|
|
216
259
|
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
217
260
|
throw new SolvaPayError(`Delete plan failed (${res.status}): ${error}`);
|
|
218
261
|
}
|
|
219
|
-
log(`\u2705 Plan deleted successfully`);
|
|
220
262
|
},
|
|
221
263
|
// POST: /payment-intents
|
|
222
264
|
async createPaymentIntent(params) {
|
|
223
265
|
const idempotencyKey = params.idempotencyKey || `payment-${params.planRef}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
224
266
|
const url = `${base}/v1/sdk/payment-intents`;
|
|
225
|
-
log(`\u{1F4E1} API Request: POST ${url}`);
|
|
226
|
-
log(` Agent Ref: ${params.agentRef}`);
|
|
227
|
-
log(` Plan Ref: ${params.planRef}`);
|
|
228
|
-
log(` Customer Ref: ${params.customerRef}`);
|
|
229
|
-
log(` Idempotency Key: ${idempotencyKey}`);
|
|
230
267
|
const res = await fetch(url, {
|
|
231
268
|
method: "POST",
|
|
232
269
|
headers: {
|
|
@@ -234,7 +271,7 @@ function createSolvaPayClient(opts) {
|
|
|
234
271
|
"Idempotency-Key": idempotencyKey
|
|
235
272
|
},
|
|
236
273
|
body: JSON.stringify({
|
|
237
|
-
|
|
274
|
+
productRef: params.productRef,
|
|
238
275
|
planRef: params.planRef,
|
|
239
276
|
customerReference: params.customerRef
|
|
240
277
|
})
|
|
@@ -245,12 +282,128 @@ function createSolvaPayClient(opts) {
|
|
|
245
282
|
throw new SolvaPayError(`Create payment intent failed (${res.status}): ${error}`);
|
|
246
283
|
}
|
|
247
284
|
const result = await res.json();
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
285
|
+
return result;
|
|
286
|
+
},
|
|
287
|
+
// POST: /v1/sdk/payment-intents/{paymentIntentId}/process
|
|
288
|
+
async processPaymentIntent(params) {
|
|
289
|
+
const url = `${base}/v1/sdk/payment-intents/${params.paymentIntentId}/process`;
|
|
290
|
+
const res = await fetch(url, {
|
|
291
|
+
method: "POST",
|
|
292
|
+
headers: {
|
|
293
|
+
...headers,
|
|
294
|
+
"Content-Type": "application/json"
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
productRef: params.productRef,
|
|
298
|
+
customerRef: params.customerRef,
|
|
299
|
+
planRef: params.planRef
|
|
300
|
+
})
|
|
253
301
|
});
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
const error = await res.text();
|
|
304
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
305
|
+
throw new SolvaPayError(`Process payment failed (${res.status}): ${error}`);
|
|
306
|
+
}
|
|
307
|
+
const result = await res.json();
|
|
308
|
+
return result;
|
|
309
|
+
},
|
|
310
|
+
// POST: /v1/sdk/purchases/{purchaseRef}/cancel
|
|
311
|
+
async cancelPurchase(params) {
|
|
312
|
+
const url = `${base}/v1/sdk/purchases/${params.purchaseRef}/cancel`;
|
|
313
|
+
const requestOptions = {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers
|
|
316
|
+
};
|
|
317
|
+
if (params.reason) {
|
|
318
|
+
requestOptions.body = JSON.stringify({ reason: params.reason });
|
|
319
|
+
}
|
|
320
|
+
const res = await fetch(url, requestOptions);
|
|
321
|
+
if (!res.ok) {
|
|
322
|
+
const error = await res.text();
|
|
323
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
324
|
+
if (res.status === 404) {
|
|
325
|
+
throw new SolvaPayError(`Purchase not found: ${error}`);
|
|
326
|
+
}
|
|
327
|
+
if (res.status === 400) {
|
|
328
|
+
throw new SolvaPayError(
|
|
329
|
+
`Purchase cannot be cancelled or does not belong to provider: ${error}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
throw new SolvaPayError(`Cancel purchase failed (${res.status}): ${error}`);
|
|
333
|
+
}
|
|
334
|
+
const responseText = await res.text();
|
|
335
|
+
let responseData;
|
|
336
|
+
try {
|
|
337
|
+
responseData = JSON.parse(responseText);
|
|
338
|
+
} catch (parseError) {
|
|
339
|
+
log(`\u274C Failed to parse response as JSON: ${parseError}`);
|
|
340
|
+
throw new SolvaPayError(
|
|
341
|
+
`Invalid JSON response from cancel purchase endpoint: ${responseText.substring(0, 200)}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (!responseData || typeof responseData !== "object") {
|
|
345
|
+
log(`\u274C Invalid response structure: ${JSON.stringify(responseData)}`);
|
|
346
|
+
throw new SolvaPayError(`Invalid response structure from cancel purchase endpoint`);
|
|
347
|
+
}
|
|
348
|
+
let result;
|
|
349
|
+
if (responseData.purchase && typeof responseData.purchase === "object") {
|
|
350
|
+
result = responseData.purchase;
|
|
351
|
+
} else if (responseData.reference) {
|
|
352
|
+
result = responseData;
|
|
353
|
+
} else {
|
|
354
|
+
result = responseData.purchase || responseData;
|
|
355
|
+
}
|
|
356
|
+
if (!result || typeof result !== "object") {
|
|
357
|
+
log(`\u274C Invalid purchase data in response. Full response:`, responseData);
|
|
358
|
+
throw new SolvaPayError(`Invalid purchase data in cancel purchase response`);
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
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
|
+
},
|
|
377
|
+
// POST: /v1/sdk/checkout-sessions
|
|
378
|
+
async createCheckoutSession(params) {
|
|
379
|
+
const url = `${base}/v1/sdk/checkout-sessions`;
|
|
380
|
+
const res = await fetch(url, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers,
|
|
383
|
+
body: JSON.stringify(params)
|
|
384
|
+
});
|
|
385
|
+
if (!res.ok) {
|
|
386
|
+
const error = await res.text();
|
|
387
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
388
|
+
throw new SolvaPayError(`Create checkout session failed (${res.status}): ${error}`);
|
|
389
|
+
}
|
|
390
|
+
const result = await res.json();
|
|
391
|
+
return result;
|
|
392
|
+
},
|
|
393
|
+
// POST: /v1/sdk/customers/customer-sessions
|
|
394
|
+
async createCustomerSession(params) {
|
|
395
|
+
const url = `${base}/v1/sdk/customers/customer-sessions`;
|
|
396
|
+
const res = await fetch(url, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers,
|
|
399
|
+
body: JSON.stringify(params)
|
|
400
|
+
});
|
|
401
|
+
if (!res.ok) {
|
|
402
|
+
const error = await res.text();
|
|
403
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
404
|
+
throw new SolvaPayError(`Create customer session failed (${res.status}): ${error}`);
|
|
405
|
+
}
|
|
406
|
+
const result = await res.json();
|
|
254
407
|
return result;
|
|
255
408
|
}
|
|
256
409
|
};
|
|
@@ -302,39 +455,137 @@ function calculateDelay(initialDelay, attempt, strategy) {
|
|
|
302
455
|
function sleep(ms) {
|
|
303
456
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
304
457
|
}
|
|
458
|
+
function createRequestDeduplicator(options = {}) {
|
|
459
|
+
const { cacheTTL = 2e3, maxCacheSize = 1e3, cacheErrors = true } = options;
|
|
460
|
+
const inFlightRequests = /* @__PURE__ */ new Map();
|
|
461
|
+
const resultCache = /* @__PURE__ */ new Map();
|
|
462
|
+
let _cleanupInterval = null;
|
|
463
|
+
if (cacheTTL > 0) {
|
|
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
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const key of entriesToDelete) {
|
|
474
|
+
resultCache.delete(key);
|
|
475
|
+
}
|
|
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
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const deduplicate = async (key, fn) => {
|
|
490
|
+
if (cacheTTL > 0) {
|
|
491
|
+
const cached = resultCache.get(key);
|
|
492
|
+
if (cached && Date.now() - cached.timestamp < cacheTTL) {
|
|
493
|
+
return cached.data;
|
|
494
|
+
} else if (cached) {
|
|
495
|
+
resultCache.delete(key);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
let requestPromise = inFlightRequests.get(key);
|
|
499
|
+
if (!requestPromise) {
|
|
500
|
+
requestPromise = (async () => {
|
|
501
|
+
try {
|
|
502
|
+
const result = await fn();
|
|
503
|
+
if (cacheTTL > 0) {
|
|
504
|
+
resultCache.set(key, {
|
|
505
|
+
data: result,
|
|
506
|
+
timestamp: Date.now()
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (cacheTTL > 0 && cacheErrors) {
|
|
512
|
+
resultCache.set(key, {
|
|
513
|
+
data: error,
|
|
514
|
+
timestamp: Date.now()
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
throw error;
|
|
518
|
+
} finally {
|
|
519
|
+
inFlightRequests.delete(key);
|
|
520
|
+
}
|
|
521
|
+
})();
|
|
522
|
+
const existingPromise = inFlightRequests.get(key);
|
|
523
|
+
if (existingPromise) {
|
|
524
|
+
requestPromise = existingPromise;
|
|
525
|
+
} else {
|
|
526
|
+
inFlightRequests.set(key, requestPromise);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return requestPromise;
|
|
530
|
+
};
|
|
531
|
+
const clearCache = (key) => {
|
|
532
|
+
resultCache.delete(key);
|
|
533
|
+
};
|
|
534
|
+
const clearAllCache = () => {
|
|
535
|
+
resultCache.clear();
|
|
536
|
+
};
|
|
537
|
+
const getStats = () => ({
|
|
538
|
+
inFlight: inFlightRequests.size,
|
|
539
|
+
cached: resultCache.size
|
|
540
|
+
});
|
|
541
|
+
return {
|
|
542
|
+
deduplicate,
|
|
543
|
+
clearCache,
|
|
544
|
+
clearAllCache,
|
|
545
|
+
getStats
|
|
546
|
+
};
|
|
547
|
+
}
|
|
305
548
|
|
|
306
549
|
// src/paywall.ts
|
|
307
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
|
+
*/
|
|
308
557
|
constructor(message, structuredContent) {
|
|
309
558
|
super(message);
|
|
310
559
|
this.structuredContent = structuredContent;
|
|
311
560
|
this.name = "PaywallError";
|
|
312
561
|
}
|
|
313
562
|
};
|
|
563
|
+
var sharedCustomerLookupDeduplicator = createRequestDeduplicator({
|
|
564
|
+
cacheTTL: 6e4,
|
|
565
|
+
// Cache results for 60 seconds (reduces API calls significantly)
|
|
566
|
+
maxCacheSize: 1e3,
|
|
567
|
+
// Maximum cache entries
|
|
568
|
+
cacheErrors: false
|
|
569
|
+
// Don't cache errors - retry on next request
|
|
570
|
+
});
|
|
314
571
|
var SolvaPayPaywall = class {
|
|
315
572
|
constructor(apiClient, options = {}) {
|
|
316
573
|
this.apiClient = apiClient;
|
|
317
|
-
this.debug = options.debug ?? process.env.SOLVAPAY_DEBUG
|
|
574
|
+
this.debug = options.debug ?? process.env.SOLVAPAY_DEBUG === "true";
|
|
575
|
+
this.limitsCacheTTL = options.limitsCacheTTL ?? 1e4;
|
|
318
576
|
}
|
|
319
577
|
customerCreationAttempts = /* @__PURE__ */ new Set();
|
|
320
578
|
customerRefMapping = /* @__PURE__ */ new Map();
|
|
321
|
-
// input ref -> backend ref
|
|
322
579
|
debug;
|
|
580
|
+
limitsCache = /* @__PURE__ */ new Map();
|
|
581
|
+
limitsCacheTTL;
|
|
323
582
|
log(...args) {
|
|
324
583
|
if (this.debug) {
|
|
325
584
|
console.log(...args);
|
|
326
585
|
}
|
|
327
586
|
}
|
|
328
|
-
|
|
329
|
-
return metadata.
|
|
330
|
-
}
|
|
331
|
-
getPackageJsonName() {
|
|
332
|
-
try {
|
|
333
|
-
const pkg = __require(process.cwd() + "/package.json");
|
|
334
|
-
return pkg.name;
|
|
335
|
-
} catch {
|
|
336
|
-
return void 0;
|
|
337
|
-
}
|
|
587
|
+
resolveProduct(metadata) {
|
|
588
|
+
return metadata.product || process.env.SOLVAPAY_PRODUCT || "default-product";
|
|
338
589
|
}
|
|
339
590
|
generateRequestId() {
|
|
340
591
|
const timestamp = Date.now();
|
|
@@ -344,47 +595,122 @@ var SolvaPayPaywall = class {
|
|
|
344
595
|
/**
|
|
345
596
|
* Core protection method - works for both MCP and HTTP
|
|
346
597
|
*/
|
|
598
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
347
599
|
async protect(handler, metadata = {}, getCustomerRef) {
|
|
348
|
-
const
|
|
349
|
-
const
|
|
600
|
+
const product = this.resolveProduct(metadata);
|
|
601
|
+
const configuredPlanRef = metadata.plan?.trim();
|
|
602
|
+
const usagePlanRef = configuredPlanRef || "unspecified";
|
|
603
|
+
const usageType = metadata.usageType || "requests";
|
|
350
604
|
return async (args) => {
|
|
351
605
|
const startTime = Date.now();
|
|
352
606
|
const requestId = this.generateRequestId();
|
|
353
607
|
const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
|
|
354
|
-
|
|
608
|
+
let backendCustomerRef;
|
|
609
|
+
if (inputCustomerRef.startsWith("cus_")) {
|
|
610
|
+
backendCustomerRef = inputCustomerRef;
|
|
611
|
+
} else {
|
|
612
|
+
backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
|
|
613
|
+
}
|
|
614
|
+
let resolvedMeterName;
|
|
355
615
|
try {
|
|
356
|
-
const
|
|
357
|
-
this.
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
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) {
|
|
364
666
|
const latencyMs2 = Date.now() - startTime;
|
|
365
|
-
this.
|
|
366
|
-
|
|
667
|
+
this.trackUsage(
|
|
668
|
+
backendCustomerRef,
|
|
669
|
+
product,
|
|
670
|
+
usagePlanRef,
|
|
671
|
+
resolvedMeterName || usageType,
|
|
672
|
+
"paywall",
|
|
673
|
+
requestId,
|
|
674
|
+
latencyMs2
|
|
675
|
+
);
|
|
367
676
|
throw new PaywallError("Payment required", {
|
|
368
677
|
kind: "payment_required",
|
|
369
|
-
|
|
370
|
-
checkoutUrl:
|
|
371
|
-
message: `
|
|
678
|
+
product,
|
|
679
|
+
checkoutUrl: checkoutUrl || "",
|
|
680
|
+
message: `Purchase required. Remaining: ${remaining}`
|
|
372
681
|
});
|
|
373
682
|
}
|
|
374
|
-
this.log(`\u26A1 Executing handler: ${toolName}`);
|
|
375
683
|
const result = await handler(args);
|
|
376
|
-
this.log(`\u2713 Handler completed successfully`);
|
|
377
684
|
const latencyMs = Date.now() - startTime;
|
|
378
|
-
this.
|
|
379
|
-
|
|
380
|
-
|
|
685
|
+
this.trackUsage(
|
|
686
|
+
backendCustomerRef,
|
|
687
|
+
product,
|
|
688
|
+
usagePlanRef,
|
|
689
|
+
resolvedMeterName || usageType,
|
|
690
|
+
"success",
|
|
691
|
+
requestId,
|
|
692
|
+
latencyMs
|
|
693
|
+
);
|
|
381
694
|
return result;
|
|
382
695
|
} catch (error) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
696
|
+
if (error instanceof Error) {
|
|
697
|
+
const errorType = error instanceof PaywallError ? "PaywallError" : "API Error";
|
|
698
|
+
this.log(`\u274C Error in paywall [${errorType}]: ${error.message}`);
|
|
699
|
+
} else {
|
|
700
|
+
this.log(`\u274C Error in paywall:`, error);
|
|
701
|
+
}
|
|
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
|
+
}
|
|
388
714
|
throw error;
|
|
389
715
|
}
|
|
390
716
|
};
|
|
@@ -394,60 +720,162 @@ var SolvaPayPaywall = class {
|
|
|
394
720
|
* This is a public helper for testing, pre-creating customers, and internal use.
|
|
395
721
|
* Only attempts creation once per customer (idempotent).
|
|
396
722
|
* Returns the backend customer reference to use in API calls.
|
|
723
|
+
*
|
|
724
|
+
* @param customerRef - The customer reference used as a cache key (e.g., Supabase user ID)
|
|
725
|
+
* @param externalRef - Optional external reference for backend lookup (e.g., Supabase user ID)
|
|
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.
|
|
728
|
+
* @param options - Optional customer details (email, name) for customer creation
|
|
397
729
|
*/
|
|
398
|
-
async ensureCustomer(customerRef) {
|
|
730
|
+
async ensureCustomer(customerRef, externalRef, options) {
|
|
399
731
|
if (this.customerRefMapping.has(customerRef)) {
|
|
400
732
|
return this.customerRefMapping.get(customerRef);
|
|
401
733
|
}
|
|
402
734
|
if (customerRef === "anonymous") {
|
|
403
735
|
return customerRef;
|
|
404
736
|
}
|
|
405
|
-
if (
|
|
737
|
+
if (customerRef.startsWith("cus_")) {
|
|
406
738
|
return customerRef;
|
|
407
739
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
740
|
+
const cacheKey = externalRef || customerRef;
|
|
741
|
+
if (this.customerRefMapping.has(customerRef)) {
|
|
742
|
+
const cached = this.customerRefMapping.get(customerRef);
|
|
743
|
+
return cached;
|
|
411
744
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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);
|
|
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}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
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;
|
|
782
|
+
}
|
|
783
|
+
if (externalRef) {
|
|
784
|
+
createParams.externalRef = externalRef;
|
|
785
|
+
}
|
|
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")) {
|
|
794
|
+
if (externalRef) {
|
|
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
|
+
}
|
|
804
|
+
}
|
|
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
|
+
);
|
|
849
|
+
}
|
|
850
|
+
this.log(
|
|
851
|
+
`\u274C Failed to auto-create customer ${customerRef}:`,
|
|
852
|
+
error instanceof Error ? error.message : error
|
|
853
|
+
);
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
if (backendRef !== customerRef) {
|
|
426
858
|
this.customerRefMapping.set(customerRef, backendRef);
|
|
427
|
-
return backendRef;
|
|
428
|
-
} catch (error) {
|
|
429
|
-
this.log(`\u274C Failed to auto-create customer ${customerRef}:`, error instanceof Error ? error.message : error);
|
|
430
|
-
return customerRef;
|
|
431
859
|
}
|
|
860
|
+
return backendRef;
|
|
432
861
|
}
|
|
433
|
-
async trackUsage(customerRef,
|
|
862
|
+
async trackUsage(customerRef, _productRef, _planRef, action, outcome, requestId, actionDuration) {
|
|
434
863
|
await withRetry(
|
|
435
864
|
() => this.apiClient.trackUsage({
|
|
436
865
|
customerRef,
|
|
437
|
-
|
|
438
|
-
|
|
866
|
+
actionType: "api_call",
|
|
867
|
+
units: 1,
|
|
439
868
|
outcome,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
869
|
+
productReference: _productRef,
|
|
870
|
+
duration: actionDuration,
|
|
871
|
+
metadata: { action: action || "api_requests", requestId },
|
|
443
872
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
444
873
|
}),
|
|
445
874
|
{
|
|
446
875
|
maxRetries: 2,
|
|
447
876
|
initialDelay: 500,
|
|
448
877
|
shouldRetry: (error) => error.message.includes("Customer not found"),
|
|
449
|
-
|
|
450
|
-
onRetry: (error, attempt) => {
|
|
878
|
+
onRetry: (_error, attempt) => {
|
|
451
879
|
console.warn(`\u26A0\uFE0F Customer not found (attempt ${attempt + 1}/3), retrying in 500ms...`);
|
|
452
880
|
}
|
|
453
881
|
}
|
|
@@ -466,9 +894,6 @@ var AdapterUtils = class {
|
|
|
466
894
|
if (!customerRef || customerRef === "anonymous") {
|
|
467
895
|
return "anonymous";
|
|
468
896
|
}
|
|
469
|
-
if (!customerRef.startsWith("customer_") && !customerRef.startsWith("demo_")) {
|
|
470
|
-
return `customer_${customerRef.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
471
|
-
}
|
|
472
897
|
return customerRef;
|
|
473
898
|
}
|
|
474
899
|
/**
|
|
@@ -476,7 +901,7 @@ var AdapterUtils = class {
|
|
|
476
901
|
*/
|
|
477
902
|
static async extractFromJWT(token, options) {
|
|
478
903
|
try {
|
|
479
|
-
const { jwtVerify } = await import("./esm-
|
|
904
|
+
const { jwtVerify } = await import("./esm-UW7WCMEK.js");
|
|
480
905
|
const jwtSecret = new TextEncoder().encode(
|
|
481
906
|
options?.secret || process.env.OAUTH_JWKS_SECRET || "test-jwt-secret"
|
|
482
907
|
);
|
|
@@ -485,19 +910,25 @@ var AdapterUtils = class {
|
|
|
485
910
|
audience: options?.audience || process.env.OAUTH_CLIENT_ID || "test-client-id"
|
|
486
911
|
});
|
|
487
912
|
return payload.sub || null;
|
|
488
|
-
} catch
|
|
913
|
+
} catch {
|
|
489
914
|
return null;
|
|
490
915
|
}
|
|
491
916
|
}
|
|
492
917
|
};
|
|
493
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);
|
|
494
922
|
return async (context) => {
|
|
495
923
|
try {
|
|
496
924
|
const args = await adapter.extractArgs(context);
|
|
497
925
|
const customerRef = await adapter.getCustomerRef(context);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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 };
|
|
501
932
|
const result = await protectedHandler(args);
|
|
502
933
|
return adapter.formatResponse(result, context);
|
|
503
934
|
} catch (error) {
|
|
@@ -555,7 +986,7 @@ var HttpAdapter = class {
|
|
|
555
986
|
const errorResponse2 = {
|
|
556
987
|
success: false,
|
|
557
988
|
error: "Payment required",
|
|
558
|
-
|
|
989
|
+
product: error.structuredContent.product,
|
|
559
990
|
checkoutUrl: error.structuredContent.checkoutUrl,
|
|
560
991
|
message: error.structuredContent.message
|
|
561
992
|
};
|
|
@@ -599,7 +1030,7 @@ var NextAdapter = class {
|
|
|
599
1030
|
if (request.method !== "GET" && request.headers.get("content-type")?.includes("application/json")) {
|
|
600
1031
|
body = await request.json();
|
|
601
1032
|
}
|
|
602
|
-
} catch
|
|
1033
|
+
} catch {
|
|
603
1034
|
}
|
|
604
1035
|
let routeParams = {};
|
|
605
1036
|
if (context?.params) {
|
|
@@ -628,6 +1059,10 @@ var NextAdapter = class {
|
|
|
628
1059
|
return AdapterUtils.ensureCustomerRef(jwtSub);
|
|
629
1060
|
}
|
|
630
1061
|
}
|
|
1062
|
+
const userId = request.headers.get("x-user-id");
|
|
1063
|
+
if (userId) {
|
|
1064
|
+
return AdapterUtils.ensureCustomerRef(userId);
|
|
1065
|
+
}
|
|
631
1066
|
const headerRef = request.headers.get("x-customer-ref");
|
|
632
1067
|
if (headerRef) {
|
|
633
1068
|
return AdapterUtils.ensureCustomerRef(headerRef);
|
|
@@ -643,24 +1078,30 @@ var NextAdapter = class {
|
|
|
643
1078
|
}
|
|
644
1079
|
formatError(error, _context) {
|
|
645
1080
|
if (error instanceof PaywallError) {
|
|
646
|
-
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({
|
|
647
1097
|
success: false,
|
|
648
|
-
error: "
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}), {
|
|
653
|
-
status: 402,
|
|
1098
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
1099
|
+
}),
|
|
1100
|
+
{
|
|
1101
|
+
status: 500,
|
|
654
1102
|
headers: { "Content-Type": "application/json" }
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return new Response(JSON.stringify({
|
|
658
|
-
success: false,
|
|
659
|
-
error: error instanceof Error ? error.message : "Internal server error"
|
|
660
|
-
}), {
|
|
661
|
-
status: 500,
|
|
662
|
-
headers: { "Content-Type": "application/json" }
|
|
663
|
-
});
|
|
1103
|
+
}
|
|
1104
|
+
);
|
|
664
1105
|
}
|
|
665
1106
|
};
|
|
666
1107
|
|
|
@@ -677,64 +1118,224 @@ var McpAdapter = class {
|
|
|
677
1118
|
const ref = await this.options.getCustomerRef(args);
|
|
678
1119
|
return AdapterUtils.ensureCustomerRef(ref);
|
|
679
1120
|
}
|
|
680
|
-
const
|
|
1121
|
+
const auth = args?.auth;
|
|
1122
|
+
const customerRef = auth?.customer_ref || "anonymous";
|
|
681
1123
|
return AdapterUtils.ensureCustomerRef(customerRef);
|
|
682
1124
|
}
|
|
683
1125
|
formatResponse(result, _context) {
|
|
684
1126
|
const transformed = this.options.transformResponse ? this.options.transformResponse(result) : result;
|
|
685
1127
|
return {
|
|
686
|
-
content: [
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1128
|
+
content: [
|
|
1129
|
+
{
|
|
1130
|
+
type: "text",
|
|
1131
|
+
text: JSON.stringify(transformed, null, 2)
|
|
1132
|
+
}
|
|
1133
|
+
]
|
|
690
1134
|
};
|
|
691
1135
|
}
|
|
692
1136
|
formatError(error, _context) {
|
|
693
1137
|
if (error instanceof PaywallError) {
|
|
694
1138
|
return {
|
|
695
|
-
content: [
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
+
],
|
|
705
1155
|
isError: true,
|
|
706
1156
|
structuredContent: error.structuredContent
|
|
707
1157
|
};
|
|
708
1158
|
}
|
|
709
1159
|
return {
|
|
710
|
-
content: [
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
+
],
|
|
717
1173
|
isError: true
|
|
718
1174
|
};
|
|
719
1175
|
}
|
|
720
1176
|
};
|
|
721
1177
|
|
|
722
1178
|
// src/factory.ts
|
|
723
|
-
import { SolvaPayError as SolvaPayError2 } from "@solvapay/core";
|
|
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
|
|
724
1314
|
function createSolvaPay(config) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1315
|
+
let resolvedConfig;
|
|
1316
|
+
if (!config) {
|
|
1317
|
+
const envConfig = getSolvaPayConfig();
|
|
1318
|
+
resolvedConfig = {
|
|
1319
|
+
apiKey: envConfig.apiKey,
|
|
1320
|
+
apiBaseUrl: envConfig.apiBaseUrl
|
|
1321
|
+
};
|
|
1322
|
+
} else {
|
|
1323
|
+
resolvedConfig = config;
|
|
1324
|
+
}
|
|
1325
|
+
const apiClient = resolvedConfig.apiClient || createSolvaPayClient({
|
|
1326
|
+
apiKey: resolvedConfig.apiKey,
|
|
1327
|
+
apiBaseUrl: resolvedConfig.apiBaseUrl
|
|
728
1328
|
});
|
|
729
1329
|
const paywall = new SolvaPayPaywall(apiClient, {
|
|
730
|
-
debug: process.env.SOLVAPAY_DEBUG !== "false"
|
|
1330
|
+
debug: process.env.SOLVAPAY_DEBUG !== "false",
|
|
1331
|
+
limitsCacheTTL: resolvedConfig.limitsCacheTTL
|
|
731
1332
|
});
|
|
732
1333
|
return {
|
|
733
1334
|
// Direct access to API client for advanced operations
|
|
734
1335
|
apiClient,
|
|
735
1336
|
// Common API methods exposed directly for convenience
|
|
736
|
-
ensureCustomer(customerRef) {
|
|
737
|
-
return paywall.ensureCustomer(customerRef);
|
|
1337
|
+
ensureCustomer(customerRef, externalRef, options) {
|
|
1338
|
+
return paywall.ensureCustomer(customerRef, externalRef, options);
|
|
738
1339
|
},
|
|
739
1340
|
createPaymentIntent(params) {
|
|
740
1341
|
if (!apiClient.createPaymentIntent) {
|
|
@@ -742,6 +1343,12 @@ function createSolvaPay(config) {
|
|
|
742
1343
|
}
|
|
743
1344
|
return apiClient.createPaymentIntent(params);
|
|
744
1345
|
},
|
|
1346
|
+
processPaymentIntent(params) {
|
|
1347
|
+
if (!apiClient.processPaymentIntent) {
|
|
1348
|
+
throw new SolvaPayError2("processPaymentIntent is not available on this API client");
|
|
1349
|
+
}
|
|
1350
|
+
return apiClient.processPaymentIntent(params);
|
|
1351
|
+
},
|
|
745
1352
|
checkLimits(params) {
|
|
746
1353
|
return apiClient.checkLimits(params);
|
|
747
1354
|
},
|
|
@@ -755,71 +1362,395 @@ function createSolvaPay(config) {
|
|
|
755
1362
|
return apiClient.createCustomer(params);
|
|
756
1363
|
},
|
|
757
1364
|
getCustomer(params) {
|
|
758
|
-
if (!apiClient.getCustomer) {
|
|
759
|
-
throw new SolvaPayError2("getCustomer is not available on this API client");
|
|
760
|
-
}
|
|
761
1365
|
return apiClient.getCustomer(params);
|
|
762
1366
|
},
|
|
1367
|
+
createCheckoutSession(params) {
|
|
1368
|
+
return apiClient.createCheckoutSession({
|
|
1369
|
+
customerReference: params.customerRef,
|
|
1370
|
+
productRef: params.productRef,
|
|
1371
|
+
planRef: params.planRef
|
|
1372
|
+
});
|
|
1373
|
+
},
|
|
1374
|
+
createCustomerSession(params) {
|
|
1375
|
+
return apiClient.createCustomerSession(params);
|
|
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
|
+
},
|
|
763
1386
|
// Payable API for framework-specific handlers
|
|
764
1387
|
payable(options = {}) {
|
|
765
|
-
const
|
|
766
|
-
const plan = options.planRef || options.plan
|
|
767
|
-
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 };
|
|
768
1392
|
return {
|
|
769
|
-
//
|
|
1393
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
770
1394
|
http(businessLogic, adapterOptions) {
|
|
771
|
-
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);
|
|
772
1400
|
return async (req, reply) => {
|
|
773
|
-
const handler = await
|
|
774
|
-
adapter,
|
|
775
|
-
paywall,
|
|
776
|
-
metadata,
|
|
777
|
-
businessLogic
|
|
778
|
-
);
|
|
1401
|
+
const handler = await handlerPromise;
|
|
779
1402
|
return handler([req, reply]);
|
|
780
1403
|
};
|
|
781
1404
|
},
|
|
782
|
-
//
|
|
1405
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
783
1406
|
next(businessLogic, adapterOptions) {
|
|
784
|
-
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);
|
|
785
1412
|
return async (request, context) => {
|
|
786
|
-
const handler = await
|
|
787
|
-
adapter,
|
|
788
|
-
paywall,
|
|
789
|
-
metadata,
|
|
790
|
-
businessLogic
|
|
791
|
-
);
|
|
1413
|
+
const handler = await handlerPromise;
|
|
792
1414
|
return handler([request, context]);
|
|
793
1415
|
};
|
|
794
1416
|
},
|
|
795
|
-
//
|
|
1417
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
796
1418
|
mcp(businessLogic, adapterOptions) {
|
|
797
|
-
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);
|
|
798
1424
|
return async (args) => {
|
|
799
|
-
const handler = await
|
|
800
|
-
adapter,
|
|
801
|
-
paywall,
|
|
802
|
-
metadata,
|
|
803
|
-
businessLogic
|
|
804
|
-
);
|
|
1425
|
+
const handler = await handlerPromise;
|
|
805
1426
|
return handler(args);
|
|
806
1427
|
};
|
|
807
1428
|
},
|
|
808
|
-
//
|
|
1429
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
809
1430
|
async function(businessLogic) {
|
|
810
|
-
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
|
+
};
|
|
811
1438
|
return paywall.protect(businessLogic, metadata, getCustomerRef);
|
|
812
1439
|
}
|
|
813
1440
|
};
|
|
814
1441
|
}
|
|
815
1442
|
};
|
|
816
1443
|
}
|
|
817
|
-
|
|
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 = {}) {
|
|
1471
|
+
try {
|
|
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) {
|
|
818
1719
|
try {
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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");
|
|
823
1754
|
}
|
|
824
1755
|
}
|
|
825
1756
|
|
|
@@ -840,14 +1771,24 @@ async function verifyWebhook({
|
|
|
840
1771
|
const sigBuf = await crypto.subtle.sign("HMAC", key, enc.encode(body));
|
|
841
1772
|
const hex = Array.from(new Uint8Array(sigBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
842
1773
|
if (hex !== signature) {
|
|
843
|
-
throw new
|
|
1774
|
+
throw new SolvaPayError5("Invalid webhook signature");
|
|
844
1775
|
}
|
|
845
1776
|
return JSON.parse(body);
|
|
846
1777
|
}
|
|
847
1778
|
export {
|
|
848
1779
|
PaywallError,
|
|
1780
|
+
cancelPurchaseCore,
|
|
1781
|
+
createCheckoutSessionCore,
|
|
1782
|
+
createCustomerSessionCore,
|
|
1783
|
+
createPaymentIntentCore,
|
|
849
1784
|
createSolvaPay,
|
|
850
1785
|
createSolvaPayClient,
|
|
1786
|
+
getAuthenticatedUserCore,
|
|
1787
|
+
handleRouteError,
|
|
1788
|
+
isErrorResult,
|
|
1789
|
+
listPlansCore,
|
|
1790
|
+
processPaymentIntentCore,
|
|
1791
|
+
syncCustomerCore,
|
|
851
1792
|
verifyWebhook,
|
|
852
1793
|
withRetry
|
|
853
1794
|
};
|