@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.
package/dist/index.d.ts CHANGED
@@ -254,6 +254,15 @@ interface components {
254
254
  };
255
255
  CheckLimitRequest: {
256
256
  customerRef: string;
257
+ /** @description SDK-only free-tool allowance. Authenticated by the provider secret key, so `cap` is the provider's own declaration, not customer input. */
258
+ freeAllowance?: {
259
+ cap?: number;
260
+ /** @description Free meter name. Must match `/^free-[a-z0-9-]+$/`. */
261
+ meter?: string;
262
+ /** @enum {string} */
263
+ scope?: 'rolling_window' | 'lifetime';
264
+ windowDays?: number;
265
+ };
257
266
  includeCheckoutSession?: boolean;
258
267
  meterName?: string;
259
268
  productRef: string;
@@ -909,7 +918,7 @@ interface components {
909
918
  * Authoritative paywall classification shared with Managed MCP. Present on denial responses only.
910
919
  * @enum {string}
911
920
  */
912
- paywallReason?: 'activation_required' | 'topup_required' | 'payment_required';
921
+ paywallReason?: 'activation_required' | 'topup_required' | 'payment_required' | 'limit_reached';
913
922
  /** @description Display name of the active or default plan */
914
923
  planName?: string;
915
924
  /** @description Active plan reference when the customer already holds a purchase */
@@ -3919,6 +3928,16 @@ type AttachBusinessDetailsParams = {
3919
3928
  } & components['schemas']['BusinessDetailsDto'];
3920
3929
  type AttachBusinessDetailsResult = components['schemas']['AttachBusinessDetailsResponse'];
3921
3930
  type CheckLimitsRequest = components['schemas']['CheckLimitRequest'];
3931
+ /**
3932
+ * Per-customer cap declared in code on a `registerFree` tool. Not a
3933
+ * registered `Meter` — the name must match `/^free-[a-z0-9-]+$/`.
3934
+ *
3935
+ * Derived from `CheckLimitRequest.freeAllowance`. nestjs-zod omits
3936
+ * nested `required`, so OpenAPI marks every property optional; the
3937
+ * object schema only treats `windowDays` as optional.
3938
+ */
3939
+ type GeneratedFreeAllowance = NonNullable<CheckLimitsRequest['freeAllowance']>;
3940
+ type FreeLimit = Required<Omit<GeneratedFreeAllowance, 'windowDays'>> & Pick<GeneratedFreeAllowance, 'windowDays'>;
3922
3941
  /**
3923
3942
  * `LimitResponse` plus a deprecated SDK-only `plan` alias.
3924
3943
  *
@@ -4242,6 +4261,11 @@ interface PaywallMetadata {
4242
4261
  * attributable. Set by `registerPayable` / `buildPayableHandler`.
4243
4262
  */
4244
4263
  toolName?: string;
4264
+ /**
4265
+ * SDK-only free-tool allowance. When set, `decide()` sends this as
4266
+ * `freeAllowance` and the free meter wins over `meterName`.
4267
+ */
4268
+ freeLimit?: FreeLimit;
4245
4269
  }
4246
4270
  /**
4247
4271
  * Structured content for paywall errors (MCP structuredContent and manual handling).
@@ -4278,9 +4302,9 @@ type PaywallGateRecoveryFields = {
4278
4302
  creditBalance?: number;
4279
4303
  /**
4280
4304
  * `PaywallState.kind` — same vocabulary as the classifier, not a third enum.
4281
- * `upgrade_required` / `limit_reached` / `reactivation_required` are
4282
- * SDK-only; the backend's `paywallReason` is a smaller set
4283
- * (`activation_required` / `topup_required` / `payment_required`).
4305
+ * `upgrade_required` / `reactivation_required` are SDK-only. The
4306
+ * backend's `paywallReason` covers `activation_required` /
4307
+ * `topup_required` / `payment_required` / `limit_reached`.
4284
4308
  */
4285
4309
  reason?: PaywallReason;
4286
4310
  /** Single primary recovery the agent should take. */
@@ -4404,6 +4428,7 @@ type PaywallDecision<T> = {
4404
4428
  * Types for configuring various aspects of the SDK including retry behavior,
4405
4429
  * payable protection, and framework adapters.
4406
4430
  */
4431
+
4407
4432
  /**
4408
4433
  * Retry configuration options
4409
4434
  */
@@ -4482,6 +4507,11 @@ interface PayableOptions {
4482
4507
  * Product reference (alias for product, preferred for consistency with backend API)
4483
4508
  */
4484
4509
  productRef?: string;
4510
+ /**
4511
+ * SDK-only free-tool allowance. When set, this meter's name wins over
4512
+ * `meterName` / `usageType`, and `checkLimits` sends a `freeAllowance` block.
4513
+ */
4514
+ freeLimit?: FreeLimit;
4485
4515
  /**
4486
4516
  * Meter to charge against (defaults to `requests`).
4487
4517
  */
@@ -5042,14 +5072,7 @@ interface SolvaPay {
5042
5072
  * }
5043
5073
  * ```
5044
5074
  */
5045
- checkLimits(params: {
5046
- customerRef: string;
5047
- productRef: string;
5048
- planRef?: string;
5049
- meterName?: string;
5050
- /** @deprecated Use `meterName`. */
5051
- usageType?: string;
5052
- }): Promise<LimitResponseWithPlan>;
5075
+ checkLimits(params: CheckLimitsRequest): Promise<LimitResponseWithPlan>;
5053
5076
  /**
5054
5077
  * Track usage for a customer action.
5055
5078
  *
@@ -5597,8 +5620,8 @@ declare const PaywallStructuredContentSchema: z.ZodDiscriminatedUnion<[z.ZodObje
5597
5620
  topup_required: "topup_required";
5598
5621
  payment_required: "payment_required";
5599
5622
  activation_required: "activation_required";
5600
- upgrade_required: "upgrade_required";
5601
5623
  limit_reached: "limit_reached";
5624
+ upgrade_required: "upgrade_required";
5602
5625
  reactivation_required: "reactivation_required";
5603
5626
  }>>;
5604
5627
  nextAction: z.ZodOptional<z.ZodEnum<{
@@ -5646,8 +5669,8 @@ declare const PaywallStructuredContentSchema: z.ZodDiscriminatedUnion<[z.ZodObje
5646
5669
  topup_required: "topup_required";
5647
5670
  payment_required: "payment_required";
5648
5671
  activation_required: "activation_required";
5649
- upgrade_required: "upgrade_required";
5650
5672
  limit_reached: "limit_reached";
5673
+ upgrade_required: "upgrade_required";
5651
5674
  reactivation_required: "reactivation_required";
5652
5675
  }>>;
5653
5676
  nextAction: z.ZodOptional<z.ZodEnum<{
@@ -5728,7 +5751,9 @@ declare function nextActionFor(state: PaywallState): PaywallNextAction;
5728
5751
  *
5729
5752
  * Precedence:
5730
5753
  * 1. `activationRequired` / `paywallReason === 'activation_required'`.
5731
- * 2. `paywallReason === 'topup_required'` — backend stays authoritative
5754
+ * 2. `paywallReason === 'limit_reached'` — free-allowance exhaustion
5755
+ * (no purchaseRef / planRef) must not fall through to upgrade_required.
5756
+ * 3. `paywallReason === 'topup_required'` — backend stays authoritative
5732
5757
  * for credit-based denials (same rule as Managed MCP).
5733
5758
  * 3. Authoritative `needsTopUp` / `needsUpgrade` flags from `decideLimit`.
5734
5759
  * 4. Credit-field presence + a real shortfall (`balance < cost`).
@@ -6555,4 +6580,4 @@ declare function verifyWebhook({ body, signature, secret, }: {
6555
6580
  secret: string;
6556
6581
  }): WebhookEvent;
6557
6582
 
6558
- export { type ActivatePlanResult, type AssignCreditsRequest, type AssignCreditsResponse, type AttachBusinessDetailsParams, type AttachBusinessDetailsResult, type AuthenticatedUser, type AutoRechargeConfig, type AutoRechargeDisplayBlock, type AutoRechargeInput, type AutoRechargeResponse, BALANCE_RECONCILE_DELAYS_MS, type ConfigureMcpPlansRequest, type ConfigureMcpPlansResponse, type CreateSolvaPayConfig, type CreditActivityEntry, type CreditActivityResult, type CreditActivityType, type CreditDebitResult, type CreditDebitSkipReason, type CreditDisplayBlock, type CreditSignals, type CustomerBalanceResult, type CustomerResponseMapped, type CustomerWebhookObject, type ErrorResult, type GetHistoryResult, type GetUsageResult, type HttpAdapterOptions, type LimitActivationBalance, type LimitActivationProduct, type LimitAutoRechargeDto, type LimitPlanSummary, type LimitResponseWithPlan, type McpBootstrapPlanInput, type McpBootstrapRequest, type McpBootstrapResponse, type McpServerLike, type McpToolPlanMappingInput, type NextAdapterOptions, type OneTimePurchaseInfo, type PayableAllowResult, type PayableFunction, type PayableGateOptions, type PayableGateResult, type PayableOptions, type PayablePaywallResult, type PaymentMethodInfo, type PaywallArgs, type PaywallDecision, PaywallError, type PaywallGateRecoveryFields, type PaywallMetadata, type PaywallNextAction, type PaywallReason, type PaywallState, type PaywallStructuredContent, PaywallStructuredContentSchema, type ProcessPaymentResult, type ProductConfigurationStatus, type ProtectHandlerContext, type PurchaseCheckResult, type PurchaseInfo, type RegisterVirtualToolsMcpOptions, type RetryOptions, type SaveAutoRechargeInput, type SaveAutoRechargeResponse, type SdkMerchantResponse, type SdkProductResponse, type ServerClientOptions, type SolvaPay, type SolvaPayClient, TOPUP_BALANCE_POLL_DELAYS_MS, type ToolPlanMappingInput, type TopupProcessResult, type TrackUsageBulkRequest, type TrackUsageBulkResponse, type TrackUsageRequest, type TrackUsageResponse, type UsageLimitsInput, VIRTUAL_TOOL_DEFINITIONS, type VerifyProductConfigurationOptions, type VirtualToolDefinition, type VirtualToolsOptions, type WebhookEvent, type WebhookEventForType, type WebhookEventObjectMap, type WebhookEventType, type WebhookProduct, activatePlanCore, attachBusinessDetailsCore, buildGateMessage, buildNudgeMessage, buildPaywallGate, cancelPurchaseCore, checkLimitsCore, checkPurchaseCore, classifyPaywallState, type components, createCheckoutSessionCore, createCustomerSessionCore, createPaymentIntentCore, createSolvaPay, createSolvaPayClient, createTopupPaymentIntentCore, createVirtualTools, creditSignals, deriveUsageSnapshot, disableAutoRechargeCore, getAuthenticatedUserCore, getAutoRechargeCore, getCustomerBalanceCore, getHistoryCore, getMerchantCore, getPaymentMethodCore, getProductCore, getUsageCore, handleRouteError, isErrorResult, isPaywallStructuredContent, jsonSchemaToZodRawShape, linkLabel, listPlansCore, nextActionFor, paywallErrorToClientPayload, planLadder, pollBalanceUntilIncreased, processPaymentIntentCore, processTopupPaymentIntentCore, reactivatePurchaseCore, registerVirtualToolsMcpImpl, saveAutoRechargeCore, syncCustomerCore, trackUsageCore, verifyProductConfiguration, verifyWebhook, withRetry };
6583
+ export { type ActivatePlanResult, type AssignCreditsRequest, type AssignCreditsResponse, type AttachBusinessDetailsParams, type AttachBusinessDetailsResult, type AuthenticatedUser, type AutoRechargeConfig, type AutoRechargeDisplayBlock, type AutoRechargeInput, type AutoRechargeResponse, BALANCE_RECONCILE_DELAYS_MS, type ConfigureMcpPlansRequest, type ConfigureMcpPlansResponse, type CreateSolvaPayConfig, type CreditActivityEntry, type CreditActivityResult, type CreditActivityType, type CreditDebitResult, type CreditDebitSkipReason, type CreditDisplayBlock, type CreditSignals, type CustomerBalanceResult, type CustomerResponseMapped, type CustomerWebhookObject, type ErrorResult, type FreeLimit, type GetHistoryResult, type GetUsageResult, type HttpAdapterOptions, type LimitActivationBalance, type LimitActivationProduct, type LimitAutoRechargeDto, type LimitPlanSummary, type LimitResponseWithPlan, type McpBootstrapPlanInput, type McpBootstrapRequest, type McpBootstrapResponse, type McpServerLike, type McpToolPlanMappingInput, type NextAdapterOptions, type OneTimePurchaseInfo, type PayableAllowResult, type PayableFunction, type PayableGateOptions, type PayableGateResult, type PayableOptions, type PayablePaywallResult, type PaymentMethodInfo, type PaywallArgs, type PaywallDecision, PaywallError, type PaywallGateRecoveryFields, type PaywallMetadata, type PaywallNextAction, type PaywallReason, type PaywallState, type PaywallStructuredContent, PaywallStructuredContentSchema, type ProcessPaymentResult, type ProductConfigurationStatus, type ProtectHandlerContext, type PurchaseCheckResult, type PurchaseInfo, type RegisterVirtualToolsMcpOptions, type RetryOptions, type SaveAutoRechargeInput, type SaveAutoRechargeResponse, type SdkMerchantResponse, type SdkProductResponse, type ServerClientOptions, type SolvaPay, type SolvaPayClient, TOPUP_BALANCE_POLL_DELAYS_MS, type ToolPlanMappingInput, type TopupProcessResult, type TrackUsageBulkRequest, type TrackUsageBulkResponse, type TrackUsageRequest, type TrackUsageResponse, type UsageLimitsInput, VIRTUAL_TOOL_DEFINITIONS, type VerifyProductConfigurationOptions, type VirtualToolDefinition, type VirtualToolsOptions, type WebhookEvent, type WebhookEventForType, type WebhookEventObjectMap, type WebhookEventType, type WebhookProduct, activatePlanCore, attachBusinessDetailsCore, buildGateMessage, buildNudgeMessage, buildPaywallGate, cancelPurchaseCore, checkLimitsCore, checkPurchaseCore, classifyPaywallState, type components, createCheckoutSessionCore, createCustomerSessionCore, createPaymentIntentCore, createSolvaPay, createSolvaPayClient, createTopupPaymentIntentCore, createVirtualTools, creditSignals, deriveUsageSnapshot, disableAutoRechargeCore, getAuthenticatedUserCore, getAutoRechargeCore, getCustomerBalanceCore, getHistoryCore, getMerchantCore, getPaymentMethodCore, getProductCore, getUsageCore, handleRouteError, isErrorResult, isPaywallStructuredContent, jsonSchemaToZodRawShape, linkLabel, listPlansCore, nextActionFor, paywallErrorToClientPayload, planLadder, pollBalanceUntilIncreased, processPaymentIntentCore, processTopupPaymentIntentCore, reactivatePurchaseCore, registerVirtualToolsMcpImpl, saveAutoRechargeCore, syncCustomerCore, trackUsageCore, verifyProductConfiguration, verifyWebhook, withRetry };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import crypto from "crypto";
3
- import { SolvaPayError as SolvaPayError7 } from "@solvapay/core";
3
+ import { SolvaPayError as SolvaPayError8 } from "@solvapay/core";
4
4
 
5
5
  // src/client.ts
6
6
  import { SolvaPayError } from "@solvapay/core";
@@ -675,6 +675,9 @@ function classifyPaywallState(limits) {
675
675
  if (limits.activationRequired === true || limits.paywallReason === "activation_required") {
676
676
  return { kind: "activation_required" };
677
677
  }
678
+ if (limits.paywallReason === "limit_reached") {
679
+ return { kind: "limit_reached" };
680
+ }
678
681
  if (limits.paywallReason === "topup_required") {
679
682
  return { kind: "topup_required" };
680
683
  }
@@ -738,9 +741,12 @@ function formatCreditsWithMoney(credits, gate) {
738
741
  }
739
742
  return `${amount} credits`;
740
743
  }
744
+ function isFreeMeter(name) {
745
+ return typeof name === "string" && name.startsWith("free-");
746
+ }
741
747
  function meterLabel(gate) {
742
748
  if (!gate.meterName) return "units";
743
- return gate.meterName.replace(/_/g, " ");
749
+ return gate.meterName.replace(/[_-]/g, " ");
744
750
  }
745
751
  function namedCheckoutMarkdown(url, label = "Open checkout") {
746
752
  return `[${label}](${url})`;
@@ -776,6 +782,12 @@ function buildGateMessage(state, gate) {
776
782
  switch (state.kind) {
777
783
  case "limit_reached": {
778
784
  const included = gate.included;
785
+ if (isFreeMeter(gate.meterName)) {
786
+ const total = included?.total;
787
+ const usedLine2 = total !== void 0 ? `You've used all ${total} ${meterLabel(gate)} for this period.` : `You've used all ${meterLabel(gate)} for this period.`;
788
+ const switchLine2 = ladder ? ` Pick a plan to keep going: ${ladder}. ${callViewer("account").replace(/^c/, "C")} for usage and recovery.` : recoverClause(url, "keep going", "checkout");
789
+ return `${usedLine2}${switchLine2}`;
790
+ }
779
791
  const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
780
792
  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.`;
781
793
  const nextLine = price ? ` The next call is ${price}.` : "";
@@ -1102,6 +1114,7 @@ function createRequestDeduplicator(options = {}) {
1102
1114
  }
1103
1115
 
1104
1116
  // src/paywall.ts
1117
+ import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
1105
1118
  var PaywallError = class extends Error {
1106
1119
  /**
1107
1120
  * Creates a new PaywallError instance.
@@ -1152,6 +1165,15 @@ var sharedLimitsFetchDeduplicator = createRequestDeduplicator({
1152
1165
  });
1153
1166
  var sharedLimitsFetchClaims = /* @__PURE__ */ new Map();
1154
1167
  var EXTRA_FORWARD_KEY = "__solvapayExtra";
1168
+ function trackUsageExtra(metadata, outcome, consequence) {
1169
+ if (metadata.freeLimit) {
1170
+ return { meterName: metadata.freeLimit.meter };
1171
+ }
1172
+ if (outcome === "success") {
1173
+ return { usageClass: consequence === "overage" ? "overage" : "included" };
1174
+ }
1175
+ return void 0;
1176
+ }
1155
1177
  var SolvaPayPaywall = class {
1156
1178
  constructor(apiClient, options = {}) {
1157
1179
  this.apiClient = apiClient;
@@ -1211,10 +1233,19 @@ var SolvaPayPaywall = class {
1211
1233
  */
1212
1234
  async decide(args, metadata = {}, getCustomerRef) {
1213
1235
  const product = this.resolveProduct(metadata);
1214
- const usageType = metadata.meterName || metadata.usageType || "requests";
1236
+ const usageType = metadata.freeLimit?.meter || metadata.meterName || metadata.usageType || "requests";
1215
1237
  const requestId = this.generateRequestId();
1216
1238
  const startTime = Date.now();
1217
1239
  const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
1240
+ if (metadata.freeLimit) {
1241
+ const resolved = typeof inputCustomerRef === "string" ? inputCustomerRef.trim() : "";
1242
+ if (!resolved || resolved === "anonymous") {
1243
+ throw new SolvaPayError3("identity required", {
1244
+ status: 401,
1245
+ code: "identity_required"
1246
+ });
1247
+ }
1248
+ }
1218
1249
  let backendCustomerRef;
1219
1250
  if (inputCustomerRef.startsWith("cus_")) {
1220
1251
  backendCustomerRef = inputCustomerRef;
@@ -1265,7 +1296,8 @@ var SolvaPayPaywall = class {
1265
1296
  // `checkLimitsCore`, which powers the React `useLimits` hook)
1266
1297
  // leave this unset and the backend skips the session-creation
1267
1298
  // side effect.
1268
- includeCheckoutSession: true
1299
+ includeCheckoutSession: true,
1300
+ ...metadata.freeLimit ? { freeAllowance: metadata.freeLimit } : {}
1269
1301
  });
1270
1302
  }
1271
1303
  );
@@ -1309,7 +1341,8 @@ var SolvaPayPaywall = class {
1309
1341
  "paywall",
1310
1342
  requestId,
1311
1343
  latencyMs,
1312
- metadata.toolName
1344
+ metadata.toolName,
1345
+ metadata.freeLimit ? { meterName: metadata.freeLimit.meter } : void 0
1313
1346
  ).catch(() => void 0);
1314
1347
  const gate = buildPaywallGate(
1315
1348
  product,
@@ -1356,7 +1389,7 @@ var SolvaPayPaywall = class {
1356
1389
  */
1357
1390
  async runAllow(decision, handler, metadata, args) {
1358
1391
  const product = this.resolveProduct(metadata);
1359
- const usageType = metadata.meterName || metadata.usageType || "requests";
1392
+ const usageType = metadata.freeLimit?.meter || metadata.meterName || metadata.usageType || "requests";
1360
1393
  const requestId = decision.requestId;
1361
1394
  const startTime = Date.now();
1362
1395
  const forwardedExtra = args[EXTRA_FORWARD_KEY];
@@ -1375,7 +1408,8 @@ var SolvaPayPaywall = class {
1375
1408
  "success",
1376
1409
  requestId,
1377
1410
  latencyMs,
1378
- metadata.toolName
1411
+ metadata.toolName,
1412
+ trackUsageExtra(metadata, "success", decision.consequence)
1379
1413
  ).catch(() => void 0);
1380
1414
  return result;
1381
1415
  } catch (error) {
@@ -1394,7 +1428,8 @@ var SolvaPayPaywall = class {
1394
1428
  "fail",
1395
1429
  requestId,
1396
1430
  latencyMs,
1397
- metadata.toolName
1431
+ metadata.toolName,
1432
+ trackUsageExtra(metadata, "fail")
1398
1433
  ).catch(() => void 0);
1399
1434
  }
1400
1435
  throw error;
@@ -1585,7 +1620,7 @@ var SolvaPayPaywall = class {
1585
1620
  }
1586
1621
  return backendRef;
1587
1622
  }
1588
- async trackUsage(customerRef, productRef, action, outcome, requestId, actionDuration, toolName) {
1623
+ async trackUsage(customerRef, productRef, action, outcome, requestId, actionDuration, toolName, extra) {
1589
1624
  await withRetry(
1590
1625
  () => this.apiClient.trackUsage({
1591
1626
  customerRef,
@@ -1598,7 +1633,9 @@ var SolvaPayPaywall = class {
1598
1633
  metadata: {
1599
1634
  action: action || "api_requests",
1600
1635
  requestId,
1601
- ...toolName ? { toolName } : {}
1636
+ ...toolName ? { toolName } : {},
1637
+ ...extra?.meterName ? { meterName: extra.meterName } : {},
1638
+ ...extra?.usageClass ? { usageClass: extra.usageClass } : {}
1602
1639
  },
1603
1640
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1604
1641
  }),
@@ -1938,7 +1975,7 @@ var McpAdapter = class {
1938
1975
  };
1939
1976
 
1940
1977
  // src/factory.ts
1941
- import { SolvaPayError as SolvaPayError3, getSolvaPayConfig } from "@solvapay/core";
1978
+ import { SolvaPayError as SolvaPayError4, getSolvaPayConfig } from "@solvapay/core";
1942
1979
 
1943
1980
  // src/virtual-tools.ts
1944
1981
  var TOOL_GET_USER_INFO = {
@@ -2216,25 +2253,25 @@ function createSolvaPay(config) {
2216
2253
  },
2217
2254
  createPaymentIntent(params) {
2218
2255
  if (!apiClient.createPaymentIntent) {
2219
- throw new SolvaPayError3("createPaymentIntent is not available on this API client");
2256
+ throw new SolvaPayError4("createPaymentIntent is not available on this API client");
2220
2257
  }
2221
2258
  return apiClient.createPaymentIntent(params);
2222
2259
  },
2223
2260
  createTopupPaymentIntent(params) {
2224
2261
  if (!apiClient.createTopupPaymentIntent) {
2225
- throw new SolvaPayError3("createTopupPaymentIntent is not available on this API client");
2262
+ throw new SolvaPayError4("createTopupPaymentIntent is not available on this API client");
2226
2263
  }
2227
2264
  return apiClient.createTopupPaymentIntent(params);
2228
2265
  },
2229
2266
  processPaymentIntent(params) {
2230
2267
  if (!apiClient.processPaymentIntent) {
2231
- throw new SolvaPayError3("processPaymentIntent is not available on this API client");
2268
+ throw new SolvaPayError4("processPaymentIntent is not available on this API client");
2232
2269
  }
2233
2270
  return apiClient.processPaymentIntent(params);
2234
2271
  },
2235
2272
  attachBusinessDetails(params) {
2236
2273
  if (!apiClient.attachBusinessDetails) {
2237
- throw new SolvaPayError3("attachBusinessDetails is not available on this API client");
2274
+ throw new SolvaPayError4("attachBusinessDetails is not available on this API client");
2238
2275
  }
2239
2276
  return apiClient.attachBusinessDetails(params);
2240
2277
  },
@@ -2246,13 +2283,13 @@ function createSolvaPay(config) {
2246
2283
  },
2247
2284
  trackUsageBulk(params) {
2248
2285
  if (!apiClient.trackUsageBulk) {
2249
- throw new SolvaPayError3("trackUsageBulk is not available on this API client");
2286
+ throw new SolvaPayError4("trackUsageBulk is not available on this API client");
2250
2287
  }
2251
2288
  return apiClient.trackUsageBulk(params);
2252
2289
  },
2253
2290
  createCustomer(params) {
2254
2291
  if (!apiClient.createCustomer) {
2255
- throw new SolvaPayError3("createCustomer is not available on this API client");
2292
+ throw new SolvaPayError4("createCustomer is not available on this API client");
2256
2293
  }
2257
2294
  return apiClient.createCustomer({ ...params, metadata: params.metadata ?? {} });
2258
2295
  },
@@ -2261,13 +2298,13 @@ function createSolvaPay(config) {
2261
2298
  },
2262
2299
  assignCredits(params) {
2263
2300
  if (!apiClient.assignCredits) {
2264
- throw new SolvaPayError3("assignCredits is not available on this API client");
2301
+ throw new SolvaPayError4("assignCredits is not available on this API client");
2265
2302
  }
2266
2303
  return apiClient.assignCredits(params);
2267
2304
  },
2268
2305
  getCustomerBalance(params) {
2269
2306
  if (!apiClient.getCustomerBalance) {
2270
- throw new SolvaPayError3("getCustomerBalance is not available on this API client");
2307
+ throw new SolvaPayError4("getCustomerBalance is not available on this API client");
2271
2308
  }
2272
2309
  return apiClient.getCustomerBalance(params);
2273
2310
  },
@@ -2285,19 +2322,19 @@ function createSolvaPay(config) {
2285
2322
  },
2286
2323
  activatePlan(params) {
2287
2324
  if (!apiClient.activatePlan) {
2288
- throw new SolvaPayError3("activatePlan is not available on this API client");
2325
+ throw new SolvaPayError4("activatePlan is not available on this API client");
2289
2326
  }
2290
2327
  return apiClient.activatePlan(params);
2291
2328
  },
2292
2329
  bootstrapMcpProduct(params) {
2293
2330
  if (!apiClient.bootstrapMcpProduct) {
2294
- throw new SolvaPayError3("bootstrapMcpProduct is not available on this API client");
2331
+ throw new SolvaPayError4("bootstrapMcpProduct is not available on this API client");
2295
2332
  }
2296
2333
  return apiClient.bootstrapMcpProduct(params);
2297
2334
  },
2298
2335
  configureMcpPlans(productRef, params) {
2299
2336
  if (!apiClient.configureMcpPlans) {
2300
- throw new SolvaPayError3("configureMcpPlans is not available on this API client");
2337
+ throw new SolvaPayError4("configureMcpPlans is not available on this API client");
2301
2338
  }
2302
2339
  return apiClient.configureMcpPlans(productRef, params);
2303
2340
  },
@@ -2310,12 +2347,13 @@ function createSolvaPay(config) {
2310
2347
  // Payable API for framework-specific handlers
2311
2348
  payable(options = {}) {
2312
2349
  const product = resolveProductRef(options.productRef || options.product);
2313
- const usageType = options.meterName || options.usageType || "requests";
2350
+ const usageType = options.freeLimit?.meter || options.meterName || options.usageType || "requests";
2314
2351
  const metadata = {
2315
2352
  product,
2316
2353
  meterName: usageType,
2317
2354
  usageType,
2318
- ...options.toolName ? { toolName: options.toolName } : {}
2355
+ ...options.toolName ? { toolName: options.toolName } : {},
2356
+ ...options.freeLimit ? { freeLimit: options.freeLimit } : {}
2319
2357
  };
2320
2358
  return {
2321
2359
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -2404,6 +2442,8 @@ function createSolvaPay(config) {
2404
2442
  const errMeta = opts?.error !== void 0 ? {
2405
2443
  error: opts.error instanceof Error ? opts.error.message : String(opts.error)
2406
2444
  } : {};
2445
+ const isFree = Boolean(decideMetadata.freeLimit);
2446
+ const usageClass = !isFree && outcome === "success" ? decision.consequence === "overage" ? "overage" : "included" : void 0;
2407
2447
  const trackPromise = apiClient.trackUsage({
2408
2448
  customerRef,
2409
2449
  productRef,
@@ -2416,6 +2456,8 @@ function createSolvaPay(config) {
2416
2456
  action: meterName,
2417
2457
  requestId,
2418
2458
  ...decideMetadata.toolName ? { toolName: decideMetadata.toolName } : {},
2459
+ ...isFree && decideMetadata.freeLimit ? { meterName: decideMetadata.freeLimit.meter } : {},
2460
+ ...usageClass ? { usageClass } : {},
2419
2461
  ...errMeta,
2420
2462
  ...opts?.metadata ?? {}
2421
2463
  },
@@ -2448,7 +2490,7 @@ async function resolveCustomerRefFromRequest(req, options) {
2448
2490
  }
2449
2491
 
2450
2492
  // src/verify-product-configuration.ts
2451
- import { evaluateProductReadiness, SolvaPayError as SolvaPayError4 } from "@solvapay/core";
2493
+ import { evaluateProductReadiness, SolvaPayError as SolvaPayError5 } from "@solvapay/core";
2452
2494
  async function verifyProductConfiguration(options) {
2453
2495
  const { apiClient, productRef } = options;
2454
2496
  const apiBaseUrl = options.apiBaseUrl ?? "(unknown API base URL)";
@@ -2472,8 +2514,8 @@ async function verifyProductConfiguration(options) {
2472
2514
  issues: readiness.issues
2473
2515
  };
2474
2516
  } catch (error) {
2475
- const status = error instanceof SolvaPayError4 ? error.status : void 0;
2476
- const code = error instanceof SolvaPayError4 ? error.code : void 0;
2517
+ const status = error instanceof SolvaPayError5 ? error.status : void 0;
2518
+ const code = error instanceof SolvaPayError5 ? error.code : void 0;
2477
2519
  if (status === 404 && code === "non_json_response") {
2478
2520
  throw new Error(
2479
2521
  `Could not reach ${apiBaseUrl} while verifying SOLVAPAY_PRODUCT_REF "${productRef}": the server returned a non-JSON response (often an offline ngrok tunnel or proxy error page). Check SOLVAPAY_API_BASE_URL and that the API tunnel is running.`
@@ -2570,13 +2612,13 @@ var PaywallStructuredContentSchema = z.discriminatedUnion("kind", [
2570
2612
  ]);
2571
2613
 
2572
2614
  // src/helpers/error.ts
2573
- import { SolvaPayError as SolvaPayError5 } from "@solvapay/core";
2615
+ import { SolvaPayError as SolvaPayError6 } from "@solvapay/core";
2574
2616
  function isErrorResult(result) {
2575
2617
  return typeof result === "object" && result !== null && "error" in result && "status" in result;
2576
2618
  }
2577
2619
  function handleRouteError(error, operationName, defaultMessage) {
2578
2620
  console.error(`[${operationName}] Error:`, error);
2579
- if (error instanceof SolvaPayError5) {
2621
+ if (error instanceof SolvaPayError6) {
2580
2622
  const errorMessage2 = error.message;
2581
2623
  return {
2582
2624
  error: errorMessage2,
@@ -3075,7 +3117,7 @@ async function createCustomerSessionCore(request, options = {}) {
3075
3117
  }
3076
3118
 
3077
3119
  // src/helpers/renewal.ts
3078
- import { SolvaPayError as SolvaPayError6 } from "@solvapay/core";
3120
+ import { SolvaPayError as SolvaPayError7 } from "@solvapay/core";
3079
3121
  async function cancelPurchaseCore(request, body, options = {}) {
3080
3122
  try {
3081
3123
  if (!body.purchaseRef) {
@@ -3121,7 +3163,7 @@ async function cancelPurchaseCore(request, body, options = {}) {
3121
3163
  await new Promise((resolve) => setTimeout(resolve, 500));
3122
3164
  return cancelledPurchase;
3123
3165
  } catch (error) {
3124
- if (error instanceof SolvaPayError6) {
3166
+ if (error instanceof SolvaPayError7) {
3125
3167
  const errorMessage = error.message;
3126
3168
  if (errorMessage.includes("not found")) {
3127
3169
  return {
@@ -3189,7 +3231,7 @@ async function reactivatePurchaseCore(request, body, options = {}) {
3189
3231
  await new Promise((resolve) => setTimeout(resolve, 500));
3190
3232
  return reactivatedPurchase;
3191
3233
  } catch (error) {
3192
- if (error instanceof SolvaPayError6) {
3234
+ if (error instanceof SolvaPayError7) {
3193
3235
  const errorMessage = error.message;
3194
3236
  if (errorMessage.includes("not found")) {
3195
3237
  return {
@@ -3655,34 +3697,34 @@ function verifyWebhook({
3655
3697
  secret
3656
3698
  }) {
3657
3699
  const toleranceSec = 300;
3658
- if (!signature) throw new SolvaPayError7("Missing webhook signature");
3700
+ if (!signature) throw new SolvaPayError8("Missing webhook signature");
3659
3701
  const parts = signature.split(",");
3660
3702
  const tPart = parts.find((p) => p.startsWith("t="));
3661
3703
  const v1Part = parts.find((p) => p.startsWith("v1="));
3662
3704
  if (!tPart || !v1Part) {
3663
- throw new SolvaPayError7("Malformed webhook signature");
3705
+ throw new SolvaPayError8("Malformed webhook signature");
3664
3706
  }
3665
3707
  const timestamp = parseInt(tPart.slice(2), 10);
3666
3708
  const receivedHmac = v1Part.slice(3);
3667
3709
  if (Number.isNaN(timestamp) || !receivedHmac) {
3668
- throw new SolvaPayError7("Malformed webhook signature");
3710
+ throw new SolvaPayError8("Malformed webhook signature");
3669
3711
  }
3670
3712
  if (toleranceSec > 0) {
3671
3713
  const age = Math.abs(Math.floor(Date.now() / 1e3) - timestamp);
3672
3714
  if (age > toleranceSec) {
3673
- throw new SolvaPayError7("Webhook signature timestamp too old");
3715
+ throw new SolvaPayError8("Webhook signature timestamp too old");
3674
3716
  }
3675
3717
  }
3676
3718
  const expectedHmac = crypto.createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
3677
3719
  if (receivedHmac.length !== expectedHmac.length) {
3678
- throw new SolvaPayError7("Invalid webhook signature");
3720
+ throw new SolvaPayError8("Invalid webhook signature");
3679
3721
  }
3680
3722
  const ok = crypto.timingSafeEqual(Buffer.from(expectedHmac), Buffer.from(receivedHmac));
3681
- if (!ok) throw new SolvaPayError7("Invalid webhook signature");
3723
+ if (!ok) throw new SolvaPayError8("Invalid webhook signature");
3682
3724
  try {
3683
3725
  return JSON.parse(body);
3684
3726
  } catch {
3685
- throw new SolvaPayError7("Invalid webhook payload: body is not valid JSON");
3727
+ throw new SolvaPayError8("Invalid webhook payload: body is not valid JSON");
3686
3728
  }
3687
3729
  }
3688
3730
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solvapay/server",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -70,8 +70,8 @@
70
70
  "build": "tsup",
71
71
  "dev": "tsup --watch",
72
72
  "generate:types": "tsx scripts/generate-types.ts",
73
- "test": "vitest run __tests__/paywall.unit.test.ts __tests__/paywall-state.unit.test.ts __tests__/paywall-gate.unit.test.ts __tests__/payable-gate.unit.test.ts __tests__/bootstrap-mcp.unit.test.ts __tests__/verify-webhook.unit.test.ts __tests__/edge-exports.unit.test.ts __tests__/credits-usage.unit.test.ts __tests__/limit-outcome-flags.unit.test.ts __tests__/client-error.unit.test.ts __tests__/checkout-core.unit.test.ts src/__tests__/edge-exports.test.ts src/helpers/payment.test.ts src/helpers/usage.test.ts src/resolve-product-ref.test.ts src/verify-product-configuration.test.ts __tests__/auth-core.unit.test.ts __tests__/ensure-customer.unit.test.ts __tests__/create-customer.unit.test.ts __tests__/fetch/cors.test.ts __tests__/fetch/handlers.test.ts __tests__/fetch/utils.test.ts",
74
- "test:unit": "vitest run __tests__/paywall.unit.test.ts __tests__/paywall-state.unit.test.ts __tests__/paywall-gate.unit.test.ts __tests__/payable-gate.unit.test.ts __tests__/bootstrap-mcp.unit.test.ts __tests__/verify-webhook.unit.test.ts __tests__/edge-exports.unit.test.ts __tests__/credits-usage.unit.test.ts __tests__/limit-outcome-flags.unit.test.ts __tests__/client-error.unit.test.ts __tests__/checkout-core.unit.test.ts src/__tests__/edge-exports.test.ts src/helpers/payment.test.ts src/helpers/usage.test.ts src/resolve-product-ref.test.ts src/verify-product-configuration.test.ts __tests__/auth-core.unit.test.ts __tests__/ensure-customer.unit.test.ts __tests__/create-customer.unit.test.ts __tests__/fetch/cors.test.ts __tests__/fetch/handlers.test.ts __tests__/fetch/utils.test.ts",
73
+ "test": "vitest run __tests__/paywall.unit.test.ts __tests__/paywall-state.unit.test.ts __tests__/free-limit.unit.test.ts __tests__/paywall-gate.unit.test.ts __tests__/payable-gate.unit.test.ts __tests__/bootstrap-mcp.unit.test.ts __tests__/verify-webhook.unit.test.ts __tests__/edge-exports.unit.test.ts __tests__/credits-usage.unit.test.ts __tests__/limit-outcome-flags.unit.test.ts __tests__/client-error.unit.test.ts __tests__/checkout-core.unit.test.ts src/__tests__/edge-exports.test.ts src/helpers/payment.test.ts src/helpers/usage.test.ts src/resolve-product-ref.test.ts src/verify-product-configuration.test.ts __tests__/auth-core.unit.test.ts __tests__/ensure-customer.unit.test.ts __tests__/create-customer.unit.test.ts __tests__/fetch/cors.test.ts __tests__/fetch/handlers.test.ts __tests__/fetch/utils.test.ts",
74
+ "test:unit": "vitest run __tests__/paywall.unit.test.ts __tests__/paywall-state.unit.test.ts __tests__/free-limit.unit.test.ts __tests__/paywall-gate.unit.test.ts __tests__/payable-gate.unit.test.ts __tests__/bootstrap-mcp.unit.test.ts __tests__/verify-webhook.unit.test.ts __tests__/edge-exports.unit.test.ts __tests__/credits-usage.unit.test.ts __tests__/limit-outcome-flags.unit.test.ts __tests__/client-error.unit.test.ts __tests__/checkout-core.unit.test.ts src/__tests__/edge-exports.test.ts src/helpers/payment.test.ts src/helpers/usage.test.ts src/resolve-product-ref.test.ts src/verify-product-configuration.test.ts __tests__/auth-core.unit.test.ts __tests__/ensure-customer.unit.test.ts __tests__/create-customer.unit.test.ts __tests__/fetch/cors.test.ts __tests__/fetch/handlers.test.ts __tests__/fetch/utils.test.ts",
75
75
  "test:integration": "vitest run --no-file-parallelism __tests__/backend.integration.test.ts __tests__/customer-update.integration.test.ts __tests__/multi-currency-plans.integration.test.ts",
76
76
  "test:integration:backend": "vitest run __tests__/backend.integration.test.ts",
77
77
  "test:integration:customer": "vitest run __tests__/customer-update.integration.test.ts",