@solvapay/server 1.0.9-preview.1 → 1.0.9

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.js CHANGED
@@ -1,5 +1,3 @@
1
- import "./chunk-MLKGABMK.js";
2
-
3
1
  // src/index.ts
4
2
  import crypto from "crypto";
5
3
  import { SolvaPayError as SolvaPayError5 } from "@solvapay/core";
@@ -64,7 +62,27 @@ function createSolvaPayClient(opts) {
64
62
  throw new SolvaPayError(`Create customer failed (${res.status}): ${error}`);
65
63
  }
66
64
  const result = await res.json();
67
- return result;
65
+ return {
66
+ customerRef: result.reference || result.customerRef
67
+ };
68
+ },
69
+ // PATCH: /v1/sdk/customers/{customerRef}
70
+ async updateCustomer(customerRef, params) {
71
+ const url = `${base}/v1/sdk/customers/${encodeURIComponent(customerRef)}`;
72
+ const res = await fetch(url, {
73
+ method: "PATCH",
74
+ headers,
75
+ body: JSON.stringify(params)
76
+ });
77
+ if (!res.ok) {
78
+ const error = await res.text();
79
+ log(`\u274C API Error: ${res.status} - ${error}`);
80
+ throw new SolvaPayError(`Update customer failed (${res.status}): ${error}`);
81
+ }
82
+ const result = await res.json();
83
+ return {
84
+ customerRef: result.reference || result.customerRef || customerRef
85
+ };
68
86
  },
69
87
  // GET: /v1/sdk/customers/{reference} or /v1/sdk/customers?externalRef={externalRef}|email={email}
70
88
  async getCustomer(params) {
@@ -110,6 +128,50 @@ function createSolvaPayClient(opts) {
110
128
  purchases: customer.purchases || []
111
129
  };
112
130
  },
131
+ // GET: /v1/sdk/merchant
132
+ async getMerchant() {
133
+ const url = `${base}/v1/sdk/merchant`;
134
+ const res = await fetch(url, {
135
+ method: "GET",
136
+ headers
137
+ });
138
+ if (!res.ok) {
139
+ const error = await res.text();
140
+ log(`\u274C API Error: ${res.status} - ${error}`);
141
+ throw new SolvaPayError(`Get merchant failed (${res.status}): ${error}`);
142
+ }
143
+ return res.json();
144
+ },
145
+ // GET: /v1/sdk/platform-config
146
+ async getPlatformConfig() {
147
+ const url = `${base}/v1/sdk/platform-config`;
148
+ const res = await fetch(url, {
149
+ method: "GET",
150
+ headers
151
+ });
152
+ if (!res.ok) {
153
+ const error = await res.text();
154
+ log(`\u274C API Error: ${res.status} - ${error}`);
155
+ throw new SolvaPayError(`Get platform config failed (${res.status}): ${error}`);
156
+ }
157
+ return res.json();
158
+ },
159
+ // GET: /v1/sdk/products/{productRef}
160
+ async getProduct(productRef) {
161
+ const url = `${base}/v1/sdk/products/${encodeURIComponent(productRef)}`;
162
+ const res = await fetch(url, {
163
+ method: "GET",
164
+ headers
165
+ });
166
+ if (!res.ok) {
167
+ const error = await res.text();
168
+ log(`\u274C API Error: ${res.status} - ${error}`);
169
+ throw new SolvaPayError(`Get product failed (${res.status}): ${error}`);
170
+ }
171
+ const result = await res.json();
172
+ const data = result.data || {};
173
+ return { ...data, ...result };
174
+ },
113
175
  // Product management methods (primarily for integration tests)
114
176
  // GET: /v1/sdk/products
115
177
  async listProducts() {
@@ -520,10 +582,67 @@ function createSolvaPayClient(opts) {
520
582
  throw new SolvaPayError(`Activate plan failed (${res.status}): ${error}`);
521
583
  }
522
584
  return await res.json();
585
+ },
586
+ async getPaymentMethod(params) {
587
+ const url = new URL(`${base}/v1/sdk/payment-method`);
588
+ url.searchParams.set("customerRef", params.customerRef);
589
+ const res = await fetch(url.toString(), { method: "GET", headers });
590
+ if (!res.ok) {
591
+ const error = await res.text();
592
+ log(`\u274C API Error: ${res.status} - ${error}`);
593
+ throw new SolvaPayError(`Get payment method failed (${res.status}): ${error}`);
594
+ }
595
+ return await res.json();
523
596
  }
524
597
  };
525
598
  }
526
599
 
600
+ // src/paywall-state.ts
601
+ function classifyPaywallState(limits) {
602
+ if (!limits) return { kind: "upgrade_required" };
603
+ if (limits.activationRequired === true) {
604
+ return { kind: "activation_required" };
605
+ }
606
+ const activePlan = limits.plans?.find((p) => p.reference === limits.plan);
607
+ const isUsageBased = activePlan?.type === "usage-based" || limits.balance !== void 0;
608
+ const creditBalance = limits.balance?.creditBalance ?? limits.creditBalance;
609
+ if (isUsageBased) {
610
+ if (creditBalance === 0) return { kind: "topup_required" };
611
+ if (creditBalance === void 0 && limits.remaining === 0) {
612
+ return { kind: "topup_required" };
613
+ }
614
+ }
615
+ return { kind: "upgrade_required" };
616
+ }
617
+ function buildGateMessage(state, gate) {
618
+ const url = gate.checkoutUrl && gate.checkoutUrl.length > 0 ? gate.checkoutUrl : null;
619
+ const openClause = url ? `, or open ${url} in a browser` : "";
620
+ switch (state.kind) {
621
+ case "activation_required":
622
+ return `Your plan needs activation before you can use this tool. Call the \`activate_plan\` tool to activate it${openClause}.`;
623
+ case "topup_required":
624
+ return `You're out of credits. Call the \`topup\` tool to add more${openClause}.`;
625
+ case "upgrade_required":
626
+ return `You don't have an active plan for this tool. Call the \`upgrade\` tool to pick a plan${openClause}.`;
627
+ case "reactivation_required":
628
+ 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.`;
629
+ }
630
+ }
631
+ function buildNudgeMessage(state, limits) {
632
+ const url = limits?.checkoutUrl && limits.checkoutUrl.length > 0 ? limits.checkoutUrl : null;
633
+ const visitClause = url ? `, or visit ${url}` : "";
634
+ switch (state.kind) {
635
+ case "topup_required":
636
+ return `Heads up \u2014 running low on credits. Call the \`topup\` tool to add more${visitClause}.`;
637
+ case "upgrade_required":
638
+ return `Heads up \u2014 approaching your plan's limit this period. Call the \`upgrade\` tool for more headroom${visitClause}.`;
639
+ case "activation_required":
640
+ return `Heads up \u2014 this plan still needs activation. Call the \`activate_plan\` tool${visitClause}.`;
641
+ case "reactivation_required":
642
+ return `Heads up \u2014 your plan is no longer active. Call the \`manage_account\` tool to reactivate it${visitClause}.`;
643
+ }
644
+ }
645
+
527
646
  // src/utils.ts
