@solvapay/server 1.0.0-preview.20 → 1.0.0-preview.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -6
- package/dist/edge.d.ts +1584 -310
- package/dist/edge.js +329 -50
- package/dist/index.cjs +395 -50
- package/dist/index.d.cts +1611 -310
- package/dist/index.d.ts +1611 -310
- package/dist/index.js +388 -50
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -39,10 +39,12 @@ function createSolvaPayClient(opts) {
|
|
|
39
39
|
// POST: /v1/sdk/usages
|
|
40
40
|
async trackUsage(params) {
|
|
41
41
|
const url = `${base}/v1/sdk/usages`;
|
|
42
|
+
const { customerRef, ...rest } = params;
|
|
43
|
+
const body = { ...rest, customerId: customerRef };
|
|
42
44
|
const res = await fetch(url, {
|
|
43
45
|
method: "POST",
|
|
44
46
|
headers,
|
|
45
|
-
body: JSON.stringify(
|
|
47
|
+
body: JSON.stringify(body)
|
|
46
48
|
});
|
|
47
49
|
if (!res.ok) {
|
|
48
50
|
const error = await res.text();
|
|
@@ -146,6 +148,21 @@ function createSolvaPayClient(opts) {
|
|
|
146
148
|
const result = await res.json();
|
|
147
149
|
return result;
|
|
148
150
|
},
|
|
151
|
+
// POST: /v1/sdk/products/mcp/bootstrap
|
|
152
|
+
async bootstrapMcpProduct(params) {
|
|
153
|
+
const url = `${base}/v1/sdk/products/mcp/bootstrap`;
|
|
154
|
+
const res = await fetch(url, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers,
|
|
157
|
+
body: JSON.stringify(params)
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const error = await res.text();
|
|
161
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
162
|
+
throw new SolvaPayError(`Bootstrap MCP product failed (${res.status}): ${error}`);
|
|
163
|
+
}
|
|
164
|
+
return await res.json();
|
|
165
|
+
},
|
|
149
166
|
// DELETE: /v1/sdk/products/{productRef}
|
|
150
167
|
async deleteProduct(productRef) {
|
|
151
168
|
const url = `${base}/v1/sdk/products/${productRef}`;
|
|
@@ -159,6 +176,21 @@ function createSolvaPayClient(opts) {
|
|
|
159
176
|
throw new SolvaPayError(`Delete product failed (${res.status}): ${error}`);
|
|
160
177
|
}
|
|
161
178
|
},
|
|
179
|
+
// POST: /v1/sdk/products/{productRef}/clone
|
|
180
|
+
async cloneProduct(productRef, overrides) {
|
|
181
|
+
const url = `${base}/v1/sdk/products/${productRef}/clone`;
|
|
182
|
+
const res = await fetch(url, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers,
|
|
185
|
+
body: JSON.stringify(overrides || {})
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
const error = await res.text();
|
|
189
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
190
|
+
throw new SolvaPayError(`Clone product failed (${res.status}): ${error}`);
|
|
191
|
+
}
|
|
192
|
+
return await res.json();
|
|
193
|
+
},
|
|
162
194
|
// GET: /v1/sdk/products/{productRef}/plans
|
|
163
195
|
async listPlans(productRef) {
|
|
164
196
|
const url = `${base}/v1/sdk/products/${productRef}/plans`;
|
|
@@ -201,6 +233,21 @@ function createSolvaPayClient(opts) {
|
|
|
201
233
|
const result = await res.json();
|
|
202
234
|
return result;
|
|
203
235
|
},
|
|
236
|
+
// PUT: /v1/sdk/products/{productRef}/plans/{planRef}
|
|
237
|
+
async updatePlan(productRef, planRef, params) {
|
|
238
|
+
const url = `${base}/v1/sdk/products/${productRef}/plans/${planRef}`;
|
|
239
|
+
const res = await fetch(url, {
|
|
240
|
+
method: "PUT",
|
|
241
|
+
headers,
|
|
242
|
+
body: JSON.stringify(params)
|
|
243
|
+
});
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
const error = await res.text();
|
|
246
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
247
|
+
throw new SolvaPayError(`Update plan failed (${res.status}): ${error}`);
|
|
248
|
+
}
|
|
249
|
+
return await res.json();
|
|
250
|
+
},
|
|
204
251
|
// DELETE: /v1/sdk/products/{productRef}/plans/{planRef}
|
|
205
252
|
async deletePlan(productRef, planRef) {
|
|
206
253
|
const url = `${base}/v1/sdk/products/${productRef}/plans/${planRef}`;
|
|
@@ -313,6 +360,21 @@ function createSolvaPayClient(opts) {
|
|
|
313
360
|
}
|
|
314
361
|
return result;
|
|
315
362
|
},
|
|
363
|
+
// POST: /v1/sdk/user-info
|
|
364
|
+
async getUserInfo(params) {
|
|
365
|
+
const url = `${base}/v1/sdk/user-info`;
|
|
366
|
+
const res = await fetch(url, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers,
|
|
369
|
+
body: JSON.stringify(params)
|
|
370
|
+
});
|
|
371
|
+
if (!res.ok) {
|
|
372
|
+
const error = await res.text();
|
|
373
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
374
|
+
throw new SolvaPayError(`Get user info failed (${res.status}): ${error}`);
|
|
375
|
+
}
|
|
376
|
+
return await res.json();
|
|
377
|
+
},
|
|
316
378
|
// POST: /v1/sdk/checkout-sessions
|
|
317
379
|
async createCheckoutSession(params) {
|
|
318
380
|
const url = `${base}/v1/sdk/checkout-sessions`;
|
|
@@ -511,11 +573,13 @@ var SolvaPayPaywall = class {
|
|
|
511
573
|
constructor(apiClient, options = {}) {
|
|
512
574
|
this.apiClient = apiClient;
|
|
513
575
|
this.debug = options.debug ?? process.env.SOLVAPAY_DEBUG === "true";
|
|
576
|
+
this.limitsCacheTTL = options.limitsCacheTTL ?? 1e4;
|
|
514
577
|
}
|
|
515
578
|
customerCreationAttempts = /* @__PURE__ */ new Set();
|
|
516
579
|
customerRefMapping = /* @__PURE__ */ new Map();
|
|
517
|
-
// input ref -> backend ref
|
|
518
580
|
debug;
|
|
581
|
+
limitsCache = /* @__PURE__ */ new Map();
|
|
582
|
+
limitsCacheTTL;
|
|
519
583
|
log(...args) {
|
|
520
584
|
if (this.debug) {
|
|
521
585
|
console.log(...args);
|
|
@@ -535,7 +599,9 @@ var SolvaPayPaywall = class {
|
|
|
535
599
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
536
600
|
async protect(handler, metadata = {}, getCustomerRef) {
|
|
537
601
|
const product = this.resolveProduct(metadata);
|
|
538
|
-
const
|
|
602
|
+
const configuredPlanRef = metadata.plan?.trim();
|
|
603
|
+
const usagePlanRef = configuredPlanRef || "unspecified";
|
|
604
|
+
const usageType = metadata.usageType || "requests";
|
|
539
605
|
return async (args) => {
|
|
540
606
|
const startTime = Date.now();
|
|
541
607
|
const requestId = this.generateRequestId();
|
|
@@ -546,19 +612,64 @@ var SolvaPayPaywall = class {
|
|
|
546
612
|
} else {
|
|
547
613
|
backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
|
|
548
614
|
}
|
|
615
|
+
let resolvedMeterName;
|
|
549
616
|
try {
|
|
550
|
-
const
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
617
|
+
const limitsCacheKey = `${backendCustomerRef}:${product}:${configuredPlanRef || ""}:${usageType}`;
|
|
618
|
+
const cachedLimits = this.limitsCache.get(limitsCacheKey);
|
|
619
|
+
const now = Date.now();
|
|
620
|
+
let withinLimits;
|
|
621
|
+
let remaining;
|
|
622
|
+
let checkoutUrl;
|
|
623
|
+
const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
|
|
624
|
+
if (hasFreshCachedLimits) {
|
|
625
|
+
checkoutUrl = cachedLimits.checkoutUrl;
|
|
626
|
+
resolvedMeterName = cachedLimits.meterName;
|
|
627
|
+
if (cachedLimits.remaining > 0) {
|
|
628
|
+
cachedLimits.remaining--;
|
|
629
|
+
if (cachedLimits.remaining <= 0) {
|
|
630
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
631
|
+
}
|
|
632
|
+
withinLimits = true;
|
|
633
|
+
remaining = cachedLimits.remaining;
|
|
634
|
+
} else {
|
|
635
|
+
withinLimits = false;
|
|
636
|
+
remaining = 0;
|
|
637
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
if (cachedLimits) {
|
|
641
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
642
|
+
}
|
|
643
|
+
const limitsCheck = await this.apiClient.checkLimits({
|
|
644
|
+
customerRef: backendCustomerRef,
|
|
645
|
+
productRef: product,
|
|
646
|
+
...configuredPlanRef ? { planRef: configuredPlanRef } : {},
|
|
647
|
+
meterName: usageType
|
|
648
|
+
});
|
|
649
|
+
withinLimits = limitsCheck.withinLimits;
|
|
650
|
+
remaining = limitsCheck.remaining;
|
|
651
|
+
checkoutUrl = limitsCheck.checkoutUrl;
|
|
652
|
+
resolvedMeterName = limitsCheck.meterName;
|
|
653
|
+
const consumedAllowance = withinLimits && remaining > 0;
|
|
654
|
+
if (consumedAllowance) {
|
|
655
|
+
remaining = Math.max(0, remaining - 1);
|
|
656
|
+
}
|
|
657
|
+
if (consumedAllowance) {
|
|
658
|
+
this.limitsCache.set(limitsCacheKey, {
|
|
659
|
+
remaining,
|
|
660
|
+
checkoutUrl,
|
|
661
|
+
meterName: resolvedMeterName,
|
|
662
|
+
timestamp: now
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!withinLimits) {
|
|
556
667
|
const latencyMs2 = Date.now() - startTime;
|
|
557
|
-
|
|
668
|
+
this.trackUsage(
|
|
558
669
|
backendCustomerRef,
|
|
559
670
|
product,
|
|
560
|
-
|
|
561
|
-
|
|
671
|
+
usagePlanRef,
|
|
672
|
+
resolvedMeterName || usageType,
|
|
562
673
|
"paywall",
|
|
563
674
|
requestId,
|
|
564
675
|
latencyMs2
|
|
@@ -566,17 +677,17 @@ var SolvaPayPaywall = class {
|
|
|
566
677
|
throw new PaywallError("Payment required", {
|
|
567
678
|
kind: "payment_required",
|
|
568
679
|
product,
|
|
569
|
-
checkoutUrl:
|
|
570
|
-
message: `
|
|
680
|
+
checkoutUrl: checkoutUrl || "",
|
|
681
|
+
message: `Purchase required. Remaining: ${remaining}`
|
|
571
682
|
});
|
|
572
683
|
}
|
|
573
684
|
const result = await handler(args);
|
|
574
685
|
const latencyMs = Date.now() - startTime;
|
|
575
|
-
|
|
686
|
+
this.trackUsage(
|
|
576
687
|
backendCustomerRef,
|
|
577
688
|
product,
|
|
578
|
-
|
|
579
|
-
|
|
689
|
+
usagePlanRef,
|
|
690
|
+
resolvedMeterName || usageType,
|
|
580
691
|
"success",
|
|
581
692
|
requestId,
|
|
582
693
|
latencyMs
|
|
@@ -589,18 +700,18 @@ var SolvaPayPaywall = class {
|
|
|
589
700
|
} else {
|
|
590
701
|
this.log(`\u274C Error in paywall:`, error);
|
|
591
702
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
703
|
+
if (!(error instanceof PaywallError)) {
|
|
704
|
+
const latencyMs = Date.now() - startTime;
|
|
705
|
+
this.trackUsage(
|
|
706
|
+
backendCustomerRef,
|
|
707
|
+
product,
|
|
708
|
+
usagePlanRef,
|
|
709
|
+
resolvedMeterName || usageType,
|
|
710
|
+
"fail",
|
|
711
|
+
requestId,
|
|
712
|
+
latencyMs
|
|
713
|
+
);
|
|
714
|
+
}
|
|
604
715
|
throw error;
|
|
605
716
|
}
|
|
606
717
|
};
|
|
@@ -749,24 +860,23 @@ var SolvaPayPaywall = class {
|
|
|
749
860
|
}
|
|
750
861
|
return backendRef;
|
|
751
862
|
}
|
|
752
|
-
async trackUsage(customerRef,
|
|
863
|
+
async trackUsage(customerRef, _productRef, _planRef, action, outcome, requestId, actionDuration) {
|
|
753
864
|
await withRetry(
|
|
754
865
|
() => this.apiClient.trackUsage({
|
|
755
866
|
customerRef,
|
|
756
|
-
|
|
757
|
-
|
|
867
|
+
actionType: "api_call",
|
|
868
|
+
units: 1,
|
|
758
869
|
outcome,
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
870
|
+
productReference: _productRef,
|
|
871
|
+
duration: actionDuration,
|
|
872
|
+
metadata: { action: action || "api_requests", requestId },
|
|
762
873
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
763
874
|
}),
|
|
764
875
|
{
|
|
765
876
|
maxRetries: 2,
|
|
766
877
|
initialDelay: 500,
|
|
767
878
|
shouldRetry: (error) => error.message.includes("Customer not found"),
|
|
768
|
-
|
|
769
|
-
onRetry: (error, attempt) => {
|
|
879
|
+
onRetry: (_error, attempt) => {
|
|
770
880
|
console.warn(`\u26A0\uFE0F Customer not found (attempt ${attempt + 1}/3), retrying in 500ms...`);
|
|
771
881
|
}
|
|
772
882
|
}
|
|
@@ -807,13 +917,19 @@ var AdapterUtils = class {
|
|
|
807
917
|
}
|
|
808
918
|
};
|
|
809
919
|
async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
|
|
920
|
+
const backendRefCache = /* @__PURE__ */ new Map();
|
|
921
|
+
const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
|
|
922
|
+
const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
|
|
810
923
|
return async (context) => {
|
|
811
924
|
try {
|
|
812
925
|
const args = await adapter.extractArgs(context);
|
|
813
926
|
const customerRef = await adapter.getCustomerRef(context);
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
927
|
+
let backendRef = backendRefCache.get(customerRef);
|
|
928
|
+
if (!backendRef) {
|
|
929
|
+
backendRef = await paywall.ensureCustomer(customerRef, customerRef);
|
|
930
|
+
backendRefCache.set(customerRef, backendRef);
|
|
931
|
+
}
|
|
932
|
+
args.auth = { customer_ref: backendRef };
|
|
817
933
|
const result = await protectedHandler(args);
|
|
818
934
|
return adapter.formatResponse(result, context);
|
|
819
935
|
} catch (error) {
|
|
@@ -1062,6 +1178,141 @@ var McpAdapter = class {
|
|
|
1062
1178
|
|
|
1063
1179
|
// src/factory.ts
|
|
1064
1180
|
import { SolvaPayError as SolvaPayError2, getSolvaPayConfig } from "@solvapay/core";
|
|
1181
|
+
|
|
1182
|
+
// src/virtual-tools.ts
|
|
1183
|
+
var TOOL_GET_USER_INFO = {
|
|
1184
|
+
name: "get_user_info",
|
|
1185
|
+
description: "Get information about the current user and their purchase status for this MCP server. Returns user profile (reference, name, email) and active purchase details including product name, type, dates, and usage limit if applicable.",
|
|
1186
|
+
inputSchema: {
|
|
1187
|
+
type: "object",
|
|
1188
|
+
properties: {},
|
|
1189
|
+
required: []
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
var TOOL_UPGRADE = {
|
|
1193
|
+
name: "upgrade",
|
|
1194
|
+
description: "Get available pricing options and checkout URLs for upgrading. Returns a list of available pricing options with their details (price, features) and checkout URLs. Users can click on a checkout URL to purchase. If a specific planRef is provided, returns only the checkout URL for that pricing option.",
|
|
1195
|
+
inputSchema: {
|
|
1196
|
+
type: "object",
|
|
1197
|
+
properties: {
|
|
1198
|
+
planRef: {
|
|
1199
|
+
type: "string",
|
|
1200
|
+
description: 'Optional pricing reference (e.g., "pln_abc123") to get a checkout URL for a specific option. If not provided, returns all available pricing options with their checkout URLs.'
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
required: []
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
var TOOL_MANAGE_ACCOUNT = {
|
|
1207
|
+
name: "manage_account",
|
|
1208
|
+
description: "Get a URL to the customer portal where users can view and manage their account. The portal shows current account status, billing history, and allows subscription changes. Returns a secure, time-limited URL that the user can click to access their account management page.",
|
|
1209
|
+
inputSchema: {
|
|
1210
|
+
type: "object",
|
|
1211
|
+
properties: {},
|
|
1212
|
+
required: []
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
var VIRTUAL_TOOL_DEFINITIONS = [TOOL_GET_USER_INFO, TOOL_UPGRADE, TOOL_MANAGE_ACCOUNT];
|
|
1216
|
+
function mcpTextResult(text) {
|
|
1217
|
+
return { content: [{ type: "text", text }] };
|
|
1218
|
+
}
|
|
1219
|
+
function mcpErrorResult(message) {
|
|
1220
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
1221
|
+
}
|
|
1222
|
+
function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
|
|
1223
|
+
return async (args) => {
|
|
1224
|
+
const customerRef = getCustomerRef(args);
|
|
1225
|
+
try {
|
|
1226
|
+
if (!apiClient.getUserInfo) {
|
|
1227
|
+
return mcpErrorResult("getUserInfo is not available on this API client");
|
|
1228
|
+
}
|
|
1229
|
+
const userInfo = await apiClient.getUserInfo({ customerRef, productRef });
|
|
1230
|
+
return mcpTextResult(JSON.stringify(userInfo, null, 2));
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
return mcpErrorResult(
|
|
1233
|
+
`Failed to retrieve user information: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function createUpgradeHandler(apiClient, productRef, getCustomerRef) {
|
|
1239
|
+
return async (args) => {
|
|
1240
|
+
const customerRef = getCustomerRef(args);
|
|
1241
|
+
const planRef = typeof args.planRef === "string" ? args.planRef : void 0;
|
|
1242
|
+
try {
|
|
1243
|
+
const result = await apiClient.createCheckoutSession({
|
|
1244
|
+
customerReference: customerRef,
|
|
1245
|
+
productRef,
|
|
1246
|
+
...planRef && { planRef }
|
|
1247
|
+
});
|
|
1248
|
+
const checkoutUrl = result.checkoutUrl;
|
|
1249
|
+
if (planRef) {
|
|
1250
|
+
const responseText2 = `## Upgrade
|
|
1251
|
+
|
|
1252
|
+
**[Click here to upgrade \u2192](${checkoutUrl})**
|
|
1253
|
+
|
|
1254
|
+
After completing the checkout, your purchase will be activated immediately.`;
|
|
1255
|
+
return mcpTextResult(responseText2);
|
|
1256
|
+
}
|
|
1257
|
+
const responseText = `## Upgrade Your Subscription
|
|
1258
|
+
|
|
1259
|
+
**[Click here to view pricing options and upgrade \u2192](${checkoutUrl})**
|
|
1260
|
+
|
|
1261
|
+
You'll be able to compare options and select the one that's right for you.`;
|
|
1262
|
+
return mcpTextResult(responseText);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
return mcpErrorResult(
|
|
1265
|
+
`Failed to create checkout session: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function createManageAccountHandler(apiClient, productRef, getCustomerRef) {
|
|
1271
|
+
return async (args) => {
|
|
1272
|
+
const customerRef = getCustomerRef(args);
|
|
1273
|
+
try {
|
|
1274
|
+
const session = await apiClient.createCustomerSession({ customerRef, productRef });
|
|
1275
|
+
const portalUrl = session.customerUrl;
|
|
1276
|
+
const responseText = `## Manage Your Account
|
|
1277
|
+
|
|
1278
|
+
Access your account management portal to:
|
|
1279
|
+
- View your current account status
|
|
1280
|
+
- See billing history and invoices
|
|
1281
|
+
- Update payment methods
|
|
1282
|
+
- Cancel or modify your subscription
|
|
1283
|
+
|
|
1284
|
+
**[Open Account Portal \u2192](${portalUrl})**
|
|
1285
|
+
|
|
1286
|
+
This link is secure and will expire after a short period.`;
|
|
1287
|
+
return mcpTextResult(responseText);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
return mcpErrorResult(
|
|
1290
|
+
`Failed to create customer portal session: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function createVirtualTools(apiClient, options) {
|
|
1296
|
+
const { product, getCustomerRef, exclude = [] } = options;
|
|
1297
|
+
const excludeSet = new Set(exclude);
|
|
1298
|
+
const allTools = [
|
|
1299
|
+
{
|
|
1300
|
+
...TOOL_GET_USER_INFO,
|
|
1301
|
+
handler: createGetUserInfoHandler(apiClient, product, getCustomerRef)
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
...TOOL_UPGRADE,
|
|
1305
|
+
handler: createUpgradeHandler(apiClient, product, getCustomerRef)
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
...TOOL_MANAGE_ACCOUNT,
|
|
1309
|
+
handler: createManageAccountHandler(apiClient, product, getCustomerRef)
|
|
1310
|
+
}
|
|
1311
|
+
];
|
|
1312
|
+
return allTools.filter((t) => !excludeSet.has(t.name));
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/factory.ts
|
|
1065
1316
|
function createSolvaPay(config) {
|
|
1066
1317
|
let resolvedConfig;
|
|
1067
1318
|
if (!config) {
|
|
@@ -1078,7 +1329,8 @@ function createSolvaPay(config) {
|
|
|
1078
1329
|
apiBaseUrl: resolvedConfig.apiBaseUrl
|
|
1079
1330
|
});
|
|
1080
1331
|
const paywall = new SolvaPayPaywall(apiClient, {
|
|
1081
|
-
debug: process.env.SOLVAPAY_DEBUG !== "false"
|
|
1332
|
+
debug: process.env.SOLVAPAY_DEBUG !== "false",
|
|
1333
|
+
limitsCacheTTL: resolvedConfig.limitsCacheTTL
|
|
1082
1334
|
});
|
|
1083
1335
|
return {
|
|
1084
1336
|
// Direct access to API client for advanced operations
|
|
@@ -1124,39 +1376,67 @@ function createSolvaPay(config) {
|
|
|
1124
1376
|
createCustomerSession(params) {
|
|
1125
1377
|
return apiClient.createCustomerSession(params);
|
|
1126
1378
|
},
|
|
1379
|
+
bootstrapMcpProduct(params) {
|
|
1380
|
+
if (!apiClient.bootstrapMcpProduct) {
|
|
1381
|
+
throw new SolvaPayError2("bootstrapMcpProduct is not available on this API client");
|
|
1382
|
+
}
|
|
1383
|
+
return apiClient.bootstrapMcpProduct(params);
|
|
1384
|
+
},
|
|
1385
|
+
getVirtualTools(options) {
|
|
1386
|
+
return createVirtualTools(apiClient, options);
|
|
1387
|
+
},
|
|
1127
1388
|
// Payable API for framework-specific handlers
|
|
1128
1389
|
payable(options = {}) {
|
|
1129
1390
|
const product = options.productRef || options.product || process.env.SOLVAPAY_PRODUCT || "default-product";
|
|
1130
|
-
const plan = options.planRef || options.plan
|
|
1131
|
-
const
|
|
1391
|
+
const plan = options.planRef || options.plan;
|
|
1392
|
+
const usageType = options.usageType || "requests";
|
|
1393
|
+
const metadata = { product, plan, usageType };
|
|
1132
1394
|
return {
|
|
1133
1395
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1134
1396
|
http(businessLogic, adapterOptions) {
|
|
1135
|
-
const adapter = new HttpAdapter(
|
|
1397
|
+
const adapter = new HttpAdapter({
|
|
1398
|
+
...adapterOptions,
|
|
1399
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
1400
|
+
});
|
|
1401
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
1136
1402
|
return async (req, reply) => {
|
|
1137
|
-
const handler = await
|
|
1403
|
+
const handler = await handlerPromise;
|
|
1138
1404
|
return handler([req, reply]);
|
|
1139
1405
|
};
|
|
1140
1406
|
},
|
|
1141
1407
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1142
1408
|
next(businessLogic, adapterOptions) {
|
|
1143
|
-
const adapter = new NextAdapter(
|
|
1409
|
+
const adapter = new NextAdapter({
|
|
1410
|
+
...adapterOptions,
|
|
1411
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
1412
|
+
});
|
|
1413
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
1144
1414
|
return async (request, context) => {
|
|
1145
|
-
const handler = await
|
|
1415
|
+
const handler = await handlerPromise;
|
|
1146
1416
|
return handler([request, context]);
|
|
1147
1417
|
};
|
|
1148
1418
|
},
|
|
1149
1419
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1150
1420
|
mcp(businessLogic, adapterOptions) {
|
|
1151
|
-
const adapter = new McpAdapter(
|
|
1421
|
+
const adapter = new McpAdapter({
|
|
1422
|
+
...adapterOptions,
|
|
1423
|
+
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
1424
|
+
});
|
|
1425
|
+
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
1152
1426
|
return async (args) => {
|
|
1153
|
-
const handler = await
|
|
1427
|
+
const handler = await handlerPromise;
|
|
1154
1428
|
return handler(args);
|
|
1155
1429
|
};
|
|
1156
1430
|
},
|
|
1157
1431
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1158
1432
|
async function(businessLogic) {
|
|
1159
|
-
const getCustomerRef = (args) =>
|
|
1433
|
+
const getCustomerRef = (args) => {
|
|
1434
|
+
const configuredRef = options.getCustomerRef?.(args);
|
|
1435
|
+
if (typeof configuredRef === "string") {
|
|
1436
|
+
return configuredRef;
|
|
1437
|
+
}
|
|
1438
|
+
return args.auth?.customer_ref || "anonymous";
|
|
1439
|
+
};
|
|
1160
1440
|
return paywall.protect(businessLogic, metadata, getCustomerRef);
|
|
1161
1441
|
}
|
|
1162
1442
|
};
|
|
@@ -1164,6 +1444,57 @@ function createSolvaPay(config) {
|
|
|
1164
1444
|
};
|
|
1165
1445
|
}
|
|
1166
1446
|
|
|
1447
|
+
// src/mcp-auth.ts
|
|
1448
|
+
var McpBearerAuthError = class extends Error {
|
|
1449
|
+
constructor(message) {
|
|
1450
|
+
super(message);
|
|
1451
|
+
this.name = "McpBearerAuthError";
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
function base64UrlDecode(input) {
|
|
1455
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1456
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
1457
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
1458
|
+
}
|
|
1459
|
+
function extractBearerToken(authorization) {
|
|
1460
|
+
if (!authorization) return null;
|
|
1461
|
+
if (!authorization.startsWith("Bearer ")) return null;
|
|
1462
|
+
return authorization.slice(7).trim() || null;
|
|
1463
|
+
}
|
|
1464
|
+
function decodeJwtPayload(token) {
|
|
1465
|
+
const parts = token.split(".");
|
|
1466
|
+
if (parts.length < 2) {
|
|
1467
|
+
throw new McpBearerAuthError("Invalid JWT format");
|
|
1468
|
+
}
|
|
1469
|
+
try {
|
|
1470
|
+
const payloadText = base64UrlDecode(parts[1]);
|
|
1471
|
+
const payload = JSON.parse(payloadText);
|
|
1472
|
+
return payload;
|
|
1473
|
+
} catch {
|
|
1474
|
+
throw new McpBearerAuthError("Invalid JWT payload");
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function getCustomerRefFromJwtPayload(payload, options = {}) {
|
|
1478
|
+
const claimPriority = options.claimPriority || ["customerRef", "customer_ref", "sub"];
|
|
1479
|
+
for (const claim of claimPriority) {
|
|
1480
|
+
const value = payload[claim];
|
|
1481
|
+
if (typeof value === "string" && value.trim()) {
|
|
1482
|
+
return value.trim();
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
throw new McpBearerAuthError(
|
|
1486
|
+
`No customer reference claim found (checked: ${claimPriority.join(", ")})`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
function getCustomerRefFromBearerAuthHeader(authorization, options = {}) {
|
|
1490
|
+
const token = extractBearerToken(authorization);
|
|
1491
|
+
if (!token) {
|
|
1492
|
+
throw new McpBearerAuthError("Missing bearer token");
|
|
1493
|
+
}
|
|
1494
|
+
const payload = decodeJwtPayload(token);
|
|
1495
|
+
return getCustomerRefFromJwtPayload(payload, options);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1167
1498
|
// src/helpers/error.ts
|
|
1168
1499
|
import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
|
|
1169
1500
|
function isErrorResult(result) {
|
|
@@ -1488,14 +1819,21 @@ function verifyWebhook({
|
|
|
1488
1819
|
return JSON.parse(body);
|
|
1489
1820
|
}
|
|
1490
1821
|
export {
|
|
1822
|
+
McpBearerAuthError,
|
|
1491
1823
|
PaywallError,
|
|
1824
|
+
VIRTUAL_TOOL_DEFINITIONS,
|
|
1492
1825
|
cancelPurchaseCore,
|
|
1493
1826
|
createCheckoutSessionCore,
|
|
1494
1827
|
createCustomerSessionCore,
|
|
1495
1828
|
createPaymentIntentCore,
|
|
1496
1829
|
createSolvaPay,
|
|
1497
1830
|
createSolvaPayClient,
|
|
1831
|
+
createVirtualTools,
|
|
1832
|
+
decodeJwtPayload,
|
|
1833
|
+
extractBearerToken,
|
|
1498
1834
|
getAuthenticatedUserCore,
|
|
1835
|
+
getCustomerRefFromBearerAuthHeader,
|
|
1836
|
+
getCustomerRefFromJwtPayload,
|
|
1499
1837
|
handleRouteError,
|
|
1500
1838
|
isErrorResult,
|
|
1501
1839
|
listPlansCore,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solvapay/server",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
},
|
|
38
38
|
"sideEffects": false,
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@solvapay/core": "1.0.0-preview.
|
|
40
|
+
"@solvapay/core": "1.0.0-preview.21"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@solvapay/auth": "1.0.0-preview.
|
|
43
|
+
"@solvapay/auth": "1.0.0-preview.21"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"dotenv": "^17.2.3",
|
|
@@ -51,13 +51,14 @@
|
|
|
51
51
|
"typescript": "^5.5.4",
|
|
52
52
|
"vitest": "^2.0.5",
|
|
53
53
|
"@solvapay/demo-services": "0.0.0",
|
|
54
|
-
"@solvapay/auth": "1.0.0-preview.
|
|
54
|
+
"@solvapay/auth": "1.0.0-preview.21",
|
|
55
|
+
"@solvapay/test-utils": "^0.0.0"
|
|
55
56
|
},
|
|
56
57
|
"scripts": {
|
|
57
58
|
"build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.build.json && tsup src/edge.ts --format esm --dts --tsconfig tsconfig.build.json",
|
|
58
59
|
"generate:types": "tsx scripts/generate-types.ts",
|
|
59
|
-
"test": "vitest run __tests__/paywall.unit.test.ts",
|
|
60
|
-
"test:unit": "vitest run __tests__/paywall.unit.test.ts",
|
|
60
|
+
"test": "vitest run __tests__/paywall.unit.test.ts __tests__/mcp-auth.unit.test.ts __tests__/bootstrap-mcp.unit.test.ts",
|
|
61
|
+
"test:unit": "vitest run __tests__/paywall.unit.test.ts __tests__/mcp-auth.unit.test.ts __tests__/bootstrap-mcp.unit.test.ts",
|
|
61
62
|
"test:integration": "vitest run __tests__/backend.integration.test.ts",
|
|
62
63
|
"test:integration:payment": "vitest run __tests__/payment-stripe.integration.test.ts",
|
|
63
64
|
"test:all": "vitest run",
|