@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/edge.js CHANGED
@@ -38,10 +38,12 @@ function createSolvaPayClient(opts) {
38
38
  // POST: /v1/sdk/usages
39
39
  async trackUsage(params) {
40
40
  const url = `${base}/v1/sdk/usages`;
41
+ const { customerRef, ...rest } = params;
42
+ const body = { ...rest, customerId: customerRef };
41
43
  const res = await fetch(url, {
42
44
  method: "POST",
43
45
  headers,
44
- body: JSON.stringify(params)
46
+ body: JSON.stringify(body)
45
47
  });
46
48
  if (!res.ok) {
47
49
  const error = await res.text();
@@ -145,6 +147,21 @@ function createSolvaPayClient(opts) {
145
147
  const result = await res.json();
146
148
  return result;
147
149
  },
150
+ // POST: /v1/sdk/products/mcp/bootstrap
151
+ async bootstrapMcpProduct(params) {
152
+ const url = `${base}/v1/sdk/products/mcp/bootstrap`;
153
+ const res = await fetch(url, {
154
+ method: "POST",
155
+ headers,
156
+ body: JSON.stringify(params)
157
+ });
158
+ if (!res.ok) {
159
+ const error = await res.text();
160
+ log(`\u274C API Error: ${res.status} - ${error}`);
161
+ throw new SolvaPayError(`Bootstrap MCP product failed (${res.status}): ${error}`);
162
+ }
163
+ return await res.json();
164
+ },
148
165
  // DELETE: /v1/sdk/products/{productRef}
149
166
  async deleteProduct(productRef) {
150
167
  const url = `${base}/v1/sdk/products/${productRef}`;
@@ -158,6 +175,21 @@ function createSolvaPayClient(opts) {
158
175
  throw new SolvaPayError(`Delete product failed (${res.status}): ${error}`);
159
176
  }
160
177
  },
178
+ // POST: /v1/sdk/products/{productRef}/clone
179
+ async cloneProduct(productRef, overrides) {
180
+ const url = `${base}/v1/sdk/products/${productRef}/clone`;
181
+ const res = await fetch(url, {
182
+ method: "POST",
183
+ headers,
184
+ body: JSON.stringify(overrides || {})
185
+ });
186
+ if (!res.ok) {
187
+ const error = await res.text();
188
+ log(`\u274C API Error: ${res.status} - ${error}`);
189
+ throw new SolvaPayError(`Clone product failed (${res.status}): ${error}`);
190
+ }
191
+ return await res.json();
192
+ },
161
193
  // GET: /v1/sdk/products/{productRef}/plans
162
194
  async listPlans(productRef) {
163
195
  const url = `${base}/v1/sdk/products/${productRef}/plans`;
@@ -200,6 +232,21 @@ function createSolvaPayClient(opts) {
200
232
  const result = await res.json();
201
233
  return result;
202
234
  },
235
+ // PUT: /v1/sdk/products/{productRef}/plans/{planRef}
236
+ async updatePlan(productRef, planRef, params) {
237
+ const url = `${base}/v1/sdk/products/${productRef}/plans/${planRef}`;
238
+ const res = await fetch(url, {
239
+ method: "PUT",
240
+ headers,
241
+ body: JSON.stringify(params)
242
+ });
243
+ if (!res.ok) {
244
+ const error = await res.text();
245
+ log(`\u274C API Error: ${res.status} - ${error}`);
246
+ throw new SolvaPayError(`Update plan failed (${res.status}): ${error}`);
247
+ }
248
+ return await res.json();
249
+ },
203
250
  // DELETE: /v1/sdk/products/{productRef}/plans/{planRef}
204
251
  async deletePlan(productRef, planRef) {
205
252
  const url = `${base}/v1/sdk/products/${productRef}/plans/${planRef}`;
@@ -312,6 +359,21 @@ function createSolvaPayClient(opts) {
312
359
  }
313
360
  return result;
314
361
  },
362
+ // POST: /v1/sdk/user-info
363
+ async getUserInfo(params) {
364
+ const url = `${base}/v1/sdk/user-info`;
365
+ const res = await fetch(url, {
366
+ method: "POST",
367
+ headers,
368
+ body: JSON.stringify(params)
369
+ });
370
+ if (!res.ok) {
371
+ const error = await res.text();
372
+ log(`\u274C API Error: ${res.status} - ${error}`);
373
+ throw new SolvaPayError(`Get user info failed (${res.status}): ${error}`);
374
+ }
375
+ return await res.json();
376
+ },
315
377
  // POST: /v1/sdk/checkout-sessions
316
378
  async createCheckoutSession(params) {
317
379
  const url = `${base}/v1/sdk/checkout-sessions`;
@@ -510,11 +572,13 @@ var SolvaPayPaywall = class {
510
572
  constructor(apiClient, options = {}) {
511
573
  this.apiClient = apiClient;
512
574
  this.debug = options.debug ?? process.env.SOLVAPAY_DEBUG === "true";
575
+ this.limitsCacheTTL = options.limitsCacheTTL ?? 1e4;
513
576
  }
514
577
  customerCreationAttempts = /* @__PURE__ */ new Set();
515
578
  customerRefMapping = /* @__PURE__ */ new Map();
516
- // input ref -> backend ref
517
579
  debug;
580
+ limitsCache = /* @__PURE__ */ new Map();
581
+ limitsCacheTTL;
518
582
  log(...args) {
519
583
  if (this.debug) {
520
584
  console.log(...args);
@@ -534,7 +598,9 @@ var SolvaPayPaywall = class {
534
598
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
599
  async protect(handler, metadata = {}, getCustomerRef) {
536
600
  const product = this.resolveProduct(metadata);
537
- const toolName = handler.name || "anonymous";
601
+ const configuredPlanRef = metadata.plan?.trim();
602
+ const usagePlanRef = configuredPlanRef || "unspecified";
603
+ const usageType = metadata.usageType || "requests";
538
604
  return async (args) => {
539
605
  const startTime = Date.now();
540
606
  const requestId = this.generateRequestId();
@@ -545,19 +611,64 @@ var SolvaPayPaywall = class {
545
611
  } else {
546
612
  backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
547
613
  }
614
+ let resolvedMeterName;
548
615
  try {
549
- const planRef = metadata.plan || toolName;
550
- const limitsCheck = await this.apiClient.checkLimits({
551
- customerRef: backendCustomerRef,
552
- productRef: product
553
- });
554
- if (!limitsCheck.withinLimits) {
616
+ const limitsCacheKey = `${backendCustomerRef}:${product}:${configuredPlanRef || ""}:${usageType}`;
617
+ const cachedLimits = this.limitsCache.get(limitsCacheKey);
618
+ const now = Date.now();
619
+ let withinLimits;
620
+ let remaining;
621
+ let checkoutUrl;
622
+ const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
623
+ if (hasFreshCachedLimits) {
624
+ checkoutUrl = cachedLimits.checkoutUrl;
625
+ resolvedMeterName = cachedLimits.meterName;
626
+ if (cachedLimits.remaining > 0) {
627
+ cachedLimits.remaining--;
628
+ if (cachedLimits.remaining <= 0) {
629
+ this.limitsCache.delete(limitsCacheKey);
630
+ }
631
+ withinLimits = true;
632
+ remaining = cachedLimits.remaining;
633
+ } else {
634
+ withinLimits = false;
635
+ remaining = 0;
636
+ this.limitsCache.delete(limitsCacheKey);
637
+ }
638
+ } else {
639
+ if (cachedLimits) {
640
+ this.limitsCache.delete(limitsCacheKey);
641
+ }
642
+ const limitsCheck = await this.apiClient.checkLimits({
643
+ customerRef: backendCustomerRef,
644
+ productRef: product,
645
+ ...configuredPlanRef ? { planRef: configuredPlanRef } : {},
646
+ meterName: usageType
647
+ });
648
+ withinLimits = limitsCheck.withinLimits;
649
+ remaining = limitsCheck.remaining;
650
+ checkoutUrl = limitsCheck.checkoutUrl;
651
+ resolvedMeterName = limitsCheck.meterName;
652
+ const consumedAllowance = withinLimits && remaining > 0;
653
+ if (consumedAllowance) {
654
+ remaining = Math.max(0, remaining - 1);
655
+ }
656
+ if (consumedAllowance) {
657
+ this.limitsCache.set(limitsCacheKey, {
658
+ remaining,
659
+ checkoutUrl,
660
+ meterName: resolvedMeterName,
661
+ timestamp: now
662
+ });
663
+ }
664
+ }
665
+ if (!withinLimits) {
555
666
  const latencyMs2 = Date.now() - startTime;
556
- await this.trackUsage(
667
+ this.trackUsage(
557
668
  backendCustomerRef,
558
669
  product,
559
- planRef,
560
- toolName,
670
+ usagePlanRef,
671
+ resolvedMeterName || usageType,
561
672
  "paywall",
562
673
  requestId,
563
674
  latencyMs2
@@ -565,17 +676,17 @@ var SolvaPayPaywall = class {
565
676
  throw new PaywallError("Payment required", {
566
677
  kind: "payment_required",
567
678
  product,
568
- checkoutUrl: limitsCheck.checkoutUrl || "",
569
- message: `Plan purchase required. Remaining: ${limitsCheck.remaining}`
679
+ checkoutUrl: checkoutUrl || "",
680
+ message: `Purchase required. Remaining: ${remaining}`
570
681
  });
571
682
  }
572
683
  const result = await handler(args);
573
684
  const latencyMs = Date.now() - startTime;
574
- await this.trackUsage(
685
+ this.trackUsage(
575
686
  backendCustomerRef,
576
687
  product,
577
- planRef,
578
- toolName,
688
+ usagePlanRef,
689
+ resolvedMeterName || usageType,
579
690
  "success",
580
691
  requestId,
581
692
  latencyMs
@@ -588,18 +699,18 @@ var SolvaPayPaywall = class {
588
699
  } else {
589
700
  this.log(`\u274C Error in paywall:`, error);
590
701
  }
591
- const latencyMs = Date.now() - startTime;
592
- const outcome = error instanceof PaywallError ? "paywall" : "fail";
593
- const planRef = metadata.plan || toolName;
594
- await this.trackUsage(
595
- backendCustomerRef,
596
- product,
597
- planRef,
598
- toolName,
599
- outcome,
600
- requestId,
601
- latencyMs
602
- );
702
+ if (!(error instanceof PaywallError)) {
703
+ const latencyMs = Date.now() - startTime;
704
+ this.trackUsage(
705
+ backendCustomerRef,
706
+ product,
707
+ usagePlanRef,
708
+ resolvedMeterName || usageType,
709
+ "fail",
710
+ requestId,
711
+ latencyMs
712
+ );
713
+ }
603
714
  throw error;
604
715
  }
605
716
  };
@@ -748,24 +859,23 @@ var SolvaPayPaywall = class {
748
859
  }
749
860
  return backendRef;
750
861
  }
751
- async trackUsage(customerRef, productRef, planRef, toolName, outcome, requestId, actionDuration) {
862
+ async trackUsage(customerRef, _productRef, _planRef, action, outcome, requestId, actionDuration) {
752
863
  await withRetry(
753
864
  () => this.apiClient.trackUsage({
754
865
  customerRef,
755
- productRef,
756
- planRef,
866
+ actionType: "api_call",
867
+ units: 1,
757
868
  outcome,
758
- action: toolName,
759
- requestId,
760
- actionDuration,
869
+ productReference: _productRef,
870
+ duration: actionDuration,
871
+ metadata: { action: action || "api_requests", requestId },
761
872
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
762
873
  }),
763
874
  {
764
875
  maxRetries: 2,
765
876
  initialDelay: 500,
766
877
  shouldRetry: (error) => error.message.includes("Customer not found"),
767
- // TODO: review if this is needed and what to check for
768
- onRetry: (error, attempt) => {
878
+ onRetry: (_error, attempt) => {
769
879
  console.warn(`\u26A0\uFE0F Customer not found (attempt ${attempt + 1}/3), retrying in 500ms...`);
770
880
  }
771
881
  }
@@ -806,13 +916,19 @@ var AdapterUtils = class {
806
916
  }
807
917
  };
808
918
  async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
919
+ const backendRefCache = /* @__PURE__ */ new Map();
920
+ const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
921
+ const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
809
922
  return async (context) => {
810
923
  try {
811
924
  const args = await adapter.extractArgs(context);
812
925
  const customerRef = await adapter.getCustomerRef(context);
813
- args.auth = { customer_ref: customerRef };
814
- const getCustomerRef = (args2) => args2.auth?.customer_ref || "anonymous";
815
- const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
926
+ let backendRef = backendRefCache.get(customerRef);
927
+ if (!backendRef) {
928
+ backendRef = await paywall.ensureCustomer(customerRef, customerRef);
929
+ backendRefCache.set(customerRef, backendRef);
930
+ }
931
+ args.auth = { customer_ref: backendRef };
816
932
  const result = await protectedHandler(args);
817
933
  return adapter.formatResponse(result, context);
818
934
  } catch (error) {
@@ -1061,6 +1177,140 @@ var McpAdapter = class {
1061
1177
 
1062
1178
  // src/factory.ts
1063
1179
  import { SolvaPayError as SolvaPayError2, getSolvaPayConfig } from "@solvapay/core";
1180
+
1181
+ // src/virtual-tools.ts
1182
+ var TOOL_GET_USER_INFO = {
1183
+ name: "get_user_info",
1184
+ description: "Get information about the current user and their purchase status for this MCP server. Returns user profile (reference, name, email) and active purchase details including product name, type, dates, and usage limit if applicable.",
1185
+ inputSchema: {
1186
+ type: "object",
1187
+ properties: {},
1188
+ required: []
1189
+ }
1190
+ };
1191
+ var TOOL_UPGRADE = {
1192
+ name: "upgrade",
1193
+ description: "Get available pricing options and checkout URLs for upgrading. Returns a list of available pricing options with their details (price, features) and checkout URLs. Users can click on a checkout URL to purchase. If a specific planRef is provided, returns only the checkout URL for that pricing option.",
1194
+ inputSchema: {
1195
+ type: "object",
1196
+ properties: {
1197
+ planRef: {
1198
+ type: "string",
1199
+ description: 'Optional pricing reference (e.g., "pln_abc123") to get a checkout URL for a specific option. If not provided, returns all available pricing options with their checkout URLs.'
1200
+ }
1201
+ },
1202
+ required: []
1203
+ }
1204
+ };
1205
+ var TOOL_MANAGE_ACCOUNT = {
1206
+ name: "manage_account",
1207
+ description: "Get a URL to the customer portal where users can view and manage their account. The portal shows current account status, billing history, and allows subscription changes. Returns a secure, time-limited URL that the user can click to access their account management page.",
1208
+ inputSchema: {
1209
+ type: "object",
1210
+ properties: {},
1211
+ required: []
1212
+ }
1213
+ };
1214
+ function mcpTextResult(text) {
1215
+ return { content: [{ type: "text", text }] };
1216
+ }
1217
+ function mcpErrorResult(message) {
1218
+ return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
1219
+ }
1220
+ function createGetUserInfoHandler(apiClient, productRef, getCustomerRef) {
1221
+ return async (args) => {
1222
+ const customerRef = getCustomerRef(args);
1223
+ try {
1224
+ if (!apiClient.getUserInfo) {
1225
+ return mcpErrorResult("getUserInfo is not available on this API client");
1226
+ }
1227
+ const userInfo = await apiClient.getUserInfo({ customerRef, productRef });
1228
+ return mcpTextResult(JSON.stringify(userInfo, null, 2));
1229
+ } catch (error) {
1230
+ return mcpErrorResult(
1231
+ `Failed to retrieve user information: ${error instanceof Error ? error.message : "Unknown error"}`
1232
+ );
1233
+ }
1234
+ };
1235
+ }
1236
+ function createUpgradeHandler(apiClient, productRef, getCustomerRef) {
1237
+ return async (args) => {
1238
+ const customerRef = getCustomerRef(args);
1239
+ const planRef = typeof args.planRef === "string" ? args.planRef : void 0;
1240
+ try {
1241
+ const result = await apiClient.createCheckoutSession({
1242
+ customerReference: customerRef,
1243
+ productRef,
1244
+ ...planRef && { planRef }
1245
+ });
1246
+ const checkoutUrl = result.checkoutUrl;
1247
+ if (planRef) {
1248
+ const responseText2 = `## Upgrade
1249
+
1250
+ **[Click here to upgrade \u2192](${checkoutUrl})**
1251
+
1252
+ After completing the checkout, your purchase will be activated immediately.`;
1253
+ return mcpTextResult(responseText2);
1254
+ }
1255
+ const responseText = `## Upgrade Your Subscription
1256
+
1257
+ **[Click here to view pricing options and upgrade \u2192](${checkoutUrl})**
1258
+
1259
+ You'll be able to compare options and select the one that's right for you.`;
1260
+ return mcpTextResult(responseText);
1261
+ } catch (error) {
1262
+ return mcpErrorResult(
1263
+ `Failed to create checkout session: ${error instanceof Error ? error.message : "Unknown error"}`
1264
+ );
1265
+ }
1266
+ };
1267
+ }
1268
+ function createManageAccountHandler(apiClient, productRef, getCustomerRef) {
1269
+ return async (args) => {
1270
+ const customerRef = getCustomerRef(args);
1271
+ try {
1272
+ const session = await apiClient.createCustomerSession({ customerRef, productRef });
1273
+ const portalUrl = session.customerUrl;
1274
+ const responseText = `## Manage Your Account
1275
+
1276
+ Access your account management portal to:
1277
+ - View your current account status
1278
+ - See billing history and invoices
1279
+ - Update payment methods
1280
+ - Cancel or modify your subscription
1281
+
1282
+ **[Open Account Portal \u2192](${portalUrl})**
1283
+
1284
+ This link is secure and will expire after a short period.`;
1285
+ return mcpTextResult(responseText);
1286
+ } catch (error) {
1287
+ return mcpErrorResult(
1288
+ `Failed to create customer portal session: ${error instanceof Error ? error.message : "Unknown error"}`
1289
+ );
1290
+ }
1291
+ };
1292
+ }
1293
+ function createVirtualTools(apiClient, options) {
1294
+ const { product, getCustomerRef, exclude = [] } = options;
1295
+ const excludeSet = new Set(exclude);
1296
+ const allTools = [
1297
+ {
1298
+ ...TOOL_GET_USER_INFO,
1299
+ handler: createGetUserInfoHandler(apiClient, product, getCustomerRef)
1300
+ },
1301
+ {
1302
+ ...TOOL_UPGRADE,
1303
+ handler: createUpgradeHandler(apiClient, product, getCustomerRef)
1304
+ },
1305
+ {
1306
+ ...TOOL_MANAGE_ACCOUNT,
1307
+ handler: createManageAccountHandler(apiClient, product, getCustomerRef)
1308
+ }
1309
+ ];
1310
+ return allTools.filter((t) => !excludeSet.has(t.name));
1311
+ }
1312
+
1313
+ // src/factory.ts
1064
1314
  function createSolvaPay(config) {
1065
1315
  let resolvedConfig;
1066
1316
  if (!config) {
@@ -1077,7 +1327,8 @@ function createSolvaPay(config) {
1077
1327
  apiBaseUrl: resolvedConfig.apiBaseUrl
1078
1328
  });
1079
1329
  const paywall = new SolvaPayPaywall(apiClient, {
1080
- debug: process.env.SOLVAPAY_DEBUG !== "false"
1330
+ debug: process.env.SOLVAPAY_DEBUG !== "false",
1331
+ limitsCacheTTL: resolvedConfig.limitsCacheTTL
1081
1332
  });
1082
1333
  return {
1083
1334
  // Direct access to API client for advanced operations
@@ -1123,39 +1374,67 @@ function createSolvaPay(config) {
1123
1374
  createCustomerSession(params) {
1124
1375
  return apiClient.createCustomerSession(params);
1125
1376
  },
1377
+ bootstrapMcpProduct(params) {
1378
+ if (!apiClient.bootstrapMcpProduct) {
1379
+ throw new SolvaPayError2("bootstrapMcpProduct is not available on this API client");
1380
+ }
1381
+ return apiClient.bootstrapMcpProduct(params);
1382
+ },
1383
+ getVirtualTools(options) {
1384
+ return createVirtualTools(apiClient, options);
1385
+ },
1126
1386
  // Payable API for framework-specific handlers
1127
1387
  payable(options = {}) {
1128
1388
  const product = options.productRef || options.product || process.env.SOLVAPAY_PRODUCT || "default-product";
1129
- const plan = options.planRef || options.plan || product;
1130
- const metadata = { product, plan };
1389
+ const plan = options.planRef || options.plan;
1390
+ const usageType = options.usageType || "requests";
1391
+ const metadata = { product, plan, usageType };
1131
1392
  return {
1132
1393
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1133
1394
  http(businessLogic, adapterOptions) {
1134
- const adapter = new HttpAdapter(adapterOptions);
1395
+ const adapter = new HttpAdapter({
1396
+ ...adapterOptions,
1397
+ getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
1398
+ });
1399
+ const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
1135
1400
  return async (req, reply) => {
1136
- const handler = await createAdapterHandler(adapter, paywall, metadata, businessLogic);
1401
+ const handler = await handlerPromise;
1137
1402
  return handler([req, reply]);
1138
1403
  };
1139
1404
  },
1140
1405
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1141
1406
  next(businessLogic, adapterOptions) {
1142
- const adapter = new NextAdapter(adapterOptions);
1407
+ const adapter = new NextAdapter({
1408
+ ...adapterOptions,
1409
+ getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
1410
+ });
1411
+ const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
1143
1412
  return async (request, context) => {
1144
- const handler = await createAdapterHandler(adapter, paywall, metadata, businessLogic);
1413
+ const handler = await handlerPromise;
1145
1414
  return handler([request, context]);
1146
1415
  };
1147
1416
  },
1148
1417
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1149
1418
  mcp(businessLogic, adapterOptions) {
1150
- const adapter = new McpAdapter(adapterOptions);
1419
+ const adapter = new McpAdapter({
1420
+ ...adapterOptions,
1421
+ getCustomerRef: adapterOptions?.getCustomerRef || options.getCustomerRef
1422
+ });
1423
+ const handlerPromise = createAdapterHandler(adapter, paywall, metadata, businessLogic);
1151
1424
  return async (args) => {
1152
- const handler = await createAdapterHandler(adapter, paywall, metadata, businessLogic);
1425
+ const handler = await handlerPromise;
1153
1426
  return handler(args);
1154
1427
  };
1155
1428
  },
1156
1429
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1157
1430
  async function(businessLogic) {
1158
- const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
1431
+ const getCustomerRef = (args) => {
1432
+ const configuredRef = options.getCustomerRef?.(args);
1433
+ if (typeof configuredRef === "string") {
1434
+ return configuredRef;
1435
+ }
1436
+ return args.auth?.customer_ref || "anonymous";
1437
+ };
1159
1438
  return paywall.protect(businessLogic, metadata, getCustomerRef);
1160
1439
  }
1161
1440
  };