@solvapay/server 1.0.8-preview.8 → 1.0.8

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.
Files changed (148) hide show
  1. package/README.md +80 -2
  2. package/dist/edge.d.ts +495 -61
  3. package/dist/edge.js +648 -187
  4. package/dist/fetch/index.cjs +3071 -0
  5. package/dist/fetch/index.d.cts +112 -0
  6. package/dist/fetch/index.d.ts +112 -0
  7. package/dist/fetch/index.js +3017 -0
  8. package/dist/index.cjs +603 -5013
  9. package/dist/index.d.cts +490 -156
  10. package/dist/index.d.ts +490 -156
  11. package/dist/index.js +571 -695
  12. package/package.json +15 -12
  13. package/dist/chunk-MLKGABMK.js +0 -9
  14. package/dist/chunk-R5U7XKVJ.js +0 -16
  15. package/dist/dist-EPVKJAIP.js +0 -94
  16. package/dist/dist-JBJ4HMP7.js +0 -94
  17. package/dist/esm-5GYCIXIY.js +0 -3475
  18. package/dist/esm-UW7WCMEK.js +0 -3475
  19. package/dist/src/adapters/base.d.ts +0 -57
  20. package/dist/src/adapters/base.d.ts.map +0 -1
  21. package/dist/src/adapters/base.js +0 -73
  22. package/dist/src/adapters/base.js.map +0 -1
  23. package/dist/src/adapters/http.d.ts +0 -25
  24. package/dist/src/adapters/http.d.ts.map +0 -1
  25. package/dist/src/adapters/http.js +0 -99
  26. package/dist/src/adapters/http.js.map +0 -1
  27. package/dist/src/adapters/index.d.ts +0 -11
  28. package/dist/src/adapters/index.d.ts.map +0 -1
  29. package/dist/src/adapters/index.js +0 -10
  30. package/dist/src/adapters/index.js.map +0 -1
  31. package/dist/src/adapters/mcp.d.ts +0 -24
  32. package/dist/src/adapters/mcp.d.ts.map +0 -1
  33. package/dist/src/adapters/mcp.js +0 -75
  34. package/dist/src/adapters/mcp.js.map +0 -1
  35. package/dist/src/adapters/next.d.ts +0 -24
  36. package/dist/src/adapters/next.d.ts.map +0 -1
  37. package/dist/src/adapters/next.js +0 -109
  38. package/dist/src/adapters/next.js.map +0 -1
  39. package/dist/src/client.d.ts +0 -58
  40. package/dist/src/client.d.ts.map +0 -1
  41. package/dist/src/client.js +0 -495
  42. package/dist/src/client.js.map +0 -1
  43. package/dist/src/edge.d.ts +0 -22
  44. package/dist/src/edge.d.ts.map +0 -1
  45. package/dist/src/edge.js +0 -91
  46. package/dist/src/edge.js.map +0 -1
  47. package/dist/src/factory.d.ts +0 -605
  48. package/dist/src/factory.d.ts.map +0 -1
  49. package/dist/src/factory.js +0 -215
  50. package/dist/src/factory.js.map +0 -1
  51. package/dist/src/helpers/auth.d.ts +0 -47
  52. package/dist/src/helpers/auth.d.ts.map +0 -1
  53. package/dist/src/helpers/auth.js +0 -73
  54. package/dist/src/helpers/auth.js.map +0 -1
  55. package/dist/src/helpers/checkout.d.ts +0 -45
  56. package/dist/src/helpers/checkout.d.ts.map +0 -1
  57. package/dist/src/helpers/checkout.js +0 -89
  58. package/dist/src/helpers/checkout.js.map +0 -1
  59. package/dist/src/helpers/customer.d.ts +0 -51
  60. package/dist/src/helpers/customer.d.ts.map +0 -1
  61. package/dist/src/helpers/customer.js +0 -77
  62. package/dist/src/helpers/customer.js.map +0 -1
  63. package/dist/src/helpers/error.d.ts +0 -15
  64. package/dist/src/helpers/error.d.ts.map +0 -1
  65. package/dist/src/helpers/error.js +0 -35
  66. package/dist/src/helpers/error.js.map +0 -1
  67. package/dist/src/helpers/index.d.ts +0 -17
  68. package/dist/src/helpers/index.d.ts.map +0 -1
  69. package/dist/src/helpers/index.js +0 -23
  70. package/dist/src/helpers/index.js.map +0 -1
  71. package/dist/src/helpers/payment.d.ts +0 -107
  72. package/dist/src/helpers/payment.d.ts.map +0 -1
  73. package/dist/src/helpers/payment.js +0 -150
  74. package/dist/src/helpers/payment.js.map +0 -1
  75. package/dist/src/helpers/plans.d.ts +0 -19
  76. package/dist/src/helpers/plans.d.ts.map +0 -1
  77. package/dist/src/helpers/plans.js +0 -53
  78. package/dist/src/helpers/plans.js.map +0 -1
  79. package/dist/src/helpers/renewal.d.ts +0 -23
  80. package/dist/src/helpers/renewal.d.ts.map +0 -1
  81. package/dist/src/helpers/renewal.js +0 -90
  82. package/dist/src/helpers/renewal.js.map +0 -1
  83. package/dist/src/helpers/subscription.d.ts +0 -23
  84. package/dist/src/helpers/subscription.d.ts.map +0 -1
  85. package/dist/src/helpers/subscription.js +0 -101
  86. package/dist/src/helpers/subscription.js.map +0 -1
  87. package/dist/src/helpers/types.d.ts +0 -22
  88. package/dist/src/helpers/types.d.ts.map +0 -1
  89. package/dist/src/helpers/types.js +0 -7
  90. package/dist/src/helpers/types.js.map +0 -1
  91. package/dist/src/index.d.ts +0 -73
  92. package/dist/src/index.d.ts.map +0 -1
  93. package/dist/src/index.js +0 -106
  94. package/dist/src/index.js.map +0 -1
  95. package/dist/src/mcp/auth-bridge.d.ts +0 -9
  96. package/dist/src/mcp/auth-bridge.d.ts.map +0 -1
  97. package/dist/src/mcp/auth-bridge.js +0 -46
  98. package/dist/src/mcp/auth-bridge.js.map +0 -1
  99. package/dist/src/mcp/oauth-bridge.d.ts +0 -48
  100. package/dist/src/mcp/oauth-bridge.d.ts.map +0 -1
  101. package/dist/src/mcp/oauth-bridge.js +0 -110
  102. package/dist/src/mcp/oauth-bridge.js.map +0 -1
  103. package/dist/src/mcp-auth.d.ts +0 -17
  104. package/dist/src/mcp-auth.d.ts.map +0 -1
  105. package/dist/src/mcp-auth.js +0 -57
  106. package/dist/src/mcp-auth.js.map +0 -1
  107. package/dist/src/paywall.d.ts +0 -119
  108. package/dist/src/paywall.d.ts.map +0 -1
  109. package/dist/src/paywall.js +0 -700
  110. package/dist/src/paywall.js.map +0 -1
  111. package/dist/src/register-virtual-tools-mcp.d.ts +0 -23
  112. package/dist/src/register-virtual-tools-mcp.d.ts.map +0 -1
  113. package/dist/src/register-virtual-tools-mcp.js +0 -86
  114. package/dist/src/register-virtual-tools-mcp.js.map +0 -1
  115. package/dist/src/types/client.d.ts +0 -216
  116. package/dist/src/types/client.d.ts.map +0 -1
  117. package/dist/src/types/client.js +0 -7
  118. package/dist/src/types/client.js.map +0 -1
  119. package/dist/src/types/generated.d.ts +0 -2834
  120. package/dist/src/types/generated.d.ts.map +0 -1
  121. package/dist/src/types/generated.js +0 -6
  122. package/dist/src/types/generated.js.map +0 -1
  123. package/dist/src/types/index.d.ts +0 -13
  124. package/dist/src/types/index.d.ts.map +0 -1
  125. package/dist/src/types/index.js +0 -8
  126. package/dist/src/types/index.js.map +0 -1
  127. package/dist/src/types/options.d.ts +0 -126
  128. package/dist/src/types/options.d.ts.map +0 -1
  129. package/dist/src/types/options.js +0 -8
  130. package/dist/src/types/options.js.map +0 -1
  131. package/dist/src/types/paywall.d.ts +0 -64
  132. package/dist/src/types/paywall.d.ts.map +0 -1
  133. package/dist/src/types/paywall.js +0 -7
  134. package/dist/src/types/paywall.js.map +0 -1
  135. package/dist/src/types/webhook.d.ts +0 -50
  136. package/dist/src/types/webhook.d.ts.map +0 -1
  137. package/dist/src/types/webhook.js +0 -2
  138. package/dist/src/types/webhook.js.map +0 -1
  139. package/dist/src/utils.d.ts +0 -110
  140. package/dist/src/utils.d.ts.map +0 -1
  141. package/dist/src/utils.js +0 -217
  142. package/dist/src/utils.js.map +0 -1
  143. package/dist/src/virtual-tools.d.ts +0 -44
  144. package/dist/src/virtual-tools.d.ts.map +0 -1
  145. package/dist/src/virtual-tools.js +0 -140
  146. package/dist/src/virtual-tools.js.map +0 -1
  147. package/dist/tsconfig.tsbuildinfo +0 -1
  148. package/dist/webapi-K5XBCEO6.js +0 -3775
