@solvapay/server 1.0.8-preview.8 → 1.0.8
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/README.md +80 -2
- package/dist/edge.d.ts +495 -61
- package/dist/edge.js +648 -187
- package/dist/fetch/index.cjs +3071 -0
- package/dist/fetch/index.d.cts +112 -0
- package/dist/fetch/index.d.ts +112 -0
- package/dist/fetch/index.js +3017 -0
- package/dist/index.cjs +603 -5013
- package/dist/index.d.cts +490 -156
- package/dist/index.d.ts +490 -156
- package/dist/index.js +571 -695
- package/package.json +15 -12
- package/dist/chunk-MLKGABMK.js +0 -9
- package/dist/chunk-R5U7XKVJ.js +0 -16
- package/dist/dist-EPVKJAIP.js +0 -94
- package/dist/dist-JBJ4HMP7.js +0 -94
- package/dist/esm-5GYCIXIY.js +0 -3475
- package/dist/esm-UW7WCMEK.js +0 -3475
- package/dist/src/adapters/base.d.ts +0 -57
- package/dist/src/adapters/base.d.ts.map +0 -1
- package/dist/src/adapters/base.js +0 -73
- package/dist/src/adapters/base.js.map +0 -1
- package/dist/src/adapters/http.d.ts +0 -25
- package/dist/src/adapters/http.d.ts.map +0 -1
- package/dist/src/adapters/http.js +0 -99
- package/dist/src/adapters/http.js.map +0 -1
- package/dist/src/adapters/index.d.ts +0 -11
- package/dist/src/adapters/index.d.ts.map +0 -1
- package/dist/src/adapters/index.js +0 -10
- package/dist/src/adapters/index.js.map +0 -1
- package/dist/src/adapters/mcp.d.ts +0 -24
- package/dist/src/adapters/mcp.d.ts.map +0 -1
- package/dist/src/adapters/mcp.js +0 -75
- package/dist/src/adapters/mcp.js.map +0 -1
- package/dist/src/adapters/next.d.ts +0 -24
- package/dist/src/adapters/next.d.ts.map +0 -1
- package/dist/src/adapters/next.js +0 -109
- package/dist/src/adapters/next.js.map +0 -1
- package/dist/src/client.d.ts +0 -58
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js +0 -495
- package/dist/src/client.js.map +0 -1
- package/dist/src/edge.d.ts +0 -22
- package/dist/src/edge.d.ts.map +0 -1
- package/dist/src/edge.js +0 -91
- package/dist/src/edge.js.map +0 -1
- package/dist/src/factory.d.ts +0 -605
- package/dist/src/factory.d.ts.map +0 -1
- package/dist/src/factory.js +0 -215
- package/dist/src/factory.js.map +0 -1
- package/dist/src/helpers/auth.d.ts +0 -47
- package/dist/src/helpers/auth.d.ts.map +0 -1
- package/dist/src/helpers/auth.js +0 -73
- package/dist/src/helpers/auth.js.map +0 -1
- package/dist/src/helpers/checkout.d.ts +0 -45
- package/dist/src/helpers/checkout.d.ts.map +0 -1
- package/dist/src/helpers/checkout.js +0 -89
- package/dist/src/helpers/checkout.js.map +0 -1
- package/dist/src/helpers/customer.d.ts +0 -51
- package/dist/src/helpers/customer.d.ts.map +0 -1
- package/dist/src/helpers/customer.js +0 -77
- package/dist/src/helpers/customer.js.map +0 -1
- package/dist/src/helpers/error.d.ts +0 -15
- package/dist/src/helpers/error.d.ts.map +0 -1
- package/dist/src/helpers/error.js +0 -35
- package/dist/src/helpers/error.js.map +0 -1
- package/dist/src/helpers/index.d.ts +0 -17
- package/dist/src/helpers/index.d.ts.map +0 -1
- package/dist/src/helpers/index.js +0 -23
- package/dist/src/helpers/index.js.map +0 -1
- package/dist/src/helpers/payment.d.ts +0 -107
- package/dist/src/helpers/payment.d.ts.map +0 -1
- package/dist/src/helpers/payment.js +0 -150
- package/dist/src/helpers/payment.js.map +0 -1
- package/dist/src/helpers/plans.d.ts +0 -19
- package/dist/src/helpers/plans.d.ts.map +0 -1
- package/dist/src/helpers/plans.js +0 -53
- package/dist/src/helpers/plans.js.map +0 -1
- package/dist/src/helpers/renewal.d.ts +0 -23
- package/dist/src/helpers/renewal.d.ts.map +0 -1
- package/dist/src/helpers/renewal.js +0 -90
- package/dist/src/helpers/renewal.js.map +0 -1
- package/dist/src/helpers/subscription.d.ts +0 -23
- package/dist/src/helpers/subscription.d.ts.map +0 -1
- package/dist/src/helpers/subscription.js +0 -101
- package/dist/src/helpers/subscription.js.map +0 -1
- package/dist/src/helpers/types.d.ts +0 -22
- package/dist/src/helpers/types.d.ts.map +0 -1
- package/dist/src/helpers/types.js +0 -7
- package/dist/src/helpers/types.js.map +0 -1
- package/dist/src/index.d.ts +0 -73
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -106
- package/dist/src/index.js.map +0 -1
- package/dist/src/mcp/auth-bridge.d.ts +0 -9
- package/dist/src/mcp/auth-bridge.d.ts.map +0 -1
- package/dist/src/mcp/auth-bridge.js +0 -46
- package/dist/src/mcp/auth-bridge.js.map +0 -1
- package/dist/src/mcp/oauth-bridge.d.ts +0 -48
- package/dist/src/mcp/oauth-bridge.d.ts.map +0 -1
- package/dist/src/mcp/oauth-bridge.js +0 -110
- package/dist/src/mcp/oauth-bridge.js.map +0 -1
- package/dist/src/mcp-auth.d.ts +0 -17
- package/dist/src/mcp-auth.d.ts.map +0 -1
- package/dist/src/mcp-auth.js +0 -57
- package/dist/src/mcp-auth.js.map +0 -1
- package/dist/src/paywall.d.ts +0 -119
- package/dist/src/paywall.d.ts.map +0 -1
- package/dist/src/paywall.js +0 -700
- package/dist/src/paywall.js.map +0 -1
- package/dist/src/register-virtual-tools-mcp.d.ts +0 -23
- package/dist/src/register-virtual-tools-mcp.d.ts.map +0 -1
- package/dist/src/register-virtual-tools-mcp.js +0 -86
- package/dist/src/register-virtual-tools-mcp.js.map +0 -1
- package/dist/src/types/client.d.ts +0 -216
- package/dist/src/types/client.d.ts.map +0 -1
- package/dist/src/types/client.js +0 -7
- package/dist/src/types/client.js.map +0 -1
- package/dist/src/types/generated.d.ts +0 -2834
- package/dist/src/types/generated.d.ts.map +0 -1
- package/dist/src/types/generated.js +0 -6
- package/dist/src/types/generated.js.map +0 -1
- package/dist/src/types/index.d.ts +0 -13
- package/dist/src/types/index.d.ts.map +0 -1
- package/dist/src/types/index.js +0 -8
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/types/options.d.ts +0 -126
- package/dist/src/types/options.d.ts.map +0 -1
- package/dist/src/types/options.js +0 -8
- package/dist/src/types/options.js.map +0 -1
- package/dist/src/types/paywall.d.ts +0 -64
- package/dist/src/types/paywall.d.ts.map +0 -1
- package/dist/src/types/paywall.js +0 -7
- package/dist/src/types/paywall.js.map +0 -1
- package/dist/src/types/webhook.d.ts +0 -50
- package/dist/src/types/webhook.d.ts.map +0 -1
- package/dist/src/types/webhook.js +0 -2
- package/dist/src/types/webhook.js.map +0 -1
- package/dist/src/utils.d.ts +0 -110
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/src/utils.js +0 -217
- package/dist/src/utils.js.map +0 -1
- package/dist/src/virtual-tools.d.ts +0 -44
- package/dist/src/virtual-tools.d.ts.map +0 -1
- package/dist/src/virtual-tools.js +0 -140
- package/dist/src/virtual-tools.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/webapi-K5XBCEO6.js +0 -3775
|
@@ -0,0 +1,3071 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/fetch/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
activatePlan: () => activatePlan,
|
|
34
|
+
cancelRenewal: () => cancelRenewal,
|
|
35
|
+
checkPurchase: () => checkPurchase,
|
|
36
|
+
configureCors: () => configureCors,
|
|
37
|
+
createCheckoutSession: () => createCheckoutSession,
|
|
38
|
+
createCustomerSession: () => createCustomerSession,
|
|
39
|
+
createPaymentIntent: () => createPaymentIntent,
|
|
40
|
+
createTopupPaymentIntent: () => createTopupPaymentIntent,
|
|
41
|
+
customerBalance: () => customerBalance,
|
|
42
|
+
getMerchant: () => getMerchant,
|
|
43
|
+
getPaymentMethod: () => getPaymentMethod,
|
|
44
|
+
getProduct: () => getProduct,
|
|
45
|
+
listPlans: () => listPlans,
|
|
46
|
+
processPayment: () => processPayment,
|
|
47
|
+
reactivateRenewal: () => reactivateRenewal,
|
|
48
|
+
solvapayWebhook: () => solvapayWebhook,
|
|
49
|
+
syncCustomer: () => syncCustomer,
|
|
50
|
+
trackUsage: () => trackUsage
|
|
51
|
+
});
|
|
52
|
+
module.exports = __toCommonJS(index_exports);
|
|
53
|
+
|
|
54
|
+
// src/helpers/error.ts
|
|
55
|
+
var import_core = require("@solvapay/core");
|
|
56
|
+
function isErrorResult(result) {
|
|
57
|
+
return typeof result === "object" && result !== null && "error" in result && "status" in result;
|
|
58
|
+
}
|
|
59
|
+
function handleRouteError(error, operationName, defaultMessage) {
|
|
60
|
+
console.error(`[${operationName}] Error:`, error);
|
|
61
|
+
if (error instanceof import_core.SolvaPayError) {
|
|
62
|
+
const errorMessage2 = error.message;
|
|
63
|
+
return {
|
|
64
|
+
error: errorMessage2,
|
|
65
|
+
status: 500,
|
|
66
|
+
details: errorMessage2
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
70
|
+
const message = defaultMessage || `${operationName} failed`;
|
|
71
|
+
return {
|
|
72
|
+
error: message,
|
|
73
|
+
status: 500,
|
|
74
|
+
details: errorMessage
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/helpers/auth.ts
|
|
79
|
+
function base64UrlDecode(input) {
|
|
80
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
81
|
+
const padding = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
|
|
82
|
+
const base64 = padded + padding;
|
|
83
|
+
if (typeof atob === "function") {
|
|
84
|
+
const binary = atob(base64);
|
|
85
|
+
let result = "";
|
|
86
|
+
for (let i = 0; i < binary.length; i++) {
|
|
87
|
+
result += String.fromCharCode(binary.charCodeAt(i));
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return decodeURIComponent(escape(result));
|
|
91
|
+
} catch {
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const BufferCtor = globalThis.Buffer;
|
|
96
|
+
if (BufferCtor) {
|
|
97
|
+
return BufferCtor.from(base64, "base64").toString("utf-8");
|
|
98
|
+
}
|
|
99
|
+
throw new Error("No base64 decoder available in this runtime");
|
|
100
|
+
}
|
|
101
|
+
function decodeJwtUnverified(token) {
|
|
102
|
+
const parts = token.split(".");
|
|
103
|
+
if (parts.length !== 3) return null;
|
|
104
|
+
try {
|
|
105
|
+
const json = base64UrlDecode(parts[1]);
|
|
106
|
+
const payload = JSON.parse(json);
|
|
107
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
108
|
+
return payload;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function extractBearerToken(request) {
|
|
114
|
+
const authHeader = request.headers.get("authorization");
|
|
115
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("bearer ")) return null;
|
|
116
|
+
const token = authHeader.slice(7).trim();
|
|
117
|
+
return token.length > 0 ? token : null;
|
|
118
|
+
}
|
|
119
|
+
function readEnv(name) {
|
|
120
|
+
const proc = globalThis.process;
|
|
121
|
+
return proc?.env?.[name];
|
|
122
|
+
}
|
|
123
|
+
function isStrictMode() {
|
|
124
|
+
return readEnv("SOLVAPAY_AUTH_STRICT") === "true";
|
|
125
|
+
}
|
|
126
|
+
function getConfiguredSecret() {
|
|
127
|
+
return readEnv("SOLVAPAY_JWT_SECRET") || readEnv("SUPABASE_JWT_SECRET");
|
|
128
|
+
}
|
|
129
|
+
async function verifyHs256(token, secret) {
|
|
130
|
+
try {
|
|
131
|
+
const { jwtVerify } = await import("jose");
|
|
132
|
+
const key = new TextEncoder().encode(secret);
|
|
133
|
+
const { payload } = await jwtVerify(token, key, { algorithms: ["HS256"] });
|
|
134
|
+
return payload;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function pickName(payload) {
|
|
140
|
+
const metadataFullName = typeof payload.user_metadata?.full_name === "string" ? payload.user_metadata.full_name : null;
|
|
141
|
+
const metadataName = typeof payload.user_metadata?.name === "string" ? payload.user_metadata.name : null;
|
|
142
|
+
const claimName = typeof payload.name === "string" ? payload.name : null;
|
|
143
|
+
return metadataFullName || metadataName || claimName || null;
|
|
144
|
+
}
|
|
145
|
+
function pickEmail(payload) {
|
|
146
|
+
return typeof payload.email === "string" ? payload.email : null;
|
|
147
|
+
}
|
|
148
|
+
function unauthorized(details) {
|
|
149
|
+
return { error: "Unauthorized", status: 401, details };
|
|
150
|
+
}
|
|
151
|
+
async function getAuthenticatedUserCore(request, options = {}) {
|
|
152
|
+
try {
|
|
153
|
+
const includeEmail = options.includeEmail !== false;
|
|
154
|
+
const includeName = options.includeName !== false;
|
|
155
|
+
const headerUserId = request.headers.get("x-user-id");
|
|
156
|
+
if (headerUserId) {
|
|
157
|
+
let email = null;
|
|
158
|
+
let name = null;
|
|
159
|
+
if (includeEmail || includeName) {
|
|
160
|
+
const token2 = extractBearerToken(request);
|
|
161
|
+
if (token2) {
|
|
162
|
+
const secret2 = getConfiguredSecret();
|
|
163
|
+
const payload2 = secret2 ? await verifyHs256(token2, secret2) : isStrictMode() ? null : decodeJwtUnverified(token2);
|
|
164
|
+
if (payload2) {
|
|
165
|
+
if (includeEmail) email = pickEmail(payload2);
|
|
166
|
+
if (includeName) name = pickName(payload2);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { userId: headerUserId, email, name };
|
|
171
|
+
}
|
|
172
|
+
const token = extractBearerToken(request);
|
|
173
|
+
if (!token) {
|
|
174
|
+
return unauthorized("User ID not found. Ensure middleware is configured.");
|
|
175
|
+
}
|
|
176
|
+
const secret = getConfiguredSecret();
|
|
177
|
+
let payload = null;
|
|
178
|
+
if (secret) {
|
|
179
|
+
payload = await verifyHs256(token, secret);
|
|
180
|
+
if (!payload) {
|
|
181
|
+
return unauthorized("Invalid or expired authentication token");
|
|
182
|
+
}
|
|
183
|
+
} else if (isStrictMode()) {
|
|
184
|
+
return unauthorized(
|
|
185
|
+
"Strict auth mode is enabled but no JWT secret is configured. Set SOLVAPAY_JWT_SECRET or SUPABASE_JWT_SECRET."
|
|
186
|
+
);
|
|
187
|
+
} else {
|
|
188
|
+
payload = decodeJwtUnverified(token);
|
|
189
|
+
if (!payload) {
|
|
190
|
+
return unauthorized("Malformed authentication token");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const userId = typeof payload.sub === "string" ? payload.sub : null;
|
|
194
|
+
if (!userId) {
|
|
195
|
+
return unauthorized("Authentication token missing subject (sub) claim");
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
userId,
|
|
199
|
+
email: includeEmail ? pickEmail(payload) : null,
|
|
200
|
+
name: includeName ? pickName(payload) : null
|
|
201
|
+
};
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return handleRouteError(error, "Get authenticated user", "Authentication failed");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/client.ts
|
|
208
|
+
var import_core2 = require("@solvapay/core");
|
|
209
|
+
function createSolvaPayClient(opts) {
|
|
210
|
+
const base = opts.apiBaseUrl ?? "https://api.solvapay.com";
|
|
211
|
+
if (!opts.apiKey) throw new import_core2.SolvaPayError("Missing apiKey");
|
|
212
|
+
const headers = {
|
|
213
|
+
"Content-Type": "application/json",
|
|
214
|
+
Authorization: `Bearer ${opts.apiKey}`
|
|
215
|
+
};
|
|
216
|
+
const debug = process.env.SOLVAPAY_DEBUG === "true";
|
|
217
|
+
const log = (...args) => {
|
|
218
|
+
if (debug) {
|
|
219
|
+
console.log(...args);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
// POST: /v1/sdk/limits
|
|
224
|
+
async checkLimits(params) {
|
|
225
|
+
const url = `${base}/v1/sdk/limits`;
|
|
226
|
+
const res = await fetch(url, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers,
|
|
229
|
+
body: JSON.stringify(params)
|
|
230
|
+
});
|
|
231
|
+
if (!res.ok) {
|
|
232
|
+
const error = await res.text();
|
|
233
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
234
|
+
throw new import_core2.SolvaPayError(`Check limits failed (${res.status}): ${error}`);
|
|
235
|
+
}
|
|
236
|
+
const result = await res.json();
|
|
237
|
+
return result;
|
|
238
|
+
},
|
|
239
|
+
// POST: /v1/sdk/usages
|
|
240
|
+
async trackUsage(params) {
|
|
241
|
+
const url = `${base}/v1/sdk/usages`;
|
|
242
|
+
const res = await fetch(url, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers,
|
|
245
|
+
body: JSON.stringify(params)
|
|
246
|
+
});
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
const error = await res.text();
|
|
249
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
250
|
+
throw new import_core2.SolvaPayError(`Track usage failed (${res.status}): ${error}`);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
// POST: /v1/sdk/customers
|
|
254
|
+
async createCustomer(params) {
|
|
255
|
+
const url = `${base}/v1/sdk/customers`;
|
|
256
|
+
const res = await fetch(url, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers,
|
|
259
|
+
body: JSON.stringify(params)
|
|
260
|
+
});
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
const error = await res.text();
|
|
263
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
264
|
+
throw new import_core2.SolvaPayError(`Create customer failed (${res.status}): ${error}`);
|
|
265
|
+
}
|
|
266
|
+
const result = await res.json();
|
|
267
|
+
return {
|
|
268
|
+
customerRef: result.reference || result.customerRef
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
// PATCH: /v1/sdk/customers/{customerRef}
|
|
272
|
+
async updateCustomer(customerRef, params) {
|
|
273
|
+
const url = `${base}/v1/sdk/customers/${encodeURIComponent(customerRef)}`;
|
|
274
|
+
const res = await fetch(url, {
|
|
275
|
+
method: "PATCH",
|
|
276
|
+
headers,
|
|
277
|
+
body: JSON.stringify(params)
|
|
278
|
+
});
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
const error = await res.text();
|
|
281
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
282
|
+
throw new import_core2.SolvaPayError(`Update customer failed (${res.status}): ${error}`);
|
|
283
|
+
}
|
|
284
|
+
const result = await res.json();
|
|
285
|
+
return {
|
|
286
|
+
customerRef: result.reference || result.customerRef || customerRef
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
// GET: /v1/sdk/customers/{reference} or /v1/sdk/customers?externalRef={externalRef}|email={email}
|
|
290
|
+
async getCustomer(params) {
|
|
291
|
+
let url;
|
|
292
|
+
let isByExternalRef = false;
|
|
293
|
+
let isByEmail = false;
|
|
294
|
+
if (params.externalRef) {
|
|
295
|
+
url = `${base}/v1/sdk/customers?externalRef=${encodeURIComponent(params.externalRef)}`;
|
|
296
|
+
isByExternalRef = true;
|
|
297
|
+
} else if (params.email) {
|
|
298
|
+
url = `${base}/v1/sdk/customers?email=${encodeURIComponent(params.email)}`;
|
|
299
|
+
isByEmail = true;
|
|
300
|
+
} else if (params.customerRef) {
|
|
301
|
+
url = `${base}/v1/sdk/customers/${params.customerRef}`;
|
|
302
|
+
} else {
|
|
303
|
+
throw new import_core2.SolvaPayError("One of customerRef, externalRef, or email must be provided");
|
|
304
|
+
}
|
|
305
|
+
const res = await fetch(url, {
|
|
306
|
+
method: "GET",
|
|
307
|
+
headers
|
|
308
|
+
});
|
|
309
|
+
if (!res.ok) {
|
|
310
|
+
const error = await res.text();
|
|
311
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
312
|
+
throw new import_core2.SolvaPayError(`Get customer failed (${res.status}): ${error}`);
|
|
313
|
+
}
|
|
314
|
+
const result = await res.json();
|
|
315
|
+
let customer = result;
|
|
316
|
+
if (isByExternalRef || isByEmail) {
|
|
317
|
+
const directCustomer = result && typeof result === "object" && (result.reference || result.customerRef || result.externalRef) ? result : void 0;
|
|
318
|
+
const wrappedCustomer = result && typeof result === "object" && result.customer ? result.customer : void 0;
|
|
319
|
+
const customers = Array.isArray(result) ? result : result && typeof result === "object" && Array.isArray(result.customers) ? result.customers : [];
|
|
320
|
+
customer = directCustomer || wrappedCustomer || customers[0];
|
|
321
|
+
if (!customer) {
|
|
322
|
+
throw new import_core2.SolvaPayError(`No customer found with externalRef: ${params.externalRef}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
customerRef: customer.reference || customer.customerRef,
|
|
327
|
+
email: customer.email,
|
|
328
|
+
name: customer.name,
|
|
329
|
+
externalRef: customer.externalRef,
|
|
330
|
+
purchases: customer.purchases || []
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
// GET: /v1/sdk/merchant
|
|
334
|
+
async getMerchant() {
|
|
335
|
+
const url = `${base}/v1/sdk/merchant`;
|
|
336
|
+
const res = await fetch(url, {
|
|
337
|
+
method: "GET",
|
|
338
|
+
headers
|
|
339
|
+
});
|
|
340
|
+
if (!res.ok) {
|
|
341
|
+
const error = await res.text();
|
|
342
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
343
|
+
throw new import_core2.SolvaPayError(`Get merchant failed (${res.status}): ${error}`);
|
|
344
|
+
}
|
|
345
|
+
return res.json();
|
|
346
|
+
},
|
|
347
|
+
// GET: /v1/sdk/platform-config
|
|
348
|
+
async getPlatformConfig() {
|
|
349
|
+
const url = `${base}/v1/sdk/platform-config`;
|
|
350
|
+
const res = await fetch(url, {
|
|
351
|
+
method: "GET",
|
|
352
|
+
headers
|
|
353
|
+
});
|
|
354
|
+
if (!res.ok) {
|
|
355
|
+
const error = await res.text();
|
|
356
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
357
|
+
throw new import_core2.SolvaPayError(`Get platform config failed (${res.status}): ${error}`);
|
|
358
|
+
}
|
|
359
|
+
return res.json();
|
|
360
|
+
},
|
|
361
|
+
// GET: /v1/sdk/products/{productRef}
|
|
362
|
+
async getProduct(productRef) {
|
|
363
|
+
const url = `${base}/v1/sdk/products/${encodeURIComponent(productRef)}`;
|
|
364
|
+
const res = await fetch(url, {
|
|
365
|
+
method: "GET",
|
|
366
|
+
headers
|
|
367
|
+
});
|
|
368
|
+
if (!res.ok) {
|
|
369
|
+
const error = await res.text();
|
|
370
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
371
|
+
throw new import_core2.SolvaPayError(`Get product failed (${res.status}): ${error}`);
|
|
372
|
+
}
|
|
373
|
+
const result = await res.json();
|
|
374
|
+
const data = result.data || {};
|
|
375
|
+
return { ...data, ...result };
|
|
376
|
+
},
|
|
377
|
+
// Product management methods (primarily for integration tests)
|
|
378
|
+
// GET: /v1/sdk/products
|
|
379
|
+
async listProducts() {
|
|
380
|
+
const url = `${base}/v1/sdk/products`;
|
|
381
|
+
const res = await fetch(url, {
|
|
382
|
+
method: "GET",
|
|
383
|
+
headers
|
|
384
|
+
});
|
|
385
|
+
if (!res.ok) {
|
|
386
|
+
const error = await res.text();
|
|
387
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
388
|
+
throw new import_core2.SolvaPayError(`List products failed (${res.status}): ${error}`);
|
|
389
|
+
}
|
|
390
|
+
const result = await res.json();
|
|
391
|
+
const products = Array.isArray(result) ? result : result.products || [];
|
|
392
|
+
return products.map((product) => ({
|
|
393
|
+
...product,
|
|
394
|
+
...product.data || {}
|
|
395
|
+
}));
|
|
396
|
+
},
|
|
397
|
+
// POST: /v1/sdk/products
|
|
398
|
+
async createProduct(params) {
|
|
399
|
+
const url = `${base}/v1/sdk/products`;
|
|
400
|
+
const res = await fetch(url, {
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers,
|
|
403
|
+
body: JSON.stringify(params)
|
|
404
|
+
});
|
|
405
|
+
if (!res.ok) {
|
|
406
|
+
const error = await res.text();
|
|
407
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
408
|
+
throw new import_core2.SolvaPayError(`Create product failed (${res.status}): ${error}`);
|
|
409
|
+
}
|
|
410
|
+
const result = await res.json();
|
|
411
|
+
return result;
|
|
412
|
+
},
|
|
413
|
+
// POST: /v1/sdk/products/mcp/bootstrap
|
|
414
|
+
async bootstrapMcpProduct(params) {
|
|
415
|
+
const url = `${base}/v1/sdk/products/mcp/bootstrap`;
|
|
416
|
+
const res = await fetch(url, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers,
|
|
419
|
+
body: JSON.stringify(params)
|
|
420
|
+
});
|
|
421
|
+
if (!res.ok) {
|
|
422
|
+
const error = await res.text();
|
|
423
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
424
|
+
throw new import_core2.SolvaPayError(`Bootstrap MCP product failed (${res.status}): ${error}`);
|
|
425
|
+
}
|
|
426
|
+
return await res.json();
|
|
427
|
+
},
|
|
428
|
+
// PUT: /v1/sdk/products/{productRef}/mcp/plans
|
|
429
|
+
async configureMcpPlans(productRef, params) {
|
|
430
|
+
const url = `${base}/v1/sdk/products/${productRef}/mcp/plans`;
|
|
431
|
+
const res = await fetch(url, {
|
|
432
|
+
method: "PUT",
|
|
433
|
+
headers,
|
|
434
|
+
body: JSON.stringify(params)
|
|
435
|
+
});
|
|
436
|
+
if (!res.ok) {
|
|
437
|
+
const error = await res.text();
|
|
438
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
439
|
+
throw new import_core2.SolvaPayError(`Configure MCP plans failed (${res.status}): ${error}`);
|
|
440
|
+
}
|
|
441
|
+
return await res.json();
|
|
442
|
+
},
|
|
443
|
+
// DELETE: /v1/sdk/products/{productRef}
|
|
444
|
+
async deleteProduct(productRef) {
|
|
445
|
+
const url = `${base}/v1/sdk/products/${productRef}`;
|
|
446
|
+
const res = await fetch(url, {
|
|
447
|
+
method: "DELETE",
|
|
448
|
+
headers
|
|
449
|
+
});
|
|
450
|
+
if (!res.ok && res.status !== 404) {
|
|
451
|
+
const error = await res.text();
|
|
452
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
453
|
+
throw new import_core2.SolvaPayError(`Delete product failed (${res.status}): ${error}`);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
// POST: /v1/sdk/products/{productRef}/clone
|
|
457
|
+
async cloneProduct(productRef, overrides) {
|
|
458
|
+
const url = `${base}/v1/sdk/products/${productRef}/clone`;
|
|
459
|
+
const res = await fetch(url, {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers,
|
|
462
|
+
body: JSON.stringify(overrides || {})
|
|
463
|
+
});
|
|
464
|
+
if (!res.ok) {
|
|
465
|
+
const error = await res.text();
|
|
466
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
467
|
+
throw new import_core2.SolvaPayError(`Clone product failed (${res.status}): ${error}`);
|
|
468
|
+
}
|
|
469
|
+
return await res.json();
|
|
470
|
+
},
|
|
471
|
+
// GET: /v1/sdk/products/{productRef}/plans
|
|
472
|
+
async listPlans(productRef) {
|
|
473
|
+
const url = `${base}/v1/sdk/products/${productRef}/plans`;
|
|
474
|
+
const res = await fetch(url, {
|
|
475
|
+
method: "GET",
|
|
476
|
+
headers
|
|
477
|
+
});
|
|
478
|
+
if (!res.ok) {
|
|
479
|
+
const error = await res.text();
|
|
480
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
481
|
+
throw new import_core2.SolvaPayError(`List plans failed (${res.status}): ${error}`);
|
|
482
|
+
}
|
|
483
|
+
const result = await res.json();
|
|
484
|
+
const plans = Array.isArray(result) ? result : result.plans || [];
|
|
485
|
+
return plans.map((plan) => {
|
|
486
|
+
const data = plan.data || {};
|
|
487
|
+
const price = plan.price ?? data.price;
|
|
488
|
+
const unwrapped = {
|
|
489
|
+
...data,
|
|
490
|
+
...plan,
|
|
491
|
+
...price !== void 0 && { price }
|
|
492
|
+
};
|
|
493
|
+
delete unwrapped.data;
|
|
494
|
+
return unwrapped;
|
|
495
|
+
});
|
|
496
|
+
},
|
|
497
|
+
// POST: /v1/sdk/products/{productRef}/plans
|
|
498
|
+
async createPlan(params) {
|
|
499
|
+
const url = `${base}/v1/sdk/products/${params.productRef}/plans`;
|
|
500
|
+
const res = await fetch(url, {
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers,
|
|
503
|
+
body: JSON.stringify(params)
|
|
504
|
+
});
|
|
505
|
+
if (!res.ok) {
|
|
506
|
+
const error = await res.text();
|
|
507
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
508
|
+
throw new import_core2.SolvaPayError(`Create plan failed (${res.status}): ${error}`);
|
|
509
|
+
}
|
|
510
|
+
const result = await res.json();
|
|
511
|
+
return result;
|
|
512
|
+
},
|
|
513
|
+
// PUT: /v1/sdk/products/{productRef}/plans/{planRef}
|
|
514
|
+
async updatePlan(productRef, planRef, params) {
|
|
515
|
+
const url = `${base}/v1/sdk/products/${productRef}/plans/${planRef}`;
|
|
516
|
+
const res = await fetch(url, {
|
|
517
|
+
method: "PUT",
|
|
518
|
+
headers,
|
|
519
|
+
body: JSON.stringify(params)
|
|
520
|
+
});
|
|
521
|
+
if (!res.ok) {
|
|
522
|
+
const error = await res.text();
|
|
523
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
524
|
+
throw new import_core2.SolvaPayError(`Update plan failed (${res.status}): ${error}`);
|
|
525
|
+
}
|
|
526
|
+
return await res.json();
|
|
527
|
+
},
|
|
528
|
+
// DELETE: /v1/sdk/products/{productRef}/plans/{planRef}
|
|
529
|
+
async deletePlan(productRef, planRef) {
|
|
530
|
+
const url = `${base}/v1/sdk/products/${productRef}/plans/${planRef}`;
|
|
531
|
+
const res = await fetch(url, {
|
|
532
|
+
method: "DELETE",
|
|
533
|
+
headers
|
|
534
|
+
});
|
|
535
|
+
if (!res.ok && res.status !== 404) {
|
|
536
|
+
const error = await res.text();
|
|
537
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
538
|
+
throw new import_core2.SolvaPayError(`Delete plan failed (${res.status}): ${error}`);
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
// POST: /payment-intents
|
|
542
|
+
async createPaymentIntent(params) {
|
|
543
|
+
const idempotencyKey = params.idempotencyKey || `payment-${params.planRef}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
544
|
+
const url = `${base}/v1/sdk/payment-intents`;
|
|
545
|
+
const res = await fetch(url, {
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: {
|
|
548
|
+
...headers,
|
|
549
|
+
"Idempotency-Key": idempotencyKey
|
|
550
|
+
},
|
|
551
|
+
body: JSON.stringify({
|
|
552
|
+
productRef: params.productRef,
|
|
553
|
+
planRef: params.planRef,
|
|
554
|
+
customerRef: params.customerRef
|
|
555
|
+
})
|
|
556
|
+
});
|
|
557
|
+
if (!res.ok) {
|
|
558
|
+
const error = await res.text();
|
|
559
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
560
|
+
throw new import_core2.SolvaPayError(`Create payment intent failed (${res.status}): ${error}`);
|
|
561
|
+
}
|
|
562
|
+
return await res.json();
|
|
563
|
+
},
|
|
564
|
+
// POST: /v1/sdk/payment-intents (purpose: credit_topup)
|
|
565
|
+
async createTopupPaymentIntent(params) {
|
|
566
|
+
const idempotencyKey = params.idempotencyKey || `topup-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
567
|
+
const url = `${base}/v1/sdk/payment-intents`;
|
|
568
|
+
const res = await fetch(url, {
|
|
569
|
+
method: "POST",
|
|
570
|
+
headers: {
|
|
571
|
+
...headers,
|
|
572
|
+
"Idempotency-Key": idempotencyKey
|
|
573
|
+
},
|
|
574
|
+
body: JSON.stringify({
|
|
575
|
+
customerRef: params.customerRef,
|
|
576
|
+
purpose: "credit_topup",
|
|
577
|
+
amount: params.amount,
|
|
578
|
+
currency: params.currency,
|
|
579
|
+
description: params.description
|
|
580
|
+
})
|
|
581
|
+
});
|
|
582
|
+
if (!res.ok) {
|
|
583
|
+
const error = await res.text();
|
|
584
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
585
|
+
throw new import_core2.SolvaPayError(`Create topup payment intent failed (${res.status}): ${error}`);
|
|
586
|
+
}
|
|
587
|
+
return await res.json();
|
|
588
|
+
},
|
|
589
|
+
// POST: /v1/sdk/payment-intents/{paymentIntentId}/process
|
|
590
|
+
async processPaymentIntent(params) {
|
|
591
|
+
const url = `${base}/v1/sdk/payment-intents/${params.paymentIntentId}/process`;
|
|
592
|
+
const res = await fetch(url, {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers: {
|
|
595
|
+
...headers,
|
|
596
|
+
"Content-Type": "application/json"
|
|
597
|
+
},
|
|
598
|
+
body: JSON.stringify({
|
|
599
|
+
productRef: params.productRef,
|
|
600
|
+
customerRef: params.customerRef,
|
|
601
|
+
planRef: params.planRef
|
|
602
|
+
})
|
|
603
|
+
});
|
|
604
|
+
if (!res.ok) {
|
|
605
|
+
const error = await res.text();
|
|
606
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
607
|
+
throw new import_core2.SolvaPayError(`Process payment failed (${res.status}): ${error}`);
|
|
608
|
+
}
|
|
609
|
+
const result = await res.json();
|
|
610
|
+
return result;
|
|
611
|
+
},
|
|
612
|
+
// POST: /v1/sdk/purchases/{purchaseRef}/cancel
|
|
613
|
+
async cancelPurchase(params) {
|
|
614
|
+
const url = `${base}/v1/sdk/purchases/${params.purchaseRef}/cancel`;
|
|
615
|
+
const requestOptions = {
|
|
616
|
+
method: "POST",
|
|
617
|
+
headers
|
|
618
|
+
};
|
|
619
|
+
if (params.reason) {
|
|
620
|
+
requestOptions.body = JSON.stringify({ reason: params.reason });
|
|
621
|
+
}
|
|
622
|
+
const res = await fetch(url, requestOptions);
|
|
623
|
+
if (!res.ok) {
|
|
624
|
+
const error = await res.text();
|
|
625
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
626
|
+
if (res.status === 404) {
|
|
627
|
+
throw new import_core2.SolvaPayError(`Purchase not found: ${error}`);
|
|
628
|
+
}
|
|
629
|
+
if (res.status === 400) {
|
|
630
|
+
throw new import_core2.SolvaPayError(
|
|
631
|
+
`Purchase cannot be cancelled or does not belong to provider: ${error}`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
throw new import_core2.SolvaPayError(`Cancel purchase failed (${res.status}): ${error}`);
|
|
635
|
+
}
|
|
636
|
+
const responseText = await res.text();
|
|
637
|
+
let responseData;
|
|
638
|
+
try {
|
|
639
|
+
responseData = JSON.parse(responseText);
|
|
640
|
+
} catch (parseError) {
|
|
641
|
+
log(`\u274C Failed to parse response as JSON: ${parseError}`);
|
|
642
|
+
throw new import_core2.SolvaPayError(
|
|
643
|
+
`Invalid JSON response from cancel purchase endpoint: ${responseText.substring(0, 200)}`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (!responseData || typeof responseData !== "object") {
|
|
647
|
+
log(`\u274C Invalid response structure: ${JSON.stringify(responseData)}`);
|
|
648
|
+
throw new import_core2.SolvaPayError(`Invalid response structure from cancel purchase endpoint`);
|
|
649
|
+
}
|
|
650
|
+
let result;
|
|
651
|
+
if (responseData.purchase && typeof responseData.purchase === "object") {
|
|
652
|
+
result = responseData.purchase;
|
|
653
|
+
} else if (responseData.reference) {
|
|
654
|
+
result = responseData;
|
|
655
|
+
} else {
|
|
656
|
+
result = responseData.purchase || responseData;
|
|
657
|
+
}
|
|
658
|
+
if (!result || typeof result !== "object") {
|
|
659
|
+
log(`\u274C Invalid purchase data in response. Full response:`, responseData);
|
|
660
|
+
throw new import_core2.SolvaPayError(`Invalid purchase data in cancel purchase response`);
|
|
661
|
+
}
|
|
662
|
+
return result;
|
|
663
|
+
},
|
|
664
|
+
// POST: /v1/sdk/purchases/{purchaseRef}/reactivate
|
|
665
|
+
async reactivatePurchase(params) {
|
|
666
|
+
const url = `${base}/v1/sdk/purchases/${params.purchaseRef}/reactivate`;
|
|
667
|
+
const res = await fetch(url, {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers
|
|
670
|
+
});
|
|
671
|
+
if (!res.ok) {
|
|
672
|
+
const error = await res.text();
|
|
673
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
674
|
+
if (res.status === 404) {
|
|
675
|
+
throw new import_core2.SolvaPayError(`Purchase not found: ${error}`);
|
|
676
|
+
}
|
|
677
|
+
if (res.status === 400) {
|
|
678
|
+
throw new import_core2.SolvaPayError(
|
|
679
|
+
`Purchase cannot be reactivated: ${error}`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
throw new import_core2.SolvaPayError(`Reactivate purchase failed (${res.status}): ${error}`);
|
|
683
|
+
}
|
|
684
|
+
const responseText = await res.text();
|
|
685
|
+
let responseData;
|
|
686
|
+
try {
|
|
687
|
+
responseData = JSON.parse(responseText);
|
|
688
|
+
} catch (parseError) {
|
|
689
|
+
log(`\u274C Failed to parse response as JSON: ${parseError}`);
|
|
690
|
+
throw new import_core2.SolvaPayError(
|
|
691
|
+
`Invalid JSON response from reactivate purchase endpoint: ${responseText.substring(0, 200)}`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if (!responseData || typeof responseData !== "object") {
|
|
695
|
+
log(`\u274C Invalid response structure: ${JSON.stringify(responseData)}`);
|
|
696
|
+
throw new import_core2.SolvaPayError(`Invalid response structure from reactivate purchase endpoint`);
|
|
697
|
+
}
|
|
698
|
+
let result;
|
|
699
|
+
if (responseData.purchase && typeof responseData.purchase === "object") {
|
|
700
|
+
result = responseData.purchase;
|
|
701
|
+
} else if (responseData.reference) {
|
|
702
|
+
result = responseData;
|
|
703
|
+
} else {
|
|
704
|
+
result = responseData.purchase || responseData;
|
|
705
|
+
}
|
|
706
|
+
if (!result || typeof result !== "object") {
|
|
707
|
+
log(`\u274C Invalid purchase data in response. Full response:`, responseData);
|
|
708
|
+
throw new import_core2.SolvaPayError(`Invalid purchase data in reactivate purchase response`);
|
|
709
|
+
}
|
|
710
|
+
return result;
|
|
711
|
+
},
|
|
712
|
+
// POST: /v1/sdk/user-info
|
|
713
|
+
async getUserInfo(params) {
|
|
714
|
+
const url = `${base}/v1/sdk/user-info`;
|
|
715
|
+
const res = await fetch(url, {
|
|
716
|
+
method: "POST",
|
|
717
|
+
headers,
|
|
718
|
+
body: JSON.stringify(params)
|
|
719
|
+
});
|
|
720
|
+
if (!res.ok) {
|
|
721
|
+
const error = await res.text();
|
|
722
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
723
|
+
throw new import_core2.SolvaPayError(`Get user info failed (${res.status}): ${error}`);
|
|
724
|
+
}
|
|
725
|
+
return await res.json();
|
|
726
|
+
},
|
|
727
|
+
// GET: /v1/sdk/customers/:customerRef/balance
|
|
728
|
+
async getCustomerBalance(params) {
|
|
729
|
+
const url = `${base}/v1/sdk/customers/${params.customerRef}/balance`;
|
|
730
|
+
const res = await fetch(url, {
|
|
731
|
+
method: "GET",
|
|
732
|
+
headers
|
|
733
|
+
});
|
|
734
|
+
if (!res.ok) {
|
|
735
|
+
const error = await res.text();
|
|
736
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
737
|
+
throw new import_core2.SolvaPayError(`Get customer balance failed (${res.status}): ${error}`);
|
|
738
|
+
}
|
|
739
|
+
return await res.json();
|
|
740
|
+
},
|
|
741
|
+
// POST: /v1/sdk/checkout-sessions
|
|
742
|
+
async createCheckoutSession(params) {
|
|
743
|
+
const url = `${base}/v1/sdk/checkout-sessions`;
|
|
744
|
+
const res = await fetch(url, {
|
|
745
|
+
method: "POST",
|
|
746
|
+
headers,
|
|
747
|
+
body: JSON.stringify(params)
|
|
748
|
+
});
|
|
749
|
+
if (!res.ok) {
|
|
750
|
+
const error = await res.text();
|
|
751
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
752
|
+
throw new import_core2.SolvaPayError(`Create checkout session failed (${res.status}): ${error}`);
|
|
753
|
+
}
|
|
754
|
+
const result = await res.json();
|
|
755
|
+
return result;
|
|
756
|
+
},
|
|
757
|
+
// POST: /v1/sdk/customers/customer-sessions
|
|
758
|
+
async createCustomerSession(params) {
|
|
759
|
+
const url = `${base}/v1/sdk/customers/customer-sessions`;
|
|
760
|
+
const res = await fetch(url, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
headers,
|
|
763
|
+
body: JSON.stringify(params)
|
|
764
|
+
});
|
|
765
|
+
if (!res.ok) {
|
|
766
|
+
const error = await res.text();
|
|
767
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
768
|
+
throw new import_core2.SolvaPayError(`Create customer session failed (${res.status}): ${error}`);
|
|
769
|
+
}
|
|
770
|
+
const result = await res.json();
|
|
771
|
+
return result;
|
|
772
|
+
},
|
|
773
|
+
// POST: /v1/sdk/activate
|
|
774
|
+
async activatePlan(params) {
|
|
775
|
+
const url = `${base}/v1/sdk/activate`;
|
|
776
|
+
const res = await fetch(url, {
|
|
777
|
+
method: "POST",
|
|
778
|
+
headers,
|
|
779
|
+
body: JSON.stringify(params)
|
|
780
|
+
});
|
|
781
|
+
if (!res.ok) {
|
|
782
|
+
const error = await res.text();
|
|
783
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
784
|
+
throw new import_core2.SolvaPayError(`Activate plan failed (${res.status}): ${error}`);
|
|
785
|
+
}
|
|
786
|
+
return await res.json();
|
|
787
|
+
},
|
|
788
|
+
async getPaymentMethod(params) {
|
|
789
|
+
const url = new URL(`${base}/v1/sdk/payment-method`);
|
|
790
|
+
url.searchParams.set("customerRef", params.customerRef);
|
|
791
|
+
const res = await fetch(url.toString(), { method: "GET", headers });
|
|
792
|
+
if (!res.ok) {
|
|
793
|
+
const error = await res.text();
|
|
794
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
795
|
+
throw new import_core2.SolvaPayError(`Get payment method failed (${res.status}): ${error}`);
|
|
796
|
+
}
|
|
797
|
+
return await res.json();
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/paywall-state.ts
|
|
803
|
+
function classifyPaywallState(limits) {
|
|
804
|
+
if (!limits) return { kind: "upgrade_required" };
|
|
805
|
+
if (limits.activationRequired === true) {
|
|
806
|
+
return { kind: "activation_required" };
|
|
807
|
+
}
|
|
808
|
+
const activePlan = limits.plans?.find((p) => p.reference === limits.plan);
|
|
809
|
+
const isUsageBased = activePlan?.type === "usage-based" || limits.balance !== void 0;
|
|
810
|
+
const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
|
|
811
|
+
if (isUsageBased) {
|
|
812
|
+
if (creditBalance === 0) return { kind: "topup_required" };
|
|
813
|
+
if (creditBalance === void 0 && limits.remaining === 0) {
|
|
814
|
+
return { kind: "topup_required" };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return { kind: "upgrade_required" };
|
|
818
|
+
}
|
|
819
|
+
function buildGateMessage(state, gate) {
|
|
820
|
+
const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
|
|
821
|
+
const openClause = url ? `, or open ${url} in a browser` : "";
|
|
822
|
+
switch (state.kind) {
|
|
823
|
+
case "activation_required":
|
|
824
|
+
return `Your plan needs activation before you can use this tool. Call the \`activate_plan\` tool to activate it${openClause}.`;
|
|
825
|
+
case "topup_required":
|
|
826
|
+
return `You're out of credits. Call the \`topup\` tool to add more${openClause}.`;
|
|
827
|
+
case "upgrade_required":
|
|
828
|
+
return `You don't have an active plan for this tool. Call the \`upgrade\` tool to pick a plan${openClause}.`;
|
|
829
|
+
case "reactivation_required":
|
|
830
|
+
return `Your previous plan is no longer active. Call the \`manage_account\` tool to reactivate it, or the \`upgrade\` tool to pick a new plan.`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/utils.ts
|
|
835
|
+
async function withRetry(fn, options = {}) {
|
|
836
|
+
const {
|
|
837
|
+
maxRetries = 2,
|
|
838
|
+
initialDelay = 500,
|
|
839
|
+
backoffStrategy = "fixed",
|
|
840
|
+
shouldRetry,
|
|
841
|
+
onRetry
|
|
842
|
+
} = options;
|
|
843
|
+
let lastError;
|
|
844
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
845
|
+
try {
|
|
846
|
+
return await fn();
|
|
847
|
+
} catch (error) {
|
|
848
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
849
|
+
const isLastAttempt = attempt === maxRetries;
|
|
850
|
+
if (isLastAttempt) {
|
|
851
|
+
throw lastError;
|
|
852
|
+
}
|
|
853
|
+
if (shouldRetry && !shouldRetry(lastError, attempt)) {
|
|
854
|
+
throw lastError;
|
|
855
|
+
}
|
|
856
|
+
const delay = calculateDelay(initialDelay, attempt, backoffStrategy);
|
|
857
|
+
if (onRetry) {
|
|
858
|
+
onRetry(lastError, attempt);
|
|
859
|
+
}
|
|
860
|
+
await sleep(delay);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
throw lastError;
|
|
864
|
+
}
|
|
865
|
+
function calculateDelay(initialDelay, attempt, strategy) {
|
|
866
|
+
switch (strategy) {
|
|
867
|
+
case "fixed":
|
|
868
|
+
return initialDelay;
|
|
869
|
+
case "linear":
|
|
870
|
+
return initialDelay * (attempt + 1);
|
|
871
|
+
case "exponential":
|
|
872
|
+
return initialDelay * Math.pow(2, attempt);
|
|
873
|
+
default:
|
|
874
|
+
return initialDelay;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function sleep(ms) {
|
|
878
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
879
|
+
}
|
|
880
|
+
function createRequestDeduplicator(options = {}) {
|
|
881
|
+
const { cacheTTL = 2e3, maxCacheSize = 1e3, cacheErrors = true } = options;
|
|
882
|
+
const inFlightRequests = /* @__PURE__ */ new Map();
|
|
883
|
+
const resultCache = /* @__PURE__ */ new Map();
|
|
884
|
+
let _cleanupInterval = null;
|
|
885
|
+
if (cacheTTL > 0) {
|
|
886
|
+
_cleanupInterval = setInterval(
|
|
887
|
+
() => {
|
|
888
|
+
const now = Date.now();
|
|
889
|
+
const entriesToDelete = [];
|
|
890
|
+
for (const [key, cached] of resultCache.entries()) {
|
|
891
|
+
if (now - cached.timestamp >= cacheTTL) {
|
|
892
|
+
entriesToDelete.push(key);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
for (const key of entriesToDelete) {
|
|
896
|
+
resultCache.delete(key);
|
|
897
|
+
}
|
|
898
|
+
if (resultCache.size > maxCacheSize) {
|
|
899
|
+
const sortedEntries = Array.from(resultCache.entries()).sort(
|
|
900
|
+
(a, b) => a[1].timestamp - b[1].timestamp
|
|
901
|
+
);
|
|
902
|
+
const toRemove = sortedEntries.slice(0, resultCache.size - maxCacheSize);
|
|
903
|
+
for (const [key] of toRemove) {
|
|
904
|
+
resultCache.delete(key);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
Math.min(cacheTTL, 1e3)
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
const deduplicate = async (key, fn) => {
|
|
912
|
+
if (cacheTTL > 0) {
|
|
913
|
+
const cached = resultCache.get(key);
|
|
914
|
+
if (cached && Date.now() - cached.timestamp < cacheTTL) {
|
|
915
|
+
return cached.data;
|
|
916
|
+
} else if (cached) {
|
|
917
|
+
resultCache.delete(key);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
let requestPromise = inFlightRequests.get(key);
|
|
921
|
+
if (!requestPromise) {
|
|
922
|
+
requestPromise = (async () => {
|
|
923
|
+
try {
|
|
924
|
+
const result = await fn();
|
|
925
|
+
if (cacheTTL > 0) {
|
|
926
|
+
resultCache.set(key, {
|
|
927
|
+
data: result,
|
|
928
|
+
timestamp: Date.now()
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
return result;
|
|
932
|
+
} catch (error) {
|
|
933
|
+
if (cacheTTL > 0 && cacheErrors) {
|
|
934
|
+
resultCache.set(key, {
|
|
935
|
+
data: error,
|
|
936
|
+
timestamp: Date.now()
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
throw error;
|
|
940
|
+
} finally {
|
|
941
|
+
inFlightRequests.delete(key);
|
|
942
|
+
}
|
|
943
|
+
})();
|
|
944
|
+
const existingPromise = inFlightRequests.get(key);
|
|
945
|
+
if (existingPromise) {
|
|
946
|
+
requestPromise = existingPromise;
|
|
947
|
+
} else {
|
|
948
|
+
inFlightRequests.set(key, requestPromise);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return requestPromise;
|
|
952
|
+
};
|
|
953
|
+
const clearCache = (key) => {
|
|
954
|
+
resultCache.delete(key);
|
|
955
|
+
};
|
|
956
|
+
const clearAllCache = () => {
|
|
957
|
+
resultCache.clear();
|
|
958
|
+
};
|
|
959
|
+
const getStats = () => ({
|
|
960
|
+
inFlight: inFlightRequests.size,
|
|
961
|
+
cached: resultCache.size
|
|
962
|
+
});
|
|
963
|
+
return {
|
|
964
|
+
deduplicate,
|
|
965
|
+
clearCache,
|
|
966
|
+
clearAllCache,
|
|
967
|
+
getStats
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/paywall.ts
|
|
972
|
+
var PaywallError = class extends Error {
|
|
973
|
+
/**
|
|
974
|
+
* Creates a new PaywallError instance.
|
|
975
|
+
*
|
|
976
|
+
* @param message - Error message
|
|
977
|
+
* @param structuredContent - Structured content with checkout URLs and metadata
|
|
978
|
+
*/
|
|
979
|
+
constructor(message, structuredContent) {
|
|
980
|
+
super(message);
|
|
981
|
+
this.structuredContent = structuredContent;
|
|
982
|
+
this.name = "PaywallError";
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
function paywallErrorToClientPayload(error) {
|
|
986
|
+
const sc = error.structuredContent;
|
|
987
|
+
const base = {
|
|
988
|
+
success: false,
|
|
989
|
+
error: sc.kind === "activation_required" ? "Activation required" : "Payment required",
|
|
990
|
+
product: sc.product,
|
|
991
|
+
checkoutUrl: sc.checkoutUrl,
|
|
992
|
+
message: sc.message
|
|
993
|
+
};
|
|
994
|
+
if (sc.kind === "activation_required") {
|
|
995
|
+
base.kind = "activation_required";
|
|
996
|
+
if (sc.plans !== void 0) base.plans = sc.plans;
|
|
997
|
+
if (sc.balance !== void 0) base.balance = sc.balance;
|
|
998
|
+
if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
|
|
999
|
+
if (sc.confirmationUrl !== void 0) base.confirmationUrl = sc.confirmationUrl;
|
|
1000
|
+
} else {
|
|
1001
|
+
base.kind = "payment_required";
|
|
1002
|
+
if (sc.balance !== void 0) base.balance = sc.balance;
|
|
1003
|
+
if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
|
|
1004
|
+
}
|
|
1005
|
+
return base;
|
|
1006
|
+
}
|
|
1007
|
+
var sharedCustomerLookupDeduplicator = createRequestDeduplicator({
|
|
1008
|
+
cacheTTL: 6e4,
|
|
1009
|
+
// Cache results for 60 seconds (reduces API calls significantly)
|
|
1010
|
+
maxCacheSize: 1e3,
|
|
1011
|
+
// Maximum cache entries
|
|
1012
|
+
cacheErrors: false
|
|
1013
|
+
// Don't cache errors - retry on next request
|
|
1014
|
+
});
|
|
1015
|
+
var EXTRA_FORWARD_KEY = "__solvapayExtra";
|
|
1016
|
+
var SolvaPayPaywall = class {
|
|
1017
|
+
constructor(apiClient, options = {}) {
|
|
1018
|
+
this.apiClient = apiClient;
|
|
1019
|
+
this.debug = options.debug ?? process.env.SOLVAPAY_DEBUG === "true";
|
|
1020
|
+
this.limitsCacheTTL = options.limitsCacheTTL ?? 1e4;
|
|
1021
|
+
}
|
|
1022
|
+
customerCreationAttempts = /* @__PURE__ */ new Set();
|
|
1023
|
+
customerRefMapping = /* @__PURE__ */ new Map();
|
|
1024
|
+
debug;
|
|
1025
|
+
limitsCache = /* @__PURE__ */ new Map();
|
|
1026
|
+
limitsCacheTTL;
|
|
1027
|
+
log(...args) {
|
|
1028
|
+
if (this.debug) {
|
|
1029
|
+
console.log(...args);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
resolveProduct(metadata) {
|
|
1033
|
+
return metadata.product || process.env.SOLVAPAY_PRODUCT || "default-product";
|
|
1034
|
+
}
|
|
1035
|
+
generateRequestId() {
|
|
1036
|
+
const timestamp = Date.now();
|
|
1037
|
+
const random = Math.random().toString(36).substring(2, 11);
|
|
1038
|
+
return `solvapay_${timestamp}_${random}`;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Pure decision routine — performs customer resolution, limits cache
|
|
1042
|
+
* lookup / fresh `checkLimits` fetch, and returns a `PaywallDecision`
|
|
1043
|
+
* describing whether the handler should run.
|
|
1044
|
+
*
|
|
1045
|
+
* Side effects kept in lockstep with the legacy `protect()` path:
|
|
1046
|
+
* - creates the backend customer on first use (`ensureCustomer`),
|
|
1047
|
+
* - updates the limits cache (consume-one-unit bookkeeping), and
|
|
1048
|
+
* - emits a `paywall` usage event on gate outcomes.
|
|
1049
|
+
*
|
|
1050
|
+
* `trackUsage` for the `success` / `fail` outcome is emitted by the
|
|
1051
|
+
* caller (adapter or `protect()`) once it has actually invoked the
|
|
1052
|
+
* handler — `decide()` never counts handler execution as usage.
|
|
1053
|
+
*
|
|
1054
|
+
* @since 1.1.0
|
|
1055
|
+
*/
|
|
1056
|
+
async decide(args, metadata = {}, getCustomerRef) {
|
|
1057
|
+
const product = this.resolveProduct(metadata);
|
|
1058
|
+
const usageType = metadata.usageType || "requests";
|
|
1059
|
+
const requestId = this.generateRequestId();
|
|
1060
|
+
const startTime = Date.now();
|
|
1061
|
+
const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
|
|
1062
|
+
let backendCustomerRef;
|
|
1063
|
+
if (inputCustomerRef.startsWith("cus_")) {
|
|
1064
|
+
backendCustomerRef = inputCustomerRef;
|
|
1065
|
+
} else {
|
|
1066
|
+
backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
|
|
1067
|
+
}
|
|
1068
|
+
const limitsCacheKey = `${backendCustomerRef}:${product}:${usageType}`;
|
|
1069
|
+
const cachedLimits = this.limitsCache.get(limitsCacheKey);
|
|
1070
|
+
const now = Date.now();
|
|
1071
|
+
let withinLimits;
|
|
1072
|
+
let remaining;
|
|
1073
|
+
let checkoutUrl;
|
|
1074
|
+
let resolvedMeterName;
|
|
1075
|
+
let lastLimitsCheck;
|
|
1076
|
+
const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
|
|
1077
|
+
if (hasFreshCachedLimits) {
|
|
1078
|
+
checkoutUrl = cachedLimits.checkoutUrl;
|
|
1079
|
+
resolvedMeterName = cachedLimits.meterName;
|
|
1080
|
+
lastLimitsCheck = cachedLimits.limits;
|
|
1081
|
+
if (cachedLimits.remaining > 0) {
|
|
1082
|
+
cachedLimits.remaining--;
|
|
1083
|
+
if (cachedLimits.remaining <= 0) {
|
|
1084
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
1085
|
+
}
|
|
1086
|
+
withinLimits = true;
|
|
1087
|
+
remaining = cachedLimits.remaining;
|
|
1088
|
+
} else {
|
|
1089
|
+
withinLimits = false;
|
|
1090
|
+
remaining = 0;
|
|
1091
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
if (cachedLimits) {
|
|
1095
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
1096
|
+
}
|
|
1097
|
+
const limitsCheck = await this.apiClient.checkLimits({
|
|
1098
|
+
customerRef: backendCustomerRef,
|
|
1099
|
+
productRef: product,
|
|
1100
|
+
meterName: usageType
|
|
1101
|
+
});
|
|
1102
|
+
lastLimitsCheck = limitsCheck;
|
|
1103
|
+
withinLimits = limitsCheck.withinLimits;
|
|
1104
|
+
remaining = limitsCheck.remaining;
|
|
1105
|
+
checkoutUrl = limitsCheck.checkoutUrl;
|
|
1106
|
+
resolvedMeterName = limitsCheck.meterName;
|
|
1107
|
+
const consumedAllowance = withinLimits && remaining > 0;
|
|
1108
|
+
if (consumedAllowance) {
|
|
1109
|
+
remaining = Math.max(0, remaining - 1);
|
|
1110
|
+
}
|
|
1111
|
+
if (consumedAllowance) {
|
|
1112
|
+
this.limitsCache.set(limitsCacheKey, {
|
|
1113
|
+
remaining,
|
|
1114
|
+
checkoutUrl,
|
|
1115
|
+
meterName: resolvedMeterName,
|
|
1116
|
+
timestamp: now,
|
|
1117
|
+
limits: limitsCheck
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (!withinLimits) {
|
|
1122
|
+
const latencyMs = Date.now() - startTime;
|
|
1123
|
+
this.trackUsage(
|
|
1124
|
+
backendCustomerRef,
|
|
1125
|
+
product,
|
|
1126
|
+
resolvedMeterName || usageType,
|
|
1127
|
+
"paywall",
|
|
1128
|
+
requestId,
|
|
1129
|
+
latencyMs
|
|
1130
|
+
);
|
|
1131
|
+
const state = classifyPaywallState(lastLimitsCheck ?? null);
|
|
1132
|
+
const preMessageGate = lastLimitsCheck?.activationRequired ? {
|
|
1133
|
+
kind: "activation_required",
|
|
1134
|
+
product,
|
|
1135
|
+
message: "",
|
|
1136
|
+
checkoutUrl: lastLimitsCheck.confirmationUrl || lastLimitsCheck.checkoutUrl || checkoutUrl || "",
|
|
1137
|
+
...lastLimitsCheck.confirmationUrl !== void 0 ? { confirmationUrl: lastLimitsCheck.confirmationUrl } : {},
|
|
1138
|
+
...lastLimitsCheck.plans !== void 0 ? { plans: lastLimitsCheck.plans } : {},
|
|
1139
|
+
...lastLimitsCheck.balance !== void 0 ? { balance: lastLimitsCheck.balance } : {},
|
|
1140
|
+
...lastLimitsCheck.product !== void 0 ? { productDetails: lastLimitsCheck.product } : {}
|
|
1141
|
+
} : {
|
|
1142
|
+
kind: "payment_required",
|
|
1143
|
+
product,
|
|
1144
|
+
checkoutUrl: checkoutUrl || "",
|
|
1145
|
+
message: "",
|
|
1146
|
+
...lastLimitsCheck?.balance !== void 0 ? { balance: lastLimitsCheck.balance } : {},
|
|
1147
|
+
...lastLimitsCheck?.product !== void 0 ? { productDetails: lastLimitsCheck.product } : {}
|
|
1148
|
+
};
|
|
1149
|
+
const gate = {
|
|
1150
|
+
...preMessageGate,
|
|
1151
|
+
message: buildGateMessage(state, preMessageGate)
|
|
1152
|
+
};
|
|
1153
|
+
return {
|
|
1154
|
+
outcome: "gate",
|
|
1155
|
+
gate,
|
|
1156
|
+
limits: lastLimitsCheck ?? null,
|
|
1157
|
+
customerRef: backendCustomerRef
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
return {
|
|
1161
|
+
outcome: "allow",
|
|
1162
|
+
args,
|
|
1163
|
+
limits: lastLimitsCheck,
|
|
1164
|
+
customerRef: backendCustomerRef
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Execute the handler for an already-obtained `allow` decision and
|
|
1169
|
+
* emit the post-handler `trackUsage('success' | 'fail', ...)` event.
|
|
1170
|
+
*
|
|
1171
|
+
* Exposed for adapter integration — the adapter layer drives the
|
|
1172
|
+
* paywall through `decide()` + `runAllow()` so `formatGate` can own
|
|
1173
|
+
* gate outcomes without routing through `PaywallError`. `protect()`
|
|
1174
|
+
* continues to offer the self-contained throw-based surface for
|
|
1175
|
+
* legacy consumers.
|
|
1176
|
+
*
|
|
1177
|
+
* `runAllow` intentionally does NOT re-throw `PaywallError` — if a
|
|
1178
|
+
* handler calls `ctx.gate(reason)` and throws from deep code, the
|
|
1179
|
+
* adapter catches that at the `formatGate` boundary instead.
|
|
1180
|
+
*
|
|
1181
|
+
* @since 1.1.0
|
|
1182
|
+
*/
|
|
1183
|
+
async runAllow(decision, handler, metadata, args) {
|
|
1184
|
+
const product = this.resolveProduct(metadata);
|
|
1185
|
+
const usageType = metadata.usageType || "requests";
|
|
1186
|
+
const requestId = this.generateRequestId();
|
|
1187
|
+
const startTime = Date.now();
|
|
1188
|
+
const forwardedExtra = args[EXTRA_FORWARD_KEY];
|
|
1189
|
+
const handlerContext = {
|
|
1190
|
+
customerRef: decision.customerRef,
|
|
1191
|
+
limits: decision.limits,
|
|
1192
|
+
...forwardedExtra !== void 0 ? { extra: forwardedExtra } : {}
|
|
1193
|
+
};
|
|
1194
|
+
try {
|
|
1195
|
+
const result = await handler(args, handlerContext);
|
|
1196
|
+
const latencyMs = Date.now() - startTime;
|
|
1197
|
+
this.trackUsage(
|
|
1198
|
+
decision.customerRef,
|
|
1199
|
+
product,
|
|
1200
|
+
decision.limits.meterName || usageType,
|
|
1201
|
+
"success",
|
|
1202
|
+
requestId,
|
|
1203
|
+
latencyMs
|
|
1204
|
+
);
|
|
1205
|
+
return result;
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
if (error instanceof Error) {
|
|
1208
|
+
const errorType = error instanceof PaywallError ? "PaywallError" : "API Error";
|
|
1209
|
+
this.log(`\u274C Error in paywall [${errorType}]: ${error.message}`);
|
|
1210
|
+
} else {
|
|
1211
|
+
this.log(`\u274C Error in paywall:`, error);
|
|
1212
|
+
}
|
|
1213
|
+
if (!(error instanceof PaywallError)) {
|
|
1214
|
+
const latencyMs = Date.now() - startTime;
|
|
1215
|
+
this.trackUsage(
|
|
1216
|
+
decision.customerRef,
|
|
1217
|
+
product,
|
|
1218
|
+
decision.limits.meterName || usageType,
|
|
1219
|
+
"fail",
|
|
1220
|
+
requestId,
|
|
1221
|
+
latencyMs
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
throw error;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Core protection method - works for both MCP and HTTP
|
|
1229
|
+
*
|
|
1230
|
+
* The `handler` may optionally declare a second positional argument
|
|
1231
|
+
* of type `ProtectHandlerContext` to receive the resolved customer
|
|
1232
|
+
* ref, the pre-check `LimitResponseWithPlan`, and an opaque `extra`
|
|
1233
|
+
* bag threaded through from the adapter layer. One-arg handlers
|
|
1234
|
+
* ignore the second argument and continue to work unchanged.
|
|
1235
|
+
*
|
|
1236
|
+
* Implemented on top of `decide()`: pre-check runs through the same
|
|
1237
|
+
* decision routine, and gate outcomes are raised as a `PaywallError`
|
|
1238
|
+
* to preserve the legacy throw-based signal for consumers that
|
|
1239
|
+
* haven't migrated to adapter-level `formatGate` routing.
|
|
1240
|
+
*/
|
|
1241
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1242
|
+
async protect(handler, metadata = {}, getCustomerRef) {
|
|
1243
|
+
return async (args) => {
|
|
1244
|
+
const decision = await this.decide(args, metadata, getCustomerRef);
|
|
1245
|
+
if (decision.outcome === "gate") {
|
|
1246
|
+
const message = decision.gate.kind === "activation_required" ? "Activation required" : "Payment required";
|
|
1247
|
+
this.log(`\u274C Error in paywall [PaywallError]: ${message}`);
|
|
1248
|
+
throw new PaywallError(message, decision.gate);
|
|
1249
|
+
}
|
|
1250
|
+
return this.runAllow(decision, handler, metadata, args);
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Ensures a customer exists in the backend, creating them if necessary.
|
|
1255
|
+
* This is a public helper for testing, pre-creating customers, and internal use.
|
|
1256
|
+
* Only attempts creation once per customer (idempotent).
|
|
1257
|
+
* Returns the backend customer reference to use in API calls.
|
|
1258
|
+
*
|
|
1259
|
+
* @param customerRef - The customer reference used as a cache key (e.g., Supabase user ID)
|
|
1260
|
+
* @param externalRef - Optional external reference for backend lookup (e.g., Supabase user ID)
|
|
1261
|
+
* If provided, will lookup existing customer by externalRef before creating new one.
|
|
1262
|
+
* The externalRef is stored on the SolvaPay backend for customer lookup.
|
|
1263
|
+
* @param options - Optional customer details (email, name) for customer creation
|
|
1264
|
+
*/
|
|
1265
|
+
async ensureCustomer(customerRef, externalRef, options) {
|
|
1266
|
+
if (this.customerRefMapping.has(customerRef)) {
|
|
1267
|
+
return this.customerRefMapping.get(customerRef);
|
|
1268
|
+
}
|
|
1269
|
+
if (customerRef === "anonymous") {
|
|
1270
|
+
return customerRef;
|
|
1271
|
+
}
|
|
1272
|
+
if (customerRef.startsWith("cus_")) {
|
|
1273
|
+
return customerRef;
|
|
1274
|
+
}
|
|
1275
|
+
const cacheKey = externalRef || customerRef;
|
|
1276
|
+
if (this.customerRefMapping.has(customerRef)) {
|
|
1277
|
+
const cached = this.customerRefMapping.get(customerRef);
|
|
1278
|
+
return cached;
|
|
1279
|
+
}
|
|
1280
|
+
const backendRef = await sharedCustomerLookupDeduplicator.deduplicate(cacheKey, async () => {
|
|
1281
|
+
if (externalRef) {
|
|
1282
|
+
try {
|
|
1283
|
+
const existingCustomer = await this.apiClient.getCustomer({ externalRef });
|
|
1284
|
+
if (existingCustomer && existingCustomer.customerRef) {
|
|
1285
|
+
const ref = existingCustomer.customerRef;
|
|
1286
|
+
this.customerRefMapping.set(customerRef, ref);
|
|
1287
|
+
this.customerCreationAttempts.add(customerRef);
|
|
1288
|
+
if (externalRef !== customerRef) {
|
|
1289
|
+
this.customerCreationAttempts.add(externalRef);
|
|
1290
|
+
}
|
|
1291
|
+
return ref;
|
|
1292
|
+
}
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1295
|
+
if (!errorMessage.includes("404") && !errorMessage.includes("not found")) {
|
|
1296
|
+
this.log(`\u26A0\uFE0F Error looking up customer by externalRef: ${errorMessage}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (this.customerCreationAttempts.has(customerRef) || externalRef && this.customerCreationAttempts.has(externalRef)) {
|
|
1301
|
+
const mappedRef = this.customerRefMapping.get(customerRef);
|
|
1302
|
+
return mappedRef || customerRef;
|
|
1303
|
+
}
|
|
1304
|
+
if (!this.apiClient.createCustomer) {
|
|
1305
|
+
console.warn(
|
|
1306
|
+
`\u26A0\uFE0F Cannot auto-create customer ${customerRef}: createCustomer method not available on API client`
|
|
1307
|
+
);
|
|
1308
|
+
return customerRef;
|
|
1309
|
+
}
|
|
1310
|
+
this.customerCreationAttempts.add(customerRef);
|
|
1311
|
+
try {
|
|
1312
|
+
const createParams = {
|
|
1313
|
+
email: options?.email || `${customerRef}-${Date.now()}@auto-created.local`,
|
|
1314
|
+
metadata: {}
|
|
1315
|
+
};
|
|
1316
|
+
if (options?.name) {
|
|
1317
|
+
createParams.name = options.name;
|
|
1318
|
+
}
|
|
1319
|
+
if (externalRef) {
|
|
1320
|
+
createParams.externalRef = externalRef;
|
|
1321
|
+
}
|
|
1322
|
+
const result = await this.apiClient.createCustomer(createParams);
|
|
1323
|
+
const resultObj = result;
|
|
1324
|
+
const ref = resultObj.customerRef || resultObj.reference || customerRef;
|
|
1325
|
+
this.customerRefMapping.set(customerRef, ref);
|
|
1326
|
+
return ref;
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1329
|
+
if (errorMessage.includes("409") || errorMessage.includes("already exists")) {
|
|
1330
|
+
if (externalRef) {
|
|
1331
|
+
try {
|
|
1332
|
+
const searchResult = await this.apiClient.getCustomer({ externalRef });
|
|
1333
|
+
if (searchResult && searchResult.customerRef) {
|
|
1334
|
+
this.customerRefMapping.set(customerRef, searchResult.customerRef);
|
|
1335
|
+
return searchResult.customerRef;
|
|
1336
|
+
}
|
|
1337
|
+
} catch (lookupError) {
|
|
1338
|
+
this.log(
|
|
1339
|
+
`\u26A0\uFE0F Failed to lookup existing customer by externalRef after 409:`,
|
|
1340
|
+
lookupError instanceof Error ? lookupError.message : lookupError
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
const isEmailConflict = errorMessage.includes("email") || errorMessage.includes("identifier email");
|
|
1345
|
+
if (externalRef && isEmailConflict && options?.email) {
|
|
1346
|
+
try {
|
|
1347
|
+
const byEmail = await this.apiClient.getCustomer({ email: options.email });
|
|
1348
|
+
if (byEmail && byEmail.customerRef) {
|
|
1349
|
+
this.customerRefMapping.set(customerRef, byEmail.customerRef);
|
|
1350
|
+
this.log(
|
|
1351
|
+
`\u26A0\uFE0F Resolved customer ${customerRef} by email after conflict; using existing customer ${byEmail.customerRef}`
|
|
1352
|
+
);
|
|
1353
|
+
if (!byEmail.externalRef && this.apiClient.updateCustomer) {
|
|
1354
|
+
try {
|
|
1355
|
+
await this.apiClient.updateCustomer(byEmail.customerRef, { externalRef });
|
|
1356
|
+
} catch (backfillError) {
|
|
1357
|
+
this.log(
|
|
1358
|
+
`\u26A0\uFE0F Failed to backfill externalRef on ${byEmail.customerRef}:`,
|
|
1359
|
+
backfillError instanceof Error ? backfillError.message : backfillError
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
return byEmail.customerRef;
|
|
1364
|
+
}
|
|
1365
|
+
} catch (emailLookupError) {
|
|
1366
|
+
this.log(
|
|
1367
|
+
`\u26A0\uFE0F Email lookup failed after customer conflict for ${customerRef}:`,
|
|
1368
|
+
emailLookupError instanceof Error ? emailLookupError.message : emailLookupError
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
try {
|
|
1372
|
+
const retryParams = {
|
|
1373
|
+
email: `${customerRef}-${Date.now()}@auto-created.local`,
|
|
1374
|
+
externalRef,
|
|
1375
|
+
metadata: {}
|
|
1376
|
+
};
|
|
1377
|
+
if (options?.name) {
|
|
1378
|
+
retryParams.name = options.name;
|
|
1379
|
+
}
|
|
1380
|
+
const retryResult = await this.apiClient.createCustomer(retryParams);
|
|
1381
|
+
const retryObj = retryResult;
|
|
1382
|
+
const retryRef = retryObj.customerRef || retryObj.reference || customerRef;
|
|
1383
|
+
this.customerRefMapping.set(customerRef, retryRef);
|
|
1384
|
+
this.log(
|
|
1385
|
+
`\u26A0\uFE0F Retried customer creation for ${customerRef} with generated email after email conflict`
|
|
1386
|
+
);
|
|
1387
|
+
return retryRef;
|
|
1388
|
+
} catch (retryError) {
|
|
1389
|
+
this.log(
|
|
1390
|
+
`\u26A0\uFE0F Retry create customer with generated email failed for ${customerRef}:`,
|
|
1391
|
+
retryError instanceof Error ? retryError.message : retryError
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const unresolvedMessage = errorMessage || "Customer already exists but could not be resolved";
|
|
1396
|
+
throw new Error(
|
|
1397
|
+
`Failed to resolve existing customer for ${customerRef} after conflict: ${unresolvedMessage}. Ensure the existing customer is linked to this externalRef.`
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
this.log(
|
|
1401
|
+
`\u274C Failed to auto-create customer ${customerRef}:`,
|
|
1402
|
+
error instanceof Error ? error.message : error
|
|
1403
|
+
);
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
if (backendRef !== customerRef) {
|
|
1408
|
+
this.customerRefMapping.set(customerRef, backendRef);
|
|
1409
|
+
}
|
|
1410
|
+
return backendRef;
|
|
1411
|
+
}
|
|
1412
|
+
async trackUsage(customerRef, productRef, action, outcome, requestId, actionDuration) {
|
|
1413
|
+
await withRetry(
|
|
1414
|
+
() => this.apiClient.trackUsage({
|
|
1415
|
+
customerRef,
|
|
1416
|
+
actionType: "api_call",
|
|
1417
|
+
units: 1,
|
|
1418
|
+
outcome,
|
|
1419
|
+
productRef,
|
|
1420
|
+
duration: actionDuration,
|
|
1421
|
+
metadata: { action: action || "api_requests", requestId },
|
|
1422
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1423
|
+
}),
|
|
1424
|
+
{
|
|
1425
|
+
maxRetries: 2,
|
|
1426
|
+
initialDelay: 500,
|
|
1427
|
+
shouldRetry: (error) => error.message.includes("Customer not found"),
|
|
1428
|
+
onRetry: (_error, attempt) => {
|
|
1429
|
+
console.warn(`\u26A0\uFE0F Customer not found (attempt ${attempt + 1}/3), retrying in 500ms...`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
).catch((error) => {
|
|
1433
|
+
console.error("Usage tracking failed:", error);
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
// src/adapters/base.ts
|
|
1439
|
+
var EXTRA_FORWARD_KEY2 = "__solvapayExtra";
|
|
1440
|
+
var AdapterUtils = class {
|
|
1441
|
+
/**
|
|
1442
|
+
* Ensure customer reference is properly formatted
|
|
1443
|
+
*/
|
|
1444
|
+
static ensureCustomerRef(customerRef) {
|
|
1445
|
+
if (!customerRef || customerRef === "anonymous") {
|
|
1446
|
+
return "anonymous";
|
|
1447
|
+
}
|
|
1448
|
+
return customerRef;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Extract customer ref from JWT token
|
|
1452
|
+
*/
|
|
1453
|
+
static async extractFromJWT(token, options) {
|
|
1454
|
+
try {
|
|
1455
|
+
const { jwtVerify } = await import("jose");
|
|
1456
|
+
const jwtSecret = new TextEncoder().encode(
|
|
1457
|
+
options?.secret || process.env.OAUTH_JWKS_SECRET || "test-jwt-secret"
|
|
1458
|
+
);
|
|
1459
|
+
const { payload } = await jwtVerify(token, jwtSecret, {
|
|
1460
|
+
issuer: options?.issuer || process.env.OAUTH_ISSUER || "http://localhost:3000",
|
|
1461
|
+
audience: options?.audience || process.env.OAUTH_CLIENT_ID || "test-client-id"
|
|
1462
|
+
});
|
|
1463
|
+
return payload.sub || null;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return null;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
|
|
1470
|
+
const backendRefCache = /* @__PURE__ */ new Map();
|
|
1471
|
+
return async (context, extra) => {
|
|
1472
|
+
let args;
|
|
1473
|
+
let customerRef;
|
|
1474
|
+
try {
|
|
1475
|
+
args = await adapter.extractArgs(context);
|
|
1476
|
+
customerRef = await adapter.getCustomerRef(context, extra);
|
|
1477
|
+
let backendRef = backendRefCache.get(customerRef);
|
|
1478
|
+
if (!backendRef) {
|
|
1479
|
+
backendRef = await paywall.ensureCustomer(customerRef, customerRef);
|
|
1480
|
+
backendRefCache.set(customerRef, backendRef);
|
|
1481
|
+
}
|
|
1482
|
+
args.auth = { customer_ref: backendRef };
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
return adapter.formatError(error, context);
|
|
1485
|
+
}
|
|
1486
|
+
const decideGetCustomerRef = (args2) => args2.auth?.customer_ref || "anonymous";
|
|
1487
|
+
try {
|
|
1488
|
+
const decision = await paywall.decide(args, metadata, decideGetCustomerRef);
|
|
1489
|
+
if (decision.outcome === "gate") {
|
|
1490
|
+
return adapter.formatGate(decision.gate, context);
|
|
1491
|
+
}
|
|
1492
|
+
if (extra !== void 0) {
|
|
1493
|
+
;
|
|
1494
|
+
args[EXTRA_FORWARD_KEY2] = extra;
|
|
1495
|
+
}
|
|
1496
|
+
try {
|
|
1497
|
+
const result = await paywall.runAllow(decision, businessLogic, metadata, args);
|
|
1498
|
+
return adapter.formatResponse(result, context);
|
|
1499
|
+
} finally {
|
|
1500
|
+
if (EXTRA_FORWARD_KEY2 in args) {
|
|
1501
|
+
delete args[EXTRA_FORWARD_KEY2];
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
if (error instanceof PaywallError) {
|
|
1506
|
+
return adapter.formatGate(error.structuredContent, context);
|
|
1507
|
+
}
|
|
1508
|
+
return adapter.formatError(error, context);
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/adapters/http.ts
|
|
1514
|
+
var HttpAdapter = class {
|
|
1515
|
+
constructor(options = {}) {
|
|
1516
|
+
this.options = options;
|
|
1517
|
+
}
|
|
1518
|
+
extractArgs([req, _reply]) {
|
|
1519
|
+
if (this.options.extractArgs) {
|
|
1520
|
+
return this.options.extractArgs(req);
|
|
1521
|
+
}
|
|
1522
|
+
return {
|
|
1523
|
+
...req.body || {},
|
|
1524
|
+
...req.params || {},
|
|
1525
|
+
...req.query || {}
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
async getCustomerRef([req, _reply]) {
|
|
1529
|
+
if (this.options.getCustomerRef) {
|
|
1530
|
+
const ref = await this.options.getCustomerRef(req);
|
|
1531
|
+
return AdapterUtils.ensureCustomerRef(ref);
|
|
1532
|
+
}
|
|
1533
|
+
const headerRef = req.headers?.["x-customer-ref"];
|
|
1534
|
+
if (headerRef) {
|
|
1535
|
+
return AdapterUtils.ensureCustomerRef(headerRef);
|
|
1536
|
+
}
|
|
1537
|
+
const authHeader = req.headers?.["authorization"];
|
|
1538
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
1539
|
+
const token = authHeader.substring(7);
|
|
1540
|
+
const jwtSub = await AdapterUtils.extractFromJWT(token);
|
|
1541
|
+
if (jwtSub) {
|
|
1542
|
+
return AdapterUtils.ensureCustomerRef(jwtSub);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return "anonymous";
|
|
1546
|
+
}
|
|
1547
|
+
formatResponse(result, [_req, reply]) {
|
|
1548
|
+
if (this.options.transformResponse) {
|
|
1549
|
+
return this.options.transformResponse(result, reply);
|
|
1550
|
+
}
|
|
1551
|
+
if (reply && reply.status && typeof reply.json === "function") {
|
|
1552
|
+
reply.json(result);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Emit a 402 Payment Required response with the same JSON body shape
|
|
1559
|
+
* REST consumers have always received (`{success:false, error, product,
|
|
1560
|
+
* checkoutUrl, message, ...}`). The shape is reused via
|
|
1561
|
+
* `paywallErrorToClientPayload` so HTTP / Next / hosted-proxy clients
|
|
1562
|
+
* don't have to branch on an SDK version.
|
|
1563
|
+
*/
|
|
1564
|
+
formatGate(gate, [_req, reply]) {
|
|
1565
|
+
const errorResponse2 = paywallErrorToClientPayload(new PaywallError(gate.message, gate));
|
|
1566
|
+
if (reply && reply.status && typeof reply.json === "function") {
|
|
1567
|
+
reply.status(402).json(errorResponse2);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
if (reply && reply.code) {
|
|
1571
|
+
reply.code(402);
|
|
1572
|
+
}
|
|
1573
|
+
return errorResponse2;
|
|
1574
|
+
}
|
|
1575
|
+
formatError(error, [_req, reply]) {
|
|
1576
|
+
const errorResponse2 = {
|
|
1577
|
+
success: false,
|
|
1578
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
1579
|
+
};
|
|
1580
|
+
if (reply && reply.status && typeof reply.json === "function") {
|
|
1581
|
+
reply.status(500).json(errorResponse2);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (reply && reply.code) {
|
|
1585
|
+
reply.code(500);
|
|
1586
|
+
}
|
|
1587
|
+
return errorResponse2;
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
// src/adapters/next.ts
|
|
1592
|
+
var NextAdapter = class {
|
|
1593
|
+
constructor(options = {}) {
|
|
1594
|
+
this.options = options;
|
|
1595
|
+
}
|
|
1596
|
+
async extractArgs([request, context]) {
|
|
1597
|
+
if (this.options.extractArgs) {
|
|
1598
|
+
return await this.options.extractArgs(request, context);
|
|
1599
|
+
}
|
|
1600
|
+
const url = new URL(request.url);
|
|
1601
|
+
const query = Object.fromEntries(url.searchParams.entries());
|
|
1602
|
+
let body = {};
|
|
1603
|
+
try {
|
|
1604
|
+
if (request.method !== "GET" && request.headers.get("content-type")?.includes("application/json")) {
|
|
1605
|
+
body = await request.json();
|
|
1606
|
+
}
|
|
1607
|
+
} catch {
|
|
1608
|
+
}
|
|
1609
|
+
let routeParams = {};
|
|
1610
|
+
if (context?.params) {
|
|
1611
|
+
if (typeof context.params === "object" && "then" in context.params) {
|
|
1612
|
+
routeParams = await context.params;
|
|
1613
|
+
} else {
|
|
1614
|
+
routeParams = context.params;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
...body,
|
|
1619
|
+
...query,
|
|
1620
|
+
...routeParams
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
async getCustomerRef([request]) {
|
|
1624
|
+
if (this.options.getCustomerRef) {
|
|
1625
|
+
const ref = await this.options.getCustomerRef(request);
|
|
1626
|
+
return AdapterUtils.ensureCustomerRef(ref);
|
|
1627
|
+
}
|
|
1628
|
+
const authHeader = request.headers.get("authorization");
|
|
1629
|
+
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
1630
|
+
const token = authHeader.substring(7);
|
|
1631
|
+
const jwtSub = await AdapterUtils.extractFromJWT(token);
|
|
1632
|
+
if (jwtSub) {
|
|
1633
|
+
return AdapterUtils.ensureCustomerRef(jwtSub);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
const userId = request.headers.get("x-user-id");
|
|
1637
|
+
if (userId) {
|
|
1638
|
+
return AdapterUtils.ensureCustomerRef(userId);
|
|
1639
|
+
}
|
|
1640
|
+
const headerRef = request.headers.get("x-customer-ref");
|
|
1641
|
+
if (headerRef) {
|
|
1642
|
+
return AdapterUtils.ensureCustomerRef(headerRef);
|
|
1643
|
+
}
|
|
1644
|
+
return "demo_user";
|
|
1645
|
+
}
|
|
1646
|
+
formatResponse(result, _context) {
|
|
1647
|
+
const transformed = this.options.transformResponse ? this.options.transformResponse(result) : result;
|
|
1648
|
+
return new Response(JSON.stringify(transformed), {
|
|
1649
|
+
status: 200,
|
|
1650
|
+
headers: { "Content-Type": "application/json" }
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Emit a 402 Payment Required `Response` with the same JSON body
|
|
1655
|
+
* REST consumers have always received. Reuses
|
|
1656
|
+
* `paywallErrorToClientPayload` so HTTP / Next / hosted-proxy
|
|
1657
|
+
* clients don't have to branch on an SDK version.
|
|
1658
|
+
*/
|
|
1659
|
+
formatGate(gate, _context) {
|
|
1660
|
+
return new Response(
|
|
1661
|
+
JSON.stringify(paywallErrorToClientPayload(new PaywallError(gate.message, gate))),
|
|
1662
|
+
{
|
|
1663
|
+
status: 402,
|
|
1664
|
+
headers: { "Content-Type": "application/json" }
|
|
1665
|
+
}
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
formatError(error, _context) {
|
|
1669
|
+
return new Response(
|
|
1670
|
+
JSON.stringify({
|
|
1671
|
+
success: false,
|
|
1672
|
+
error: error instanceof Error ? error.message : "Internal server error"
|
|
1673
|
+
}),
|
|
1674
|
+
{
|
|
1675
|
+
status: 500,
|
|
1676
|
+
headers: { "Content-Type": "application/json" }
|
|
1677
|
+
}
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
// src/adapters/mcp.ts
|
|
1683
|
+
var McpAdapter = class {
|
|
1684
|
+
constructor(options = {}) {
|
|
1685
|
+
this.options = options;
|
|
1686
|
+
}
|
|
1687
|
+
extractArgs(args) {
|
|
1688
|
+
return args;
|
|
1689
|
+
}
|
|
1690
|
+
async getCustomerRef(args, extra) {
|
|
1691
|
+
if (this.options.getCustomerRef) {
|
|
1692
|
+
const ref = await this.options.getCustomerRef(args, extra);
|
|
1693
|
+
return AdapterUtils.ensureCustomerRef(ref);
|
|
1694
|
+
}
|
|
1695
|
+
const customerRefFromExtra = typeof extra?.authInfo?.extra?.customer_ref === "string" ? String(extra.authInfo.extra.customer_ref) : void 0;
|
|
1696
|
+
const customerRefFromArgs = typeof args.auth === "object" && args.auth !== null && typeof args.auth.customer_ref === "string" ? String(args.auth.customer_ref) : void 0;
|
|
1697
|
+
const directCustomerRef = typeof args.customer_ref === "string" ? args.customer_ref : void 0;
|
|
1698
|
+
const customerRef = (customerRefFromExtra || customerRefFromArgs || directCustomerRef || "anonymous").trim();
|
|
1699
|
+
return AdapterUtils.ensureCustomerRef(customerRef);
|
|
1700
|
+
}
|
|
1701
|
+
formatResponse(result, _context) {
|
|
1702
|
+
const transformed = this.options.transformResponse ? this.options.transformResponse(result) : result;
|
|
1703
|
+
const response = {
|
|
1704
|
+
content: [
|
|
1705
|
+
{
|
|
1706
|
+
type: "text",
|
|
1707
|
+
text: JSON.stringify(transformed, null, 2)
|
|
1708
|
+
}
|
|
1709
|
+
]
|
|
1710
|
+
};
|
|
1711
|
+
if (transformed && typeof transformed === "object" && !Array.isArray(transformed)) {
|
|
1712
|
+
response.structuredContent = transformed;
|
|
1713
|
+
}
|
|
1714
|
+
return response;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Emit a plain-narration paywall response — `content[0].text` carries
|
|
1718
|
+
* the gate's human message (LLM-actionable), `structuredContent`
|
|
1719
|
+
* carries the machine-readable gate payload, and `isError` stays
|
|
1720
|
+
* `false` per the MCP spec's own `isError` definition (paywall is
|
|
1721
|
+
* not a self-correctable tool execution error; it is a user-facing
|
|
1722
|
+
* control transfer to the UI).
|
|
1723
|
+
*
|
|
1724
|
+
* Hosts that read widget metadata from `tools/list` or tool-result
|
|
1725
|
+
* `_meta.ui` open the paywall iframe on top of this response;
|
|
1726
|
+
* `buildPayableHandler` stamps the `_meta.ui.resourceUri` envelope
|
|
1727
|
+
* before returning.
|
|
1728
|
+
*/
|
|
1729
|
+
formatGate(gate, _context) {
|
|
1730
|
+
return {
|
|
1731
|
+
content: [{ type: "text", text: gate.message }],
|
|
1732
|
+
isError: false,
|
|
1733
|
+
structuredContent: gate
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
formatError(error, _context) {
|
|
1737
|
+
return {
|
|
1738
|
+
content: [
|
|
1739
|
+
{
|
|
1740
|
+
type: "text",
|
|
1741
|
+
text: JSON.stringify(
|
|
1742
|
+
{
|
|
1743
|
+
success: false,
|
|
1744
|
+
error: error instanceof Error ? error.message : "Unknown error occurred"
|
|
1745
|
+
},
|
|
1746
|
+
null,
|
|
1747
|
+
2
|
|
1748
|
+
)
|
|
1749
|
+
}
|
|
1750
|
+
],
|
|
1751
|
+
isError: true
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
// src/factory.ts
|
|
1757
|
+
var import_core3 = require("@solvapay/core");
|
|
1758
|
+
|
|
1759
|
+
// src/virtual-tools.ts
|
|
1760
|
+
var TOOL_GET_USER_INFO = {
|
|
1761
|
+
name: "get_user_info",
|
|
1762
|
+
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.",
|
|
1763
|
+
inputSchema: {
|
|
1764
|
+
type: "object",
|
|
1765
|
+
properties: {},
|
|
1766
|
+
required: []
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
var TOOL_UPGRADE = {
|
|
1770
|
+
name: "upgrade",
|
|
1771
|
+
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.",
|
|
1772
|
+
inputSchema: {
|
|
1773
|
+
type: "object",
|
|
1774
|
+
properties: {
|
|
1775
|
+
planRef: {
|
|
1776
|
+
type: "string",
|
|
1777
|
+
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.'
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
required: []
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
var TOOL_MANAGE_ACCOUNT = {
|
|
1784
|
+
name: "manage_account",
|
|
1785
|
+
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.",
|
|
1786
|
+
inputSchema: {
|
|
1787
|
+
type: "object",
|
|
1788
|
+
properties: {},
|
|
1789
|
+
required: []
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
function mcpTextResult(text) {
|
|
1793
|
+
return { content: [{ type: "text", text }] };
|
|
1794
|
+
}
|
|
1795
|
+
function mcpErrorResult(message) {
|
|
1796
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
1797
|
+
}
|
|
1798
|
+
function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
|
|
1799
|
+
return async (args, extra) => {
|
|
1800
|
+
const customerRef = getCustomerRef(args, extra);
|
|
1801
|
+
try {
|
|
1802
|
+
if (!apiClient.getUserInfo) {
|
|
1803
|
+
return mcpErrorResult("getUserInfo is not available on this API client");
|
|
1804
|
+
}
|
|
1805
|
+
const userInfo = await apiClient.getUserInfo({ customerRef, productRef });
|
|
1806
|
+
return mcpTextResult(JSON.stringify(userInfo, null, 2));
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
return mcpErrorResult(
|
|
1809
|
+
`Failed to retrieve user information: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
function createUpgradeHandler(apiClient, productRef, getCustomerRef) {
|
|
1815
|
+
return async (args, extra) => {
|
|
1816
|
+
const customerRef = getCustomerRef(args, extra);
|
|
1817
|
+
const planRef = typeof args.planRef === "string" ? args.planRef : void 0;
|
|
1818
|
+
try {
|
|
1819
|
+
const result = await apiClient.createCheckoutSession({
|
|
1820
|
+
customerRef,
|
|
1821
|
+
productRef,
|
|
1822
|
+
...planRef && { planRef }
|
|
1823
|
+
});
|
|
1824
|
+
const checkoutUrl = result.checkoutUrl;
|
|
1825
|
+
if (planRef) {
|
|
1826
|
+
const responseText2 = `## Upgrade
|
|
1827
|
+
|
|
1828
|
+
**[Click here to upgrade \u2192](${checkoutUrl})**
|
|
1829
|
+
|
|
1830
|
+
After completing the checkout, your purchase will be activated immediately.`;
|
|
1831
|
+
return mcpTextResult(responseText2);
|
|
1832
|
+
}
|
|
1833
|
+
const responseText = `## Upgrade Your Subscription
|
|
1834
|
+
|
|
1835
|
+
**[Click here to view pricing options and upgrade \u2192](${checkoutUrl})**
|
|
1836
|
+
|
|
1837
|
+
You'll be able to compare options and select the one that's right for you.`;
|
|
1838
|
+
return mcpTextResult(responseText);
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
return mcpErrorResult(
|
|
1841
|
+
`Failed to create checkout session: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
function createManageAccountHandler(apiClient, productRef, getCustomerRef) {
|
|
1847
|
+
return async (args, extra) => {
|
|
1848
|
+
const customerRef = getCustomerRef(args, extra);
|
|
1849
|
+
try {
|
|
1850
|
+
const session = await apiClient.createCustomerSession({ customerRef, productRef });
|
|
1851
|
+
const portalUrl = session.customerUrl;
|
|
1852
|
+
const responseText = `## Manage Your Account
|
|
1853
|
+
|
|
1854
|
+
Access your account management portal to:
|
|
1855
|
+
- View your current account status
|
|
1856
|
+
- See billing history and invoices
|
|
1857
|
+
- Update payment methods
|
|
1858
|
+
- Cancel or modify your subscription
|
|
1859
|
+
|
|
1860
|
+
**[Open Account Portal \u2192](${portalUrl})**
|
|
1861
|
+
|
|
1862
|
+
This link is secure and will expire after a short period.`;
|
|
1863
|
+
return mcpTextResult(responseText);
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
return mcpErrorResult(
|
|
1866
|
+
`Failed to create customer portal session: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function createVirtualTools(apiClient, options) {
|
|
1872
|
+
const { product, getCustomerRef, exclude = [] } = options;
|
|
1873
|
+
const excludeSet = new Set(exclude);
|
|
1874
|
+
const allTools = [
|
|
1875
|
+
{
|
|
1876
|
+
...TOOL_GET_USER_INFO,
|
|
1877
|
+
handler: createGetUserInfoHandler(apiClient, product, getCustomerRef)
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
...TOOL_UPGRADE,
|
|
1881
|
+
handler: createUpgradeHandler(apiClient, product, getCustomerRef)
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
...TOOL_MANAGE_ACCOUNT,
|
|
1885
|
+
handler: createManageAccountHandler(apiClient, product, getCustomerRef)
|
|
1886
|
+
}
|
|
1887
|
+
];
|
|
1888
|
+
return allTools.filter((t) => !excludeSet.has(t.name));
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// src/register-virtual-tools-mcp.ts
|
|
1892
|
+
var import_node_module = require("module");
|
|
1893
|
+
function getNodeRequire() {
|
|
1894
|
+
try {
|
|
1895
|
+
return (0, eval)("require");
|
|
1896
|
+
} catch {
|
|
1897
|
+
return (0, import_node_module.createRequire)(`${process.cwd()}/package.json`);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
var nodeRequire = getNodeRequire();
|
|
1901
|
+
function getZod() {
|
|
1902
|
+
try {
|
|
1903
|
+
const zodModule = nodeRequire("zod");
|
|
1904
|
+
return zodModule.z ?? zodModule;
|
|
1905
|
+
} catch {
|
|
1906
|
+
throw new Error(
|
|
1907
|
+
"zod is required to use registerVirtualToolsMcp(). Install it as a dependency in your MCP server project."
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
function isJsonSchemaObject(value) {
|
|
1912
|
+
return Boolean(value && typeof value === "object");
|
|
1913
|
+
}
|
|
1914
|
+
function toZodSchema(property) {
|
|
1915
|
+
const z = getZod();
|
|
1916
|
+
if (Array.isArray(property.enum) && property.enum.length > 0) {
|
|
1917
|
+
if (property.enum.every((value) => typeof value === "string")) {
|
|
1918
|
+
const [first, ...rest] = property.enum;
|
|
1919
|
+
return z.enum([first, ...rest]);
|
|
1920
|
+
}
|
|
1921
|
+
if (property.enum.length === 1) {
|
|
1922
|
+
return z.literal(property.enum[0]);
|
|
1923
|
+
}
|
|
1924
|
+
return z.union(property.enum.map((value) => z.literal(value)));
|
|
1925
|
+
}
|
|
1926
|
+
switch (property.type) {
|
|
1927
|
+
case "string":
|
|
1928
|
+
return z.string();
|
|
1929
|
+
case "number":
|
|
1930
|
+
return z.number();
|
|
1931
|
+
case "integer":
|
|
1932
|
+
return z.number().int();
|
|
1933
|
+
case "boolean":
|
|
1934
|
+
return z.boolean();
|
|
1935
|
+
case "array":
|
|
1936
|
+
return z.array(
|
|
1937
|
+
isJsonSchemaObject(property.items) ? toZodSchema(property.items) : z.any()
|
|
1938
|
+
);
|
|
1939
|
+
case "object":
|
|
1940
|
+
return z.object(jsonSchemaToZodRawShape(property.properties || {}));
|
|
1941
|
+
default:
|
|
1942
|
+
return z.any();
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
function jsonSchemaToZodRawShape(properties, required = []) {
|
|
1946
|
+
const requiredSet = new Set(required);
|
|
1947
|
+
return Object.fromEntries(
|
|
1948
|
+
Object.entries(properties).map(([name, property]) => {
|
|
1949
|
+
const schema = toZodSchema(property);
|
|
1950
|
+
const finalSchema = requiredSet.has(name) ? schema : schema.optional();
|
|
1951
|
+
return [name, finalSchema];
|
|
1952
|
+
})
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
function defaultGetCustomerRef(_args, extra) {
|
|
1956
|
+
const fromExtra = extra?.authInfo?.extra?.customer_ref;
|
|
1957
|
+
return typeof fromExtra === "string" && fromExtra.trim() ? fromExtra.trim() : "anonymous";
|
|
1958
|
+
}
|
|
1959
|
+
function registerVirtualToolsMcpImpl(server, apiClient, options) {
|
|
1960
|
+
const { filter, mapDefinition, wrapHandler, getCustomerRef, ...virtualToolsOptions } = options;
|
|
1961
|
+
const virtualTools = createVirtualTools(apiClient, {
|
|
1962
|
+
...virtualToolsOptions,
|
|
1963
|
+
getCustomerRef: getCustomerRef || defaultGetCustomerRef
|
|
1964
|
+
});
|
|
1965
|
+
for (const toolDefinition of virtualTools) {
|
|
1966
|
+
if (filter && !filter(toolDefinition)) continue;
|
|
1967
|
+
const mappedDefinition = mapDefinition ? mapDefinition(toolDefinition) : toolDefinition;
|
|
1968
|
+
const wrappedHandler = wrapHandler ? wrapHandler(mappedDefinition.handler, mappedDefinition) : mappedDefinition.handler;
|
|
1969
|
+
server.registerTool(
|
|
1970
|
+
mappedDefinition.name,
|
|
1971
|
+
{
|
|
1972
|
+
description: mappedDefinition.description,
|
|
1973
|
+
inputSchema: jsonSchemaToZodRawShape(
|
|
1974
|
+
mappedDefinition.inputSchema.properties,
|
|
1975
|
+
mappedDefinition.inputSchema.required || []
|
|
1976
|
+
)
|
|
1977
|
+
},
|
|
1978
|
+
wrappedHandler
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/factory.ts
|
|
1984
|
+
function createSolvaPay(config) {
|
|
1985
|
+
let resolvedConfig;
|
|
1986
|
+
if (!config) {
|
|
1987
|
+
const envConfig = (0, import_core3.getSolvaPayConfig)();
|
|
1988
|
+
resolvedConfig = {
|
|
1989
|
+
apiKey: envConfig.apiKey,
|
|
1990
|
+
apiBaseUrl: envConfig.apiBaseUrl
|
|
1991
|
+
};
|
|
1992
|
+
} else {
|
|
1993
|
+
resolvedConfig = config;
|
|
1994
|
+
}
|
|
1995
|
+
const apiClient = resolvedConfig.apiClient || createSolvaPayClient({
|
|
1996
|
+
apiKey: resolvedConfig.apiKey,
|
|
1997
|
+
apiBaseUrl: resolvedConfig.apiBaseUrl
|
|
1998
|
+
});
|
|
1999
|
+
const paywall = new SolvaPayPaywall(apiClient, {
|
|
2000
|
+
debug: process.env.SOLVAPAY_DEBUG !== "false",
|
|
2001
|
+
limitsCacheTTL: resolvedConfig.limitsCacheTTL
|
|
2002
|
+
});
|
|
2003
|
+
return {
|
|
2004
|
+
// Direct access to API client for advanced operations
|
|
2005
|
+
apiClient,
|
|
2006
|
+
// Common API methods exposed directly for convenience
|
|
2007
|
+
ensureCustomer(customerRef, externalRef, options) {
|
|
2008
|
+
return paywall.ensureCustomer(customerRef, externalRef, options);
|
|
2009
|
+
},
|
|
2010
|
+
createPaymentIntent(params) {
|
|
2011
|
+
if (!apiClient.createPaymentIntent) {
|
|
2012
|
+
throw new import_core3.SolvaPayError("createPaymentIntent is not available on this API client");
|
|
2013
|
+
}
|
|
2014
|
+
return apiClient.createPaymentIntent(params);
|
|
2015
|
+
},
|
|
2016
|
+
createTopupPaymentIntent(params) {
|
|
2017
|
+
if (!apiClient.createTopupPaymentIntent) {
|
|
2018
|
+
throw new import_core3.SolvaPayError("createTopupPaymentIntent is not available on this API client");
|
|
2019
|
+
}
|
|
2020
|
+
return apiClient.createTopupPaymentIntent(params);
|
|
2021
|
+
},
|
|
2022
|
+
processPaymentIntent(params) {
|
|
2023
|
+
if (!apiClient.processPaymentIntent) {
|
|
2024
|
+
throw new import_core3.SolvaPayError("processPaymentIntent is not available on this API client");
|
|
2025
|
+
}
|
|
2026
|
+
return apiClient.processPaymentIntent(params);
|
|
2027
|
+
},
|
|
2028
|
+
checkLimits(params) {
|
|
2029
|
+
return apiClient.checkLimits(params);
|
|
2030
|
+
},
|
|
2031
|
+
trackUsage(params) {
|
|
2032
|
+
return apiClient.trackUsage(params);
|
|
2033
|
+
},
|
|
2034
|
+
createCustomer(params) {
|
|
2035
|
+
if (!apiClient.createCustomer) {
|
|
2036
|
+
throw new import_core3.SolvaPayError("createCustomer is not available on this API client");
|
|
2037
|
+
}
|
|
2038
|
+
return apiClient.createCustomer({ ...params, metadata: params.metadata ?? {} });
|
|
2039
|
+
},
|
|
2040
|
+
getCustomer(params) {
|
|
2041
|
+
return apiClient.getCustomer(params);
|
|
2042
|
+
},
|
|
2043
|
+
getCustomerBalance(params) {
|
|
2044
|
+
if (!apiClient.getCustomerBalance) {
|
|
2045
|
+
throw new import_core3.SolvaPayError("getCustomerBalance is not available on this API client");
|
|
2046
|
+
}
|
|
2047
|
+
return apiClient.getCustomerBalance(params);
|
|
2048
|
+
},
|
|
2049
|
+
createCheckoutSession(params) {
|
|
2050
|
+
return apiClient.createCheckoutSession({
|
|
2051
|
+
customerRef: params.customerRef,
|
|
2052
|
+
productRef: params.productRef,
|
|
2053
|
+
planRef: params.planRef
|
|
2054
|
+
});
|
|
2055
|
+
},
|
|
2056
|
+
createCustomerSession(params) {
|
|
2057
|
+
return apiClient.createCustomerSession(params);
|
|
2058
|
+
},
|
|
2059
|
+
activatePlan(params) {
|
|
2060
|
+
if (!apiClient.activatePlan) {
|
|
2061
|
+
throw new import_core3.SolvaPayError("activatePlan is not available on this API client");
|
|
2062
|
+
}
|
|
2063
|
+
return apiClient.activatePlan(params);
|
|
2064
|
+
},
|
|
2065
|
+
bootstrapMcpProduct(params) {
|
|
2066
|
+
if (!apiClient.bootstrapMcpProduct) {
|
|
2067
|
+
throw new import_core3.SolvaPayError("bootstrapMcpProduct is not available on this API client");
|
|
2068
|
+
}
|
|
2069
|
+
return apiClient.bootstrapMcpProduct(params);
|
|
2070
|
+
},
|
|
2071
|
+
configureMcpPlans(productRef, params) {
|
|
2072
|
+
if (!apiClient.configureMcpPlans) {
|
|
2073
|
+
throw new import_core3.SolvaPayError("configureMcpPlans is not available on this API client");
|
|
2074
|
+
}
|
|
2075
|
+
return apiClient.configureMcpPlans(productRef, params);
|
|
2076
|
+
},
|
|
2077
|
+
getVirtualTools(options) {
|
|
2078
|
+
return createVirtualTools(apiClient, options);
|
|
2079
|
+
},
|
|
2080
|
+
async registerVirtualToolsMcp(server, options) {
|
|
2081
|
+
await registerVirtualToolsMcpImpl(server, apiClient, options);
|
|
2082
|
+
},
|
|
2083
|
+
// Payable API for framework-specific handlers
|
|
2084
|
+
payable(options = {}) {
|
|
2085
|
+
const product = options.productRef || options.product || process.env.SOLVAPAY_PRODUCT || "default-product";
|
|
2086
|
+
const usageType = options.usageType || "requests";
|
|
2087
|
+
const metadata = { product, usageType };
|
|
2088
|
+
return {
|
|
2089
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2090
|
+
http(businessLogic, adapterOptions) {
|
|
2091
|
+
const adapter = new HttpAdapter({
|
|
2092
|
+
...adapterOptions,
|
|
2093
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
2094
|
+
});
|
|
2095
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
2096
|
+
return async (req, reply) => {
|
|
2097
|
+
const handler = await handlerPromise;
|
|
2098
|
+
return handler([req, reply]);
|
|
2099
|
+
};
|
|
2100
|
+
},
|
|
2101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2102
|
+
next(businessLogic, adapterOptions) {
|
|
2103
|
+
const adapter = new NextAdapter({
|
|
2104
|
+
...adapterOptions,
|
|
2105
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
2106
|
+
});
|
|
2107
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
2108
|
+
return async (request, context) => {
|
|
2109
|
+
const handler = await handlerPromise;
|
|
2110
|
+
return handler([request, context]);
|
|
2111
|
+
};
|
|
2112
|
+
},
|
|
2113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2114
|
+
mcp(businessLogic, adapterOptions) {
|
|
2115
|
+
const adapter = new McpAdapter({
|
|
2116
|
+
...adapterOptions,
|
|
2117
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
2118
|
+
});
|
|
2119
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
2120
|
+
return async (args, extra) => {
|
|
2121
|
+
const handler = await handlerPromise;
|
|
2122
|
+
return handler(args, extra);
|
|
2123
|
+
};
|
|
2124
|
+
},
|
|
2125
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2126
|
+
async function(businessLogic) {
|
|
2127
|
+
const getCustomerRef = (args) => {
|
|
2128
|
+
const configuredRef = options.getCustomerRef?.(args);
|
|
2129
|
+
if (typeof configuredRef === "string") {
|
|
2130
|
+
return configuredRef;
|
|
2131
|
+
}
|
|
2132
|
+
return args.auth?.customer_ref || "anonymous";
|
|
2133
|
+
};
|
|
2134
|
+
return paywall.protect(businessLogic, metadata, getCustomerRef);
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// src/helpers/customer.ts
|
|
2142
|
+
async function syncCustomerCore(request, options = {}) {
|
|
2143
|
+
try {
|
|
2144
|
+
const userResult = await getAuthenticatedUserCore(request, {
|
|
2145
|
+
includeEmail: options.includeEmail,
|
|
2146
|
+
includeName: options.includeName
|
|
2147
|
+
});
|
|
2148
|
+
if (isErrorResult(userResult)) {
|
|
2149
|
+
return userResult;
|
|
2150
|
+
}
|
|
2151
|
+
const { userId, email, name } = userResult;
|
|
2152
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2153
|
+
const customerRef = await solvaPay.ensureCustomer(userId, userId, {
|
|
2154
|
+
email: email || void 0,
|
|
2155
|
+
name: name || void 0
|
|
2156
|
+
});
|
|
2157
|
+
return customerRef;
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
return handleRouteError(error, "Sync customer", "Failed to sync customer");
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
async function getCustomerBalanceCore(request, options = {}) {
|
|
2163
|
+
try {
|
|
2164
|
+
const userResult = await getAuthenticatedUserCore(request);
|
|
2165
|
+
if (isErrorResult(userResult)) {
|
|
2166
|
+
return userResult;
|
|
2167
|
+
}
|
|
2168
|
+
const { userId, email, name } = userResult;
|
|
2169
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2170
|
+
const customerRef = await solvaPay.ensureCustomer(userId, userId, {
|
|
2171
|
+
email: email || void 0,
|
|
2172
|
+
name: name || void 0
|
|
2173
|
+
});
|
|
2174
|
+
const result = await solvaPay.getCustomerBalance({ customerRef });
|
|
2175
|
+
return result;
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
return handleRouteError(error, "Get customer credits", "Failed to get customer credits");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// src/helpers/payment.ts
|
|
2182
|
+
async function createPaymentIntentCore(request, body, options = {}) {
|
|
2183
|
+
try {
|
|
2184
|
+
if (!body.planRef || !body.productRef) {
|
|
2185
|
+
return {
|
|
2186
|
+
error: "Missing required parameters: planRef and productRef are required",
|
|
2187
|
+
status: 400
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
const customerResult = await syncCustomerCore(request, {
|
|
2191
|
+
solvaPay: options.solvaPay,
|
|
2192
|
+
includeEmail: options.includeEmail,
|
|
2193
|
+
includeName: options.includeName
|
|
2194
|
+
});
|
|
2195
|
+
if (isErrorResult(customerResult)) {
|
|
2196
|
+
return customerResult;
|
|
2197
|
+
}
|
|
2198
|
+
const customerRef = customerResult;
|
|
2199
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2200
|
+
const paymentIntent = await solvaPay.createPaymentIntent({
|
|
2201
|
+
productRef: body.productRef,
|
|
2202
|
+
planRef: body.planRef,
|
|
2203
|
+
customerRef
|
|
2204
|
+
});
|
|
2205
|
+
return {
|
|
2206
|
+
processorPaymentId: paymentIntent.processorPaymentId,
|
|
2207
|
+
clientSecret: paymentIntent.clientSecret,
|
|
2208
|
+
publishableKey: paymentIntent.publishableKey,
|
|
2209
|
+
accountId: paymentIntent.accountId,
|
|
2210
|
+
customerRef
|
|
2211
|
+
};
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
return handleRouteError(error, "Create payment intent", "Payment intent creation failed");
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
async function createTopupPaymentIntentCore(request, body, options = {}) {
|
|
2217
|
+
try {
|
|
2218
|
+
if (!body.amount || body.amount <= 0) {
|
|
2219
|
+
return {
|
|
2220
|
+
error: "Missing or invalid amount: must be a positive number",
|
|
2221
|
+
status: 400
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
if (!body.currency) {
|
|
2225
|
+
return {
|
|
2226
|
+
error: "Missing required parameter: currency",
|
|
2227
|
+
status: 400
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
if (body.currency !== body.currency.toUpperCase()) {
|
|
2231
|
+
return {
|
|
2232
|
+
error: `Invalid currency "${body.currency}": must be an uppercase ISO 4217 code (e.g. "USD", "EUR")`,
|
|
2233
|
+
status: 400
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
const customerResult = await syncCustomerCore(request, {
|
|
2237
|
+
solvaPay: options.solvaPay,
|
|
2238
|
+
includeEmail: options.includeEmail,
|
|
2239
|
+
includeName: options.includeName
|
|
2240
|
+
});
|
|
2241
|
+
if (isErrorResult(customerResult)) {
|
|
2242
|
+
return customerResult;
|
|
2243
|
+
}
|
|
2244
|
+
const customerRef = customerResult;
|
|
2245
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2246
|
+
const paymentIntent = await solvaPay.createTopupPaymentIntent({
|
|
2247
|
+
customerRef,
|
|
2248
|
+
amount: body.amount,
|
|
2249
|
+
currency: body.currency,
|
|
2250
|
+
description: body.description
|
|
2251
|
+
});
|
|
2252
|
+
return {
|
|
2253
|
+
processorPaymentId: paymentIntent.processorPaymentId,
|
|
2254
|
+
clientSecret: paymentIntent.clientSecret,
|
|
2255
|
+
publishableKey: paymentIntent.publishableKey,
|
|
2256
|
+
accountId: paymentIntent.accountId,
|
|
2257
|
+
customerRef
|
|
2258
|
+
};
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
return handleRouteError(
|
|
2261
|
+
error,
|
|
2262
|
+
"Create topup payment intent",
|
|
2263
|
+
"Topup payment intent creation failed"
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
async function processPaymentIntentCore(request, body, options = {}) {
|
|
2268
|
+
try {
|
|
2269
|
+
if (!body.paymentIntentId || !body.productRef) {
|
|
2270
|
+
return {
|
|
2271
|
+
error: "paymentIntentId and productRef are required",
|
|
2272
|
+
status: 400
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
const customerResult = await syncCustomerCore(request, {
|
|
2276
|
+
solvaPay: options.solvaPay
|
|
2277
|
+
});
|
|
2278
|
+
if (isErrorResult(customerResult)) {
|
|
2279
|
+
return customerResult;
|
|
2280
|
+
}
|
|
2281
|
+
const customerRef = customerResult;
|
|
2282
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2283
|
+
const result = await solvaPay.processPaymentIntent({
|
|
2284
|
+
paymentIntentId: body.paymentIntentId,
|
|
2285
|
+
productRef: body.productRef,
|
|
2286
|
+
customerRef,
|
|
2287
|
+
planRef: body.planRef
|
|
2288
|
+
});
|
|
2289
|
+
return result;
|
|
2290
|
+
} catch (error) {
|
|
2291
|
+
return handleRouteError(error, "Process payment intent", "Payment processing failed");
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// src/helpers/checkout.ts
|
|
2296
|
+
async function createCheckoutSessionCore(request, body, options = {}) {
|
|
2297
|
+
try {
|
|
2298
|
+
if (!body.productRef) {
|
|
2299
|
+
return {
|
|
2300
|
+
error: "Missing required parameter: productRef is required",
|
|
2301
|
+
status: 400
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
const customerResult = await syncCustomerCore(request, {
|
|
2305
|
+
solvaPay: options.solvaPay,
|
|
2306
|
+
includeEmail: options.includeEmail,
|
|
2307
|
+
includeName: options.includeName
|
|
2308
|
+
});
|
|
2309
|
+
if (isErrorResult(customerResult)) {
|
|
2310
|
+
return customerResult;
|
|
2311
|
+
}
|
|
2312
|
+
const customerRef = customerResult;
|
|
2313
|
+
let returnUrl = body.returnUrl || options.returnUrl;
|
|
2314
|
+
if (!returnUrl) {
|
|
2315
|
+
try {
|
|
2316
|
+
const url = new URL(request.url);
|
|
2317
|
+
returnUrl = url.origin;
|
|
2318
|
+
} catch {
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2322
|
+
const session = await solvaPay.createCheckoutSession({
|
|
2323
|
+
productRef: body.productRef,
|
|
2324
|
+
customerRef,
|
|
2325
|
+
planRef: body.planRef || void 0,
|
|
2326
|
+
returnUrl
|
|
2327
|
+
});
|
|
2328
|
+
return {
|
|
2329
|
+
sessionId: session.sessionId,
|
|
2330
|
+
checkoutUrl: session.checkoutUrl
|
|
2331
|
+
};
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
return handleRouteError(error, "Create checkout session", "Checkout session creation failed");
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
async function createCustomerSessionCore(request, options = {}) {
|
|
2337
|
+
try {
|
|
2338
|
+
const customerResult = await syncCustomerCore(request, {
|
|
2339
|
+
solvaPay: options.solvaPay,
|
|
2340
|
+
includeEmail: options.includeEmail,
|
|
2341
|
+
includeName: options.includeName
|
|
2342
|
+
});
|
|
2343
|
+
if (isErrorResult(customerResult)) {
|
|
2344
|
+
return customerResult;
|
|
2345
|
+
}
|
|
2346
|
+
const customerRef = customerResult;
|
|
2347
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2348
|
+
const session = await solvaPay.createCustomerSession({
|
|
2349
|
+
customerRef
|
|
2350
|
+
});
|
|
2351
|
+
return session;
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
return handleRouteError(error, "Create customer session", "Customer session creation failed");
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// src/helpers/renewal.ts
|
|
2358
|
+
var import_core4 = require("@solvapay/core");
|
|
2359
|
+
async function cancelPurchaseCore(request, body, options = {}) {
|
|
2360
|
+
try {
|
|
2361
|
+
if (!body.purchaseRef) {
|
|
2362
|
+
return {
|
|
2363
|
+
error: "Missing required parameter: purchaseRef is required",
|
|
2364
|
+
status: 400
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2368
|
+
if (!solvaPay.apiClient.cancelPurchase) {
|
|
2369
|
+
return {
|
|
2370
|
+
error: "Cancel purchase method not available on SDK client",
|
|
2371
|
+
status: 500
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
let cancelledPurchase = await solvaPay.apiClient.cancelPurchase({
|
|
2375
|
+
purchaseRef: body.purchaseRef,
|
|
2376
|
+
reason: body.reason
|
|
2377
|
+
});
|
|
2378
|
+
if (!cancelledPurchase || typeof cancelledPurchase !== "object") {
|
|
2379
|
+
return {
|
|
2380
|
+
error: "Invalid response from cancel purchase endpoint",
|
|
2381
|
+
status: 500
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
const responseObj = cancelledPurchase;
|
|
2385
|
+
if (responseObj.purchase && typeof responseObj.purchase === "object") {
|
|
2386
|
+
cancelledPurchase = responseObj.purchase;
|
|
2387
|
+
}
|
|
2388
|
+
if (!cancelledPurchase.reference) {
|
|
2389
|
+
return {
|
|
2390
|
+
error: "Cancel purchase response missing required fields",
|
|
2391
|
+
status: 500
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
const isCancelled = cancelledPurchase.status === "cancelled" || cancelledPurchase.cancelledAt;
|
|
2395
|
+
if (!isCancelled) {
|
|
2396
|
+
return {
|
|
2397
|
+
error: `Purchase cancellation failed: backend returned status '${cancelledPurchase.status}' without cancelledAt timestamp`,
|
|
2398
|
+
status: 500
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2402
|
+
return cancelledPurchase;
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
if (error instanceof import_core4.SolvaPayError) {
|
|
2405
|
+
const errorMessage = error.message;
|
|
2406
|
+
if (errorMessage.includes("not found")) {
|
|
2407
|
+
return {
|
|
2408
|
+
error: "Purchase not found",
|
|
2409
|
+
status: 404,
|
|
2410
|
+
details: errorMessage
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
if (errorMessage.includes("cannot be cancelled") || errorMessage.includes("does not belong to provider")) {
|
|
2414
|
+
return {
|
|
2415
|
+
error: "Purchase cannot be cancelled or does not belong to provider",
|
|
2416
|
+
status: 400,
|
|
2417
|
+
details: errorMessage
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
return {
|
|
2421
|
+
error: errorMessage,
|
|
2422
|
+
status: 500,
|
|
2423
|
+
details: errorMessage
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
return handleRouteError(error, "Cancel purchase", "Failed to cancel purchase");
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
async function reactivatePurchaseCore(request, body, options = {}) {
|
|
2430
|
+
try {
|
|
2431
|
+
if (!body.purchaseRef) {
|
|
2432
|
+
return {
|
|
2433
|
+
error: "Missing required parameter: purchaseRef is required",
|
|
2434
|
+
status: 400
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2438
|
+
if (!solvaPay.apiClient.reactivatePurchase) {
|
|
2439
|
+
return {
|
|
2440
|
+
error: "Reactivate purchase method not available on SDK client",
|
|
2441
|
+
status: 500
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
let reactivatedPurchase = await solvaPay.apiClient.reactivatePurchase({
|
|
2445
|
+
purchaseRef: body.purchaseRef
|
|
2446
|
+
});
|
|
2447
|
+
if (!reactivatedPurchase || typeof reactivatedPurchase !== "object") {
|
|
2448
|
+
return {
|
|
2449
|
+
error: "Invalid response from reactivate purchase endpoint",
|
|
2450
|
+
status: 500
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
const responseObj = reactivatedPurchase;
|
|
2454
|
+
if (responseObj.purchase && typeof responseObj.purchase === "object") {
|
|
2455
|
+
reactivatedPurchase = responseObj.purchase;
|
|
2456
|
+
}
|
|
2457
|
+
if (!reactivatedPurchase.reference) {
|
|
2458
|
+
return {
|
|
2459
|
+
error: "Reactivate purchase response missing required fields",
|
|
2460
|
+
status: 500
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
if (reactivatedPurchase.cancelledAt) {
|
|
2464
|
+
return {
|
|
2465
|
+
error: `Purchase reactivation failed: cancelledAt is still set`,
|
|
2466
|
+
status: 500
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2470
|
+
return reactivatedPurchase;
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
if (error instanceof import_core4.SolvaPayError) {
|
|
2473
|
+
const errorMessage = error.message;
|
|
2474
|
+
if (errorMessage.includes("not found")) {
|
|
2475
|
+
return {
|
|
2476
|
+
error: "Purchase not found",
|
|
2477
|
+
status: 404,
|
|
2478
|
+
details: errorMessage
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
if (errorMessage.includes("cannot be reactivated") || errorMessage.includes("not pending cancellation") || errorMessage.includes("already been fully cancelled") || errorMessage.includes("already ended")) {
|
|
2482
|
+
return {
|
|
2483
|
+
error: "Purchase cannot be reactivated",
|
|
2484
|
+
status: 400,
|
|
2485
|
+
details: errorMessage
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
return {
|
|
2489
|
+
error: errorMessage,
|
|
2490
|
+
status: 500,
|
|
2491
|
+
details: errorMessage
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
return handleRouteError(error, "Reactivate purchase", "Failed to reactivate purchase");
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// src/helpers/activation.ts
|
|
2499
|
+
async function activatePlanCore(request, body, options = {}) {
|
|
2500
|
+
try {
|
|
2501
|
+
if (!body.productRef || !body.planRef) {
|
|
2502
|
+
return {
|
|
2503
|
+
error: "Missing required parameters: productRef and planRef are required",
|
|
2504
|
+
status: 400
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
const customerResult = await syncCustomerCore(request, {
|
|
2508
|
+
solvaPay: options.solvaPay,
|
|
2509
|
+
includeEmail: options.includeEmail,
|
|
2510
|
+
includeName: options.includeName
|
|
2511
|
+
});
|
|
2512
|
+
if (isErrorResult(customerResult)) {
|
|
2513
|
+
return customerResult;
|
|
2514
|
+
}
|
|
2515
|
+
const customerRef = customerResult;
|
|
2516
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2517
|
+
return await solvaPay.activatePlan({
|
|
2518
|
+
customerRef,
|
|
2519
|
+
productRef: body.productRef,
|
|
2520
|
+
planRef: body.planRef
|
|
2521
|
+
});
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
return handleRouteError(error, "Activate plan", "Plan activation failed");
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// src/helpers/payment-method.ts
|
|
2528
|
+
async function getPaymentMethodCore(request, options = {}) {
|
|
2529
|
+
try {
|
|
2530
|
+
const customerResult = await syncCustomerCore(request, {
|
|
2531
|
+
solvaPay: options.solvaPay,
|
|
2532
|
+
includeEmail: options.includeEmail,
|
|
2533
|
+
includeName: options.includeName
|
|
2534
|
+
});
|
|
2535
|
+
if (isErrorResult(customerResult)) {
|
|
2536
|
+
return customerResult;
|
|
2537
|
+
}
|
|
2538
|
+
const customerRef = customerResult;
|
|
2539
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2540
|
+
if (!solvaPay.apiClient.getPaymentMethod) {
|
|
2541
|
+
return {
|
|
2542
|
+
error: "getPaymentMethod is not implemented on this API client",
|
|
2543
|
+
status: 500
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
return await solvaPay.apiClient.getPaymentMethod({ customerRef });
|
|
2547
|
+
} catch (error) {
|
|
2548
|
+
return handleRouteError(error, "Get payment method", "Failed to load payment method");
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// src/helpers/plans.ts
|
|
2553
|
+
var import_core5 = require("@solvapay/core");
|
|
2554
|
+
async function listPlansCore(request, options = {}) {
|
|
2555
|
+
try {
|
|
2556
|
+
const url = new URL(request.url);
|
|
2557
|
+
const productRef = url.searchParams.get("productRef");
|
|
2558
|
+
if (!productRef) {
|
|
2559
|
+
return {
|
|
2560
|
+
error: "Missing required parameter: productRef",
|
|
2561
|
+
status: 400
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
const apiClient = options.solvaPay?.apiClient ?? (() => {
|
|
2565
|
+
const config = (0, import_core5.getSolvaPayConfig)();
|
|
2566
|
+
if (!config.apiKey) return null;
|
|
2567
|
+
return createSolvaPayClient({
|
|
2568
|
+
apiKey: config.apiKey,
|
|
2569
|
+
apiBaseUrl: config.apiBaseUrl
|
|
2570
|
+
});
|
|
2571
|
+
})();
|
|
2572
|
+
if (!apiClient) {
|
|
2573
|
+
return {
|
|
2574
|
+
error: "Server configuration error: SolvaPay secret key not configured",
|
|
2575
|
+
status: 500
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
if (!apiClient.listPlans) {
|
|
2579
|
+
return {
|
|
2580
|
+
error: "List plans method not available",
|
|
2581
|
+
status: 500
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2584
|
+
const plans = await apiClient.listPlans(productRef);
|
|
2585
|
+
return {
|
|
2586
|
+
plans: plans || [],
|
|
2587
|
+
productRef
|
|
2588
|
+
};
|
|
2589
|
+
} catch (error) {
|
|
2590
|
+
return handleRouteError(error, "List plans", "Failed to fetch plans");
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// src/helpers/merchant.ts
|
|
2595
|
+
var import_core6 = require("@solvapay/core");
|
|
2596
|
+
async function getMerchantCore(_request, options = {}) {
|
|
2597
|
+
try {
|
|
2598
|
+
const apiClient = options.solvaPay?.apiClient ?? (() => {
|
|
2599
|
+
const config = (0, import_core6.getSolvaPayConfig)();
|
|
2600
|
+
if (!config.apiKey) return null;
|
|
2601
|
+
return createSolvaPayClient({
|
|
2602
|
+
apiKey: config.apiKey,
|
|
2603
|
+
apiBaseUrl: config.apiBaseUrl
|
|
2604
|
+
});
|
|
2605
|
+
})();
|
|
2606
|
+
if (!apiClient) {
|
|
2607
|
+
return {
|
|
2608
|
+
error: "Server configuration error: SolvaPay secret key not configured",
|
|
2609
|
+
status: 500
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
if (!apiClient.getMerchant) {
|
|
2613
|
+
return {
|
|
2614
|
+
error: "Get merchant method not available",
|
|
2615
|
+
status: 500
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
const merchant = await apiClient.getMerchant();
|
|
2619
|
+
return merchant;
|
|
2620
|
+
} catch (error) {
|
|
2621
|
+
return handleRouteError(error, "Get merchant", "Failed to fetch merchant");
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// src/helpers/product.ts
|
|
2626
|
+
var import_core7 = require("@solvapay/core");
|
|
2627
|
+
async function getProductCore(request, options = {}) {
|
|
2628
|
+
try {
|
|
2629
|
+
const url = new URL(request.url);
|
|
2630
|
+
const productRef = url.searchParams.get("productRef");
|
|
2631
|
+
if (!productRef) {
|
|
2632
|
+
return {
|
|
2633
|
+
error: "Missing required parameter: productRef",
|
|
2634
|
+
status: 400
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
const apiClient = options.solvaPay?.apiClient ?? (() => {
|
|
2638
|
+
const config = (0, import_core7.getSolvaPayConfig)();
|
|
2639
|
+
if (!config.apiKey) return null;
|
|
2640
|
+
return createSolvaPayClient({
|
|
2641
|
+
apiKey: config.apiKey,
|
|
2642
|
+
apiBaseUrl: config.apiBaseUrl
|
|
2643
|
+
});
|
|
2644
|
+
})();
|
|
2645
|
+
if (!apiClient) {
|
|
2646
|
+
return {
|
|
2647
|
+
error: "Server configuration error: SolvaPay secret key not configured",
|
|
2648
|
+
status: 500
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
if (!apiClient.getProduct) {
|
|
2652
|
+
return {
|
|
2653
|
+
error: "Get product method not available",
|
|
2654
|
+
status: 500
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
const product = await apiClient.getProduct(productRef);
|
|
2658
|
+
return product;
|
|
2659
|
+
} catch (error) {
|
|
2660
|
+
return handleRouteError(error, "Get product", "Failed to fetch product");
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// src/helpers/purchase.ts
|
|
2665
|
+
async function checkPurchaseCore(request, options = {}) {
|
|
2666
|
+
try {
|
|
2667
|
+
const userResult = await getAuthenticatedUserCore(request, {
|
|
2668
|
+
includeEmail: options.includeEmail,
|
|
2669
|
+
includeName: options.includeName
|
|
2670
|
+
});
|
|
2671
|
+
if (isErrorResult(userResult)) {
|
|
2672
|
+
return userResult;
|
|
2673
|
+
}
|
|
2674
|
+
const { userId, email, name } = userResult;
|
|
2675
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2676
|
+
const cachedCustomerRef = request.headers.get("x-solvapay-customer-ref");
|
|
2677
|
+
if (cachedCustomerRef) {
|
|
2678
|
+
try {
|
|
2679
|
+
const customer = await solvaPay.getCustomer({ customerRef: cachedCustomerRef });
|
|
2680
|
+
if (customer && customer.customerRef) {
|
|
2681
|
+
if (customer.externalRef && customer.externalRef === userId) {
|
|
2682
|
+
const filteredPurchases = (customer.purchases || []).filter(
|
|
2683
|
+
(p) => p.status === "active"
|
|
2684
|
+
);
|
|
2685
|
+
return {
|
|
2686
|
+
customerRef: customer.customerRef,
|
|
2687
|
+
email: customer.email,
|
|
2688
|
+
name: customer.name,
|
|
2689
|
+
purchases: filteredPurchases
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
} catch {
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
try {
|
|
2697
|
+
const customerRef = await solvaPay.ensureCustomer(userId, userId, {
|
|
2698
|
+
email: email || void 0,
|
|
2699
|
+
name: name || void 0
|
|
2700
|
+
});
|
|
2701
|
+
const customer = await solvaPay.getCustomer({ customerRef });
|
|
2702
|
+
const filteredPurchases = (customer.purchases || []).filter((p) => p.status === "active");
|
|
2703
|
+
return {
|
|
2704
|
+
customerRef: customer.customerRef || userId,
|
|
2705
|
+
email: customer.email,
|
|
2706
|
+
name: customer.name,
|
|
2707
|
+
purchases: filteredPurchases
|
|
2708
|
+
};
|
|
2709
|
+
} catch {
|
|
2710
|
+
return {
|
|
2711
|
+
customerRef: userId,
|
|
2712
|
+
purchases: []
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
return handleRouteError(error, "Check purchase", "Failed to check purchase");
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// src/helpers/usage.ts
|
|
2721
|
+
async function trackUsageCore(request, body, options = {}) {
|
|
2722
|
+
try {
|
|
2723
|
+
const userResult = await getAuthenticatedUserCore(request);
|
|
2724
|
+
if (isErrorResult(userResult)) {
|
|
2725
|
+
return userResult;
|
|
2726
|
+
}
|
|
2727
|
+
const { userId, email, name } = userResult;
|
|
2728
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2729
|
+
const customerRef = await solvaPay.ensureCustomer(userId, userId, {
|
|
2730
|
+
email: email || void 0,
|
|
2731
|
+
name: name || void 0
|
|
2732
|
+
});
|
|
2733
|
+
await solvaPay.trackUsage({
|
|
2734
|
+
customerRef,
|
|
2735
|
+
actionType: body.actionType,
|
|
2736
|
+
units: body.units,
|
|
2737
|
+
productRef: body.productRef,
|
|
2738
|
+
description: body.description,
|
|
2739
|
+
metadata: body.metadata
|
|
2740
|
+
});
|
|
2741
|
+
return { success: true };
|
|
2742
|
+
} catch (error) {
|
|
2743
|
+
return handleRouteError(error, "Track usage", "Track usage failed");
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// src/edge.ts
|
|
2748
|
+
var import_core8 = require("@solvapay/core");
|
|
2749
|
+
function timingSafeEqual(a, b) {
|
|
2750
|
+
if (a.length !== b.length) return false;
|
|
2751
|
+
let mismatch = 0;
|
|
2752
|
+
for (let i = 0; i < a.length; i++) {
|
|
2753
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2754
|
+
}
|
|
2755
|
+
return mismatch === 0;
|
|
2756
|
+
}
|
|
2757
|
+
async function verifyWebhook({
|
|
2758
|
+
body,
|
|
2759
|
+
signature,
|
|
2760
|
+
secret
|
|
2761
|
+
}) {
|
|
2762
|
+
const toleranceSec = 300;
|
|
2763
|
+
if (!signature) throw new import_core8.SolvaPayError("Missing webhook signature");
|
|
2764
|
+
const parts = signature.split(",");
|
|
2765
|
+
const tPart = parts.find((p) => p.startsWith("t="));
|
|
2766
|
+
const v1Part = parts.find((p) => p.startsWith("v1="));
|
|
2767
|
+
if (!tPart || !v1Part) {
|
|
2768
|
+
throw new import_core8.SolvaPayError("Malformed webhook signature");
|
|
2769
|
+
}
|
|
2770
|
+
const timestamp = parseInt(tPart.slice(2), 10);
|
|
2771
|
+
const receivedHmac = v1Part.slice(3);
|
|
2772
|
+
if (Number.isNaN(timestamp) || !receivedHmac) {
|
|
2773
|
+
throw new import_core8.SolvaPayError("Malformed webhook signature");
|
|
2774
|
+
}
|
|
2775
|
+
if (toleranceSec > 0) {
|
|
2776
|
+
const age = Math.abs(Math.floor(Date.now() / 1e3) - timestamp);
|
|
2777
|
+
if (age > toleranceSec) {
|
|
2778
|
+
throw new import_core8.SolvaPayError("Webhook signature timestamp too old");
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
const enc = new TextEncoder();
|
|
2782
|
+
const key = await crypto.subtle.importKey(
|
|
2783
|
+
"raw",
|
|
2784
|
+
enc.encode(secret),
|
|
2785
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
2786
|
+
false,
|
|
2787
|
+
["sign"]
|
|
2788
|
+
);
|
|
2789
|
+
const sigBuf = await crypto.subtle.sign("HMAC", key, enc.encode(`${timestamp}.${body}`));
|
|
2790
|
+
const expectedHmac = Array.from(new Uint8Array(sigBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2791
|
+
if (!timingSafeEqual(expectedHmac, receivedHmac)) {
|
|
2792
|
+
throw new import_core8.SolvaPayError("Invalid webhook signature");
|
|
2793
|
+
}
|
|
2794
|
+
try {
|
|
2795
|
+
return JSON.parse(body);
|
|
2796
|
+
} catch {
|
|
2797
|
+
throw new import_core8.SolvaPayError("Invalid webhook payload: body is not valid JSON");
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// src/fetch/cors.ts
|
|
2802
|
+
var corsConfig = { origins: ["*"] };
|
|
2803
|
+
function configureCors(config) {
|
|
2804
|
+
corsConfig = config;
|
|
2805
|
+
}
|
|
2806
|
+
function getCorsHeaders(req) {
|
|
2807
|
+
const origin = req.headers.get("Origin");
|
|
2808
|
+
const headers = {
|
|
2809
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
2810
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, x-solvapay-customer-ref",
|
|
2811
|
+
"Access-Control-Max-Age": "86400"
|
|
2812
|
+
};
|
|
2813
|
+
if (corsConfig.origins.includes("*")) {
|
|
2814
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
2815
|
+
} else if (origin && corsConfig.origins.includes(origin)) {
|
|
2816
|
+
headers["Access-Control-Allow-Origin"] = origin;
|
|
2817
|
+
headers["Vary"] = "Origin";
|
|
2818
|
+
}
|
|
2819
|
+
return headers;
|
|
2820
|
+
}
|
|
2821
|
+
function handleCors(req) {
|
|
2822
|
+
if (req.method !== "OPTIONS") {
|
|
2823
|
+
return null;
|
|
2824
|
+
}
|
|
2825
|
+
const headers = getCorsHeaders(req);
|
|
2826
|
+
return new Response(null, {
|
|
2827
|
+
status: 204,
|
|
2828
|
+
headers
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// src/fetch/utils.ts
|
|
2833
|
+
function errorResponse(result, req) {
|
|
2834
|
+
const corsHeaders = req ? getCorsHeaders(req) : {};
|
|
2835
|
+
return new Response(
|
|
2836
|
+
JSON.stringify({ error: result.error, ...result.details ? { details: result.details } : {} }),
|
|
2837
|
+
{
|
|
2838
|
+
status: result.status,
|
|
2839
|
+
headers: {
|
|
2840
|
+
"Content-Type": "application/json",
|
|
2841
|
+
...corsHeaders
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
);
|
|
2845
|
+
}
|
|
2846
|
+
function jsonResponseWithCors(data, req, status = 200) {
|
|
2847
|
+
const corsHeaders = getCorsHeaders(req);
|
|
2848
|
+
return new Response(JSON.stringify(data), {
|
|
2849
|
+
status,
|
|
2850
|
+
headers: {
|
|
2851
|
+
"Content-Type": "application/json",
|
|
2852
|
+
...corsHeaders
|
|
2853
|
+
}
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
// src/fetch/handlers.ts
|
|
2858
|
+
async function parseJsonBody(req) {
|
|
2859
|
+
try {
|
|
2860
|
+
return await req.json();
|
|
2861
|
+
} catch {
|
|
2862
|
+
return {};
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
async function checkPurchase(req) {
|
|
2866
|
+
const corsResponse = handleCors(req);
|
|
2867
|
+
if (corsResponse) return corsResponse;
|
|
2868
|
+
const result = await checkPurchaseCore(req);
|
|
2869
|
+
if (isErrorResult(result)) {
|
|
2870
|
+
return errorResponse(result, req);
|
|
2871
|
+
}
|
|
2872
|
+
return jsonResponseWithCors(result, req);
|
|
2873
|
+
}
|
|
2874
|
+
async function trackUsage(req) {
|
|
2875
|
+
const corsResponse = handleCors(req);
|
|
2876
|
+
if (corsResponse) return corsResponse;
|
|
2877
|
+
const body = await parseJsonBody(req);
|
|
2878
|
+
const result = await trackUsageCore(req, body, {});
|
|
2879
|
+
if (isErrorResult(result)) {
|
|
2880
|
+
return errorResponse(result, req);
|
|
2881
|
+
}
|
|
2882
|
+
return jsonResponseWithCors(result, req);
|
|
2883
|
+
}
|
|
2884
|
+
async function createPaymentIntent(req) {
|
|
2885
|
+
const corsResponse = handleCors(req);
|
|
2886
|
+
if (corsResponse) return corsResponse;
|
|
2887
|
+
const body = await parseJsonBody(req);
|
|
2888
|
+
const result = await createPaymentIntentCore(req, body);
|
|
2889
|
+
if (isErrorResult(result)) {
|
|
2890
|
+
return errorResponse(result, req);
|
|
2891
|
+
}
|
|
2892
|
+
return jsonResponseWithCors(result, req);
|
|
2893
|
+
}
|
|
2894
|
+
async function processPayment(req) {
|
|
2895
|
+
const corsResponse = handleCors(req);
|
|
2896
|
+
if (corsResponse) return corsResponse;
|
|
2897
|
+
const body = await parseJsonBody(req);
|
|
2898
|
+
const result = await processPaymentIntentCore(req, body);
|
|
2899
|
+
if (isErrorResult(result)) {
|
|
2900
|
+
return errorResponse(result, req);
|
|
2901
|
+
}
|
|
2902
|
+
return jsonResponseWithCors(result, req);
|
|
2903
|
+
}
|
|
2904
|
+
async function createTopupPaymentIntent(req) {
|
|
2905
|
+
const corsResponse = handleCors(req);
|
|
2906
|
+
if (corsResponse) return corsResponse;
|
|
2907
|
+
const body = await parseJsonBody(req);
|
|
2908
|
+
const result = await createTopupPaymentIntentCore(req, body);
|
|
2909
|
+
if (isErrorResult(result)) {
|
|
2910
|
+
return errorResponse(result, req);
|
|
2911
|
+
}
|
|
2912
|
+
return jsonResponseWithCors(result, req);
|
|
2913
|
+
}
|
|
2914
|
+
async function customerBalance(req) {
|
|
2915
|
+
const corsResponse = handleCors(req);
|
|
2916
|
+
if (corsResponse) return corsResponse;
|
|
2917
|
+
const result = await getCustomerBalanceCore(req);
|
|
2918
|
+
if (isErrorResult(result)) {
|
|
2919
|
+
return errorResponse(result, req);
|
|
2920
|
+
}
|
|
2921
|
+
return jsonResponseWithCors(result, req);
|
|
2922
|
+
}
|
|
2923
|
+
async function cancelRenewal(req) {
|
|
2924
|
+
const corsResponse = handleCors(req);
|
|
2925
|
+
if (corsResponse) return corsResponse;
|
|
2926
|
+
const body = await parseJsonBody(req);
|
|
2927
|
+
const result = await cancelPurchaseCore(req, body);
|
|
2928
|
+
if (isErrorResult(result)) {
|
|
2929
|
+
return errorResponse(result, req);
|
|
2930
|
+
}
|
|
2931
|
+
return jsonResponseWithCors(result, req);
|
|
2932
|
+
}
|
|
2933
|
+
async function reactivateRenewal(req) {
|
|
2934
|
+
const corsResponse = handleCors(req);
|
|
2935
|
+
if (corsResponse) return corsResponse;
|
|
2936
|
+
const body = await parseJsonBody(req);
|
|
2937
|
+
const result = await reactivatePurchaseCore(req, body);
|
|
2938
|
+
if (isErrorResult(result)) {
|
|
2939
|
+
return errorResponse(result, req);
|
|
2940
|
+
}
|
|
2941
|
+
return jsonResponseWithCors(result, req);
|
|
2942
|
+
}
|
|
2943
|
+
async function activatePlan(req) {
|
|
2944
|
+
const corsResponse = handleCors(req);
|
|
2945
|
+
if (corsResponse) return corsResponse;
|
|
2946
|
+
const body = await parseJsonBody(req);
|
|
2947
|
+
const result = await activatePlanCore(req, body);
|
|
2948
|
+
if (isErrorResult(result)) {
|
|
2949
|
+
return errorResponse(result, req);
|
|
2950
|
+
}
|
|
2951
|
+
return jsonResponseWithCors(result, req);
|
|
2952
|
+
}
|
|
2953
|
+
async function getPaymentMethod(req) {
|
|
2954
|
+
const corsResponse = handleCors(req);
|
|
2955
|
+
if (corsResponse) return corsResponse;
|
|
2956
|
+
const result = await getPaymentMethodCore(req);
|
|
2957
|
+
if (isErrorResult(result)) {
|
|
2958
|
+
return errorResponse(result, req);
|
|
2959
|
+
}
|
|
2960
|
+
return jsonResponseWithCors(result, req);
|
|
2961
|
+
}
|
|
2962
|
+
async function listPlans(req) {
|
|
2963
|
+
const corsResponse = handleCors(req);
|
|
2964
|
+
if (corsResponse) return corsResponse;
|
|
2965
|
+
const result = await listPlansCore(req);
|
|
2966
|
+
if (isErrorResult(result)) {
|
|
2967
|
+
return errorResponse(result, req);
|
|
2968
|
+
}
|
|
2969
|
+
return jsonResponseWithCors(result, req);
|
|
2970
|
+
}
|
|
2971
|
+
async function syncCustomer(req) {
|
|
2972
|
+
const corsResponse = handleCors(req);
|
|
2973
|
+
if (corsResponse) return corsResponse;
|
|
2974
|
+
const result = await syncCustomerCore(req);
|
|
2975
|
+
if (isErrorResult(result)) {
|
|
2976
|
+
return errorResponse(result, req);
|
|
2977
|
+
}
|
|
2978
|
+
return jsonResponseWithCors({ customerRef: result }, req);
|
|
2979
|
+
}
|
|
2980
|
+
async function createCheckoutSession(req) {
|
|
2981
|
+
const corsResponse = handleCors(req);
|
|
2982
|
+
if (corsResponse) return corsResponse;
|
|
2983
|
+
const body = await parseJsonBody(req);
|
|
2984
|
+
const result = await createCheckoutSessionCore(req, body);
|
|
2985
|
+
if (isErrorResult(result)) {
|
|
2986
|
+
return errorResponse(result, req);
|
|
2987
|
+
}
|
|
2988
|
+
return jsonResponseWithCors(result, req);
|
|
2989
|
+
}
|
|
2990
|
+
async function createCustomerSession(req) {
|
|
2991
|
+
const corsResponse = handleCors(req);
|
|
2992
|
+
if (corsResponse) return corsResponse;
|
|
2993
|
+
const result = await createCustomerSessionCore(req);
|
|
2994
|
+
if (isErrorResult(result)) {
|
|
2995
|
+
return errorResponse(result, req);
|
|
2996
|
+
}
|
|
2997
|
+
return jsonResponseWithCors(result, req);
|
|
2998
|
+
}
|
|
2999
|
+
async function getMerchant(req) {
|
|
3000
|
+
const corsResponse = handleCors(req);
|
|
3001
|
+
if (corsResponse) return corsResponse;
|
|
3002
|
+
const result = await getMerchantCore(req);
|
|
3003
|
+
if (isErrorResult(result)) {
|
|
3004
|
+
return errorResponse(result, req);
|
|
3005
|
+
}
|
|
3006
|
+
return jsonResponseWithCors(result, req);
|
|
3007
|
+
}
|
|
3008
|
+
async function getProduct(req) {
|
|
3009
|
+
const corsResponse = handleCors(req);
|
|
3010
|
+
if (corsResponse) return corsResponse;
|
|
3011
|
+
const result = await getProductCore(req);
|
|
3012
|
+
if (isErrorResult(result)) {
|
|
3013
|
+
return errorResponse(result, req);
|
|
3014
|
+
}
|
|
3015
|
+
return jsonResponseWithCors(result, req);
|
|
3016
|
+
}
|
|
3017
|
+
function solvapayWebhook(options) {
|
|
3018
|
+
return async (req) => {
|
|
3019
|
+
const secret = options.secret || (typeof process !== "undefined" ? process.env.SOLVAPAY_WEBHOOK_SECRET : void 0);
|
|
3020
|
+
if (!secret) {
|
|
3021
|
+
return new Response(JSON.stringify({ error: "Webhook secret not configured" }), {
|
|
3022
|
+
status: 500,
|
|
3023
|
+
headers: { "Content-Type": "application/json" }
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
const body = await req.text();
|
|
3027
|
+
const signature = req.headers.get("sv-signature") ?? "";
|
|
3028
|
+
let event;
|
|
3029
|
+
try {
|
|
3030
|
+
event = await verifyWebhook({ body, signature, secret });
|
|
3031
|
+
} catch {
|
|
3032
|
+
return new Response(JSON.stringify({ error: "Invalid webhook signature" }), {
|
|
3033
|
+
status: 401,
|
|
3034
|
+
headers: { "Content-Type": "application/json" }
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
try {
|
|
3038
|
+
await options.onEvent(event);
|
|
3039
|
+
} catch {
|
|
3040
|
+
return new Response(JSON.stringify({ error: "Webhook handler failed" }), {
|
|
3041
|
+
status: 500,
|
|
3042
|
+
headers: { "Content-Type": "application/json" }
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
return new Response(JSON.stringify({ received: true }), {
|
|
3046
|
+
status: 200,
|
|
3047
|
+
headers: { "Content-Type": "application/json" }
|
|
3048
|
+
});
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3052
|
+
0 && (module.exports = {
|
|
3053
|
+
activatePlan,
|
|
3054
|
+
cancelRenewal,
|
|
3055
|
+
checkPurchase,
|
|
3056
|
+
configureCors,
|
|
3057
|
+
createCheckoutSession,
|
|
3058
|
+
createCustomerSession,
|
|
3059
|
+
createPaymentIntent,
|
|
3060
|
+
createTopupPaymentIntent,
|
|
3061
|
+
customerBalance,
|
|
3062
|
+
getMerchant,
|
|
3063
|
+
getPaymentMethod,
|
|
3064
|
+
getProduct,
|
|
3065
|
+
listPlans,
|
|
3066
|
+
processPayment,
|
|
3067
|
+
reactivateRenewal,
|
|
3068
|
+
solvapayWebhook,
|
|
3069
|
+
syncCustomer,
|
|
3070
|
+
trackUsage
|
|
3071
|
+
});
|