@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/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(params)
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 toolName = handler.name || "anonymous";
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 planRef = metadata.plan || toolName;
551
- const limitsCheck = await this.apiClient.checkLimits({
552
- customerRef: backendCustomerRef,
553
- productRef: product
554
- });
555
- if (!limitsCheck.withinLimits) {
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
- await this.trackUsage(
668
+ this.trackUsage(
558
669
  backendCustomerRef,
559
670
  product,
560
- planRef,
561
- toolName,
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: limitsCheck.checkoutUrl || "",
570
- message: `Plan purchase required. Remaining: ${limitsCheck.remaining}`
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
- await this.trackUsage(
686
+ this.trackUsage(
576
687
  backendCustomerRef,
577
688
  product,
578
- planRef,
579
- toolName,
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
- const latencyMs = Date.now() - startTime;
593
- const outcome = error instanceof PaywallError ? "paywall" : "fail";
594
- const planRef = metadata.plan || toolName;
595
- await this.trackUsage(
596
- backendCustomerRef,
597
- product,
598
- planRef,
599
- toolName,
600
- outcome,
601
- requestId,
602
- latencyMs
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, productRef, planRef, toolName, outcome, requestId, actionDuration) {
863
+ async trackUsage(customerRef, _productRef, _planRef, action, outcome, requestId, actionDuration) {
753
864
  await withRetry(
754
865
  () => this.apiClient.trackUsage({
755
866
  customerRef,
756
- productRef,
757
- planRef,
867
+ actionType: "api_call",
868
+ units: 1,
758
869
  outcome,
759
- action: toolName,
760
- requestId,
761
- actionDuration,
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
- // TODO: review if this is needed and what to check for
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
- args.auth = { customer_ref: customerRef };
815
- const getCustomerRef = (args2) => args2.auth?.customer_ref || "anonymous";
816
- const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
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 || product;
1131
- const metadata = { product, plan };
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(adapterOptions);
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 createAdapterHandler(adapter, paywall, metadata, businessLogic);
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(adapterOptions);
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 createAdapterHandler(adapter, paywall, metadata, businessLogic);
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(adapterOptions);
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 createAdapterHandler(adapter, paywall, metadata, businessLogic);
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) => args.auth?.customer_ref || "anonymous";
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.20",
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.20"
40
+ "@solvapay/core": "1.0.0-preview.21"
41
41
  },
42
42
  "peerDependencies": {
43
- "@solvapay/auth": "1.0.0-preview.20"
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.20"
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",