package/dist/edge.js CHANGED
@@ -1,5 +1,3 @@
1
- import "./chunk-MLKGABMK.js";
2
-
3
1
  // src/edge.ts
4
2
  import { SolvaPayError as SolvaPayError5 } from "@solvapay/core";
5
3
 
@@ -63,7 +61,27 @@ function createSolvaPayClient(opts) {
63
61
  throw new SolvaPayError(`Create customer failed (${res.status}): ${error}`);
64
62
  }
65
63
  const result = await res.json();
66
- return result;
64
+ return {
65
+ customerRef: result.reference || result.customerRef
66
+ };
67
+ },
68
+ // PATCH: /v1/sdk/customers/{customerRef}
69
+ async updateCustomer(customerRef, params) {
70
+ const url = `${base}/v1/sdk/customers/${encodeURIComponent(customerRef)}`;
71
+ const res = await fetch(url, {
72
+ method: "PATCH",
73
+ headers,
74
+ body: JSON.stringify(params)
75
+ });
76
+ if (!res.ok) {
77
+ const error = await res.text();
78
+ log(`\u274C API Error: ${res.status} - ${error}`);
79
+ throw new SolvaPayError(`Update customer failed (${res.status}): ${error}`);
80
+ }
81
+ const result = await res.json();
82
+ return {
83
+ customerRef: result.reference || result.customerRef || customerRef
84
+ };
67
85
  },
68
86
  // GET: /v1/sdk/customers/{reference} or /v1/sdk/customers?externalRef={externalRef}|email={email}
69
87
  async getCustomer(params) {
@@ -123,6 +141,20 @@ function createSolvaPayClient(opts) {
123
141
  }
124
142
  return res.json();
125
143
  },
144
+ // GET: /v1/sdk/platform-config
145
+ async getPlatformConfig() {
146
+ const url = `${base}/v1/sdk/platform-config`;
147
+ const res = await fetch(url, {
148
+ method: "GET",
149
+ headers
150
+ });
151
+ if (!res.ok) {
152
+ const error = await res.text();
153
+ log(`\u274C API Error: ${res.status} - ${error}`);
154
+ throw new SolvaPayError(`Get platform config failed (${res.status}): ${error}`);
155
+ }
156
+ return res.json();
157
+ },
126
158
  // GET: /v1/sdk/products/{productRef}
127
159
  async getProduct(productRef) {
128
160
  const url = `${base}/v1/sdk/products/${encodeURIComponent(productRef)}`;
@@ -549,10 +581,67 @@ function createSolvaPayClient(opts) {
549
581
  throw new SolvaPayError(`Activate plan failed (${res.status}): ${error}`);
550
582
  }
551
583
  return await res.json();
584
+ },
585
+ async getPaymentMethod(params) {
586
+ const url = new URL(`${base}/v1/sdk/payment-method`);
587
+ url.searchParams.set("customerRef", params.customerRef);
588
+ const res = await fetch(url.toString(), { method: "GET", headers });
589
+ if (!res.ok) {
590
+ const error = await res.text();
591
+ log(`\u274C API Error: ${res.status} - ${error}`);
592
+ throw new SolvaPayError(`Get payment method failed (${res.status}): ${error}`);
593
+ }
594
+ return await res.json();
552
595
  }
553
596
  };
554
597
  }
555
598
 
