@solvapay/server 1.0.1-preview.2 → 1.0.1-preview.4
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 +1 -1
- package/dist/edge.d.ts +1722 -1194
- package/dist/edge.js +182 -32
- package/dist/index.cjs +356 -32
- package/dist/index.d.cts +1780 -1187
- package/dist/index.d.ts +1780 -1187
- package/dist/index.js +349 -32
- package/package.json +14 -4
package/dist/index.js
CHANGED
|
@@ -163,6 +163,21 @@ function createSolvaPayClient(opts) {
|
|
|
163
163
|
}
|
|
164
164
|
return await res.json();
|
|
165
165
|
},
|
|
166
|
+
// PUT: /v1/sdk/products/{productRef}/mcp/plans
|
|
167
|
+
async configureMcpPlans(productRef, params) {
|
|
168
|
+
const url = `${base}/v1/sdk/products/${productRef}/mcp/plans`;
|
|
169
|
+
const res = await fetch(url, {
|
|
170
|
+
method: "PUT",
|
|
171
|
+
headers,
|
|
172
|
+
body: JSON.stringify(params)
|
|
173
|
+
});
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const error = await res.text();
|
|
176
|
+
log(`\u274C API Error: ${res.status} - ${error}`);
|
|
177
|
+
throw new SolvaPayError(`Configure MCP plans failed (${res.status}): ${error}`);
|
|
178
|
+
}
|
|
179
|
+
return await res.json();
|
|
180
|
+
},
|
|
166
181
|
// DELETE: /v1/sdk/products/{productRef}
|
|
167
182
|
async deleteProduct(productRef) {
|
|
168
183
|
const url = `${base}/v1/sdk/products/${productRef}`;
|
|
@@ -561,6 +576,24 @@ var PaywallError = class extends Error {
|
|
|
561
576
|
this.name = "PaywallError";
|
|
562
577
|
}
|
|
563
578
|
};
|
|
579
|
+
function paywallErrorToClientPayload(error) {
|
|
580
|
+
const sc = error.structuredContent;
|
|
581
|
+
const base = {
|
|
582
|
+
success: false,
|
|
583
|
+
error: sc.kind === "activation_required" ? "Activation required" : "Payment required",
|
|
584
|
+
product: sc.product,
|
|
585
|
+
checkoutUrl: sc.checkoutUrl,
|
|
586
|
+
message: sc.message
|
|
587
|
+
};
|
|
588
|
+
if (sc.kind === "activation_required") {
|
|
589
|
+
base.kind = "activation_required";
|
|
590
|
+
if (sc.plans !== void 0) base.plans = sc.plans;
|
|
591
|
+
if (sc.balance !== void 0) base.balance = sc.balance;
|
|
592
|
+
if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
|
|
593
|
+
if (sc.confirmationUrl !== void 0) base.confirmationUrl = sc.confirmationUrl;
|
|
594
|
+
}
|
|
595
|
+
return base;
|
|
596
|
+
}
|
|
564
597
|
var sharedCustomerLookupDeduplicator = createRequestDeduplicator({
|
|
565
598
|
cacheTTL: 6e4,
|
|
566
599
|
// Cache results for 60 seconds (reduces API calls significantly)
|
|
@@ -620,6 +653,7 @@ var SolvaPayPaywall = class {
|
|
|
620
653
|
let withinLimits;
|
|
621
654
|
let remaining;
|
|
622
655
|
let checkoutUrl;
|
|
656
|
+
let lastLimitsCheck;
|
|
623
657
|
const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
|
|
624
658
|
if (hasFreshCachedLimits) {
|
|
625
659
|
checkoutUrl = cachedLimits.checkoutUrl;
|
|
@@ -646,6 +680,7 @@ var SolvaPayPaywall = class {
|
|
|
646
680
|
...configuredPlanRef ? { planRef: configuredPlanRef } : {},
|
|
647
681
|
meterName: usageType
|
|
648
682
|
});
|
|
683
|
+
lastLimitsCheck = limitsCheck;
|
|
649
684
|
withinLimits = limitsCheck.withinLimits;
|
|
650
685
|
remaining = limitsCheck.remaining;
|
|
651
686
|
checkoutUrl = limitsCheck.checkoutUrl;
|
|
@@ -674,6 +709,20 @@ var SolvaPayPaywall = class {
|
|
|
674
709
|
requestId,
|
|
675
710
|
latencyMs2
|
|
676
711
|
);
|
|
712
|
+
if (lastLimitsCheck?.activationRequired) {
|
|
713
|
+
const confirmationUrl = lastLimitsCheck.confirmationUrl;
|
|
714
|
+
const payCheckoutUrl = confirmationUrl || lastLimitsCheck.checkoutUrl || checkoutUrl || "";
|
|
715
|
+
throw new PaywallError("Activation required", {
|
|
716
|
+
kind: "activation_required",
|
|
717
|
+
product,
|
|
718
|
+
message: "Product activation is required before this tool can be used.",
|
|
719
|
+
checkoutUrl: payCheckoutUrl,
|
|
720
|
+
confirmationUrl,
|
|
721
|
+
plans: lastLimitsCheck.plans,
|
|
722
|
+
balance: lastLimitsCheck.balance,
|
|
723
|
+
productDetails: lastLimitsCheck.product
|
|
724
|
+
});
|
|
725
|
+
}
|
|
677
726
|
throw new PaywallError("Payment required", {
|
|
678
727
|
kind: "payment_required",
|
|
679
728
|
product,
|
|
@@ -776,7 +825,8 @@ var SolvaPayPaywall = class {
|
|
|
776
825
|
this.customerCreationAttempts.add(customerRef);
|
|
777
826
|
try {
|
|
778
827
|
const createParams = {
|
|
779
|
-
email: options?.email || `${customerRef}-${Date.now()}@auto-created.local
|
|
828
|
+
email: options?.email || `${customerRef}-${Date.now()}@auto-created.local`,
|
|
829
|
+
metadata: {}
|
|
780
830
|
};
|
|
781
831
|
if (options?.name) {
|
|
782
832
|
createParams.name = options.name;
|
|
@@ -800,7 +850,10 @@ var SolvaPayPaywall = class {
|
|
|
800
850
|
return searchResult.customerRef;
|
|
801
851
|
}
|
|
802
852
|
} catch (lookupError) {
|
|
803
|
-
this.log(
|
|
853
|
+
this.log(
|
|
854
|
+
`\u26A0\uFE0F Failed to lookup existing customer by externalRef after 409:`,
|
|
855
|
+
lookupError instanceof Error ? lookupError.message : lookupError
|
|
856
|
+
);
|
|
804
857
|
}
|
|
805
858
|
}
|
|
806
859
|
const isEmailConflict = errorMessage.includes("email") || errorMessage.includes("identifier email");
|
|
@@ -823,7 +876,8 @@ var SolvaPayPaywall = class {
|
|
|
823
876
|
try {
|
|
824
877
|
const retryParams = {
|
|
825
878
|
email: `${customerRef}-${Date.now()}@auto-created.local`,
|
|
826
|
-
externalRef
|
|
879
|
+
externalRef,
|
|
880
|
+
metadata: {}
|
|
827
881
|
};
|
|
828
882
|
if (options?.name) {
|
|
829
883
|
retryParams.name = options.name;
|
|
@@ -920,10 +974,10 @@ async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
|
|
|
920
974
|
const backendRefCache = /* @__PURE__ */ new Map();
|
|
921
975
|
const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
|
|
922
976
|
const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
|
|
923
|
-
return async (context) => {
|
|
977
|
+
return async (context, extra) => {
|
|
924
978
|
try {
|
|
925
979
|
const args = await adapter.extractArgs(context);
|
|
926
|
-
const customerRef = await adapter.getCustomerRef(context);
|
|
980
|
+
const customerRef = await adapter.getCustomerRef(context, extra);
|
|
927
981
|
let backendRef = backendRefCache.get(customerRef);
|
|
928
982
|
if (!backendRef) {
|
|
929
983
|
backendRef = await paywall.ensureCustomer(customerRef, customerRef);
|
|
@@ -1114,18 +1168,18 @@ var McpAdapter = class {
|
|
|
1114
1168
|
extractArgs(args) {
|
|
1115
1169
|
return args;
|
|
1116
1170
|
}
|
|
1117
|
-
async getCustomerRef(args) {
|
|
1171
|
+
async getCustomerRef(args, extra) {
|
|
1118
1172
|
if (this.options.getCustomerRef) {
|
|
1119
|
-
const ref = await this.options.getCustomerRef(args);
|
|
1173
|
+
const ref = await this.options.getCustomerRef(args, extra);
|
|
1120
1174
|
return AdapterUtils.ensureCustomerRef(ref);
|
|
1121
1175
|
}
|
|
1122
|
-
const
|
|
1123
|
-
const customerRef =
|
|
1176
|
+
const customerRefFromExtra = extra?.authInfo?.extra?.customer_ref;
|
|
1177
|
+
const customerRef = typeof customerRefFromExtra === "string" && customerRefFromExtra.trim() ? customerRefFromExtra.trim() : "anonymous";
|
|
1124
1178
|
return AdapterUtils.ensureCustomerRef(customerRef);
|
|
1125
1179
|
}
|
|
1126
1180
|
formatResponse(result, _context) {
|
|
1127
1181
|
const transformed = this.options.transformResponse ? this.options.transformResponse(result) : result;
|
|
1128
|
-
|
|
1182
|
+
const response = {
|
|
1129
1183
|
content: [
|
|
1130
1184
|
{
|
|
1131
1185
|
type: "text",
|
|
@@ -1133,6 +1187,10 @@ var McpAdapter = class {
|
|
|
1133
1187
|
}
|
|
1134
1188
|
]
|
|
1135
1189
|
};
|
|
1190
|
+
if (transformed && typeof transformed === "object" && !Array.isArray(transformed)) {
|
|
1191
|
+
response.structuredContent = transformed;
|
|
1192
|
+
}
|
|
1193
|
+
return response;
|
|
1136
1194
|
}
|
|
1137
1195
|
formatError(error, _context) {
|
|
1138
1196
|
if (error instanceof PaywallError) {
|
|
@@ -1140,17 +1198,7 @@ var McpAdapter = class {
|
|
|
1140
1198
|
content: [
|
|
1141
1199
|
{
|
|
1142
1200
|
type: "text",
|
|
1143
|
-
text: JSON.stringify(
|
|
1144
|
-
{
|
|
1145
|
-
success: false,
|
|
1146
|
-
error: "Payment required",
|
|
1147
|
-
product: error.structuredContent.product,
|
|
1148
|
-
checkoutUrl: error.structuredContent.checkoutUrl,
|
|
1149
|
-
message: error.structuredContent.message
|
|
1150
|
-
},
|
|
1151
|
-
null,
|
|
1152
|
-
2
|
|
1153
|
-
)
|
|
1201
|
+
text: JSON.stringify(paywallErrorToClientPayload(error), null, 2)
|
|
1154
1202
|
}
|
|
1155
1203
|
],
|
|
1156
1204
|
isError: true,
|
|
@@ -1220,8 +1268,8 @@ function mcpErrorResult(message) {
|
|
|
1220
1268
|
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
1221
1269
|
}
|
|
1222
1270
|
function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
|
|
1223
|
-
return async (args) => {
|
|
1224
|
-
const customerRef = getCustomerRef(args);
|
|
1271
|
+
return async (args, extra) => {
|
|
1272
|
+
const customerRef = getCustomerRef(args, extra);
|
|
1225
1273
|
try {
|
|
1226
1274
|
if (!apiClient.getUserInfo) {
|
|
1227
1275
|
return mcpErrorResult("getUserInfo is not available on this API client");
|
|
@@ -1236,12 +1284,12 @@ function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
|
|
|
1236
1284
|
};
|
|
1237
1285
|
}
|
|
1238
1286
|
function createUpgradeHandler(apiClient, productRef, getCustomerRef) {
|
|
1239
|
-
return async (args) => {
|
|
1240
|
-
const customerRef = getCustomerRef(args);
|
|
1287
|
+
return async (args, extra) => {
|
|
1288
|
+
const customerRef = getCustomerRef(args, extra);
|
|
1241
1289
|
const planRef = typeof args.planRef === "string" ? args.planRef : void 0;
|
|
1242
1290
|
try {
|
|
1243
1291
|
const result = await apiClient.createCheckoutSession({
|
|
1244
|
-
|
|
1292
|
+
customerRef,
|
|
1245
1293
|
productRef,
|
|
1246
1294
|
...planRef && { planRef }
|
|
1247
1295
|
});
|
|
@@ -1268,8 +1316,8 @@ You'll be able to compare options and select the one that's right for you.`;
|
|
|
1268
1316
|
};
|
|
1269
1317
|
}
|
|
1270
1318
|
function createManageAccountHandler(apiClient, productRef, getCustomerRef) {
|
|
1271
|
-
return async (args) => {
|
|
1272
|
-
const customerRef = getCustomerRef(args);
|
|
1319
|
+
return async (args, extra) => {
|
|
1320
|
+
const customerRef = getCustomerRef(args, extra);
|
|
1273
1321
|
try {
|
|
1274
1322
|
const session = await apiClient.createCustomerSession({ customerRef, productRef });
|
|
1275
1323
|
const portalUrl = session.customerUrl;
|
|
@@ -1312,6 +1360,98 @@ function createVirtualTools(apiClient, options) {
|
|
|
1312
1360
|
return allTools.filter((t) => !excludeSet.has(t.name));
|
|
1313
1361
|
}
|
|
1314
1362
|
|
|
1363
|
+
// src/register-virtual-tools-mcp.ts
|
|
1364
|
+
import { createRequire } from "module";
|
|
1365
|
+
function getNodeRequire() {
|
|
1366
|
+
try {
|
|
1367
|
+
return (0, eval)("require");
|
|
1368
|
+
} catch {
|
|
1369
|
+
return createRequire(`${process.cwd()}/package.json`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
var nodeRequire = getNodeRequire();
|
|
1373
|
+
function getZod() {
|
|
1374
|
+
try {
|
|
1375
|
+
const zodModule = nodeRequire("zod");
|
|
1376
|
+
return zodModule.z ?? zodModule;
|
|
1377
|
+
} catch {
|
|
1378
|
+
throw new Error(
|
|
1379
|
+
"zod is required to use registerVirtualToolsMcp(). Install it as a dependency in your MCP server project."
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
function isJsonSchemaObject(value) {
|
|
1384
|
+
return Boolean(value && typeof value === "object");
|
|
1385
|
+
}
|
|
1386
|
+
function toZodSchema(property) {
|
|
1387
|
+
const z = getZod();
|
|
1388
|
+
if (Array.isArray(property.enum) && property.enum.length > 0) {
|
|
1389
|
+
if (property.enum.every((value) => typeof value === "string")) {
|
|
1390
|
+
const [first, ...rest] = property.enum;
|
|
1391
|
+
return z.enum([first, ...rest]);
|
|
1392
|
+
}
|
|
1393
|
+
if (property.enum.length === 1) {
|
|
1394
|
+
return z.literal(property.enum[0]);
|
|
1395
|
+
}
|
|
1396
|
+
return z.union(property.enum.map((value) => z.literal(value)));
|
|
1397
|
+
}
|
|
1398
|
+
switch (property.type) {
|
|
1399
|
+
case "string":
|
|
1400
|
+
return z.string();
|
|
1401
|
+
case "number":
|
|
1402
|
+
return z.number();
|
|
1403
|
+
case "integer":
|
|
1404
|
+
return z.number().int();
|
|
1405
|
+
case "boolean":
|
|
1406
|
+
return z.boolean();
|
|
1407
|
+
case "array":
|
|
1408
|
+
return z.array(
|
|
1409
|
+
isJsonSchemaObject(property.items) ? toZodSchema(property.items) : z.any()
|
|
1410
|
+
);
|
|
1411
|
+
case "object":
|
|
1412
|
+
return z.object(jsonSchemaToZodRawShape(property.properties || {}));
|
|
1413
|
+
default:
|
|
1414
|
+
return z.any();
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
function jsonSchemaToZodRawShape(properties, required = []) {
|
|
1418
|
+
const requiredSet = new Set(required);
|
|
1419
|
+
return Object.fromEntries(
|
|
1420
|
+
Object.entries(properties).map(([name, property]) => {
|
|
1421
|
+
const schema = toZodSchema(property);
|
|
1422
|
+
const finalSchema = requiredSet.has(name) ? schema : schema.optional();
|
|
1423
|
+
return [name, finalSchema];
|
|
1424
|
+
})
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
function defaultGetCustomerRef(_args, extra) {
|
|
1428
|
+
const fromExtra = extra?.authInfo?.extra?.customer_ref;
|
|
1429
|
+
return typeof fromExtra === "string" && fromExtra.trim() ? fromExtra.trim() : "anonymous";
|
|
1430
|
+
}
|
|
1431
|
+
function registerVirtualToolsMcpImpl(server, apiClient, options) {
|
|
1432
|
+
const { filter, mapDefinition, wrapHandler, getCustomerRef, ...virtualToolsOptions } = options;
|
|
1433
|
+
const virtualTools = createVirtualTools(apiClient, {
|
|
1434
|
+
...virtualToolsOptions,
|
|
1435
|
+
getCustomerRef: getCustomerRef || defaultGetCustomerRef
|
|
1436
|
+
});
|
|
1437
|
+
for (const toolDefinition of virtualTools) {
|
|
1438
|
+
if (filter && !filter(toolDefinition)) continue;
|
|
1439
|
+
const mappedDefinition = mapDefinition ? mapDefinition(toolDefinition) : toolDefinition;
|
|
1440
|
+
const wrappedHandler = wrapHandler ? wrapHandler(mappedDefinition.handler, mappedDefinition) : mappedDefinition.handler;
|
|
1441
|
+
server.registerTool(
|
|
1442
|
+
mappedDefinition.name,
|
|
1443
|
+
{
|
|
1444
|
+
description: mappedDefinition.description,
|
|
1445
|
+
inputSchema: jsonSchemaToZodRawShape(
|
|
1446
|
+
mappedDefinition.inputSchema.properties,
|
|
1447
|
+
mappedDefinition.inputSchema.required || []
|
|
1448
|
+
)
|
|
1449
|
+
},
|
|
1450
|
+
wrappedHandler
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1315
1455
|
// src/factory.ts
|
|
1316
1456
|
function createSolvaPay(config) {
|
|
1317
1457
|
let resolvedConfig;
|
|
@@ -1361,14 +1501,14 @@ function createSolvaPay(config) {
|
|
|
1361
1501
|
if (!apiClient.createCustomer) {
|
|
1362
1502
|
throw new SolvaPayError2("createCustomer is not available on this API client");
|
|
1363
1503
|
}
|
|
1364
|
-
return apiClient.createCustomer(params);
|
|
1504
|
+
return apiClient.createCustomer({ ...params, metadata: params.metadata ?? {} });
|
|
1365
1505
|
},
|
|
1366
1506
|
getCustomer(params) {
|
|
1367
1507
|
return apiClient.getCustomer(params);
|
|
1368
1508
|
},
|
|
1369
1509
|
createCheckoutSession(params) {
|
|
1370
1510
|
return apiClient.createCheckoutSession({
|
|
1371
|
-
|
|
1511
|
+
customerRef: params.customerRef,
|
|
1372
1512
|
productRef: params.productRef,
|
|
1373
1513
|
planRef: params.planRef
|
|
1374
1514
|
});
|
|
@@ -1382,9 +1522,18 @@ function createSolvaPay(config) {
|
|
|
1382
1522
|
}
|
|
1383
1523
|
return apiClient.bootstrapMcpProduct(params);
|
|
1384
1524
|
},
|
|
1525
|
+
configureMcpPlans(productRef, params) {
|
|
1526
|
+
if (!apiClient.configureMcpPlans) {
|
|
1527
|
+
throw new SolvaPayError2("configureMcpPlans is not available on this API client");
|
|
1528
|
+
}
|
|
1529
|
+
return apiClient.configureMcpPlans(productRef, params);
|
|
1530
|
+
},
|
|
1385
1531
|
getVirtualTools(options) {
|
|
1386
1532
|
return createVirtualTools(apiClient, options);
|
|
1387
1533
|
},
|
|
1534
|
+
async registerVirtualToolsMcp(server, options) {
|
|
1535
|
+
await registerVirtualToolsMcpImpl(server, apiClient, options);
|
|
1536
|
+
},
|
|
1388
1537
|
// Payable API for framework-specific handlers
|
|
1389
1538
|
payable(options = {}) {
|
|
1390
1539
|
const product = options.productRef || options.product || process.env.SOLVAPAY_PRODUCT || "default-product";
|
|
@@ -1423,9 +1572,9 @@ function createSolvaPay(config) {
|
|
|
1423
1572
|
getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
|
|
1424
1573
|
});
|
|
1425
1574
|
const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
|
|
1426
|
-
return async (args) => {
|
|
1575
|
+
return async (args, extra) => {
|
|
1427
1576
|
const handler = await handlerPromise;
|
|
1428
|
-
return handler(args);
|
|
1577
|
+
return handler(args, extra);
|
|
1429
1578
|
};
|
|
1430
1579
|
},
|
|
1431
1580
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -1495,6 +1644,167 @@ function getCustomerRefFromBearerAuthHeader(authorization, options = {}) {
|
|
|
1495
1644
|
return getCustomerRefFromJwtPayload(payload, options);
|
|
1496
1645
|
}
|
|
1497
1646
|
|
|
1647
|
+
// src/mcp/auth-bridge.ts
|
|
1648
|
+
function getClientId(payload, explicitClientId) {
|
|
1649
|
+
if (explicitClientId) return explicitClientId;
|
|
1650
|
+
const payloadClientId = typeof payload.client_id === "string" && payload.client_id || typeof payload.azp === "string" && payload.azp || typeof payload.aud === "string" && payload.aud || null;
|
|
1651
|
+
return payloadClientId || "solvapay-mcp-client";
|
|
1652
|
+
}
|
|
1653
|
+
function getScopes(payload, defaultScopes) {
|
|
1654
|
+
if (Array.isArray(payload.scp)) {
|
|
1655
|
+
return payload.scp.filter((scope) => typeof scope === "string");
|
|
1656
|
+
}
|
|
1657
|
+
if (typeof payload.scope === "string" && payload.scope.trim()) {
|
|
1658
|
+
return payload.scope.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
|
|
1659
|
+
}
|
|
1660
|
+
return defaultScopes;
|
|
1661
|
+
}
|
|
1662
|
+
function getExpiresAt(payload) {
|
|
1663
|
+
return typeof payload.exp === "number" ? payload.exp : void 0;
|
|
1664
|
+
}
|
|
1665
|
+
function buildAuthInfoFromBearer(authorization, options = {}) {
|
|
1666
|
+
const token = extractBearerToken(authorization);
|
|
1667
|
+
if (!token) return null;
|
|
1668
|
+
const payload = decodeJwtPayload(token);
|
|
1669
|
+
const customerRef = getCustomerRefFromJwtPayload(payload, options);
|
|
1670
|
+
const clientId = getClientId(payload, options.clientId);
|
|
1671
|
+
const scopes = getScopes(payload, options.defaultScopes || []);
|
|
1672
|
+
const expiresAt = getExpiresAt(payload);
|
|
1673
|
+
return {
|
|
1674
|
+
token,
|
|
1675
|
+
clientId,
|
|
1676
|
+
scopes,
|
|
1677
|
+
expiresAt,
|
|
1678
|
+
extra: {
|
|
1679
|
+
customer_ref: customerRef,
|
|
1680
|
+
...options.includePayload ? { payload } : {}
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/mcp/oauth-bridge.ts
|
|
1686
|
+
function withoutTrailingSlash(value) {
|
|
1687
|
+
return value.replace(/\/$/, "");
|
|
1688
|
+
}
|
|
1689
|
+
function getRequestAuthHeader(req) {
|
|
1690
|
+
const header = req.headers?.authorization;
|
|
1691
|
+
if (typeof header === "string") return header;
|
|
1692
|
+
if (Array.isArray(header)) return header[0] || null;
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
function getRequestJsonRpcId(body) {
|
|
1696
|
+
if (body && typeof body === "object" && "id" in body) {
|
|
1697
|
+
const id = body.id;
|
|
1698
|
+
return id ?? null;
|
|
1699
|
+
}
|
|
1700
|
+
return null;
|
|
1701
|
+
}
|
|
1702
|
+
function makeUnauthorizedJsonRpc(id) {
|
|
1703
|
+
return {
|
|
1704
|
+
jsonrpc: "2.0",
|
|
1705
|
+
id,
|
|
1706
|
+
error: {
|
|
1707
|
+
code: -32001,
|
|
1708
|
+
message: "Unauthorized"
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
function setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath) {
|
|
1713
|
+
res.setHeader(
|
|
1714
|
+
"WWW-Authenticate",
|
|
1715
|
+
`Bearer resource_metadata="${withoutTrailingSlash(publicBaseUrl)}${protectedResourcePath}"`
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
function getOAuthProtectedResourceResponse(publicBaseUrl) {
|
|
1719
|
+
const resource = withoutTrailingSlash(publicBaseUrl);
|
|
1720
|
+
return {
|
|
1721
|
+
resource,
|
|
1722
|
+
authorization_servers: [resource],
|
|
1723
|
+
scopes_supported: ["openid", "profile", "email"]
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function getOAuthAuthorizationServerResponse({
|
|
1727
|
+
apiBaseUrl,
|
|
1728
|
+
productRef
|
|
1729
|
+
}) {
|
|
1730
|
+
const normalizedApiBaseUrl = withoutTrailingSlash(apiBaseUrl);
|
|
1731
|
+
const registrationEndpoint = `${normalizedApiBaseUrl}/v1/customer/auth/register?product_ref=${encodeURIComponent(productRef)}`;
|
|
1732
|
+
return {
|
|
1733
|
+
issuer: normalizedApiBaseUrl,
|
|
1734
|
+
authorization_endpoint: `${normalizedApiBaseUrl}/v1/customer/auth/authorize`,
|
|
1735
|
+
token_endpoint: `${normalizedApiBaseUrl}/v1/customer/auth/token`,
|
|
1736
|
+
registration_endpoint: registrationEndpoint,
|
|
1737
|
+
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
1738
|
+
response_types_supported: ["code"],
|
|
1739
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
1740
|
+
scopes_supported: ["openid", "profile", "email"],
|
|
1741
|
+
code_challenge_methods_supported: ["S256"]
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function createMcpOAuthBridge(options) {
|
|
1745
|
+
const {
|
|
1746
|
+
publicBaseUrl,
|
|
1747
|
+
apiBaseUrl,
|
|
1748
|
+
productRef,
|
|
1749
|
+
mcpPath = "/mcp",
|
|
1750
|
+
requireAuth = true,
|
|
1751
|
+
authInfo,
|
|
1752
|
+
protectedResourcePath = "/.well-known/oauth-protected-resource",
|
|
1753
|
+
authorizationServerPath = "/.well-known/oauth-authorization-server"
|
|
1754
|
+
} = options;
|
|
1755
|
+
const protectedResourceMiddleware = (req, res, next) => {
|
|
1756
|
+
if (req.method !== "GET" || req.path !== protectedResourcePath) {
|
|
1757
|
+
next();
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
res.json(getOAuthProtectedResourceResponse(publicBaseUrl));
|
|
1761
|
+
};
|
|
1762
|
+
const authorizationServerMiddleware = (req, res, next) => {
|
|
1763
|
+
if (req.method !== "GET" || req.path !== authorizationServerPath) {
|
|
1764
|
+
next();
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (!productRef) {
|
|
1768
|
+
res.status(500).json({ error: "SOLVAPAY_PRODUCT_REF missing" });
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
res.json(
|
|
1772
|
+
getOAuthAuthorizationServerResponse({
|
|
1773
|
+
apiBaseUrl,
|
|
1774
|
+
productRef
|
|
1775
|
+
})
|
|
1776
|
+
);
|
|
1777
|
+
};
|
|
1778
|
+
const mcpAuthMiddleware = (req, res, next) => {
|
|
1779
|
+
if (req.path !== mcpPath) {
|
|
1780
|
+
next();
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
const authHeader = getRequestAuthHeader(req);
|
|
1784
|
+
const id = getRequestJsonRpcId(req.body);
|
|
1785
|
+
if (!authHeader && !requireAuth) {
|
|
1786
|
+
next();
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
try {
|
|
1790
|
+
const auth = buildAuthInfoFromBearer(authHeader, authInfo);
|
|
1791
|
+
if (!auth) {
|
|
1792
|
+
throw new McpBearerAuthError("Missing bearer token");
|
|
1793
|
+
}
|
|
1794
|
+
req.auth = auth;
|
|
1795
|
+
next();
|
|
1796
|
+
} catch {
|
|
1797
|
+
setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath);
|
|
1798
|
+
if (req.method === "POST") {
|
|
1799
|
+
res.status(401).json(makeUnauthorizedJsonRpc(id));
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
return [protectedResourceMiddleware, authorizationServerMiddleware, mcpAuthMiddleware];
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1498
1808
|
// src/helpers/error.ts
|
|
1499
1809
|
import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
|
|
1500
1810
|
function isErrorResult(result) {
|
|
@@ -1848,9 +2158,11 @@ export {
|
|
|
1848
2158
|
McpBearerAuthError,
|
|
1849
2159
|
PaywallError,
|
|
1850
2160
|
VIRTUAL_TOOL_DEFINITIONS,
|
|
2161
|
+
buildAuthInfoFromBearer,
|
|
1851
2162
|
cancelPurchaseCore,
|
|
1852
2163
|
createCheckoutSessionCore,
|
|
1853
2164
|
createCustomerSessionCore,
|
|
2165
|
+
createMcpOAuthBridge,
|
|
1854
2166
|
createPaymentIntentCore,
|
|
1855
2167
|
createSolvaPay,
|
|
1856
2168
|
createSolvaPayClient,
|
|
@@ -1860,10 +2172,15 @@ export {
|
|
|
1860
2172
|
getAuthenticatedUserCore,
|
|
1861
2173
|
getCustomerRefFromBearerAuthHeader,
|
|
1862
2174
|
getCustomerRefFromJwtPayload,
|
|
2175
|
+
getOAuthAuthorizationServerResponse,
|
|
2176
|
+
getOAuthProtectedResourceResponse,
|
|
1863
2177
|
handleRouteError,
|
|
1864
2178
|
isErrorResult,
|
|
2179
|
+
jsonSchemaToZodRawShape,
|
|
1865
2180
|
listPlansCore,
|
|
2181
|
+
paywallErrorToClientPayload,
|
|
1866
2182
|
processPaymentIntentCore,
|
|
2183
|
+
registerVirtualToolsMcpImpl,
|
|
1867
2184
|
syncCustomerCore,
|
|
1868
2185
|
verifyWebhook,
|
|
1869
2186
|
withRetry
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solvapay/server",
|
|
3
|
-
"version": "1.0.1-preview.
|
|
3
|
+
"version": "1.0.1-preview.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -37,10 +37,20 @@
|
|
|
37
37
|
},
|
|
38
38
|
"sideEffects": false,
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@solvapay/core": "1.0.1-preview.
|
|
40
|
+
"@solvapay/core": "1.0.1-preview.4"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
44
|
+
"zod": "^3.25.0 || ^4.0.0",
|
|
45
|
+
"@solvapay/auth": "1.0.1-preview.4"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@modelcontextprotocol/sdk": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"zod": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
44
54
|
},
|
|
45
55
|
"devDependencies": {
|
|
46
56
|
"dotenv": "^17.3.1",
|
|
@@ -50,7 +60,7 @@
|
|
|
50
60
|
"tsx": "^4.21.0",
|
|
51
61
|
"typescript": "^5.9.3",
|
|
52
62
|
"vitest": "^4.1.2",
|
|
53
|
-
"@solvapay/auth": "1.0.1-preview.
|
|
63
|
+
"@solvapay/auth": "1.0.1-preview.4",
|
|
54
64
|
"@solvapay/demo-services": "0.0.0",
|
|
55
65
|
"@solvapay/test-utils": "^0.0.0"
|
|
56
66
|
},
|