528
647
  async function withRetry(fn, options = {}) {
529
648
  const {
@@ -575,7 +694,8 @@ function createRequestDeduplicator(options = {}) {
575
694
  const inFlightRequests = /* @__PURE__ */ new Map();
576
695
  const resultCache = /* @__PURE__ */ new Map();
577
696
  let _cleanupInterval = null;
578
- if (cacheTTL > 0) {
697
+ function ensureCleanupInterval() {
698
+ if (_cleanupInterval || cacheTTL <= 0) return;
579
699
  _cleanupInterval = setInterval(
580
700
  () => {
581
701
  const now = Date.now();
@@ -602,6 +722,7 @@ function createRequestDeduplicator(options = {}) {
602
722
  );
603
723
  }
604
724
  const deduplicate = async (key, fn) => {
725
+ ensureCleanupInterval();
605
726
  if (cacheTTL > 0) {
606
727
  const cached = resultCache.get(key);
607
728
  if (cached && Date.now() - cached.timestamp < cacheTTL) {
@@ -690,6 +811,10 @@ function paywallErrorToClientPayload(error) {
690
811
  if (sc.balance !== void 0) base.balance = sc.balance;
691
812
  if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
692
813
  if (sc.confirmationUrl !== void 0) base.confirmationUrl = sc.confirmationUrl;
814
+ } else {
815
+ base.kind = "payment_required";
816
+ if (sc.balance !== void 0) base.balance = sc.balance;
817
+ if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
693
818
  }
694
819
  return base;
695
820
  }
@@ -701,6 +826,7 @@ var sharedCustomerLookupDeduplicator = createRequestDeduplicator({
701
826
  cacheErrors: false
702
827
  // Don't cache errors - retry on next request
703
828
  });
829
+ var EXTRA_FORWARD_KEY = "__solvapayExtra";
704
830
  var SolvaPayPaywall = class {
705
831
  constructor(apiClient, options = {}) {
706
832
  this.apiClient = apiClient;
@@ -726,136 +852,216 @@ var SolvaPayPaywall = class {
726
852
  return `solvapay_${timestamp}_${random}`;
727
853
  }
728
854
  /**
729
- * Core protection method - works for both MCP and HTTP
855
+ * Pure decision routine performs customer resolution, limits cache
856
+ * lookup / fresh `checkLimits` fetch, and returns a `PaywallDecision`
857
+ * describing whether the handler should run.
858
+ *
859
+ * Side effects kept in lockstep with the legacy `protect()` path:
860
+ * - creates the backend customer on first use (`ensureCustomer`),
861
+ * - updates the limits cache (consume-one-unit bookkeeping), and
862
+ * - emits a `paywall` usage event on gate outcomes.
863
+ *
864
+ * `trackUsage` for the `success` / `fail` outcome is emitted by the
865
+ * caller (adapter or `protect()`) once it has actually invoked the
866
+ * handler — `decide()` never counts handler execution as usage.
867
+ *
868
+ * @since 1.1.0
730
869
  */
731
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
732
- async protect(handler, metadata = {}, getCustomerRef) {
870
+ async decide(args, metadata = {}, getCustomerRef) {
733
871
  const product = this.resolveProduct(metadata);
734
872
  const usageType = metadata.usageType || "requests";
735
- return async (args) => {
736
- const startTime = Date.now();
737
- const requestId = this.generateRequestId();
738
- const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
739
- let backendCustomerRef;
740
- if (inputCustomerRef.startsWith("cus_")) {
741
- backendCustomerRef = inputCustomerRef;
873
+ const requestId = this.generateRequestId();
874
+ const startTime = Date.now();
875
+ const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
876
+ let backendCustomerRef;
877
+ if (inputCustomerRef.startsWith("cus_")) {
878
+ backendCustomerRef = inputCustomerRef;
879
+ } else {
880
+ backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
881
+ }
882
+ const limitsCacheKey = `${backendCustomerRef}:${product}:${usageType}`;
883
+ const cachedLimits = this.limitsCache.get(limitsCacheKey);
884
+ const now = Date.now();
885
+ let withinLimits;
886
+ let remaining;
887
+ let checkoutUrl;
888
+ let resolvedMeterName;
889
+ let lastLimitsCheck;
890
+ const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
891
+ if (hasFreshCachedLimits) {
892
+ checkoutUrl = cachedLimits.checkoutUrl;
893
+ resolvedMeterName = cachedLimits.meterName;
894
+ lastLimitsCheck = cachedLimits.limits;
895
+ if (cachedLimits.remaining > 0) {
896
+ cachedLimits.remaining--;
897
+ if (cachedLimits.remaining <= 0) {
898
+ this.limitsCache.delete(limitsCacheKey);
899
+ }
900
+ withinLimits = true;
901
+ remaining = cachedLimits.remaining;
742
902
  } else {
743
- backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
903
+ withinLimits = false;
904
+ remaining = 0;
905
+ this.limitsCache.delete(limitsCacheKey);
906
+ }
907
+ } else {
908
+ if (cachedLimits) {
909
+ this.limitsCache.delete(limitsCacheKey);
910
+ }
911
+ const limitsCheck = await this.apiClient.checkLimits({
912
+ customerRef: backendCustomerRef,
913
+ productRef: product,
914
+ meterName: usageType
915
+ });
916
+ lastLimitsCheck = limitsCheck;
917
+ withinLimits = limitsCheck.withinLimits;
918
+ remaining = limitsCheck.remaining;
919
+ checkoutUrl = limitsCheck.checkoutUrl;
920
+ resolvedMeterName = limitsCheck.meterName;
921
+ const consumedAllowance = withinLimits && remaining > 0;
922
+ if (consumedAllowance) {
923
+ remaining = Math.max(0, remaining - 1);
924
+ }
925
+ if (consumedAllowance) {
926
+ this.limitsCache.set(limitsCacheKey, {
927
+ remaining,
928
+ checkoutUrl,
929
+ meterName: resolvedMeterName,
930
+ timestamp: now,
931
+ limits: limitsCheck
932
+ });
933
+ }
934
+ }
935
+ if (!withinLimits) {
936
+ const latencyMs = Date.now() - startTime;
937
+ this.trackUsage(
938
+ backendCustomerRef,
939
+ product,
940
+ resolvedMeterName || usageType,
941
+ "paywall",
942
+ requestId,
943
+ latencyMs
944
+ );
945
+ const state = classifyPaywallState(lastLimitsCheck ?? null);
946
+ const preMessageGate = lastLimitsCheck?.activationRequired ? {
947
+ kind: "activation_required",
948
+ product,
949
+ message: "",
950
+ checkoutUrl: lastLimitsCheck.confirmationUrl || lastLimitsCheck.checkoutUrl || checkoutUrl || "",
951
+ ...lastLimitsCheck.confirmationUrl !== void 0 ? { confirmationUrl: lastLimitsCheck.confirmationUrl } : {},
952
+ ...lastLimitsCheck.plans !== void 0 ? { plans: lastLimitsCheck.plans } : {},
953
+ ...lastLimitsCheck.balance !== void 0 ? { balance: lastLimitsCheck.balance } : {},
954
+ ...lastLimitsCheck.product !== void 0 ? { productDetails: lastLimitsCheck.product } : {}
955
+ } : {
956
+ kind: "payment_required",
957
+ product,
958
+ checkoutUrl: checkoutUrl || "",
959
+ message: "",
960
+ ...lastLimitsCheck?.balance !== void 0 ? { balance: lastLimitsCheck.balance } : {},
961
+ ...lastLimitsCheck?.product !== void 0 ? { productDetails: lastLimitsCheck.product } : {}
962
+ };
963
+ const gate = {
964
+ ...preMessageGate,
965
+ message: buildGateMessage(state, preMessageGate)
966
+ };
967
+ return {
968
+ outcome: "gate",
969
+ gate,
970
+ limits: lastLimitsCheck ?? null,
971
+ customerRef: backendCustomerRef
972
+ };
973
+ }
974
+ return {
975
+ outcome: "allow",
976
+ args,
977
+ limits: lastLimitsCheck,
978
+ customerRef: backendCustomerRef
979
+ };
980
+ }
981
+ /**
982
+ * Execute the handler for an already-obtained `allow` decision and
983
+ * emit the post-handler `trackUsage('success' | 'fail', ...)` event.
984
+ *
985
+ * Exposed for adapter integration — the adapter layer drives the
986
+ * paywall through `decide()` + `runAllow()` so `formatGate` can own
987
+ * gate outcomes without routing through `PaywallError`. `protect()`
988
+ * continues to offer the self-contained throw-based surface for
989
+ * legacy consumers.
990
+ *
991
+ * `runAllow` intentionally does NOT re-throw `PaywallError` — if a
992
+ * handler calls `ctx.gate(reason)` and throws from deep code, the
993
+ * adapter catches that at the `formatGate` boundary instead.
994
+ *
995
+ * @since 1.1.0
996
+ */
997
+ async runAllow(decision, handler, metadata, args) {
998
+ const product = this.resolveProduct(metadata);
999
+ const usageType = metadata.usageType || "requests";
1000
+ const requestId = this.generateRequestId();
1001
+ const startTime = Date.now();
1002
+ const forwardedExtra = args[EXTRA_FORWARD_KEY];
1003
+ const handlerContext = {
1004
+ customerRef: decision.customerRef,
1005
+ limits: decision.limits,
1006
+ ...forwardedExtra !== void 0 ? { extra: forwardedExtra } : {}
1007
+ };
1008
+ try {
1009
+ const result = await handler(args, handlerContext);
1010
+ const latencyMs = Date.now() - startTime;
1011
+ this.trackUsage(
1012
+ decision.customerRef,
1013
+ product,
1014
+ decision.limits.meterName || usageType,
1015
+ "success",
1016
+ requestId,
1017
+ latencyMs
1018
+ );
1019
+ return result;
1020
+ } catch (error) {
1021
+ if (error instanceof Error) {
1022
+ const errorType = error instanceof PaywallError ? "PaywallError" : "API Error";
1023
+ this.log(`\u274C Error in paywall [${errorType}]: ${error.message}`);
1024
+ } else {
1025
+ this.log(`\u274C Error in paywall:`, error);
744
1026
  }
745
- let resolvedMeterName;
746
- try {
747
- const limitsCacheKey = `${backendCustomerRef}:${product}:${usageType}`;
748
- const cachedLimits = this.limitsCache.get(limitsCacheKey);
749
- const now = Date.now();
750
- let withinLimits;
751
- let remaining;
752
- let checkoutUrl;
753
- let lastLimitsCheck;
754
- const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
755
- if (hasFreshCachedLimits) {
756
- checkoutUrl = cachedLimits.checkoutUrl;
757
- resolvedMeterName = cachedLimits.meterName;
758
- if (cachedLimits.remaining > 0) {
759
- cachedLimits.remaining--;
760
- if (cachedLimits.remaining <= 0) {
761
- this.limitsCache.delete(limitsCacheKey);
762
- }
763
- withinLimits = true;
764
- remaining = cachedLimits.remaining;
765
- } else {
766
- withinLimits = false;
767
- remaining = 0;
768
- this.limitsCache.delete(limitsCacheKey);
769
- }
770
- } else {
771
- if (cachedLimits) {
772
- this.limitsCache.delete(limitsCacheKey);
773
- }
774
- const limitsCheck = await this.apiClient.checkLimits({
775
- customerRef: backendCustomerRef,
776
- productRef: product,
777
- meterName: usageType
778
- });
779
- lastLimitsCheck = limitsCheck;
780
- withinLimits = limitsCheck.withinLimits;
781
- remaining = limitsCheck.remaining;
782
- checkoutUrl = limitsCheck.checkoutUrl;
783
- resolvedMeterName = limitsCheck.meterName;
784
- const consumedAllowance = withinLimits && remaining > 0;
785
- if (consumedAllowance) {
786
- remaining = Math.max(0, remaining - 1);
787
- }
788
- if (consumedAllowance) {
789
- this.limitsCache.set(limitsCacheKey, {
790
- remaining,
791
- checkoutUrl,
792
- meterName: resolvedMeterName,
793
- timestamp: now
794
- });
795
- }
796
- }
797
- if (!withinLimits) {
798
- const latencyMs2 = Date.now() - startTime;
799
- this.trackUsage(
800
- backendCustomerRef,
801
- product,
802
- resolvedMeterName || usageType,
803
- "paywall",
804
- requestId,
805
- latencyMs2
806
- );
807
- if (lastLimitsCheck?.activationRequired) {
808
- const confirmationUrl = lastLimitsCheck.confirmationUrl;
809
- const payCheckoutUrl = confirmationUrl || lastLimitsCheck.checkoutUrl || checkoutUrl || "";
810
- throw new PaywallError("Activation required", {
811
- kind: "activation_required",
812
- product,
813
- message: "Product activation is required before this tool can be used.",
814
- checkoutUrl: payCheckoutUrl,
815
- confirmationUrl,
816
- plans: lastLimitsCheck.plans,
817
- balance: lastLimitsCheck.balance,
818
- productDetails: lastLimitsCheck.product
819
- });
820
- }
821
- throw new PaywallError("Payment required", {
822
- kind: "payment_required",
823
- product,
824
- checkoutUrl: checkoutUrl || "",
825
- message: `Purchase required. Remaining: ${remaining}`
826
- });
827
- }
828
- const result = await handler(args);
1027
+ if (!(error instanceof PaywallError)) {
829
1028
  const latencyMs = Date.now() - startTime;
830
1029
  this.trackUsage(
831
- backendCustomerRef,
1030
+ decision.customerRef,
832
1031
  product,
833
- resolvedMeterName || usageType,
834
- "success",
1032
+ decision.limits.meterName || usageType,
1033
+ "fail",
835
1034
  requestId,
836
1035
  latencyMs
837
1036
  );
838
- return result;
839
- } catch (error) {
840
- if (error instanceof Error) {
841
- const errorType = error instanceof PaywallError ? "PaywallError" : "API Error";
842
- this.log(`\u274C Error in paywall [${errorType}]: ${error.message}`);
843
- } else {
844
- this.log(`\u274C Error in paywall:`, error);
845
- }
846
- if (!(error instanceof PaywallError)) {
847
- const latencyMs = Date.now() - startTime;
848
- this.trackUsage(
849
- backendCustomerRef,
850
- product,
851
- resolvedMeterName || usageType,
852
- "fail",
853
- requestId,
854
- latencyMs
855
- );
856
- }
857
- throw error;
858
1037
  }
1038
+ throw error;
1039
+ }
1040
+ }
1041
+ /**
1042
+ * Core protection method - works for both MCP and HTTP
1043
+ *
1044
+ * The `handler` may optionally declare a second positional argument
1045
+ * of type `ProtectHandlerContext` to receive the resolved customer
1046
+ * ref, the pre-check `LimitResponseWithPlan`, and an opaque `extra`
1047
+ * bag threaded through from the adapter layer. One-arg handlers
1048
+ * ignore the second argument and continue to work unchanged.
1049
+ *
1050
+ * Implemented on top of `decide()`: pre-check runs through the same
1051
+ * decision routine, and gate outcomes are raised as a `PaywallError`
1052
+ * to preserve the legacy throw-based signal for consumers that
1053
+ * haven't migrated to adapter-level `formatGate` routing.
1054
+ */
1055
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1056
+ async protect(handler, metadata = {}, getCustomerRef) {
1057
+ return async (args) => {
1058
+ const decision = await this.decide(args, metadata, getCustomerRef);
1059
+ if (decision.outcome === "gate") {
1060
+ const message = decision.gate.kind === "activation_required" ? "Activation required" : "Payment required";
1061
+ this.log(`\u274C Error in paywall [PaywallError]: ${message}`);
1062
+ throw new PaywallError(message, decision.gate);
1063
+ }
1064
+ return this.runAllow(decision, handler, metadata, args);
859
1065
  };
860
1066
  }
861
1067
  /**
@@ -958,6 +1164,16 @@ var SolvaPayPaywall = class {
958
1164
  this.log(
959
1165
  `\u26A0\uFE0F Resolved customer ${customerRef} by email after conflict; using existing customer ${byEmail.customerRef}`
960
1166
  );
1167
+ if (!byEmail.externalRef && this.apiClient.updateCustomer) {
1168
+ try {
1169
+ await this.apiClient.updateCustomer(byEmail.customerRef, { externalRef });
1170
+ } catch (backfillError) {
1171
+ this.log(
1172
+ `\u26A0\uFE0F Failed to backfill externalRef on ${byEmail.customerRef}:`,
1173
+ backfillError instanceof Error ? backfillError.message : backfillError
1174
+ );
1175
+ }
1176
+ }
961
1177
  return byEmail.customerRef;
962
1178
  }
963
1179
  } catch (emailLookupError) {
@@ -1034,6 +1250,7 @@ var SolvaPayPaywall = class {
1034
1250
  };
1035
1251
 
1036
1252
  // src/adapters/base.ts
1253
+ var EXTRA_FORWARD_KEY2 = "__solvapayExtra";
1037
1254
  var AdapterUtils = class {
1038
1255
  /**
1039
1256
  * Ensure customer reference is properly formatted
@@ -1049,7 +1266,7 @@ var AdapterUtils = class {
1049
1266
  */
1050
1267
  static async extractFromJWT(token, options) {
1051
1268
  try {
1052
- const { jwtVerify } = await import("./webapi-K5XBCEO6.js");
1269
+ const { jwtVerify } = await import("jose");
1053
1270
  const jwtSecret = new TextEncoder().encode(
1054
1271
  options?.secret || process.env.OAUTH_JWKS_SECRET || "test-jwt-secret"
1055
1272
  );
@@ -1065,23 +1282,45 @@ var AdapterUtils = class {
1065
1282
  };
1066
1283
  async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
1067
1284
  const backendRefCache = /* @__PURE__ */ new Map();
1068
- const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
1069
- const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
1070
1285
  return async (context, extra) => {
1286
+ let args;
1287
+ let customerRef;
1071
1288
  try {
1072
- const args = await adapter.extractArgs(context);
1073
- const customerRef = await adapter.getCustomerRef(context, extra);
1289
+ args = await adapter.extractArgs(context);
1290
+ customerRef = await adapter.getCustomerRef(context, extra);
1074
1291
  let backendRef = backendRefCache.get(customerRef);
1075
1292
  if (!backendRef) {
1076
1293
  backendRef = await paywall.ensureCustomer(customerRef, customerRef);
1077
1294
  backendRefCache.set(customerRef, backendRef);
1078
1295
  }
1079
1296
  args.auth = { customer_ref: backendRef };
1080
- const result = await protectedHandler(args);
1081
- return adapter.formatResponse(result, context);
1082
1297
  } catch (error) {
1083
1298
  return adapter.formatError(error, context);
1084
1299
  }
1300
+ const decideGetCustomerRef = (args2) => args2.auth?.customer_ref || "anonymous";
1301
+ try {
1302
+ const decision = await paywall.decide(args, metadata, decideGetCustomerRef);
1303
+ if (decision.outcome === "gate") {
1304
+ return adapter.formatGate(decision.gate, context);
1305
+ }
1306
+ if (extra !== void 0) {
1307
+ ;
1308
+ args[EXTRA_FORWARD_KEY2] = extra;
1309
+ }
1310
+ try {
1311
+ const result = await paywall.runAllow(decision, businessLogic, metadata, args);
1312
+ return adapter.formatResponse(result, context);
1313
+ } finally {
1314
+ if (EXTRA_FORWARD_KEY2 in args) {
1315
+ delete args[EXTRA_FORWARD_KEY2];
1316
+ }
1317
+ }
1318
+ } catch (error) {
1319
+ if (error instanceof PaywallError) {
1320
+ return adapter.formatGate(error.structuredContent, context);
1321
+ }
1322
+ return adapter.formatError(error, context);
1323
+ }
1085
1324
  };
1086
1325
  }
1087
1326
 
@@ -1129,24 +1368,25 @@ var HttpAdapter = class {
1129
1368
  }
1130
1369
  return result;
1131
1370
  }
1132
- formatError(error, [_req, reply]) {
1133
- if (error instanceof PaywallError) {
1134
- const errorResponse2 = {
1135
- success: false,
1136
- error: "Payment required",
1137
- product: error.structuredContent.product,
1138
- checkoutUrl: error.structuredContent.checkoutUrl,
1139
- message: error.structuredContent.message
1140
- };
1141
- if (reply && reply.status && typeof reply.json === "function") {
1142
- reply.status(402).json(errorResponse2);
1143
- return;
1144
- }
1145
- if (reply && reply.code) {
1146
- reply.code(402);
1147
- }
1148
- return errorResponse2;
1371
+ /**
1372
+ * Emit a 402 Payment Required response with the same JSON body shape
1373
+ * REST consumers have always received (`{success:false, error, product,
1374
+ * checkoutUrl, message, ...}`). The shape is reused via
1375
+ * `paywallErrorToClientPayload` so HTTP / Next / hosted-proxy clients
1376
+ * don't have to branch on an SDK version.
1377
+ */
1378
+ formatGate(gate, [_req, reply]) {
1379
+ const errorResponse = paywallErrorToClientPayload(new PaywallError(gate.message, gate));
1380
+ if (reply && reply.status && typeof reply.json === "function") {
1381
+ reply.status(402).json(errorResponse);
1382
+ return;
1149
1383
  }
1384
+ if (reply && reply.code) {
1385
+ reply.code(402);
1386
+ }
1387
+ return errorResponse;
1388
+ }
1389
+ formatError(error, [_req, reply]) {
1150
1390
  const errorResponse = {
1151
1391
  success: false,
1152
1392
  error: error instanceof Error ? error.message : "Internal server error"
@@ -1224,22 +1464,22 @@ var NextAdapter = class {
1224
1464
  headers: { "Content-Type": "application/json" }
1225
1465
  });
1226
1466
  }
1467
+ /**
1468
+ * Emit a 402 Payment Required `Response` with the same JSON body
1469
+ * REST consumers have always received. Reuses
1470
+ * `paywallErrorToClientPayload` so HTTP / Next / hosted-proxy
1471
+ * clients don't have to branch on an SDK version.
1472
+ */
1473
+ formatGate(gate, _context) {
1474
+ return new Response(
1475
+ JSON.stringify(paywallErrorToClientPayload(new PaywallError(gate.message, gate))),
1476
+ {
1477
+ status: 402,
1478
+ headers: { "Content-Type": "application/json" }
1479
+ }
1480
+ );
1481
+ }
1227
1482
  formatError(error, _context) {
1228
- if (error instanceof PaywallError) {
1229
- return new Response(
1230
- JSON.stringify({
1231
- success: false,
1232
- error: "Payment required",
1233
- product: error.structuredContent.product,
1234
- checkoutUrl: error.structuredContent.checkoutUrl,
1235
- message: error.structuredContent.message
1236
- }),
1237
- {
1238
- status: 402,
1239
- headers: { "Content-Type": "application/json" }
1240
- }
1241
- );
1242
- }
1243
1483
  return new Response(
1244
1484
  JSON.stringify({
1245
1485
  success: false,
@@ -1287,19 +1527,27 @@ var McpAdapter = class {
1287
1527
  }
1288
1528
  return response;
1289
1529
  }
1530
+ /**
1531
+ * Emit a plain-narration paywall response — `content[0].text` carries
1532
+ * the gate's human message (LLM-actionable), `structuredContent`
1533
+ * carries the machine-readable gate payload, and `isError` stays
1534
+ * `false` per the MCP spec's own `isError` definition (paywall is
1535
+ * not a self-correctable tool execution error; it is a user-facing
1536
+ * control transfer to the UI).
1537
+ *
1538
+ * Hosts that read widget metadata from `tools/list` or tool-result
1539
+ * `_meta.ui` open the paywall iframe on top of this response;
1540
+ * `buildPayableHandler` stamps the `_meta.ui.resourceUri` envelope
1541
+ * before returning.
1542
+ */
1543
+ formatGate(gate, _context) {
1544
+ return {
1545
+ content: [{ type: "text", text: gate.message }],
1546
+ isError: false,
1547
+ structuredContent: gate
1548
+ };
1549
+ }
1290
1550
  formatError(error, _context) {
1291
- if (error instanceof PaywallError) {
1292
- return {
1293
- content: [
1294
- {
1295
- type: "text",
1296
- text: JSON.stringify(paywallErrorToClientPayload(error), null, 2)
1297
- }
1298
- ],
1299
- isError: true,
1300
- structuredContent: error.structuredContent
1301
- };
1302
- }
1303
1551
  return {
1304
1552
  content: [
1305
1553
  {
@@ -1705,217 +1953,13 @@ function createSolvaPay(config) {
1705
1953
  };
1706
1954
  }
1707
1955
 
1708
- // src/mcp-auth.ts
1709
- var McpBearerAuthError = class extends Error {
1710
- constructor(message) {
1711
- super(message);
1712
- this.name = "McpBearerAuthError";
1713
- }
1714
- };
1715
- function base64UrlDecode(input) {
1716
- const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
1717
- const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
1718
- const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
1719
- return new TextDecoder().decode(bytes);
1720
- }
1721
- function extractBearerToken(authorization) {
1722
- if (!authorization) return null;
1723
- if (!authorization.startsWith("Bearer ")) return null;
1724
- return authorization.slice(7).trim() || null;
1725
- }
1726
- function decodeJwtPayload(token) {
1727
- const parts = token.split(".");
1728
- if (parts.length < 2) {
1729
- throw new McpBearerAuthError("Invalid JWT format");
1730
- }
1731
- try {
1732
- const payloadText = base64UrlDecode(parts[1]);
1733
- const payload = JSON.parse(payloadText);
1734
- return payload;
1735
- } catch {
1736
- throw new McpBearerAuthError("Invalid JWT payload");
1737
- }
1738
- }
1739
- function getCustomerRefFromJwtPayload(payload, options = {}) {
1740
- const claimPriority = options.claimPriority || ["customerRef", "customer_ref", "sub"];
1741
- for (const claim of claimPriority) {
1742
- const value = payload[claim];
1743
- if (typeof value === "string" && value.trim()) {
1744
- return value.trim();
1745
- }
1746
- }
1747
- throw new McpBearerAuthError(
1748
- `No customer reference claim found (checked: ${claimPriority.join(", ")})`
1749
- );
1750
- }
1751
- function getCustomerRefFromBearerAuthHeader(authorization, options = {}) {
1752
- const token = extractBearerToken(authorization);
1753
- if (!token) {
1754
- throw new McpBearerAuthError("Missing bearer token");
1755
- }
1756
- const payload = decodeJwtPayload(token);
1757
- return getCustomerRefFromJwtPayload(payload, options);
1758
- }
1759
-
1760
- // src/mcp/auth-bridge.ts
1761
- function getClientId(payload, explicitClientId) {
1762
- if (explicitClientId) return explicitClientId;
1763
- const payloadClientId = typeof payload.client_id === "string" && payload.client_id || typeof payload.azp === "string" && payload.azp || typeof payload.aud === "string" && payload.aud || null;
1764
- return payloadClientId || "solvapay-mcp-client";
1765
- }
1766
- function getScopes(payload, defaultScopes) {
1767
- if (Array.isArray(payload.scp)) {
1768
- return payload.scp.filter((scope) => typeof scope === "string");
1769
- }
1770
- if (typeof payload.scope === "string" && payload.scope.trim()) {
1771
- return payload.scope.split(/\s+/).map((scope) => scope.trim()).filter(Boolean);
1772
- }
1773
- return defaultScopes;
1774
- }
1775
- function getExpiresAt(payload) {
1776
- return typeof payload.exp === "number" ? payload.exp : void 0;
1777
- }
1778
- function buildAuthInfoFromBearer(authorization, options = {}) {
1779
- const token = extractBearerToken(authorization);
1780
- if (!token) return null;
1781
- const payload = decodeJwtPayload(token);
1782
- const customerRef = getCustomerRefFromJwtPayload(payload, options);
1783
- const clientId = getClientId(payload, options.clientId);
1784
- const scopes = getScopes(payload, options.defaultScopes || []);
1785
- const expiresAt = getExpiresAt(payload);
1786
- return {
1787
- token,
1788
- clientId,
1789
- scopes,
1790
- expiresAt,
1791
- extra: {
1792
- customer_ref: customerRef,
1793
- ...options.includePayload ? { payload } : {}
1794
- }
1795
- };
1796
- }
1797
-
1798
- // src/mcp/oauth-bridge.ts
1799
- function withoutTrailingSlash(value) {
1800
- return value.replace(/\/$/, "");
1801
- }
1802
- function getRequestAuthHeader(req) {
1803
- const header = req.headers?.authorization;
1804
- if (typeof header === "string") return header;
1805
- if (Array.isArray(header)) return header[0] || null;
1806
- return null;
1807
- }
1808
- function getRequestJsonRpcId(body) {
1809
- if (body && typeof body === "object" && "id" in body) {
1810
- const id = body.id;
1811
- return id ?? null;
1956
+ // src/types/paywall.ts
1957
+ function isPaywallStructuredContent(value) {
1958
+ if (typeof value !== "object" || value === null || !("kind" in value)) {
1959
+ return false;
1812
1960
  }
1813
- return null;
1814
- }
1815
- function makeUnauthorizedJsonRpc(id) {
1816
- return {
1817
- jsonrpc: "2.0",
1818
- id,
1819
- error: {
1820
- code: -32001,
1821
- message: "Unauthorized"
1822
- }
1823
- };
1824
- }
1825
- function setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath) {
1826
- res.setHeader(
1827
- "WWW-Authenticate",
1828
- `Bearer resource_metadata="${withoutTrailingSlash(publicBaseUrl)}${protectedResourcePath}"`
1829
- );
1830
- }
1831
- function getOAuthProtectedResourceResponse(publicBaseUrl) {
1832
- const resource = withoutTrailingSlash(publicBaseUrl);
1833
- return {
1834
- resource,
1835
- authorization_servers: [resource],
1836
- scopes_supported: ["openid", "profile", "email"]
1837
- };
1838
- }
1839
- function getOAuthAuthorizationServerResponse({
1840
- apiBaseUrl,
1841
- productRef
1842
- }) {
1843
- const normalizedApiBaseUrl = withoutTrailingSlash(apiBaseUrl);
1844
- const registrationEndpoint = `${normalizedApiBaseUrl}/v1/customer/auth/register?product_ref=${encodeURIComponent(productRef)}`;
1845
- return {
1846
- issuer: normalizedApiBaseUrl,
1847
- authorization_endpoint: `${normalizedApiBaseUrl}/v1/customer/auth/authorize`,
1848
- token_endpoint: `${normalizedApiBaseUrl}/v1/customer/auth/token`,
1849
- registration_endpoint: registrationEndpoint,
1850
- token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
1851
- response_types_supported: ["code"],
1852
- grant_types_supported: ["authorization_code", "refresh_token"],
1853
- scopes_supported: ["openid", "profile", "email"],
1854
- code_challenge_methods_supported: ["S256"]
1855
- };
1856
- }
1857
- function createMcpOAuthBridge(options) {
1858
- const {
1859
- publicBaseUrl,
1860
- apiBaseUrl,
1861
- productRef,
1862
- mcpPath = "/mcp",
1863
- requireAuth = true,
1864
- authInfo,
1865
- protectedResourcePath = "/.well-known/oauth-protected-resource",
1866
- authorizationServerPath = "/.well-known/oauth-authorization-server"
1867
- } = options;
1868
- const protectedResourceMiddleware = (req, res, next) => {
1869
- if (req.method !== "GET" || req.path !== protectedResourcePath) {
1870
- next();
1871
- return;
1872
- }
1873
- res.json(getOAuthProtectedResourceResponse(publicBaseUrl));
1874
- };
1875
- const authorizationServerMiddleware = (req, res, next) => {
1876
- if (req.method !== "GET" || req.path !== authorizationServerPath) {
1877
- next();
1878
- return;
1879
- }
1880
- if (!productRef) {
1881
- res.status(500).json({ error: "SOLVAPAY_PRODUCT_REF missing" });
1882
- return;
1883
- }
1884
- res.json(
1885
- getOAuthAuthorizationServerResponse({
1886
- apiBaseUrl,
1887
- productRef
1888
- })
1889
- );
1890
- };
1891
- const mcpAuthMiddleware = (req, res, next) => {
1892
- if (req.path !== mcpPath) {
1893
- next();
1894
- return;
1895
- }
1896
- const authHeader = getRequestAuthHeader(req);
1897
- const id = getRequestJsonRpcId(req.body);
1898
- if (!authHeader && !requireAuth) {
1899
- next();
1900
- return;
1901
- }
1902
- try {
1903
- const auth = buildAuthInfoFromBearer(authHeader, authInfo);
1904
- if (!auth) {
1905
- throw new McpBearerAuthError("Missing bearer token");
1906
- }
1907
- req.auth = auth;
1908
- next();
1909
- } catch {
1910
- setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath);
1911
- if (req.method === "POST") {
1912
- res.status(401).json(makeUnauthorizedJsonRpc(id));
1913
- return;
1914
- }
1915
- res.status(401).json({ error: "Unauthorized" });
1916
- }
1917
- };
1918
- return [protectedResourceMiddleware, authorizationServerMiddleware, mcpAuthMiddleware];
1961
+ const kind = value.kind;
1962
+ return kind === "payment_required" || kind === "activation_required";
1919
1963
  }
1920
1964
 
1921
1965
  // src/helpers/error.ts
@@ -1943,26 +1987,128 @@ function handleRouteError(error, operationName, defaultMessage) {
1943
1987
  }
1944
1988
 
1945
1989
  // src/helpers/auth.ts
1990
+ function base64UrlDecode(input) {
1991
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/");
1992
+ const padding = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
1993
+ const base64 = padded + padding;
1994
+ if (typeof atob === "function") {
1995
+ const binary = atob(base64);
1996
+ let result = "";
1997
+ for (let i = 0; i < binary.length; i++) {
1998
+ result += String.fromCharCode(binary.charCodeAt(i));
1999
+ }
2000
+ try {
2001
+ return decodeURIComponent(escape(result));
2002
+ } catch {
2003
+ return result;
2004
+ }
2005
+ }
2006
+ const BufferCtor = globalThis.Buffer;
2007
+ if (BufferCtor) {
2008
+ return BufferCtor.from(base64, "base64").toString("utf-8");
2009
+ }
2010
+ throw new Error("No base64 decoder available in this runtime");
2011
+ }
2012
+ function decodeJwtUnverified(token) {
2013
+ const parts = token.split(".");
2014
+ if (parts.length !== 3) return null;
2015
+ try {
2016
+ const json = base64UrlDecode(parts[1]);
2017
+ const payload = JSON.parse(json);
2018
+ if (typeof payload !== "object" || payload === null) return null;
2019
+ return payload;
2020
+ } catch {
2021
+ return null;
2022
+ }
2023
+ }
2024
+ function extractBearerToken(request) {
2025
+ const authHeader = request.headers.get("authorization");
2026
+ if (!authHeader || !authHeader.toLowerCase().startsWith("bearer ")) return null;
2027
+ const token = authHeader.slice(7).trim();
2028
+ return token.length > 0 ? token : null;
2029
+ }
2030
+ function readEnv(name) {
2031
+ const proc = globalThis.process;
2032
+ return proc?.env?.[name];
2033
+ }
2034
+ function isStrictMode() {
2035
+ return readEnv("SOLVAPAY_AUTH_STRICT") === "true";
2036
+ }
2037
+ function getConfiguredSecret() {
2038
+ return readEnv("SOLVAPAY_JWT_SECRET") || readEnv("SUPABASE_JWT_SECRET");
2039
+ }
2040
+ async function verifyHs256(token, secret) {
2041
+ try {
2042
+ const { jwtVerify } = await import("jose");
2043
+ const key = new TextEncoder().encode(secret);
2044
+ const { payload } = await jwtVerify(token, key, { algorithms: ["HS256"] });
2045
+ return payload;
2046
+ } catch {
2047
+ return null;
2048
+ }
2049
+ }
2050
+ function pickName(payload) {
2051
+ const metadataFullName = typeof payload.user_metadata?.full_name === "string" ? payload.user_metadata.full_name : null;
2052
+ const metadataName = typeof payload.user_metadata?.name === "string" ? payload.user_metadata.name : null;
2053
+ const claimName = typeof payload.name === "string" ? payload.name : null;
2054
+ return metadataFullName || metadataName || claimName || null;
2055
+ }
2056
+ function pickEmail(payload) {
2057
+ return typeof payload.email === "string" ? payload.email : null;
2058
+ }
2059
+ function unauthorized(details) {
2060
+ return { error: "Unauthorized", status: 401, details };
2061
+ }
1946
2062
  async function getAuthenticatedUserCore(request, options = {}) {
1947
2063
  try {
1948
- const { requireUserId, getUserEmailFromRequest, getUserNameFromRequest } = await import("@solvapay/auth");
1949
- const userIdOrError = requireUserId(request);
1950
- if (userIdOrError instanceof Response) {
1951
- const clonedResponse = userIdOrError.clone();
1952
- const body = await clonedResponse.json().catch(() => ({ error: "Unauthorized" }));
1953
- return {
1954
- error: body.error || "Unauthorized",
1955
- status: userIdOrError.status,
1956
- details: body.error || "Unauthorized"
1957
- };
2064
+ const includeEmail = options.includeEmail !== false;
2065
+ const includeName = options.includeName !== false;
2066
+ const headerUserId = request.headers.get("x-user-id");
2067
+ if (headerUserId) {
2068
+ let email = null;
2069
+ let name = null;
2070
+ if (includeEmail || includeName) {
2071
+ const token2 = extractBearerToken(request);
2072
+ if (token2) {
2073
+ const secret2 = getConfiguredSecret();
2074
+ const payload2 = secret2 ? await verifyHs256(token2, secret2) : isStrictMode() ? null : decodeJwtUnverified(token2);
2075
+ if (payload2) {
2076
+ if (includeEmail) email = pickEmail(payload2);
2077
+ if (includeName) name = pickName(payload2);
2078
+ }
2079
+ }
2080
+ }
2081
+ return { userId: headerUserId, email, name };
2082
+ }
2083
+ const token = extractBearerToken(request);
2084
+ if (!token) {
2085
+ return unauthorized("User ID not found. Ensure middleware is configured.");
2086
+ }
2087
+ const secret = getConfiguredSecret();
2088
+ let payload = null;
2089
+ if (secret) {
2090
+ payload = await verifyHs256(token, secret);
2091
+ if (!payload) {
2092
+ return unauthorized("Invalid or expired authentication token");
2093
+ }
2094
+ } else if (isStrictMode()) {
2095
+ return unauthorized(
2096
+ "Strict auth mode is enabled but no JWT secret is configured. Set SOLVAPAY_JWT_SECRET or SUPABASE_JWT_SECRET."
2097
+ );
2098
+ } else {
2099
+ payload = decodeJwtUnverified(token);
2100
+ if (!payload) {
2101
+ return unauthorized("Malformed authentication token");
2102
+ }
2103
+ }
2104
+ const userId = typeof payload.sub === "string" ? payload.sub : null;
2105
+ if (!userId) {
2106
+ return unauthorized("Authentication token missing subject (sub) claim");
1958
2107
  }
1959
- const userId = userIdOrError;
1960
- const email = options.includeEmail !== false ? await getUserEmailFromRequest(request) : null;
1961
- const name = options.includeName !== false ? await getUserNameFromRequest(request) : null;
1962
2108
  return {
1963
2109
  userId,
1964
- email,
1965
- name
2110
+ email: includeEmail ? pickEmail(payload) : null,
2111
+ name: includeName ? pickName(payload) : null
1966
2112
  };
1967
2113
  } catch (error) {
1968
2114
  return handleRouteError(error, "Get authenticated user", "Authentication failed");
@@ -2355,9 +2501,34 @@ async function activatePlanCore(request, body, options = {}) {
2355
2501
  }
2356
2502
  }
2357
2503
 
2504
+ // src/helpers/payment-method.ts
2505
+ async function getPaymentMethodCore(request, options = {}) {
2506
+ try {
2507
+ const customerResult = await syncCustomerCore(request, {
2508
+ solvaPay: options.solvaPay,
2509
+ includeEmail: options.includeEmail,
2510
+ includeName: options.includeName
2511
+ });
2512
+ if (isErrorResult(customerResult)) {
2513
+ return customerResult;
2514
+ }
2515
+ const customerRef = customerResult;
2516
+ const solvaPay = options.solvaPay || createSolvaPay();
2517
+ if (!solvaPay.apiClient.getPaymentMethod) {
2518
+ return {
2519
+ error: "getPaymentMethod is not implemented on this API client",
2520
+ status: 500
2521
+ };
2522
+ }
2523
+ return await solvaPay.apiClient.getPaymentMethod({ customerRef });
2524
+ } catch (error) {
2525
+ return handleRouteError(error, "Get payment method", "Failed to load payment method");
2526
+ }
2527
+ }
2528
+
2358
2529
  // src/helpers/plans.ts
2359
2530
  import { getSolvaPayConfig as getSolvaPayConfig2 } from "@solvapay/core";
2360
- async function listPlansCore(request) {
2531
+ async function listPlansCore(request, options = {}) {
2361
2532
  try {
2362
2533
  const url = new URL(request.url);
2363
2534
  const productRef = url.searchParams.get("productRef");
@@ -2367,19 +2538,20 @@ async function listPlansCore(request) {
2367
2538
  status: 400
2368
2539
  };
2369
2540
  }
2370
- const config = getSolvaPayConfig2();
2371
- const solvapaySecretKey = config.apiKey;
2372
- const solvapayApiBaseUrl = config.apiBaseUrl;
2373
- if (!solvapaySecretKey) {
2541
+ const apiClient = options.solvaPay?.apiClient ?? (() => {
2542
+ const config = getSolvaPayConfig2();
2543
+ if (!config.apiKey) return null;
2544
+ return createSolvaPayClient({
2545
+ apiKey: config.apiKey,
2546
+ apiBaseUrl: config.apiBaseUrl
2547
+ });
2548
+ })();
2549
+ if (!apiClient) {
2374
2550
  return {
2375
2551
  error: "Server configuration error: SolvaPay secret key not configured",
2376
2552
  status: 500
2377
2553
  };
2378
2554
  }
2379
- const apiClient = createSolvaPayClient({
2380
- apiKey: solvapaySecretKey,
2381
- apiBaseUrl: solvapayApiBaseUrl
2382
- });
2383
2555
  if (!apiClient.listPlans) {
2384
2556
  return {
2385
2557
  error: "List plans method not available",
@@ -2396,6 +2568,76 @@ async function listPlansCore(request) {
2396
2568
  }
2397
2569
  }
2398
2570
 
2571
+ // src/helpers/merchant.ts
2572
+ import { getSolvaPayConfig as getSolvaPayConfig3 } from "@solvapay/core";
2573
+ async function getMerchantCore(_request, options = {}) {
2574
+ try {
2575
+ const apiClient = options.solvaPay?.apiClient ?? (() => {
2576
+ const config = getSolvaPayConfig3();
2577
+ if (!config.apiKey) return null;
2578
+ return createSolvaPayClient({
2579
+ apiKey: config.apiKey,
2580
+ apiBaseUrl: config.apiBaseUrl
2581
+ });
2582
+ })();
2583
+ if (!apiClient) {
2584
+ return {
2585
+ error: "Server configuration error: SolvaPay secret key not configured",
2586
+ status: 500
2587
+ };
2588
+ }
2589
+ if (!apiClient.getMerchant) {
2590
+ return {
2591
+ error: "Get merchant method not available",
2592
+ status: 500
2593
+ };
2594
+ }
2595
+ const merchant = await apiClient.getMerchant();
2596
+ return merchant;
2597
+ } catch (error) {
2598
+ return handleRouteError(error, "Get merchant", "Failed to fetch merchant");
2599
+ }
2600
+ }
2601
+
2602
+ // src/helpers/product.ts
2603
+ import { getSolvaPayConfig as getSolvaPayConfig4 } from "@solvapay/core";
2604
+ async function getProductCore(request, options = {}) {
2605
+ try {
2606
+ const url = new URL(request.url);
2607
+ const productRef = url.searchParams.get("productRef");
2608
+ if (!productRef) {
2609
+ return {
2610
+ error: "Missing required parameter: productRef",
2611
+ status: 400
2612
+ };
2613
+ }
2614
+ const apiClient = options.solvaPay?.apiClient ?? (() => {
2615
+ const config = getSolvaPayConfig4();
2616
+ if (!config.apiKey) return null;
2617
+ return createSolvaPayClient({
2618
+ apiKey: config.apiKey,
2619
+ apiBaseUrl: config.apiBaseUrl
2620
+ });
2621
+ })();
2622
+ if (!apiClient) {
2623
+ return {
2624
+ error: "Server configuration error: SolvaPay secret key not configured",
2625
+ status: 500
2626
+ };
2627
+ }
2628
+ if (!apiClient.getProduct) {
2629
+ return {
2630
+ error: "Get product method not available",
2631
+ status: 500
2632
+ };
2633
+ }
2634
+ const product = await apiClient.getProduct(productRef);
2635
+ return product;
2636
+ } catch (error) {
2637
+ return handleRouteError(error, "Get product", "Failed to fetch product");
2638
+ }
2639
+ }
2640
+
2399
2641
  // src/helpers/purchase.ts
2400
2642
  async function checkPurchaseCore(request, options = {}) {
2401
2643
  try {
@@ -2453,6 +2695,37 @@ async function checkPurchaseCore(request, options = {}) {
2453
2695
  }
2454
2696
 
2455
2697
  // src/helpers/usage.ts
2698
+ async function getUsageCore(request, options = {}) {
2699
+ const purchaseResult = await checkPurchaseCore(request, options);
2700
+ if (isErrorResult(purchaseResult)) return purchaseResult;
2701
+ const activePurchase = (purchaseResult.purchases ?? []).find((p) => p.status === "active");
2702
+ if (!activePurchase) {
2703
+ return {
2704
+ meterRef: null,
2705
+ total: null,
2706
+ used: 0,
2707
+ remaining: null,
2708
+ percentUsed: null
2709
+ };
2710
+ }
2711
+ const snap = activePurchase.planSnapshot;
2712
+ const usage = activePurchase.usage;
2713
+ const meterRef = snap?.meterRef ?? snap?.meterId ?? null;
2714
+ const total = typeof snap?.limit === "number" ? snap.limit : null;
2715
+ const used = typeof usage?.used === "number" ? usage.used : 0;
2716
+ const remaining = total !== null ? Math.max(0, total - used) : null;
2717
+ const percentUsed = total !== null && total > 0 ? Math.min(100, Math.round(used / total * 1e4) / 100) : null;
2718
+ return {
2719
+ meterRef,
2720
+ total,
2721
+ used,
2722
+ remaining,
2723
+ percentUsed,
2724
+ ...usage?.periodStart ? { periodStart: usage.periodStart } : {},
2725
+ ...usage?.periodEnd ? { periodEnd: usage.periodEnd } : {},
2726
+ purchaseRef: activePurchase.reference
2727
+ };
2728
+ }
2456
2729
  async function trackUsageCore(request, body, options = {}) {
2457
2730
  try {
2458
2731
  const userResult = await getAuthenticatedUserCore(request);
@@ -2517,31 +2790,30 @@ function verifyWebhook({
2517
2790
  }
2518
2791
  }
2519
2792
  export {
2520
- McpBearerAuthError,
2521
2793
  PaywallError,
2522
2794
  VIRTUAL_TOOL_DEFINITIONS,
2523
2795
  activatePlanCore,
2524
- buildAuthInfoFromBearer,
2796
+ buildGateMessage,
2797
+ buildNudgeMessage,
2525
2798
  cancelPurchaseCore,
2526
2799
  checkPurchaseCore,
2800
+ classifyPaywallState,
2527
2801
  createCheckoutSessionCore,
2528
2802
  createCustomerSessionCore,
2529
- createMcpOAuthBridge,
2530
2803
  createPaymentIntentCore,
2531
2804
  createSolvaPay,
2532
2805
  createSolvaPayClient,
2533
2806
  createTopupPaymentIntentCore,
2534
2807
  createVirtualTools,
2535
- decodeJwtPayload,
2536
- extractBearerToken,
2537
2808
  getAuthenticatedUserCore,
2538
2809
  getCustomerBalanceCore,
2539
- getCustomerRefFromBearerAuthHeader,
2540
- getCustomerRefFromJwtPayload,
2541
- getOAuthAuthorizationServerResponse,
2542
- getOAuthProtectedResourceResponse,
2810
+ getMerchantCore,
2811
+ getPaymentMethodCore,
2812
+ getProductCore,
2813
+ getUsageCore,
2543
2814
  handleRouteError,
2544
2815
  isErrorResult,
2816
+ isPaywallStructuredContent,
2545
2817
  jsonSchemaToZodRawShape,
2546
2818
  listPlansCore,
2547
2819
  paywallErrorToClientPayload,