599
+ // src/paywall-state.ts
600
+ function classifyPaywallState(limits) {
601
+ if (!limits) return { kind: "upgrade_required" };
602
+ if (limits.activationRequired === true) {
603
+ return { kind: "activation_required" };
604
+ }
605
+ const activePlan = limits.plans?.find((p) => p.reference === limits.plan);
606
+ const isUsageBased = activePlan?.type === "usage-based" || limits.balance !== void 0;
607
+ const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
608
+ if (isUsageBased) {
609
+ if (creditBalance === 0) return { kind: "topup_required" };
610
+ if (creditBalance === void 0 && limits.remaining === 0) {
611
+ return { kind: "topup_required" };
612
+ }
613
+ }
614
+ return { kind: "upgrade_required" };
615
+ }
616
+ function buildGateMessage(state, gate) {
617
+ const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
618
+ const openClause = url ? `, or open ${url} in a browser` : "";
619
+ switch (state.kind) {
620
+ case "activation_required":
621
+ return `Your plan needs activation before you can use this tool. Call the \`activate_plan\` tool to activate it${openClause}.`;
622
+ case "topup_required":
623
+ return `You're out of credits. Call the \`topup\` tool to add more${openClause}.`;
624
+ case "upgrade_required":
625
+ return `You don't have an active plan for this tool. Call the \`upgrade\` tool to pick a plan${openClause}.`;
626
+ case "reactivation_required":
627
+ 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.`;
628
+ }
629
+ }
630
+ function buildNudgeMessage(state, limits) {
631
+ const url = limits?.checkoutUrl && limits.checkoutUrl.length > 0 ? limits.checkoutUrl : null;
632
+ const visitClause = url ? `, or visit ${url}` : "";
633
+ switch (state.kind) {
634
+ case "topup_required":
635
+ return `Heads up \u2014 running low on credits. Call the \`topup\` tool to add more${visitClause}.`;
636
+ case "upgrade_required":
637
+ return `Heads up \u2014 approaching your plan's limit this period. Call the \`upgrade\` tool for more headroom${visitClause}.`;
638
+ case "activation_required":
639
+ return `Heads up \u2014 this plan still needs activation. Call the \`activate_plan\` tool${visitClause}.`;
640
+ case "reactivation_required":
641
+ return `Heads up \u2014 your plan is no longer active. Call the \`manage_account\` tool to reactivate it${visitClause}.`;
642
+ }
643
+ }
644
+
556
645
  // src/utils.ts
