@solvapay/server 2.7.0 → 2.8.0

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.
@@ -825,6 +825,9 @@ function classifyPaywallState(limits) {
825
825
  if (limits.activationRequired === true || limits.paywallReason === "activation_required") {
826
826
  return { kind: "activation_required" };
827
827
  }
828
+ if (limits.paywallReason === "limit_reached") {
829
+ return { kind: "limit_reached" };
830
+ }
828
831
  if (limits.paywallReason === "topup_required") {
829
832
  return { kind: "topup_required" };
830
833
  }
@@ -888,9 +891,12 @@ function formatCreditsWithMoney(credits, gate) {
888
891
  }
889
892
  return `${amount} credits`;
890
893
  }
894
+ function isFreeMeter(name) {
895
+ return typeof name === "string" && name.startsWith("free-");
896
+ }
891
897
  function meterLabel(gate) {
892
898
  if (!gate.meterName) return "units";
893
- return gate.meterName.replace(/_/g, " ");
899
+ return gate.meterName.replace(/[_-]/g, " ");
894
900
  }
895
901
  function namedCheckoutMarkdown(url, label = "Open checkout") {
896
902
  return `[${label}](${url})`;
@@ -926,6 +932,12 @@ function buildGateMessage(state, gate) {
926
932
  switch (state.kind) {
927
933
  case "limit_reached": {
928
934
  const included = gate.included;
935
+ if (isFreeMeter(gate.meterName)) {
936
+ const total = included?.total;
937
+ const usedLine2 = total !== void 0 ? `You've used all ${total} ${meterLabel(gate)} for this period.` : `You've used all ${meterLabel(gate)} for this period.`;
938
+ const switchLine2 = ladder ? ` Pick a plan to keep going: ${ladder}. ${callViewer("account").replace(/^c/, "C")} for usage and recovery.` : recoverClause(url, "keep going", "checkout");
939
+ return `${usedLine2}${switchLine2}`;
940
+ }
929
941
  const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
930
942
  const usedLine = included ? `You've used ${included.used} of ${included.total} included ${meterLabel(gate)} this period.` : `You've reached the included usage for this period.`;
931
943
  const nextLine = price ? ` The next call is ${price}.` : "";
@@ -1228,6 +1240,7 @@ function createRequestDeduplicator(options = {}) {
1228
1240
  }
1229
1241
 
1230
1242
  // src/paywall.ts
1243
+ import { SolvaPayError as SolvaPayError4 } from "@solvapay/core";
1231
1244
  var PaywallError = class extends Error {
1232
1245
  /**
1233
1246
  * Creates a new PaywallError instance.
@@ -1278,6 +1291,15 @@ var sharedLimitsFetchDeduplicator = createRequestDeduplicator({
1278
1291
  });
1279
1292
  var sharedLimitsFetchClaims = /* @__PURE__ */ new Map();
1280
1293
  var EXTRA_FORWARD_KEY = "__solvapayExtra";
1294
+ function trackUsageExtra(metadata, outcome, consequence) {
1295
+ if (metadata.freeLimit) {
1296
+ return { meterName: metadata.freeLimit.meter };
1297
+ }
1298
+ if (outcome === "success") {
1299
+ return { usageClass: consequence === "overage" ? "overage" : "included" };
1300
+ }
1301
+ return void 0;
1302
+ }
1281
1303
  var SolvaPayPaywall = class {
1282
1304
  constructor(apiClient, options = {}) {
1283
1305
  this.apiClient = apiClient;
@@ -1337,10 +1359,19 @@ var SolvaPayPaywall = class {
1337
1359
  */
1338
1360
  async decide(args, metadata = {}, getCustomerRef) {
1339
1361
  const product = this.resolveProduct(metadata);
1340
- const usageType = metadata.meterName || metadata.usageType || "requests";
1362
+ const usageType = metadata.freeLimit?.meter || metadata.meterName || metadata.usageType || "requests";
1341
1363
  const requestId = this.generateRequestId();
1342
1364
  const startTime = Date.now();
1343
1365
  const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
1366
+ if (metadata.freeLimit) {
1367
+ const resolved = typeof inputCustomerRef === "string" ? inputCustomerRef.trim() : "";
1368
+ if (!resolved || resolved === "anonymous") {
1369
+ throw new SolvaPayError4("identity required", {
1370
+ status: 401,
1371
+ code: "identity_required"
1372
+ });
1373
+ }
1374
+ }
1344
1375
  let backendCustomerRef;
1345
1376
  if (inputCustomerRef.startsWith("cus_")) {
1346
1377
  backendCustomerRef = inputCustomerRef;
@@ -1391,7 +1422,8 @@ var SolvaPayPaywall = class {
1391
1422
  // `checkLimitsCore`, which powers the React `useLimits` hook)
1392
1423
  // leave this unset and the backend skips the session-creation
1393
1424
  // side effect.
1394
- includeCheckoutSession: true
1425
+ includeCheckoutSession: true,
1426
+ ...metadata.freeLimit ? { freeAllowance: metadata.freeLimit } : {}
1395
1427
  });
1396
1428
  }
1397
1429
  );
@@ -1435,7 +1467,8 @@ var SolvaPayPaywall = class {
1435
1467
  "paywall",
1436
1468
  requestId,
1437
1469
  latencyMs,
1438
- metadata.toolName
1470
+ metadata.toolName,
1471
+ metadata.freeLimit ? { meterName: metadata.freeLimit.meter } : void 0
1439
1472
  ).catch(() => void 0);
1440
1473
  const gate = buildPaywallGate(
1441
1474
  product,
@@ -1482,7 +1515,7 @@ var SolvaPayPaywall = class {
1482
1515
  */
1483
1516
  async runAllow(decision, handler, metadata, args) {
1484
1517
  const product = this.resolveProduct(metadata);
1485
- const usageType = metadata.meterName || metadata.usageType || "requests";
1518
+ const usageType = metadata.freeLimit?.meter || metadata.meterName || metadata.usageType || "requests";
1486
1519
  const requestId = decision.requestId;
1487
1520
  const startTime = Date.now();
1488
1521
  const forwardedExtra = args[EXTRA_FORWARD_KEY];
@@ -1501,7 +1534,8 @@ var SolvaPayPaywall = class {
1501
1534
  "success",
1502
1535
  requestId,
1503
1536
  latencyMs,
1504
- metadata.toolName
1537
+ metadata.toolName,
1538
+ trackUsageExtra(metadata, "success", decision.consequence)
1505
1539
  ).catch(() => void 0);
1506
1540
  return result;
1507
1541
  } catch (error) {
@@ -1520,7 +1554,8 @@ var SolvaPayPaywall = class {
1520
1554
  "fail",
1521
1555
  requestId,
1522
1556
  latencyMs,
1523
- metadata.toolName
1557
+ metadata.toolName,
1558
+ trackUsageExtra(metadata, "fail")
1524
1559
  ).catch(() => void 0);
1525
1560
  }
1526
1561
  throw error;
@@ -1711,7 +1746,7 @@ var SolvaPayPaywall = class {
1711
1746
  }
1712
1747
  return backendRef;
1713
1748
  }
1714
- async trackUsage(customerRef, productRef, action, outcome, requestId, actionDuration, toolName) {
1749
+ async trackUsage(customerRef, productRef, action, outcome, requestId, actionDuration, toolName, extra) {
1715
1750
  await withRetry(
1716
1751
  () => this.apiClient.trackUsage({
1717
1752
  customerRef,
@@ -1724,7 +1759,9 @@ var SolvaPayPaywall = class {
1724
1759
  metadata: {
1725
1760
  action: action || "api_requests",
1726
1761
  requestId,
1727
- ...toolName ? { toolName } : {}
1762
+ ...toolName ? { toolName } : {},
1763
+ ...extra?.meterName ? { meterName: extra.meterName } : {},
1764
+ ...extra?.usageClass ? { usageClass: extra.usageClass } : {}
1728
1765
  },
1729
1766
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1730
1767
  }),
@@ -2064,7 +2101,7 @@ var McpAdapter = class {
2064
2101
  };
2065
2102
 
2066
2103
  // src/factory.ts
2067
- import { SolvaPayError as SolvaPayError4, getSolvaPayConfig } from "@solvapay/core";
2104
+ import { SolvaPayError as SolvaPayError5, getSolvaPayConfig } from "@solvapay/core";
2068
2105
 
2069
2106
  // src/virtual-tools.ts
2070
2107
  var TOOL_GET_USER_INFO = {
@@ -2341,25 +2378,25 @@ function createSolvaPay(config) {
2341
2378
  },
2342
2379
  createPaymentIntent(params) {
2343
2380
  if (!apiClient.createPaymentIntent) {
2344
- throw new SolvaPayError4("createPaymentIntent is not available on this API client");
2381
+ throw new SolvaPayError5("createPaymentIntent is not available on this API client");
2345
2382
  }
2346
2383
  return apiClient.createPaymentIntent(params);
2347
2384
  },
2348
2385
  createTopupPaymentIntent(params) {
2349
2386
  if (!apiClient.createTopupPaymentIntent) {
2350
- throw new SolvaPayError4("createTopupPaymentIntent is not available on this API client");
2387
+ throw new SolvaPayError5("createTopupPaymentIntent is not available on this API client");
2351
2388
  }
2352
2389
  return apiClient.createTopupPaymentIntent(params);
2353
2390
  },
2354
2391
  processPaymentIntent(params) {
2355
2392
  if (!apiClient.processPaymentIntent) {
2356
- throw new SolvaPayError4("processPaymentIntent is not available on this API client");
2393
+ throw new SolvaPayError5("processPaymentIntent is not available on this API client");
2357
2394
  }
2358
2395
  return apiClient.processPaymentIntent(params);
2359
2396
  },
2360
2397
  attachBusinessDetails(params) {
2361
2398
  if (!apiClient.attachBusinessDetails) {
2362
- throw new SolvaPayError4("attachBusinessDetails is not available on this API client");
2399
+ throw new SolvaPayError5("attachBusinessDetails is not available on this API client");
2363
2400
  }
2364
2401
  return apiClient.attachBusinessDetails(params);
2365
2402
  },
@@ -2371,13 +2408,13 @@ function createSolvaPay(config) {
2371
2408
  },
2372
2409
  trackUsageBulk(params) {
2373
2410
  if (!apiClient.trackUsageBulk) {
2374
- throw new SolvaPayError4("trackUsageBulk is not available on this API client");
2411
+ throw new SolvaPayError5("trackUsageBulk is not available on this API client");
2375
2412
  }
2376
2413
  return apiClient.trackUsageBulk(params);
2377
2414
  },
2378
2415
  createCustomer(params) {
2379
2416
  if (!apiClient.createCustomer) {
2380
- throw new SolvaPayError4("createCustomer is not available on this API client");
2417
+ throw new SolvaPayError5("createCustomer is not available on this API client");
2381
2418
  }
2382
2419
  return apiClient.createCustomer({ ...params, metadata: params.metadata ?? {} });
2383
2420
  },
@@ -2386,13 +2423,13 @@ function createSolvaPay(config) {
2386
2423
  },
2387
2424
  assignCredits(params) {
2388
2425
  if (!apiClient.assignCredits) {
2389
- throw new SolvaPayError4("assignCredits is not available on this API client");
2426
+ throw new SolvaPayError5("assignCredits is not available on this API client");
2390
2427
  }
2391
2428
  return apiClient.assignCredits(params);
2392
2429
  },
2393
2430
  getCustomerBalance(params) {
2394
2431
  if (!apiClient.getCustomerBalance) {
2395
- throw new SolvaPayError4("getCustomerBalance is not available on this API client");
2432
+ throw new SolvaPayError5("getCustomerBalance is not available on this API client");
2396
2433
  }
2397
2434
  return apiClient.getCustomerBalance(params);
2398
2435
  },
@@ -2410,19 +2447,19 @@ function createSolvaPay(config) {
2410
2447
  },
2411
2448
  activatePlan(params) {
2412
2449
  if (!apiClient.activatePlan) {
2413
- throw new SolvaPayError4("activatePlan is not available on this API client");
2450
+ throw new SolvaPayError5("activatePlan is not available on this API client");
2414
2451
  }
2415
2452
  return apiClient.activatePlan(params);
2416
2453
  },
2417
2454
  bootstrapMcpProduct(params) {
2418
2455
  if (!apiClient.bootstrapMcpProduct) {
2419
- throw new SolvaPayError4("bootstrapMcpProduct is not available on this API client");
2456
+ throw new SolvaPayError5("bootstrapMcpProduct is not available on this API client");
2420
2457
  }
2421
2458
  return apiClient.bootstrapMcpProduct(params);
2422
2459
  },
2423
2460
  configureMcpPlans(productRef, params) {
2424
2461
  if (!apiClient.configureMcpPlans) {
2425
- throw new SolvaPayError4("configureMcpPlans is not available on this API client");
2462
+ throw new SolvaPayError5("configureMcpPlans is not available on this API client");
2426
2463
  }
2427
2464
  return apiClient.configureMcpPlans(productRef, params);
2428
2465
  },
@@ -2435,12 +2472,13 @@ function createSolvaPay(config) {
2435
2472
  // Payable API for framework-specific handlers
2436
2473
  payable(options = {}) {
2437
2474
  const product = resolveProductRef(options.productRef || options.product);
2438
- const usageType = options.meterName || options.usageType || "requests";
2475
+ const usageType = options.freeLimit?.meter || options.meterName || options.usageType || "requests";
2439
2476
  const metadata = {
2440
2477
  product,
2441
2478
  meterName: usageType,
2442
2479
  usageType,
2443
- ...options.toolName ? { toolName: options.toolName } : {}
2480
+ ...options.toolName ? { toolName: options.toolName } : {},
2481
+ ...options.freeLimit ? { freeLimit: options.freeLimit } : {}
2444
2482
  };
2445
2483
  return {
2446
2484
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -2529,6 +2567,8 @@ function createSolvaPay(config) {
2529
2567
  const errMeta = opts?.error !== void 0 ? {
2530
2568
  error: opts.error instanceof Error ? opts.error.message : String(opts.error)
2531
2569
  } : {};
2570
+ const isFree = Boolean(decideMetadata.freeLimit);
2571
+ const usageClass = !isFree && outcome === "success" ? decision.consequence === "overage" ? "overage" : "included" : void 0;
2532
2572
  const trackPromise = apiClient.trackUsage({
2533
2573
  customerRef,
2534
2574
  productRef,
@@ -2541,6 +2581,8 @@ function createSolvaPay(config) {
2541
2581
  action: meterName,
2542
2582
  requestId,
2543
2583
  ...decideMetadata.toolName ? { toolName: decideMetadata.toolName } : {},
2584
+ ...isFree && decideMetadata.freeLimit ? { meterName: decideMetadata.freeLimit.meter } : {},
2585
+ ...usageClass ? { usageClass } : {},
2544
2586
  ...errMeta,
2545
2587
  ...opts?.metadata ?? {}
2546
2588
  },
@@ -2798,7 +2840,7 @@ async function createCustomerSessionCore(request, options = {}) {
2798
2840
  }
2799
2841
 
2800
2842
  // src/helpers/renewal.ts
2801
- import { SolvaPayError as SolvaPayError5 } from "@solvapay/core";
2843
+ import { SolvaPayError as SolvaPayError6 } from "@solvapay/core";
2802
2844
  async function cancelPurchaseCore(request, body, options = {}) {
2803
2845
  try {
2804
2846
  if (!body.purchaseRef) {
@@ -2844,7 +2886,7 @@ async function cancelPurchaseCore(request, body, options = {}) {
2844
2886
  await new Promise((resolve) => setTimeout(resolve, 500));
2845
2887
  return cancelledPurchase;
2846
2888
  } catch (error) {
2847
- if (error instanceof SolvaPayError5) {
2889
+ if (error instanceof SolvaPayError6) {
2848
2890
  const errorMessage = error.message;
2849
2891
  if (errorMessage.includes("not found")) {
2850
2892
  return {
@@ -2912,7 +2954,7 @@ async function reactivatePurchaseCore(request, body, options = {}) {
2912
2954
  await new Promise((resolve) => setTimeout(resolve, 500));
2913
2955
  return reactivatedPurchase;
2914
2956
  } catch (error) {
2915
- if (error instanceof SolvaPayError5) {
2957
+ if (error instanceof SolvaPayError6) {
2916
2958
  const errorMessage = error.message;
2917
2959
  if (errorMessage.includes("not found")) {
2918
2960
  return {
@@ -3231,7 +3273,7 @@ async function trackUsageCore(request, body, options = {}) {
3231
3273
  }
3232
3274
 
3233
3275
  // src/edge.ts
3234
- import { SolvaPayError as SolvaPayError6 } from "@solvapay/core";
3276
+ import { SolvaPayError as SolvaPayError7 } from "@solvapay/core";
3235
3277
  function timingSafeEqual(a, b) {
3236
3278
  if (a.length !== b.length) return false;
3237
3279
  let mismatch = 0;
@@ -3246,22 +3288,22 @@ async function verifyWebhook({
3246
3288
  secret
3247
3289
  }) {
3248
3290
  const toleranceSec = 300;
3249
- if (!signature) throw new SolvaPayError6("Missing webhook signature");
3291
+ if (!signature) throw new SolvaPayError7("Missing webhook signature");
3250
3292
  const parts = signature.split(",");
3251
3293
  const tPart = parts.find((p) => p.startsWith("t="));
3252
3294
  const v1Part = parts.find((p) => p.startsWith("v1="));
3253
3295
  if (!tPart || !v1Part) {
3254
- throw new SolvaPayError6("Malformed webhook signature");
3296
+ throw new SolvaPayError7("Malformed webhook signature");
3255
3297
  }
3256
3298
  const timestamp = parseInt(tPart.slice(2), 10);
3257
3299
  const receivedHmac = v1Part.slice(3);
3258
3300
  if (Number.isNaN(timestamp) || !receivedHmac) {
3259
- throw new SolvaPayError6("Malformed webhook signature");
3301
+ throw new SolvaPayError7("Malformed webhook signature");
3260
3302
  }
3261
3303
  if (toleranceSec > 0) {
3262
3304
  const age = Math.abs(Math.floor(Date.now() / 1e3) - timestamp);
3263
3305
  if (age > toleranceSec) {
3264
- throw new SolvaPayError6("Webhook signature timestamp too old");
3306
+ throw new SolvaPayError7("Webhook signature timestamp too old");
3265
3307
  }
3266
3308
  }
3267
3309
  const enc = new TextEncoder();
@@ -3275,12 +3317,12 @@ async function verifyWebhook({
3275
3317
  const sigBuf = await crypto.subtle.sign("HMAC", key, enc.encode(`${timestamp}.${body}`));
3276
3318
  const expectedHmac = Array.from(new Uint8Array(sigBuf)).map((b) => b.toString(16).padStart(2, "0")).join("");
3277
3319
  if (!timingSafeEqual(expectedHmac, receivedHmac)) {
3278
- throw new SolvaPayError6("Invalid webhook signature");
3320
+ throw new SolvaPayError7("Invalid webhook signature");
3279
3321
  }
3280
3322
  try {
3281
3323
  return JSON.parse(body);
3282
3324
  } catch {
3283
- throw new SolvaPayError6("Invalid webhook payload: body is not valid JSON");
3325
+ throw new SolvaPayError7("Invalid webhook payload: body is not valid JSON");
3284
3326
  }
3285
3327
  }
3286
3328