@solvapay/server 2.3.1 → 2.4.0-preview-11dbbe603cd1b9a44cc41b5eee3686bc4000f97f

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.d.ts CHANGED
@@ -4085,7 +4085,36 @@ interface PaywallMetadata {
4085
4085
  /**
4086
4086
  * Structured content for paywall errors (MCP structuredContent and manual handling).
4087
4087
  */
4088
- type PaywallStructuredContent = {
4088
+ /**
4089
+ * Fields shared by every `PaywallStructuredContent` kind so a text-only
4090
+ * host can recover from a gate without a second tool call. Every field
4091
+ * other than `kind` / `product` / `checkoutUrl` / `message` is omitted
4092
+ * when the backend did not send it — never defaulted.
4093
+ */
4094
+ type PaywallGateRecoveryFields = {
4095
+ /** Active plan reference from `limits.plan`. */
4096
+ planRef?: string;
4097
+ /** Product plans from `checkLimits` (activation already had this). */
4098
+ plans?: LimitPlanSummary[];
4099
+ /** Meter the gated call was charged against. */
4100
+ meterName?: string;
4101
+ /** Per-unit charge of the active plan, in minor currency units. */
4102
+ unitPriceMinor?: number;
4103
+ /** ISO 4217 currency for `unitPriceMinor`. */
4104
+ currency?: string;
4105
+ /**
4106
+ * Included-usage counters. Omitted when `freeUnits` is absent or `0`
4107
+ * (the unlimited sentinel) so we never emit misleading zeros.
4108
+ */
4109
+ included?: {
4110
+ total: number;
4111
+ used: number;
4112
+ remaining: number;
4113
+ };
4114
+ /** Prepaid credit balance, coalesced from nested or top-level fields. */
4115
+ creditBalance?: number;
4116
+ };
4117
+ type PaywallStructuredContent = ({
4089
4118
  kind: 'payment_required';
4090
4119
  /** Product ref from paywall metadata (or env default) */
4091
4120
  product: string;
@@ -4100,7 +4129,7 @@ type PaywallStructuredContent = {
4100
4129
  balance?: LimitActivationBalance;
4101
4130
  /** Rich product context from checkLimits (name, ref, provider slug/id) */
4102
4131
  productDetails?: LimitActivationProduct;
4103
- } | {
4132
+ } & PaywallGateRecoveryFields) | ({
4104
4133
  kind: 'activation_required';
4105
4134
  /** Product ref from paywall metadata (or env default) */
4106
4135
  product: string;
@@ -4110,11 +4139,10 @@ type PaywallStructuredContent = {
4110
4139
  */
4111
4140
  checkoutUrl: string;
4112
4141
  confirmationUrl?: string;
4113
- plans?: LimitPlanSummary[];
4114
4142
  balance?: LimitActivationBalance;
4115
4143
  /** Rich product context from checkLimits (name, ref, provider slug/id) */
4116
4144
  productDetails?: LimitActivationProduct;
4117
- };
4145
+ } & PaywallGateRecoveryFields);
4118
4146
  /**
4119
4147
  * Runtime type guard for `PaywallStructuredContent`.
4120
4148
  *
@@ -5281,6 +5309,8 @@ type PaywallState = {
5281
5309
  kind: 'topup_required';
5282
5310
  } | {
5283
5311
  kind: 'upgrade_required';
5312
+ } | {
5313
+ kind: 'limit_reached';
5284
5314
  } | {
5285
5315
  kind: 'reactivation_required';
5286
5316
  };
@@ -5302,10 +5332,12 @@ type PaywallState = {
5302
5332
  * `creditBalance === 0` field, or `remaining === 0` as a
5303
5333
  * fallback for older backend responses that omit both credit
5304
5334
  * fields on usage-based plans.
5305
- * 4. Everything else → `upgrade_required`, including:
5335
+ * 4. Active plan at included cap (`plan` is a non-empty ref and
5336
+ * `remaining <= 0`) → `limit_reached`. This is the "on Free, used
5337
+ * 3 of 3" case — not "no plan".
5338
+ * 5. Everything else → `upgrade_required`, including:
5306
5339
  * - `limits === null` (defensive),
5307
- * - no active plan on the product,
5308
- * - recurring plan at period cap (`remaining <= 0`).
5340
+ * - no active plan on the product.
5309
5341
  *
5310
5342
  * `reactivation_required` is deferred — it needs a distinct backend
5311
5343
  * signal (future `LimitResponse.inactivePurchaseRef`) which isn't
@@ -5315,12 +5347,13 @@ type PaywallState = {
5315
5347
  */
5316
5348
  declare function classifyPaywallState(limits: LimitResponseWithPlan | null): PaywallState;
5317
5349
  /**
5318
- * Produce the terminal-friendly gate message. Names exactly one
5319
- * recovery tool (`upgrade` / `topup` / `activate_plan`), except for
5320
- * the rare `reactivation_required` path which names two alternatives
5321
- * (`manage_account` / `upgrade`). Inlines `gate.checkoutUrl` when
5322
- * present so terminal-only MCP hosts (Claude Code, CLI clients) can
5323
- * open a browser directly.
5350
+ * Produce the terminal-friendly gate message. Names the `account`
5351
+ * viewer (with a `view` hint when the landing screen matters) or
5352
+ * `activate_plan` when a specific plan needs activating. Inlines
5353
+ * `gate.checkoutUrl` when present so terminal-only MCP hosts (Claude
5354
+ * Code, CLI clients) can open a browser directly. States the
5355
+ * 15-minute session lifetime inline — the URL is a bearer credential,
5356
+ * not a durable link.
5324
5357
  *
5325
5358
  * Kept as a pure string so the adapter layer can concatenate it with
5326
5359
  * an optional narrator prefix without parsing structured copy.
@@ -5333,10 +5366,10 @@ declare function buildGateMessage(state: PaywallState, gate: PaywallStructuredCo
5333
5366
  * like a softer version of the same text-only nudge path.
5334
5367
  *
5335
5368
  * Receives the `PaywallState` the classifier would have produced if
5336
- * the customer had tripped the gate. `upgrade_required` and
5337
- * `topup_required` are the only kinds that currently produce nudge
5338
- * copy; the others are no-ops (shouldn't happen — nudges only fire
5339
- * on successful calls).
5369
+ * the customer had tripped the gate. `upgrade_required`,
5370
+ * `limit_reached`, and `topup_required` are the kinds that currently
5371
+ * produce nudge copy; the others are no-ops (shouldn't happen —
5372
+ * nudges only fire on successful calls).
5340
5373
  */
5341
5374
  declare function buildNudgeMessage(state: PaywallState, limits: LimitResponseWithPlan | null): string;
5342
5375
 
package/dist/edge.js CHANGED
@@ -804,6 +804,8 @@ async function verifyProductConfiguration(options) {
804
804
  }
805
805
 
806
806
  // src/paywall-state.ts
807
+ var CHECKOUT_SESSION_TTL_MINUTES = 15;
808
+ var DOCS_HINT = "See docs://solvapay/overview.md.";
807
809
  function classifyPaywallState(limits) {
808
810
  if (!limits) return { kind: "upgrade_required" };
809
811
  if (limits.activationRequired === true) {
@@ -824,34 +826,77 @@ function classifyPaywallState(limits) {
824
826
  return { kind: "topup_required" };
825
827
  }
826
828
  }
829
+ if (limits.plan.length > 0 && limits.remaining <= 0) {
830
+ return { kind: "limit_reached" };
831
+ }
827
832
  return { kind: "upgrade_required" };
828
833
  }
834
+ function formatMinor(amountMinor, currency) {
835
+ const major = amountMinor / 100;
836
+ try {
837
+ return new Intl.NumberFormat("en-US", {
838
+ style: "currency",
839
+ currency: currency.toUpperCase(),
840
+ minimumFractionDigits: 2,
841
+ maximumFractionDigits: 2
842
+ }).format(major);
843
+ } catch {
844
+ return `${currency.toUpperCase()} ${major.toFixed(2)}`;
845
+ }
846
+ }
847
+ function meterLabel(gate) {
848
+ if (!gate.meterName) return "units";
849
+ return gate.meterName.replace(/_/g, " ");
850
+ }
851
+ function namedCheckoutMarkdown(url) {
852
+ return `[Open checkout](${url})`;
853
+ }
854
+ var VIEWER_TOOL = "account";
855
+ function callViewer(view) {
856
+ return view ? `call the \`${VIEWER_TOOL}\` tool with view: '${view}'` : `call the \`${VIEWER_TOOL}\` tool`;
857
+ }
858
+ function recoverClause(url, verb, view) {
859
+ if (url) {
860
+ return ` ${namedCheckoutMarkdown(url)} to ${verb} (expires in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer(view)}.`;
861
+ }
862
+ return ` ${callViewer(view).replace(/^c/, "C")}.`;
863
+ }
829
864
  function buildGateMessage(state, gate) {
830
865
  const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
831
- const openClause = url ? `, or open ${url} in a browser` : "";
832
866
  switch (state.kind) {
867
+ case "limit_reached": {
868
+ const included = gate.included;
869
+ const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
870
+ 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.`;
871
+ const nextLine = price ? ` The next call is ${price}.` : "";
872
+ return `${usedLine}${nextLine}${recoverClause(url, "continue", "checkout")} ${DOCS_HINT}`;
873
+ }
833
874
  case "activation_required":
834
- return `Your plan needs activation before you can use this tool. Call the \`activate_plan\` tool to activate it${openClause}.`;
835
- case "topup_required":
836
- return `You're out of credits. Call the \`topup\` tool to add more${openClause}.`;
875
+ return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
876
+ case "topup_required": {
877
+ const currency = gate.currency ?? gate.balance?.currency ?? "USD";
878
+ const presets = [1e3, 2500, 5e3, 1e4].map((m) => formatMinor(m, currency)).join(" \xB7 ");
879
+ return `You're out of credits. Top up first (${presets}).${recoverClause(url, "add credits", "topup")} ${DOCS_HINT}`;
880
+ }
837
881
  case "upgrade_required":
838
- return `You don't have an active plan for this tool. Call the \`upgrade\` tool to pick a plan${openClause}.`;
882
+ return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
839
883
  case "reactivation_required":
840
- return `Your previous plan is no longer active. Call the \`manage_account\` tool to reactivate it, or the \`upgrade\` tool to pick a new plan.`;
884
+ return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
841
885
  }
842
886
  }
843
887
  function buildNudgeMessage(state, limits) {
844
888
  const url = limits?.checkoutUrl && limits.checkoutUrl.length > 0 ? limits.checkoutUrl : null;
845
- const visitClause = url ? `, or visit ${url}` : "";
889
+ const visitClause = url ? `, or ${namedCheckoutMarkdown(url)}` : "";
846
890
  switch (state.kind) {
847
891
  case "topup_required":
848
- return `Heads up \u2014 running low on credits. Call the \`topup\` tool to add more${visitClause}.`;
892
+ return `Heads up \u2014 running low on credits. ${callViewer("topup").replace(/^c/, "C")} to add more${visitClause}.`;
849
893
  case "upgrade_required":
850
- return `Heads up \u2014 approaching your plan's limit this period. Call the \`upgrade\` tool for more headroom${visitClause}.`;
894
+ case "limit_reached":
895
+ return `Heads up \u2014 approaching your plan's limit this period. ${callViewer("checkout").replace(/^c/, "C")} for more headroom${visitClause}.`;
851
896
  case "activation_required":
852
- return `Heads up \u2014 this plan still needs activation. Call the \`activate_plan\` tool${visitClause}.`;
897
+ return `Heads up \u2014 this plan still needs activation. Call the \`activate_plan\` tool with a \`planRef\`${visitClause}.`;
853
898
  case "reactivation_required":
854
- return `Heads up \u2014 your plan is no longer active. Call the \`manage_account\` tool to reactivate it${visitClause}.`;
899
+ return `Heads up \u2014 your plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it${visitClause}.`;
855
900
  }
856
901
  }
857
902
 
@@ -862,26 +907,55 @@ function allPaidPlansArePayg(plans) {
862
907
  if (paidPlans.length === 0) return false;
863
908
  return paidPlans.every((p) => p.type === "usage-based" || p.type === "hybrid");
864
909
  }
910
+ function activePlanOf(limits) {
911
+ if (!limits.plan) return void 0;
912
+ return limits.plans?.find((p) => p.reference === limits.plan);
913
+ }
914
+ function includedFromLimits(limits) {
915
+ const total = activePlanOf(limits)?.freeUnits;
916
+ if (total == null || total === 0) return void 0;
917
+ const remaining = limits.remaining;
918
+ return {
919
+ total,
920
+ used: Math.max(0, total - remaining),
921
+ remaining
922
+ };
923
+ }
924
+ function recoveryFields(limits) {
925
+ const plan = activePlanOf(limits);
926
+ const included = includedFromLimits(limits);
927
+ const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
928
+ return {
929
+ ...limits.plan ? { planRef: limits.plan } : {},
930
+ ...limits.plans !== void 0 ? { plans: limits.plans } : {},
931
+ ...limits.meterName !== void 0 ? { meterName: limits.meterName } : {},
932
+ ...plan?.perUnitChargeMinor != null ? { unitPriceMinor: plan.perUnitChargeMinor, currency: plan.currency } : limits.currency ? { currency: limits.currency } : {},
933
+ ...included ? { included } : {},
934
+ ...creditBalance !== void 0 ? { creditBalance } : {}
935
+ };
936
+ }
865
937
  function buildPaywallGate(productRef, limits) {
866
938
  const checkoutUrl = limits.checkoutUrl;
867
939
  const state = classifyPaywallState({ ...limits, plan: limits.plan ?? "" });
868
940
  const useActivationForTopup = !limits.activationRequired && state.kind === "topup_required" && allPaidPlansArePayg(limits.plans);
941
+ const recovery = recoveryFields(limits);
869
942
  const preMessage = limits.activationRequired || useActivationForTopup ? {
870
943
  kind: "activation_required",
871
944
  product: productRef,
872
945
  message: "",
873
946
  checkoutUrl: limits.confirmationUrl || checkoutUrl || "",
874
947
  ...limits.confirmationUrl !== void 0 ? { confirmationUrl: limits.confirmationUrl } : {},
875
- ...limits.plans !== void 0 ? { plans: limits.plans } : {},
876
948
  ...limits.balance !== void 0 ? { balance: limits.balance } : {},
877
- ...limits.product !== void 0 ? { productDetails: limits.product } : {}
949
+ ...limits.product !== void 0 ? { productDetails: limits.product } : {},
950
+ ...recovery
878
951
  } : {
879
952
  kind: "payment_required",
880
953
  product: productRef,
881
954
  checkoutUrl: checkoutUrl || "",
882
955
  message: "",
883
956
  ...limits.balance !== void 0 ? { balance: limits.balance } : {},
884
- ...limits.product !== void 0 ? { productDetails: limits.product } : {}
957
+ ...limits.product !== void 0 ? { productDetails: limits.product } : {},
958
+ ...recovery
885
959
  };
886
960
  return { ...preMessage, message: buildGateMessage(state, preMessage) };
887
961
  }
@@ -965,6 +965,8 @@ function createSolvaPayClient(opts) {
965
965
  }
966
966
 
967
967
  // src/paywall-state.ts
968
+ var CHECKOUT_SESSION_TTL_MINUTES = 15;
969
+ var DOCS_HINT = "See docs://solvapay/overview.md.";
968
970
  function classifyPaywallState(limits) {
969
971
  if (!limits) return { kind: "upgrade_required" };
970
972
  if (limits.activationRequired === true) {
@@ -985,20 +987,62 @@ function classifyPaywallState(limits) {
985
987
  return { kind: "topup_required" };
986
988
  }
987
989
  }
990
+ if (limits.plan.length > 0 && limits.remaining <= 0) {
991
+ return { kind: "limit_reached" };
992
+ }
988
993
  return { kind: "upgrade_required" };
989
994
  }
995
+ function formatMinor(amountMinor, currency) {
996
+ const major = amountMinor / 100;
997
+ try {
998
+ return new Intl.NumberFormat("en-US", {
999
+ style: "currency",
1000
+ currency: currency.toUpperCase(),
1001
+ minimumFractionDigits: 2,
1002
+ maximumFractionDigits: 2
1003
+ }).format(major);
1004
+ } catch {
1005
+ return `${currency.toUpperCase()} ${major.toFixed(2)}`;
1006
+ }
1007
+ }
1008
+ function meterLabel(gate) {
1009
+ if (!gate.meterName) return "units";
1010
+ return gate.meterName.replace(/_/g, " ");
1011
+ }
1012
+ function namedCheckoutMarkdown(url) {
1013
+ return `[Open checkout](${url})`;
1014
+ }
1015
+ var VIEWER_TOOL = "account";
1016
+ function callViewer(view) {
1017
+ return view ? `call the \`${VIEWER_TOOL}\` tool with view: '${view}'` : `call the \`${VIEWER_TOOL}\` tool`;
1018
+ }
1019
+ function recoverClause(url, verb, view) {
1020
+ if (url) {
1021
+ return ` ${namedCheckoutMarkdown(url)} to ${verb} (expires in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer(view)}.`;
1022
+ }
1023
+ return ` ${callViewer(view).replace(/^c/, "C")}.`;
1024
+ }
990
1025
  function buildGateMessage(state, gate) {
991
1026
  const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
992
- const openClause = url ? `, or open ${url} in a browser` : "";
993
1027
  switch (state.kind) {
1028
+ case "limit_reached": {
1029
+ const included = gate.included;
1030
+ const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
1031
+ 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.`;
1032
+ const nextLine = price ? ` The next call is ${price}.` : "";
1033
+ return `${usedLine}${nextLine}${recoverClause(url, "continue", "checkout")} ${DOCS_HINT}`;
1034
+ }
994
1035
  case "activation_required":
995
- return `Your plan needs activation before you can use this tool. Call the \`activate_plan\` tool to activate it${openClause}.`;
996
- case "topup_required":
997
- return `You're out of credits. Call the \`topup\` tool to add more${openClause}.`;
1036
+ return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
1037
+ case "topup_required": {
1038
+ const currency = gate.currency ?? gate.balance?.currency ?? "USD";
1039
+ const presets = [1e3, 2500, 5e3, 1e4].map((m) => formatMinor(m, currency)).join(" \xB7 ");
1040
+ return `You're out of credits. Top up first (${presets}).${recoverClause(url, "add credits", "topup")} ${DOCS_HINT}`;
1041
+ }
998
1042
  case "upgrade_required":
999
- return `You don't have an active plan for this tool. Call the \`upgrade\` tool to pick a plan${openClause}.`;
1043
+ return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
1000
1044
  case "reactivation_required":
1001
- return `Your previous plan is no longer active. Call the \`manage_account\` tool to reactivate it, or the \`upgrade\` tool to pick a new plan.`;
1045
+ return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
1002
1046
  }
1003
1047
  }
1004
1048
 
@@ -1009,26 +1053,55 @@ function allPaidPlansArePayg(plans) {
1009
1053
  if (paidPlans.length === 0) return false;
1010
1054
  return paidPlans.every((p) => p.type === "usage-based" || p.type === "hybrid");
1011
1055
  }
1056
+ function activePlanOf(limits) {
1057
+ if (!limits.plan) return void 0;
1058
+ return limits.plans?.find((p) => p.reference === limits.plan);
1059
+ }
1060
+ function includedFromLimits(limits) {
1061
+ const total = activePlanOf(limits)?.freeUnits;
1062
+ if (total == null || total === 0) return void 0;
1063
+ const remaining = limits.remaining;
1064
+ return {
1065
+ total,
1066
+ used: Math.max(0, total - remaining),
1067
+ remaining
1068
+ };
1069
+ }
1070
+ function recoveryFields(limits) {
1071
+ const plan = activePlanOf(limits);
1072
+ const included = includedFromLimits(limits);
1073
+ const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
1074
+ return {
1075
+ ...limits.plan ? { planRef: limits.plan } : {},
1076
+ ...limits.plans !== void 0 ? { plans: limits.plans } : {},
1077
+ ...limits.meterName !== void 0 ? { meterName: limits.meterName } : {},
1078
+ ...plan?.perUnitChargeMinor != null ? { unitPriceMinor: plan.perUnitChargeMinor, currency: plan.currency } : limits.currency ? { currency: limits.currency } : {},
1079
+ ...included ? { included } : {},
1080
+ ...creditBalance !== void 0 ? { creditBalance } : {}
1081
+ };
1082
+ }
1012
1083
  function buildPaywallGate(productRef, limits) {
1013
1084
  const checkoutUrl = limits.checkoutUrl;
1014
1085
  const state = classifyPaywallState({ ...limits, plan: limits.plan ?? "" });
1015
1086
  const useActivationForTopup = !limits.activationRequired && state.kind === "topup_required" && allPaidPlansArePayg(limits.plans);
1087
+ const recovery = recoveryFields(limits);
1016
1088
  const preMessage = limits.activationRequired || useActivationForTopup ? {
1017
1089
  kind: "activation_required",
1018
1090
  product: productRef,
1019
1091
  message: "",
1020
1092
  checkoutUrl: limits.confirmationUrl || checkoutUrl || "",
1021
1093
  ...limits.confirmationUrl !== void 0 ? { confirmationUrl: limits.confirmationUrl } : {},
1022
- ...limits.plans !== void 0 ? { plans: limits.plans } : {},
1023
1094
  ...limits.balance !== void 0 ? { balance: limits.balance } : {},
1024
- ...limits.product !== void 0 ? { productDetails: limits.product } : {}
1095
+ ...limits.product !== void 0 ? { productDetails: limits.product } : {},
1096
+ ...recovery
1025
1097
  } : {
1026
1098
  kind: "payment_required",
1027
1099
  product: productRef,
1028
1100
  checkoutUrl: checkoutUrl || "",
1029
1101
  message: "",
1030
1102
  ...limits.balance !== void 0 ? { balance: limits.balance } : {},
1031
- ...limits.product !== void 0 ? { productDetails: limits.product } : {}
1103
+ ...limits.product !== void 0 ? { productDetails: limits.product } : {},
1104
+ ...recovery
1032
1105
  };
1033
1106
  return { ...preMessage, message: buildGateMessage(state, preMessage) };
1034
1107
  }
@@ -912,6 +912,8 @@ function createSolvaPayClient(opts) {
912
912
  }
913
913
 
914
914
  // src/paywall-state.ts
915
+ var CHECKOUT_SESSION_TTL_MINUTES = 15;
916
+ var DOCS_HINT = "See docs://solvapay/overview.md.";
915
917
  function classifyPaywallState(limits) {
916
918
  if (!limits) return { kind: "upgrade_required" };
917
919
  if (limits.activationRequired === true) {
@@ -932,20 +934,62 @@ function classifyPaywallState(limits) {
932
934
  return { kind: "topup_required" };
933
935
  }
934
936
  }
937
+ if (limits.plan.length > 0 && limits.remaining <= 0) {
938
+ return { kind: "limit_reached" };
939
+ }
935
940
  return { kind: "upgrade_required" };
936
941
  }
942
+ function formatMinor(amountMinor, currency) {
943
+ const major = amountMinor / 100;
944
+ try {
945
+ return new Intl.NumberFormat("en-US", {
946
+ style: "currency",
947
+ currency: currency.toUpperCase(),
948
+ minimumFractionDigits: 2,
949
+ maximumFractionDigits: 2
950
+ }).format(major);
951
+ } catch {
952
+ return `${currency.toUpperCase()} ${major.toFixed(2)}`;
953
+ }
954
+ }
955
+ function meterLabel(gate) {
956
+ if (!gate.meterName) return "units";
957
+ return gate.meterName.replace(/_/g, " ");
958
+ }
959
+ function namedCheckoutMarkdown(url) {
960
+ return `[Open checkout](${url})`;
961
+ }
962
+ var VIEWER_TOOL = "account";
963
+ function callViewer(view) {
964
+ return view ? `call the \`${VIEWER_TOOL}\` tool with view: '${view}'` : `call the \`${VIEWER_TOOL}\` tool`;
965
+ }
966
+ function recoverClause(url, verb, view) {
967
+ if (url) {
968
+ return ` ${namedCheckoutMarkdown(url)} to ${verb} (expires in ${CHECKOUT_SESSION_TTL_MINUTES} minutes), or ${callViewer(view)}.`;
969
+ }
970
+ return ` ${callViewer(view).replace(/^c/, "C")}.`;
971
+ }
937
972
  function buildGateMessage(state, gate) {
938
973
  const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
939
- const openClause = url ? `, or open ${url} in a browser` : "";
940
974
  switch (state.kind) {
975
+ case "limit_reached": {
976
+ const included = gate.included;
977
+ const price = gate.unitPriceMinor != null && gate.currency ? formatMinor(gate.unitPriceMinor, gate.currency) : null;
978
+ 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.`;
979
+ const nextLine = price ? ` The next call is ${price}.` : "";
980
+ return `${usedLine}${nextLine}${recoverClause(url, "continue", "checkout")} ${DOCS_HINT}`;
981
+ }
941
982
  case "activation_required":
942
- return `Your plan needs activation before you can use this tool. Call the \`activate_plan\` tool to activate it${openClause}.`;
943
- case "topup_required":
944
- return `You're out of credits. Call the \`topup\` tool to add more${openClause}.`;
983
+ return `Your plan needs activation.${recoverClause(url, "activate", "checkout")} Or call \`activate_plan\` with a \`planRef\`. ${DOCS_HINT}`;
984
+ case "topup_required": {
985
+ const currency = gate.currency ?? gate.balance?.currency ?? "USD";
986
+ const presets = [1e3, 2500, 5e3, 1e4].map((m) => formatMinor(m, currency)).join(" \xB7 ");
987
+ return `You're out of credits. Top up first (${presets}).${recoverClause(url, "add credits", "topup")} ${DOCS_HINT}`;
988
+ }
945
989
  case "upgrade_required":
946
- return `You don't have an active plan for this tool. Call the \`upgrade\` tool to pick a plan${openClause}.`;
990
+ return `You don't have an active plan for this tool.${recoverClause(url, "pick a plan", "checkout")} ${DOCS_HINT}`;
947
991
  case "reactivation_required":
948
- return `Your previous plan is no longer active. Call the \`manage_account\` tool to reactivate it, or the \`upgrade\` tool to pick a new plan.`;
992
+ return `Your previous plan is no longer active. ${callViewer("account").replace(/^c/, "C")} to reactivate it, or ${callViewer("checkout")} to pick a new plan. ${DOCS_HINT}`;
949
993
  }
950
994
  }
951
995
 
@@ -956,26 +1000,55 @@ function allPaidPlansArePayg(plans) {
956
1000
  if (paidPlans.length === 0) return false;
957
1001
  return paidPlans.every((p) => p.type === "usage-based" || p.type === "hybrid");
958
1002
  }
1003
+ function activePlanOf(limits) {
1004
+ if (!limits.plan) return void 0;
1005
+ return limits.plans?.find((p) => p.reference === limits.plan);
1006
+ }
1007
+ function includedFromLimits(limits) {
1008
+ const total = activePlanOf(limits)?.freeUnits;
1009
+ if (total == null || total === 0) return void 0;
1010
+ const remaining = limits.remaining;
1011
+ return {
1012
+ total,
1013
+ used: Math.max(0, total - remaining),
1014
+ remaining
1015
+ };
1016
+ }
1017
+ function recoveryFields(limits) {
1018
+ const plan = activePlanOf(limits);
1019
+ const included = includedFromLimits(limits);
1020
+ const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
1021
+ return {
1022
+ ...limits.plan ? { planRef: limits.plan } : {},
1023
+ ...limits.plans !== void 0 ? { plans: limits.plans } : {},
1024
+ ...limits.meterName !== void 0 ? { meterName: limits.meterName } : {},
1025
+ ...plan?.perUnitChargeMinor != null ? { unitPriceMinor: plan.perUnitChargeMinor, currency: plan.currency } : limits.currency ? { currency: limits.currency } : {},
1026
+ ...included ? { included } : {},
1027
+ ...creditBalance !== void 0 ? { creditBalance } : {}
1028
+ };
1029
+ }
959
1030
  function buildPaywallGate(productRef, limits) {
960
1031
  const checkoutUrl = limits.checkoutUrl;
961
1032
  const state = classifyPaywallState({ ...limits, plan: limits.plan ?? "" });
962
1033
  const useActivationForTopup = !limits.activationRequired && state.kind === "topup_required" && allPaidPlansArePayg(limits.plans);
1034
+ const recovery = recoveryFields(limits);
963
1035
  const preMessage = limits.activationRequired || useActivationForTopup ? {
964
1036
  kind: "activation_required",
965
1037
  product: productRef,
966
1038
  message: "",
967
1039
  checkoutUrl: limits.confirmationUrl || checkoutUrl || "",
968
1040
  ...limits.confirmationUrl !== void 0 ? { confirmationUrl: limits.confirmationUrl } : {},
969
- ...limits.plans !== void 0 ? { plans: limits.plans } : {},
970
1041
  ...limits.balance !== void 0 ? { balance: limits.balance } : {},
971
- ...limits.product !== void 0 ? { productDetails: limits.product } : {}
1042
+ ...limits.product !== void 0 ? { productDetails: limits.product } : {},
1043
+ ...recovery
972
1044
  } : {
973
1045
  kind: "payment_required",
974
1046
  product: productRef,
975
1047
  checkoutUrl: checkoutUrl || "",
976
1048
  message: "",
977
1049
  ...limits.balance !== void 0 ? { balance: limits.balance } : {},
978
- ...limits.product !== void 0 ? { productDetails: limits.product } : {}
1050
+ ...limits.product !== void 0 ? { productDetails: limits.product } : {},
1051
+ ...recovery
979
1052
  };
980
1053
  return { ...preMessage, message: buildGateMessage(state, preMessage) };
981
1054
  }