557
646
  async function withRetry(fn, options = {}) {
558
647
  const {
@@ -719,6 +808,10 @@ function paywallErrorToClientPayload(error) {
719
808
  if (sc.balance !== void 0) base.balance = sc.balance;
720
809
  if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
721
810
  if (sc.confirmationUrl !== void 0) base.confirmationUrl = sc.confirmationUrl;
811
+ } else {
812
+ base.kind = "payment_required";
813
+ if (sc.balance !== void 0) base.balance = sc.balance;
814
+ if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
722
815
  }
723
816
  return base;
724
817
  }
@@ -730,6 +823,7 @@ var sharedCustomerLookupDeduplicator = createRequestDeduplicator({
730
823
  cacheErrors: false
731
824
  // Don't cache errors - retry on next request
732
825
  });
826
+ var EXTRA_FORWARD_KEY = "__solvapayExtra";
733
827
  var SolvaPayPaywall = class {
734
828
  constructor(apiClient, options = {}) {
735
829
  this.apiClient = apiClient;
@@ -755,136 +849,216 @@ var SolvaPayPaywall = class {
755
849
  return `solvapay_${timestamp}_${random}`;
756
850
  }
757
851
  /**
758
- * Core protection method - works for both MCP and HTTP
852
+ * Pure decision routine performs customer resolution, limits cache
853
+ * lookup / fresh `checkLimits` fetch, and returns a `PaywallDecision`
854
+ * describing whether the handler should run.
855
+ *
856
+ * Side effects kept in lockstep with the legacy `protect()` path:
857
+ * - creates the backend customer on first use (`ensureCustomer`),
858
+ * - updates the limits cache (consume-one-unit bookkeeping), and
859
+ * - emits a `paywall` usage event on gate outcomes.
860
+ *
861
+ * `trackUsage` for the `success` / `fail` outcome is emitted by the
862
+ * caller (adapter or `protect()`) once it has actually invoked the
863
+ * handler — `decide()` never counts handler execution as usage.
864
+ *
865
+ * @since 1.1.0
759
866
  */
760
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
761
- async protect(handler, metadata = {}, getCustomerRef) {
867
+ async decide(args, metadata = {}, getCustomerRef) {
762
868
  const product = this.resolveProduct(metadata);
763
869
  const usageType = metadata.usageType || "requests";
764
- return async (args) => {
765
- const startTime = Date.now();
766
- const requestId = this.generateRequestId();
767
- const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
768
- let backendCustomerRef;
769
- if (inputCustomerRef.startsWith("cus_")) {
770
- backendCustomerRef = inputCustomerRef;
870
+ const requestId = this.generateRequestId();
871
+ const startTime = Date.now();
872
+ const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
873
+ let backendCustomerRef;
874
+ if (inputCustomerRef.startsWith("cus_")) {
875
+ backendCustomerRef = inputCustomerRef;
876
+ } else {
877
+ backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
878
+ }
879
+ const limitsCacheKey = `${backendCustomerRef}:${product}:${usageType}`;
880
+ const cachedLimits = this.limitsCache.get(limitsCacheKey);
881
+ const now = Date.now();
882
+ let withinLimits;
883
+ let remaining;
884
+ let checkoutUrl;
885
+ let resolvedMeterName;
886
+ let lastLimitsCheck;
887
+ const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
888
+ if (hasFreshCachedLimits) {
889
+ checkoutUrl = cachedLimits.checkoutUrl;
890
+ resolvedMeterName = cachedLimits.meterName;
891
+ lastLimitsCheck = cachedLimits.limits;
892
+ if (cachedLimits.remaining > 0) {
893
+ cachedLimits.remaining--;
894
+ if (cachedLimits.remaining <= 0) {
895
+ this.limitsCache.delete(limitsCacheKey);
896
+ }
897
+ withinLimits = true;
898
+ remaining = cachedLimits.remaining;
771
899
  } else {
772
- backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
900
+ withinLimits = false;
901
+ remaining = 0;
902
+ this.limitsCache.delete(limitsCacheKey);
903
+ }
904
+ } else {
905
+ if (cachedLimits) {
906
+ this.limitsCache.delete(limitsCacheKey);
907
+ }
908
+ const limitsCheck = await this.apiClient.checkLimits({
909
+ customerRef: backendCustomerRef,
910
+ productRef: product,
911
+ meterName: usageType
912
+ });
913
+ lastLimitsCheck = limitsCheck;
914
+ withinLimits = limitsCheck.withinLimits;
915
+ remaining = limitsCheck.remaining;
916
+ checkoutUrl = limitsCheck.checkoutUrl;
917
+ resolvedMeterName = limitsCheck.meterName;
918
+ const consumedAllowance = withinLimits && remaining > 0;
919
+ if (consumedAllowance) {
920
+ remaining = Math.max(0, remaining - 1);
921
+ }
922
+ if (consumedAllowance) {
923
+ this.limitsCache.set(limitsCacheKey, {
924
+ remaining,
925
+ checkoutUrl,
926
+ meterName: resolvedMeterName,
927
+ timestamp: now,
928
+ limits: limitsCheck
929
+ });
930
+ }
931
+ }
932
+ if (!withinLimits) {
933
+ const latencyMs = Date.now() - startTime;
934
+ this.trackUsage(
935
+ backendCustomerRef,
936
+ product,
937
+ resolvedMeterName || usageType,
938
+ "paywall",
939
+ requestId,
940
+ latencyMs
941
+ );
942
+ const state = classifyPaywallState(lastLimitsCheck ?? null);
943
+ const preMessageGate = lastLimitsCheck?.activationRequired ? {
944
+ kind: "activation_required",
945
+ product,
946
+ message: "",
947
+ checkoutUrl: lastLimitsCheck.confirmationUrl || lastLimitsCheck.checkoutUrl || checkoutUrl || "",
948
+ ...lastLimitsCheck.confirmationUrl !== void 0 ? { confirmationUrl: lastLimitsCheck.confirmationUrl } : {},
949
+ ...lastLimitsCheck.plans !== void 0 ? { plans: lastLimitsCheck.plans } : {},
950
+ ...lastLimitsCheck.balance !== void 0 ? { balance: lastLimitsCheck.balance } : {},
951
+ ...lastLimitsCheck.product !== void 0 ? { productDetails: lastLimitsCheck.product } : {}
952
+ } : {
953
+ kind: "payment_required",
954
+ product,
955
+ checkoutUrl: checkoutUrl || "",
956
+ message: "",
957
+ ...lastLimitsCheck?.balance !== void 0 ? { balance: lastLimitsCheck.balance } : {},
958
+ ...lastLimitsCheck?.product !== void 0 ? { productDetails: lastLimitsCheck.product } : {}
959
+ };
960
+ const gate = {
961
+ ...preMessageGate,
962
+ message: buildGateMessage(state, preMessageGate)
963
+ };
964
+ return {
965
+ outcome: "gate",
966
+ gate,
967
+ limits: lastLimitsCheck ?? null,
968
+ customerRef: backendCustomerRef
969
+ };
970
+ }
971
+ return {
972
+ outcome: "allow",
973
+ args,
974
+ limits: lastLimitsCheck,
975
+ customerRef: backendCustomerRef
976
+ };
977
+ }
978
+ /**
979
+ * Execute the handler for an already-obtained `allow` decision and
980
+ * emit the post-handler `trackUsage('success' | 'fail', ...)` event.
981
+ *
982
+ * Exposed for adapter integration — the adapter layer drives the
983
+ * paywall through `decide()` + `runAllow()` so `formatGate` can own
984
+ * gate outcomes without routing through `PaywallError`. `protect()`
985
+ * continues to offer the self-contained throw-based surface for
986
+ * legacy consumers.
987
+ *
988
+ * `runAllow` intentionally does NOT re-throw `PaywallError` — if a
989
+ * handler calls `ctx.gate(reason)` and throws from deep code, the
990
+ * adapter catches that at the `formatGate` boundary instead.
991
+ *
992
+ * @since 1.1.0
993
+ */
994
+ async runAllow(decision, handler, metadata, args) {
995
+ const product = this.resolveProduct(metadata);
996
+ const usageType = metadata.usageType || "requests";
997
+ const requestId = this.generateRequestId();
998
+ const startTime = Date.now();
999
+ const forwardedExtra = args[EXTRA_FORWARD_KEY];
1000
+ const handlerContext = {
1001
+ customerRef: decision.customerRef,
1002
+ limits: decision.limits,
1003
+ ...forwardedExtra !== void 0 ? { extra: forwardedExtra } : {}
1004
+ };
1005
+ try {
1006
+ const result = await handler(args, handlerContext);
1007
+ const latencyMs = Date.now() - startTime;
1008
+ this.trackUsage(
1009
+ decision.customerRef,
1010
+ product,
1011
+ decision.limits.meterName || usageType,
1012
+ "success",
1013
+ requestId,
1014
+ latencyMs
1015
+ );
1016
+ return result;
1017
+ } catch (error) {
1018
+ if (error instanceof Error) {
1019
+ const errorType = error instanceof PaywallError ? "PaywallError" : "API Error";
1020
+ this.log(`\u274C Error in paywall [${errorType}]: ${error.message}`);
1021
+ } else {
1022
+ this.log(`\u274C Error in paywall:`, error);
773
1023
  }
774
- let resolvedMeterName;
775
- try {
776
- const limitsCacheKey = `${backendCustomerRef}:${product}:${usageType}`;
777
- const cachedLimits = this.limitsCache.get(limitsCacheKey);
778
- const now = Date.now();
779
- let withinLimits;
780
- let remaining;
781
- let checkoutUrl;
782
- let lastLimitsCheck;
783
- const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
784
- if (hasFreshCachedLimits) {
785
- checkoutUrl = cachedLimits.checkoutUrl;
786
- resolvedMeterName = cachedLimits.meterName;
787
- if (cachedLimits.remaining > 0) {
788
- cachedLimits.remaining--;
789
- if (cachedLimits.remaining <= 0) {
790
- this.limitsCache.delete(limitsCacheKey);
791
- }
792
- withinLimits = true;
793
- remaining = cachedLimits.remaining;
794
- } else {
795
- withinLimits = false;
796
- remaining = 0;
797
- this.limitsCache.delete(limitsCacheKey);
798
- }
799
- } else {
800
- if (cachedLimits) {
801
- this.limitsCache.delete(limitsCacheKey);
802
- }
803
- const limitsCheck = await this.apiClient.checkLimits({
804
- customerRef: backendCustomerRef,
805
- productRef: product,
806
- meterName: usageType
807
- });
808
- lastLimitsCheck = limitsCheck;
809
- withinLimits = limitsCheck.withinLimits;
810
- remaining = limitsCheck.remaining;
811
- checkoutUrl = limitsCheck.checkoutUrl;
812
- resolvedMeterName = limitsCheck.meterName;
813
- const consumedAllowance = withinLimits && remaining > 0;
814
- if (consumedAllowance) {
815
- remaining = Math.max(0, remaining - 1);
816
- }
817
- if (consumedAllowance) {
818
- this.limitsCache.set(limitsCacheKey, {
819
- remaining,
820
- checkoutUrl,
821
- meterName: resolvedMeterName,
822
- timestamp: now
823
- });
824
- }
825
- }
826
- if (!withinLimits) {
827
- const latencyMs2 = Date.now() - startTime;
828
- this.trackUsage(
829
- backendCustomerRef,
830
- product,
831
- resolvedMeterName || usageType,
832
- "paywall",
833
- requestId,
834
- latencyMs2
835
- );
836
- if (lastLimitsCheck?.activationRequired) {
837
- const confirmationUrl = lastLimitsCheck.confirmationUrl;
838
- const payCheckoutUrl = confirmationUrl || lastLimitsCheck.checkoutUrl || checkoutUrl || "";
839
- throw new PaywallError("Activation required", {
840
- kind: "activation_required",
841
- product,
842
- message: "Product activation is required before this tool can be used.",
843
- checkoutUrl: payCheckoutUrl,
844
- confirmationUrl,
845
- plans: lastLimitsCheck.plans,
846
- balance: lastLimitsCheck.balance,
847
- productDetails: lastLimitsCheck.product
848
- });
849
- }
850
- throw new PaywallError("Payment required", {
851
- kind: "payment_required",
852
- product,
853
- checkoutUrl: checkoutUrl || "",
854
- message: `Purchase required. Remaining: ${remaining}`
855
- });
856
- }
857
- const result = await handler(args);
1024
+ if (!(error instanceof PaywallError)) {
858
1025
  const latencyMs = Date.now() - startTime;
859
1026
  this.trackUsage(
860
- backendCustomerRef,
1027
+ decision.customerRef,
861
1028
  product,
862
- resolvedMeterName || usageType,
863
- "success",
1029
+ decision.limits.meterName || usageType,
1030
+ "fail",
864
1031
  requestId,
865
1032
  latencyMs
866
1033
  );
867
- return result;
868
- } catch (error) {
869
- if (error instanceof Error) {
870
- const errorType = error instanceof PaywallError ? "PaywallError" : "API Error";
871
- this.log(`\u274C Error in paywall [${errorType}]: ${error.message}`);
872
- } else {
873
- this.log(`\u274C Error in paywall:`, error);
874
- }
875
- if (!(error instanceof PaywallError)) {
876
- const latencyMs = Date.now() - startTime;
877
- this.trackUsage(
878
- backendCustomerRef,
879
- product,
880
- resolvedMeterName || usageType,
881
- "fail",
882
- requestId,
883
- latencyMs
884
- );
885
- }
886
- throw error;
887
1034
  }
1035
+ throw error;
1036
+ }
1037
+ }
1038
+ /**
1039
+ * Core protection method - works for both MCP and HTTP
1040
+ *
1041
+ * The `handler` may optionally declare a second positional argument
1042
+ * of type `ProtectHandlerContext` to receive the resolved customer
1043
+ * ref, the pre-check `LimitResponseWithPlan`, and an opaque `extra`
1044
+ * bag threaded through from the adapter layer. One-arg handlers
1045
+ * ignore the second argument and continue to work unchanged.
1046
+ *
1047
+ * Implemented on top of `decide()`: pre-check runs through the same
1048
+ * decision routine, and gate outcomes are raised as a `PaywallError`
1049
+ * to preserve the legacy throw-based signal for consumers that
1050
+ * haven't migrated to adapter-level `formatGate` routing.
1051
+ */
1052
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1053
+ async protect(handler, metadata = {}, getCustomerRef) {
1054
+ return async (args) => {
1055
+ const decision = await this.decide(args, metadata, getCustomerRef);
1056
+ if (decision.outcome === "gate") {
1057
+ const message = decision.gate.kind === "activation_required" ? "Activation required" : "Payment required";
1058
+ this.log(`\u274C Error in paywall [PaywallError]: ${message}`);
1059
+ throw new PaywallError(message, decision.gate);
1060
+ }
1061
+ return this.runAllow(decision, handler, metadata, args);
888
1062
  };
889
1063
  }
890
1064
  /**
@@ -987,6 +1161,16 @@ var SolvaPayPaywall = class {
987
1161
  this.log(
988
1162
  `\u26A0\uFE0F Resolved customer ${customerRef} by email after conflict; using existing customer ${byEmail.customerRef}`
989
1163
  );
1164
+ if (!byEmail.externalRef && this.apiClient.updateCustomer) {
1165
+ try {
1166
+ await this.apiClient.updateCustomer(byEmail.customerRef, { externalRef });
1167
+ } catch (backfillError) {
1168
+ this.log(
1169
+ `\u26A0\uFE0F Failed to backfill externalRef on ${byEmail.customerRef}:`,
1170
+ backfillError instanceof Error ? backfillError.message : backfillError
1171
+ );
1172
+ }
1173
+ }
990
1174
  return byEmail.customerRef;
991
1175
  }
992
1176
  } catch (emailLookupError) {
@@ -1063,6 +1247,7 @@ var SolvaPayPaywall = class {
1063
1247
  };
1064
1248
 
1065
1249
  // src/adapters/base.ts
1250
+ var EXTRA_FORWARD_KEY2 = "__solvapayExtra";
1066
1251
  var AdapterUtils = class {
1067
1252
  /**
1068
1253
  * Ensure customer reference is properly formatted
@@ -1078,7 +1263,7 @@ var AdapterUtils = class {
1078
1263
  */
1079
1264
  static async extractFromJWT(token, options) {
1080
1265
  try {
1081
- const { jwtVerify } = await import("./webapi-K5XBCEO6.js");
1266
+ const { jwtVerify } = await import("jose");
1082
1267
  const jwtSecret = new TextEncoder().encode(
1083
1268
  options?.secret || process.env.OAUTH_JWKS_SECRET || "test-jwt-secret"
1084
1269
  );
@@ -1094,23 +1279,45 @@ var AdapterUtils = class {
1094
1279
  };
1095
1280
  async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
1096
1281
  const backendRefCache = /* @__PURE__ */ new Map();
1097
- const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
1098
- const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
1099
1282
  return async (context, extra) => {
1283
+ let args;
1284
+ let customerRef;
1100
1285
  try {
1101
- const args = await adapter.extractArgs(context);
1102
- const customerRef = await adapter.getCustomerRef(context, extra);
1286
+ args = await adapter.extractArgs(context);
1287
+ customerRef = await adapter.getCustomerRef(context, extra);
1103
1288
  let backendRef = backendRefCache.get(customerRef);
1104
1289
  if (!backendRef) {
1105
1290
  backendRef = await paywall.ensureCustomer(customerRef, customerRef);
1106
1291
  backendRefCache.set(customerRef, backendRef);
1107
1292
  }
1108
1293
  args.auth = { customer_ref: backendRef };
1109
- const result = await protectedHandler(args);
1110
- return adapter.formatResponse(result, context);
1111
1294
  } catch (error) {
1112
1295
  return adapter.formatError(error, context);
1113
1296
  }
1297
+ const decideGetCustomerRef = (args2) => args2.auth?.customer_ref || "anonymous";
1298
+ try {
1299
+ const decision = await paywall.decide(args, metadata, decideGetCustomerRef);
1300
+ if (decision.outcome === "gate") {
1301
+ return adapter.formatGate(decision.gate, context);
1302
+ }
1303
+ if (extra !== void 0) {
1304
+ ;
1305
+ args[EXTRA_FORWARD_KEY2] = extra;
1306
+ }
1307
+ try {
1308
+ const result = await paywall.runAllow(decision, businessLogic, metadata, args);
1309
+ return adapter.formatResponse(result, context);
1310
+ } finally {
1311
+ if (EXTRA_FORWARD_KEY2 in args) {
1312
+ delete args[EXTRA_FORWARD_KEY2];
1313
+ }
1314
+ }
1315
+ } catch (error) {
1316
+ if (error instanceof PaywallError) {
1317
+ return adapter.formatGate(error.structuredContent, context);
1318
+ }
1319
+ return adapter.formatError(error, context);
1320
+ }
1114
1321
  };
1115
1322
  }
1116
1323
 
@@ -1158,24 +1365,25 @@ var HttpAdapter = class {
1158
1365
  }
1159
1366
  return result;
1160
1367
  }
1161
- formatError(error, [_req, reply]) {
1162
- if (error instanceof PaywallError) {
1163
- const errorResponse2 = {
1164
- success: false,
1165
- error: "Payment required",
1166
- product: error.structuredContent.product,
1167
- checkoutUrl: error.structuredContent.checkoutUrl,
1168
- message: error.structuredContent.message
1169
- };
1170
- if (reply && reply.status && typeof reply.json === "function") {
1171
- reply.status(402).json(errorResponse2);
1172
- return;
1173
- }
1174
- if (reply && reply.code) {
1175
- reply.code(402);
1176
- }
1177
- return errorResponse2;
1368
+ /**
1369
+ * Emit a 402 Payment Required response with the same JSON body shape
1370
+ * REST consumers have always received (`{success:false, error, product,
1371
+ * checkoutUrl, message, ...}`). The shape is reused via
1372
+ * `paywallErrorToClientPayload` so HTTP / Next / hosted-proxy clients
1373
+ * don't have to branch on an SDK version.
1374
+ */
1375
+ formatGate(gate, [_req, reply]) {
1376
+ const errorResponse = paywallErrorToClientPayload(new PaywallError(gate.message, gate));
1377
+ if (reply && reply.status && typeof reply.json === "function") {
1378
+ reply.status(402).json(errorResponse);
1379
+ return;
1178
1380
  }
1381
+ if (reply && reply.code) {
1382
+ reply.code(402);
1383
+ }
1384
+ return errorResponse;
1385
+ }
1386
+ formatError(error, [_req, reply]) {
1179
1387
  const errorResponse = {
1180
1388
  success: false,
1181
1389
  error: error instanceof Error ? error.message : "Internal server error"
@@ -1253,22 +1461,22 @@ var NextAdapter = class {
1253
1461
  headers: { "Content-Type": "application/json" }
1254
1462
  });
1255
1463
  }
1464
+ /**
1465
+ * Emit a 402 Payment Required `Response` with the same JSON body
1466
+ * REST consumers have always received. Reuses
1467
+ * `paywallErrorToClientPayload` so HTTP / Next / hosted-proxy
1468
+ * clients don't have to branch on an SDK version.
1469
+ */
1470
+ formatGate(gate, _context) {
1471
+ return new Response(
1472
+ JSON.stringify(paywallErrorToClientPayload(new PaywallError(gate.message, gate))),
1473
+ {
1474
+ status: 402,
1475
+ headers: { "Content-Type": "application/json" }
1476
+ }
1477
+ );
1478
+ }
1256
1479
  formatError(error, _context) {
1257
- if (error instanceof PaywallError) {
1258
- return new Response(
1259
- JSON.stringify({
1260
- success: false,
1261
- error: "Payment required",
1262
- product: error.structuredContent.product,
1263
- checkoutUrl: error.structuredContent.checkoutUrl,
1264
- message: error.structuredContent.message
1265
- }),
1266
- {
1267
- status: 402,
1268
- headers: { "Content-Type": "application/json" }
1269
- }
1270
- );
1271
- }
1272
1480
  return new Response(
1273
1481
  JSON.stringify({
1274
1482
  success: false,
@@ -1316,19 +1524,27 @@ var McpAdapter = class {
1316
1524
  }
1317
1525
  return response;
1318
1526
  }
1527
+ /**
1528
+ * Emit a plain-narration paywall response — `content[0].text` carries
1529
+ * the gate's human message (LLM-actionable), `structuredContent`
1530
+ * carries the machine-readable gate payload, and `isError` stays
1531
+ * `false` per the MCP spec's own `isError` definition (paywall is
1532
+ * not a self-correctable tool execution error; it is a user-facing
1533
+ * control transfer to the UI).
1534
+ *
1535
+ * Hosts that read widget metadata from `tools/list` or tool-result
1536
+ * `_meta.ui` open the paywall iframe on top of this response;
1537
+ * `buildPayableHandler` stamps the `_meta.ui.resourceUri` envelope
1538
+ * before returning.
1539
+ */
1540
+ formatGate(gate, _context) {
1541
+ return {
1542
+ content: [{ type: "text", text: gate.message }],
1543
+ isError: false,
1544
+ structuredContent: gate
1545
+ };
1546
+ }
1319
1547
  formatError(error, _context) {
1320
- if (error instanceof PaywallError) {
1321
- return {
1322
- content: [
1323
- {
1324
- type: "text",
1325
- text: JSON.stringify(paywallErrorToClientPayload(error), null, 2)
1326
- }
1327
- ],
1328
- isError: true,
1329
- structuredContent: error.structuredContent
1330
- };
1331
- }
1332
1548
  return {
1333
1549
  content: [
1334
1550
  {
@@ -1733,6 +1949,15 @@ function createSolvaPay(config) {
1733
1949
  };
1734
1950
  }
1735
1951
 
1952
+ // src/types/paywall.ts
1953
+ function isPaywallStructuredContent(value) {
1954
+ if (typeof value !== "object" || value === null || !("kind" in value)) {
1955
+ return false;
1956
+ }
1957
+ const kind = value.kind;
1958
+ return kind === "payment_required" || kind === "activation_required";
1959
+ }
1960
+
1736
1961
  // src/helpers/error.ts
1737
1962
  import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
1738
1963
  function isErrorResult(result) {
@@ -1758,26 +1983,128 @@ function handleRouteError(error, operationName, defaultMessage) {
1758
1983
  }
1759
1984
 
1760
1985
  // src/helpers/auth.ts
1986
+ function base64UrlDecode(input) {
1987
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/");
1988
+ const padding = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
1989
+ const base64 = padded + padding;
1990
+ if (typeof atob === "function") {
1991
+ const binary = atob(base64);
1992
+ let result = "";
1993
+ for (let i = 0; i < binary.length; i++) {
1994
+ result += String.fromCharCode(binary.charCodeAt(i));
1995
+ }
1996
+ try {
1997
+ return decodeURIComponent(escape(result));
1998
+ } catch {
1999
+ return result;
2000
+ }
2001
+ }
2002
+ const BufferCtor = globalThis.Buffer;
2003
+ if (BufferCtor) {
2004
+ return BufferCtor.from(base64, "base64").toString("utf-8");
2005
+ }
2006
+ throw new Error("No base64 decoder available in this runtime");
2007
+ }
2008
+ function decodeJwtUnverified(token) {
2009
+ const parts = token.split(".");
2010
+ if (parts.length !== 3) return null;
2011
+ try {
2012
+ const json = base64UrlDecode(parts[1]);
2013
+ const payload = JSON.parse(json);
2014
+ if (typeof payload !== "object" || payload === null) return null;
2015
+ return payload;
2016
+ } catch {
2017
+ return null;
2018
+ }
2019
+ }
2020
+ function extractBearerToken(request) {
2021
+ const authHeader = request.headers.get("authorization");
2022
+ if (!authHeader || !authHeader.toLowerCase().startsWith("bearer ")) return null;
2023
+ const token = authHeader.slice(7).trim();
2024
+ return token.length > 0 ? token : null;
2025
+ }
2026
+ function readEnv(name) {
2027
+ const proc = globalThis.process;
2028
+ return proc?.env?.[name];
2029
+ }
2030
+ function isStrictMode() {
2031
+ return readEnv("SOLVAPAY_AUTH_STRICT") === "true";
2032
+ }
2033
+ function getConfiguredSecret() {
2034
+ return readEnv("SOLVAPAY_JWT_SECRET") || readEnv("SUPABASE_JWT_SECRET");
2035
+ }
2036
+ async function verifyHs256(token, secret) {
2037
+ try {
2038
+ const { jwtVerify } = await import("jose");
2039
+ const key = new TextEncoder().encode(secret);
2040
+ const { payload } = await jwtVerify(token, key, { algorithms: ["HS256"] });
2041
+ return payload;
2042
+ } catch {
2043
+ return null;
2044
+ }
2045
+ }
2046
+ function pickName(payload) {
2047
+ const metadataFullName = typeof payload.user_metadata?.full_name === "string" ? payload.user_metadata.full_name : null;
2048
+ const metadataName = typeof payload.user_metadata?.name === "string" ? payload.user_metadata.name : null;
2049
+ const claimName = typeof payload.name === "string" ? payload.name : null;
2050
+ return metadataFullName || metadataName || claimName || null;
2051
+ }
2052
+ function pickEmail(payload) {
2053
+ return typeof payload.email === "string" ? payload.email : null;
2054
+ }
2055
+ function unauthorized(details) {
2056
+ return { error: "Unauthorized", status: 401, details };
2057
+ }
1761
2058
  async function getAuthenticatedUserCore(request, options = {}) {
1762
2059
  try {
1763
- const { requireUserId, getUserEmailFromRequest, getUserNameFromRequest } = await import("@solvapay/auth");
1764
- const userIdOrError = requireUserId(request);
1765
- if (userIdOrError instanceof Response) {
1766
- const clonedResponse = userIdOrError.clone();
1767
- const body = await clonedResponse.json().catch(() => ({ error: "Unauthorized" }));
1768
- return {
1769
- error: body.error || "Unauthorized",
1770
- status: userIdOrError.status,
1771
- details: body.error || "Unauthorized"
1772
- };
2060
+ const includeEmail = options.includeEmail !== false;
2061
+ const includeName = options.includeName !== false;
2062
+ const headerUserId = request.headers.get("x-user-id");
2063
+ if (headerUserId) {
2064
+ let email = null;
2065
+ let name = null;
2066
+ if (includeEmail || includeName) {
2067
+ const token2 = extractBearerToken(request);
2068
+ if (token2) {
2069
+ const secret2 = getConfiguredSecret();
2070
+ const payload2 = secret2 ? await verifyHs256(token2, secret2) : isStrictMode() ? null : decodeJwtUnverified(token2);
2071
+ if (payload2) {
2072
+ if (includeEmail) email = pickEmail(payload2);
2073
+ if (includeName) name = pickName(payload2);
2074
+ }
2075
+ }
2076
+ }
2077
+ return { userId: headerUserId, email, name };
2078
+ }
2079
+ const token = extractBearerToken(request);
2080
+ if (!token) {
2081
+ return unauthorized("User ID not found. Ensure middleware is configured.");
2082
+ }
2083
+ const secret = getConfiguredSecret();
2084
+ let payload = null;
2085
+ if (secret) {
2086
+ payload = await verifyHs256(token, secret);
2087
+ if (!payload) {
2088
+ return unauthorized("Invalid or expired authentication token");
2089
+ }
2090
+ } else if (isStrictMode()) {
2091
+ return unauthorized(
2092
+ "Strict auth mode is enabled but no JWT secret is configured. Set SOLVAPAY_JWT_SECRET or SUPABASE_JWT_SECRET."
2093
+ );
2094
+ } else {
2095
+ payload = decodeJwtUnverified(token);
2096
+ if (!payload) {
2097
+ return unauthorized("Malformed authentication token");
2098
+ }
2099
+ }
2100
+ const userId = typeof payload.sub === "string" ? payload.sub : null;
2101
+ if (!userId) {
2102
+ return unauthorized("Authentication token missing subject (sub) claim");
1773
2103
  }
1774
- const userId = userIdOrError;
1775
- const email = options.includeEmail !== false ? await getUserEmailFromRequest(request) : null;
1776
- const name = options.includeName !== false ? await getUserNameFromRequest(request) : null;
1777
2104
  return {
1778
2105
  userId,
1779
- email,
1780
- name
2106
+ email: includeEmail ? pickEmail(payload) : null,
2107
+ name: includeName ? pickName(payload) : null
1781
2108
  };
1782
2109
  } catch (error) {
1783
2110
  return handleRouteError(error, "Get authenticated user", "Authentication failed");
@@ -2170,6 +2497,31 @@ async function activatePlanCore(request, body, options = {}) {
2170
2497
  }
2171
2498
  }
2172
2499
 
2500
+ // src/helpers/payment-method.ts
2501
+ async function getPaymentMethodCore(request, options = {}) {
2502
+ try {
2503
+ const customerResult = await syncCustomerCore(request, {
2504
+ solvaPay: options.solvaPay,
2505
+ includeEmail: options.includeEmail,
2506
+ includeName: options.includeName
2507
+ });
2508
+ if (isErrorResult(customerResult)) {
2509
+ return customerResult;
2510
+ }
2511
+ const customerRef = customerResult;
2512
+ const solvaPay = options.solvaPay || createSolvaPay();
2513
+ if (!solvaPay.apiClient.getPaymentMethod) {
2514
+ return {
2515
+ error: "getPaymentMethod is not implemented on this API client",
2516
+ status: 500
2517
+ };
2518
+ }
2519
+ return await solvaPay.apiClient.getPaymentMethod({ customerRef });
2520
+ } catch (error) {
2521
+ return handleRouteError(error, "Get payment method", "Failed to load payment method");
2522
+ }
2523
+ }
2524
+
2173
2525
  // src/helpers/plans.ts
2174
2526
  import { getSolvaPayConfig as getSolvaPayConfig2 } from "@solvapay/core";
2175
2527
  async function listPlansCore(request, options = {}) {
@@ -2212,6 +2564,76 @@ async function listPlansCore(request, options = {}) {
2212
2564
  }
2213
2565
  }
2214
2566
 
2567
+ // src/helpers/merchant.ts
2568
+ import { getSolvaPayConfig as getSolvaPayConfig3 } from "@solvapay/core";
2569
+ async function getMerchantCore(_request, options = {}) {
2570
+ try {
2571
+ const apiClient = options.solvaPay?.apiClient ?? (() => {
2572
+ const config = getSolvaPayConfig3();
2573
+ if (!config.apiKey) return null;
2574
+ return createSolvaPayClient({
2575
+ apiKey: config.apiKey,
2576
+ apiBaseUrl: config.apiBaseUrl
2577
+ });
2578
+ })();
2579
+ if (!apiClient) {
2580
+ return {
2581
+ error: "Server configuration error: SolvaPay secret key not configured",
2582
+ status: 500
2583
+ };
2584
+ }
2585
+ if (!apiClient.getMerchant) {
2586
+ return {
2587
+ error: "Get merchant method not available",
2588
+ status: 500
2589
+ };
2590
+ }
2591
+ const merchant = await apiClient.getMerchant();
2592
+ return merchant;
2593
+ } catch (error) {
2594
+ return handleRouteError(error, "Get merchant", "Failed to fetch merchant");
2595
+ }
2596
+ }
2597
+
2598
+ // src/helpers/product.ts
2599
+ import { getSolvaPayConfig as getSolvaPayConfig4 } from "@solvapay/core";
2600
+ async function getProductCore(request, options = {}) {
2601
+ try {
2602
+ const url = new URL(request.url);
2603
+ const productRef = url.searchParams.get("productRef");
2604
+ if (!productRef) {
2605
+ return {
2606
+ error: "Missing required parameter: productRef",
2607
+ status: 400
2608
+ };
2609
+ }
2610
+ const apiClient = options.solvaPay?.apiClient ?? (() => {
2611
+ const config = getSolvaPayConfig4();
2612
+ if (!config.apiKey) return null;
2613
+ return createSolvaPayClient({
2614
+ apiKey: config.apiKey,
2615
+ apiBaseUrl: config.apiBaseUrl
2616
+ });
2617
+ })();
2618
+ if (!apiClient) {
2619
+ return {
2620
+ error: "Server configuration error: SolvaPay secret key not configured",
2621
+ status: 500
2622
+ };
2623
+ }
2624
+ if (!apiClient.getProduct) {
2625
+ return {
2626
+ error: "Get product method not available",
2627
+ status: 500
2628
+ };
2629
+ }
2630
+ const product = await apiClient.getProduct(productRef);
2631
+ return product;
2632
+ } catch (error) {
2633
+ return handleRouteError(error, "Get product", "Failed to fetch product");
2634
+ }
2635
+ }
2636
+
2215
2637
  // src/helpers/purchase.ts
2216
2638
  async function checkPurchaseCore(request, options = {}) {
2217
2639
  try {
@@ -2269,6 +2691,37 @@ async function checkPurchaseCore(request, options = {}) {
2269
2691
  }
2270
2692
 
2271
2693
  // src/helpers/usage.ts
2694
+ async function getUsageCore(request, options = {}) {
2695
+ const purchaseResult = await checkPurchaseCore(request, options);
2696
+ if (isErrorResult(purchaseResult)) return purchaseResult;
2697
+ const activePurchase = (purchaseResult.purchases ?? []).find((p) => p.status === "active");
2698
+ if (!activePurchase) {
2699
+ return {
2700
+ meterRef: null,
2701
+ total: null,
2702
+ used: 0,
2703
+ remaining: null,
2704
+ percentUsed: null
2705
+ };
2706
+ }
2707
+ const snap = activePurchase.planSnapshot;
2708
+ const usage = activePurchase.usage;
2709
+ const meterRef = snap?.meterRef ?? snap?.meterId ?? null;
2710
+ const total = typeof snap?.limit === "number" ? snap.limit : null;
2711
+ const used = typeof usage?.used === "number" ? usage.used : 0;
2712
+ const remaining = total !== null ? Math.max(0, total - used) : null;
2713
+ const percentUsed = total !== null && total > 0 ? Math.min(100, Math.round(used / total * 1e4) / 100) : null;
2714
+ return {
2715
+ meterRef,
2716
+ total,
2717
+ used,
2718
+ remaining,
2719
+ percentUsed,
2720
+ ...usage?.periodStart ? { periodStart: usage.periodStart } : {},
2721
+ ...usage?.periodEnd ? { periodEnd: usage.periodEnd } : {},
2722
+ purchaseRef: activePurchase.reference
2723
+ };
2724
+ }
2272
2725
  async function trackUsageCore(request, body, options = {}) {
2273
2726
  try {
2274
2727
  const userResult = await getAuthenticatedUserCore(request);
@@ -2350,8 +2803,11 @@ async function verifyWebhook({
2350
2803
  export {
2351
2804
  PaywallError,
2352
2805
  activatePlanCore,
2806
+ buildGateMessage,
2807
+ buildNudgeMessage,
2353
2808
  cancelPurchaseCore,
2354
2809
  checkPurchaseCore,
2810
+ classifyPaywallState,
2355
2811
  createCheckoutSessionCore,
2356
2812
  createCustomerSessionCore,
2357
2813
  createPaymentIntentCore,
@@ -2360,8 +2816,13 @@ export {
2360
2816
  createTopupPaymentIntentCore,
2361
2817
  getAuthenticatedUserCore,
2362
2818
  getCustomerBalanceCore,
2819
+ getMerchantCore,
2820
+ getPaymentMethodCore,
2821
+ getProductCore,
2822
+ getUsageCore,
2363
2823
  handleRouteError,
2364
2824
  isErrorResult,
2825
+ isPaywallStructuredContent,
2365
2826
  listPlansCore,
2366
2827
  paywallErrorToClientPayload,
2367
2828
  processPaymentIntentCore,