@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.
- package/README.md +80 -2
- package/dist/edge.d.ts +495 -61
- package/dist/edge.js +648 -187
- package/dist/fetch/index.cjs +3071 -0
- package/dist/fetch/index.d.cts +112 -0
- package/dist/fetch/index.d.ts +112 -0
- package/dist/fetch/index.js +3017 -0
- package/dist/index.cjs +603 -5013
- package/dist/index.d.cts +490 -156
- package/dist/index.d.ts +490 -156
- package/dist/index.js +571 -695
- package/package.json +15 -12
- package/dist/chunk-MLKGABMK.js +0 -9
- package/dist/chunk-R5U7XKVJ.js +0 -16
- package/dist/dist-EPVKJAIP.js +0 -94
- package/dist/dist-JBJ4HMP7.js +0 -94
- package/dist/esm-5GYCIXIY.js +0 -3475
- package/dist/esm-UW7WCMEK.js +0 -3475
- package/dist/src/adapters/base.d.ts +0 -57
- package/dist/src/adapters/base.d.ts.map +0 -1
- package/dist/src/adapters/base.js +0 -73
- package/dist/src/adapters/base.js.map +0 -1
- package/dist/src/adapters/http.d.ts +0 -25
- package/dist/src/adapters/http.d.ts.map +0 -1
- package/dist/src/adapters/http.js +0 -99
- package/dist/src/adapters/http.js.map +0 -1
- package/dist/src/adapters/index.d.ts +0 -11
- package/dist/src/adapters/index.d.ts.map +0 -1
- package/dist/src/adapters/index.js +0 -10
- package/dist/src/adapters/index.js.map +0 -1
- package/dist/src/adapters/mcp.d.ts +0 -24
- package/dist/src/adapters/mcp.d.ts.map +0 -1
- package/dist/src/adapters/mcp.js +0 -75
- package/dist/src/adapters/mcp.js.map +0 -1
- package/dist/src/adapters/next.d.ts +0 -24
- package/dist/src/adapters/next.d.ts.map +0 -1
- package/dist/src/adapters/next.js +0 -109
- package/dist/src/adapters/next.js.map +0 -1
- package/dist/src/client.d.ts +0 -58
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js +0 -495
- package/dist/src/client.js.map +0 -1
- package/dist/src/edge.d.ts +0 -22
- package/dist/src/edge.d.ts.map +0 -1
- package/dist/src/edge.js +0 -91
- package/dist/src/edge.js.map +0 -1
- package/dist/src/factory.d.ts +0 -605
- package/dist/src/factory.d.ts.map +0 -1
- package/dist/src/factory.js +0 -215
- package/dist/src/factory.js.map +0 -1
- package/dist/src/helpers/auth.d.ts +0 -47
- package/dist/src/helpers/auth.d.ts.map +0 -1
- package/dist/src/helpers/auth.js +0 -73
- package/dist/src/helpers/auth.js.map +0 -1
- package/dist/src/helpers/checkout.d.ts +0 -45
- package/dist/src/helpers/checkout.d.ts.map +0 -1
- package/dist/src/helpers/checkout.js +0 -89
- package/dist/src/helpers/checkout.js.map +0 -1
- package/dist/src/helpers/customer.d.ts +0 -51
- package/dist/src/helpers/customer.d.ts.map +0 -1
- package/dist/src/helpers/customer.js +0 -77
- package/dist/src/helpers/customer.js.map +0 -1
- package/dist/src/helpers/error.d.ts +0 -15
- package/dist/src/helpers/error.d.ts.map +0 -1
- package/dist/src/helpers/error.js +0 -35
- package/dist/src/helpers/error.js.map +0 -1
- package/dist/src/helpers/index.d.ts +0 -17
- package/dist/src/helpers/index.d.ts.map +0 -1
- package/dist/src/helpers/index.js +0 -23
- package/dist/src/helpers/index.js.map +0 -1
- package/dist/src/helpers/payment.d.ts +0 -107
- package/dist/src/helpers/payment.d.ts.map +0 -1
- package/dist/src/helpers/payment.js +0 -150
- package/dist/src/helpers/payment.js.map +0 -1
- package/dist/src/helpers/plans.d.ts +0 -19
- package/dist/src/helpers/plans.d.ts.map +0 -1
- package/dist/src/helpers/plans.js +0 -53
- package/dist/src/helpers/plans.js.map +0 -1
- package/dist/src/helpers/renewal.d.ts +0 -23
- package/dist/src/helpers/renewal.d.ts.map +0 -1
- package/dist/src/helpers/renewal.js +0 -90
- package/dist/src/helpers/renewal.js.map +0 -1
- package/dist/src/helpers/subscription.d.ts +0 -23
- package/dist/src/helpers/subscription.d.ts.map +0 -1
- package/dist/src/helpers/subscription.js +0 -101
- package/dist/src/helpers/subscription.js.map +0 -1
- package/dist/src/helpers/types.d.ts +0 -22
- package/dist/src/helpers/types.d.ts.map +0 -1
- package/dist/src/helpers/types.js +0 -7
- package/dist/src/helpers/types.js.map +0 -1
- package/dist/src/index.d.ts +0 -73
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -106
- package/dist/src/index.js.map +0 -1
- package/dist/src/mcp/auth-bridge.d.ts +0 -9
- package/dist/src/mcp/auth-bridge.d.ts.map +0 -1
- package/dist/src/mcp/auth-bridge.js +0 -46
- package/dist/src/mcp/auth-bridge.js.map +0 -1
- package/dist/src/mcp/oauth-bridge.d.ts +0 -48
- package/dist/src/mcp/oauth-bridge.d.ts.map +0 -1
- package/dist/src/mcp/oauth-bridge.js +0 -110
- package/dist/src/mcp/oauth-bridge.js.map +0 -1
- package/dist/src/mcp-auth.d.ts +0 -17
- package/dist/src/mcp-auth.d.ts.map +0 -1
- package/dist/src/mcp-auth.js +0 -57
- package/dist/src/mcp-auth.js.map +0 -1
- package/dist/src/paywall.d.ts +0 -119
- package/dist/src/paywall.d.ts.map +0 -1
- package/dist/src/paywall.js +0 -700
- package/dist/src/paywall.js.map +0 -1
- package/dist/src/register-virtual-tools-mcp.d.ts +0 -23
- package/dist/src/register-virtual-tools-mcp.d.ts.map +0 -1
- package/dist/src/register-virtual-tools-mcp.js +0 -86
- package/dist/src/register-virtual-tools-mcp.js.map +0 -1
- package/dist/src/types/client.d.ts +0 -216
- package/dist/src/types/client.d.ts.map +0 -1
- package/dist/src/types/client.js +0 -7
- package/dist/src/types/client.js.map +0 -1
- package/dist/src/types/generated.d.ts +0 -2834
- package/dist/src/types/generated.d.ts.map +0 -1
- package/dist/src/types/generated.js +0 -6
- package/dist/src/types/generated.js.map +0 -1
- package/dist/src/types/index.d.ts +0 -13
- package/dist/src/types/index.d.ts.map +0 -1
- package/dist/src/types/index.js +0 -8
- package/dist/src/types/index.js.map +0 -1
- package/dist/src/types/options.d.ts +0 -126
- package/dist/src/types/options.d.ts.map +0 -1
- package/dist/src/types/options.js +0 -8
- package/dist/src/types/options.js.map +0 -1
- package/dist/src/types/paywall.d.ts +0 -64
- package/dist/src/types/paywall.d.ts.map +0 -1
- package/dist/src/types/paywall.js +0 -7
- package/dist/src/types/paywall.js.map +0 -1
- package/dist/src/types/webhook.d.ts +0 -50
- package/dist/src/types/webhook.d.ts.map +0 -1
- package/dist/src/types/webhook.js +0 -2
- package/dist/src/types/webhook.js.map +0 -1
- package/dist/src/utils.d.ts +0 -110
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/src/utils.js +0 -217
- package/dist/src/utils.js.map +0 -1
- package/dist/src/virtual-tools.d.ts +0 -44
- package/dist/src/virtual-tools.d.ts.map +0 -1
- package/dist/src/virtual-tools.js +0 -140
- package/dist/src/virtual-tools.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/webapi-K5XBCEO6.js +0 -3775
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
|
|
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) {
|
|
@@ -124,6 +142,20 @@ function createSolvaPayClient(opts) {
|
|
|
124
142
|
}
|
|
125
143
|
return res.json();
|
|
126
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
|
+
},
|
|
127
159
|
// GET: /v1/sdk/products/{productRef}
|
|
128
160
|
async getProduct(productRef) {
|
|
129
161
|
const url = `${base}/v1/sdk/products/${encodeURIComponent(productRef)}`;
|
|
@@ -550,10 +582,67 @@ function createSolvaPayClient(opts) {
|
|
|
550
582
|
throw new SolvaPayError(`Activate plan failed (${res.status}): ${error}`);
|
|
551
583
|
}
|
|
552
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();
|
|
553
596
|
}
|
|
554
597
|
};
|
|
555
598
|
}
|
|
556
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
|
+
|
|
557
646
|
// src/utils.ts
|
|
558
647
|
async function withRetry(fn, options = {}) {
|
|
559
648
|
const {
|
|
@@ -720,6 +809,10 @@ function paywallErrorToClientPayload(error) {
|
|
|
720
809
|
if (sc.balance !== void 0) base.balance = sc.balance;
|
|
721
810
|
if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
|
|
722
811
|
if (sc.confirmationUrl !== void 0) base.confirmationUrl = sc.confirmationUrl;
|
|
812
|
+
} else {
|
|
813
|
+
base.kind = "payment_required";
|
|
814
|
+
if (sc.balance !== void 0) base.balance = sc.balance;
|
|
815
|
+
if (sc.productDetails !== void 0) base.productDetails = sc.productDetails;
|
|
723
816
|
}
|
|
724
817
|
return base;
|
|
725
818
|
}
|
|
@@ -731,6 +824,7 @@ var sharedCustomerLookupDeduplicator = createRequestDeduplicator({
|
|
|
731
824
|
cacheErrors: false
|
|
732
825
|
// Don't cache errors - retry on next request
|
|
733
826
|
});
|
|
827
|
+
var EXTRA_FORWARD_KEY = "__solvapayExtra";
|
|
734
828
|
var SolvaPayPaywall = class {
|
|
735
829
|
constructor(apiClient, options = {}) {
|
|
736
830
|
this.apiClient = apiClient;
|
|
@@ -756,136 +850,216 @@ var SolvaPayPaywall = class {
|
|
|
756
850
|
return `solvapay_${timestamp}_${random}`;
|
|
757
851
|
}
|
|
758
852
|
/**
|
|
759
|
-
*
|
|
853
|
+
* Pure decision routine — performs customer resolution, limits cache
|
|
854
|
+
* lookup / fresh `checkLimits` fetch, and returns a `PaywallDecision`
|
|
855
|
+
* describing whether the handler should run.
|
|
856
|
+
*
|
|
857
|
+
* Side effects kept in lockstep with the legacy `protect()` path:
|
|
858
|
+
* - creates the backend customer on first use (`ensureCustomer`),
|
|
859
|
+
* - updates the limits cache (consume-one-unit bookkeeping), and
|
|
860
|
+
* - emits a `paywall` usage event on gate outcomes.
|
|
861
|
+
*
|
|
862
|
+
* `trackUsage` for the `success` / `fail` outcome is emitted by the
|
|
863
|
+
* caller (adapter or `protect()`) once it has actually invoked the
|
|
864
|
+
* handler — `decide()` never counts handler execution as usage.
|
|
865
|
+
*
|
|
866
|
+
* @since 1.1.0
|
|
760
867
|
*/
|
|
761
|
-
|
|
762
|
-
async protect(handler, metadata = {}, getCustomerRef) {
|
|
868
|
+
async decide(args, metadata = {}, getCustomerRef) {
|
|
763
869
|
const product = this.resolveProduct(metadata);
|
|
764
870
|
const usageType = metadata.usageType || "requests";
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
871
|
+
const requestId = this.generateRequestId();
|
|
872
|
+
const startTime = Date.now();
|
|
873
|
+
const inputCustomerRef = getCustomerRef ? getCustomerRef(args) : args.auth?.customer_ref || "anonymous";
|
|
874
|
+
let backendCustomerRef;
|
|
875
|
+
if (inputCustomerRef.startsWith("cus_")) {
|
|
876
|
+
backendCustomerRef = inputCustomerRef;
|
|
877
|
+
} else {
|
|
878
|
+
backendCustomerRef = await this.ensureCustomer(inputCustomerRef, inputCustomerRef);
|
|
879
|
+
}
|
|
880
|
+
const limitsCacheKey = `${backendCustomerRef}:${product}:${usageType}`;
|
|
881
|
+
const cachedLimits = this.limitsCache.get(limitsCacheKey);
|
|
882
|
+
const now = Date.now();
|
|
883
|
+
let withinLimits;
|
|
884
|
+
let remaining;
|
|
885
|
+
let checkoutUrl;
|
|
886
|
+
let resolvedMeterName;
|
|
887
|
+
let lastLimitsCheck;
|
|
888
|
+
const hasFreshCachedLimits = cachedLimits && now - cachedLimits.timestamp < this.limitsCacheTTL;
|
|
889
|
+
if (hasFreshCachedLimits) {
|
|
890
|
+
checkoutUrl = cachedLimits.checkoutUrl;
|
|
891
|
+
resolvedMeterName = cachedLimits.meterName;
|
|
892
|
+
lastLimitsCheck = cachedLimits.limits;
|
|
893
|
+
if (cachedLimits.remaining > 0) {
|
|
894
|
+
cachedLimits.remaining--;
|
|
895
|
+
if (cachedLimits.remaining <= 0) {
|
|
896
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
897
|
+
}
|
|
898
|
+
withinLimits = true;
|
|
899
|
+
remaining = cachedLimits.remaining;
|
|
772
900
|
} else {
|
|
773
|
-
|
|
901
|
+
withinLimits = false;
|
|
902
|
+
remaining = 0;
|
|
903
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
774
904
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
905
|
+
} else {
|
|
906
|
+
if (cachedLimits) {
|
|
907
|
+
this.limitsCache.delete(limitsCacheKey);
|
|
908
|
+
}
|
|
909
|
+
const limitsCheck = await this.apiClient.checkLimits({
|
|
910
|
+
customerRef: backendCustomerRef,
|
|
911
|
+
productRef: product,
|
|
912
|
+
meterName: usageType
|
|
913
|
+
});
|
|
914
|
+
lastLimitsCheck = limitsCheck;
|
|
915
|
+
withinLimits = limitsCheck.withinLimits;
|
|
916
|
+
remaining = limitsCheck.remaining;
|
|
917
|
+
checkoutUrl = limitsCheck.checkoutUrl;
|
|
918
|
+
resolvedMeterName = limitsCheck.meterName;
|
|
919
|
+
const consumedAllowance = withinLimits && remaining > 0;
|
|
920
|
+
if (consumedAllowance) {
|
|
921
|
+
remaining = Math.max(0, remaining - 1);
|
|
922
|
+
}
|
|
923
|
+
if (consumedAllowance) {
|
|
924
|
+
this.limitsCache.set(limitsCacheKey, {
|
|
925
|
+
remaining,
|
|
926
|
+
checkoutUrl,
|
|
927
|
+
meterName: resolvedMeterName,
|
|
928
|
+
timestamp: now,
|
|
929
|
+
limits: limitsCheck
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (!withinLimits) {
|
|
934
|
+
const latencyMs = Date.now() - startTime;
|
|
935
|
+
this.trackUsage(
|
|
936
|
+
backendCustomerRef,
|
|
937
|
+
product,
|
|
938
|
+
resolvedMeterName || usageType,
|
|
939
|
+
"paywall",
|
|
940
|
+
requestId,
|
|
941
|
+
latencyMs
|
|
942
|
+
);
|
|
943
|
+
const state = classifyPaywallState(lastLimitsCheck ?? null);
|
|
944
|
+
const preMessageGate = lastLimitsCheck?.activationRequired ? {
|
|
945
|
+
kind: "activation_required",
|
|
946
|
+
product,
|
|
947
|
+
message: "",
|
|
948
|
+
checkoutUrl: lastLimitsCheck.confirmationUrl || lastLimitsCheck.checkoutUrl || checkoutUrl || "",
|
|
949
|
+
...lastLimitsCheck.confirmationUrl !== void 0 ? { confirmationUrl: lastLimitsCheck.confirmationUrl } : {},
|
|
950
|
+
...lastLimitsCheck.plans !== void 0 ? { plans: lastLimitsCheck.plans } : {},
|
|
951
|
+
...lastLimitsCheck.balance !== void 0 ? { balance: lastLimitsCheck.balance } : {},
|
|
952
|
+
...lastLimitsCheck.product !== void 0 ? { productDetails: lastLimitsCheck.product } : {}
|
|
953
|
+
} : {
|
|
954
|
+
kind: "payment_required",
|
|
955
|
+
product,
|
|
956
|
+
checkoutUrl: checkoutUrl || "",
|
|
957
|
+
message: "",
|
|
958
|
+
...lastLimitsCheck?.balance !== void 0 ? { balance: lastLimitsCheck.balance } : {},
|
|
959
|
+
...lastLimitsCheck?.product !== void 0 ? { productDetails: lastLimitsCheck.product } : {}
|
|
960
|
+
};
|
|
961
|
+
const gate = {
|
|
962
|
+
...preMessageGate,
|
|
963
|
+
message: buildGateMessage(state, preMessageGate)
|
|
964
|
+
};
|
|
965
|
+
return {
|
|
966
|
+
outcome: "gate",
|
|
967
|
+
gate,
|
|
968
|
+
limits: lastLimitsCheck ?? null,
|
|
969
|
+
customerRef: backendCustomerRef
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
outcome: "allow",
|
|
974
|
+
args,
|
|
975
|
+
limits: lastLimitsCheck,
|
|
976
|
+
customerRef: backendCustomerRef
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Execute the handler for an already-obtained `allow` decision and
|
|
981
|
+
* emit the post-handler `trackUsage('success' | 'fail', ...)` event.
|
|
982
|
+
*
|
|
983
|
+
* Exposed for adapter integration — the adapter layer drives the
|
|
984
|
+
* paywall through `decide()` + `runAllow()` so `formatGate` can own
|
|
985
|
+
* gate outcomes without routing through `PaywallError`. `protect()`
|
|
986
|
+
* continues to offer the self-contained throw-based surface for
|
|
987
|
+
* legacy consumers.
|
|
988
|
+
*
|
|
989
|
+
* `runAllow` intentionally does NOT re-throw `PaywallError` — if a
|
|
990
|
+
* handler calls `ctx.gate(reason)` and throws from deep code, the
|
|
991
|
+
* adapter catches that at the `formatGate` boundary instead.
|
|
992
|
+
*
|
|
993
|
+
* @since 1.1.0
|
|
994
|
+
*/
|
|
995
|
+
async runAllow(decision, handler, metadata, args) {
|
|
996
|
+
const product = this.resolveProduct(metadata);
|
|
997
|
+
const usageType = metadata.usageType || "requests";
|
|
998
|
+
const requestId = this.generateRequestId();
|
|
999
|
+
const startTime = Date.now();
|
|
1000
|
+
const forwardedExtra = args[EXTRA_FORWARD_KEY];
|
|
1001
|
+
const handlerContext = {
|
|
1002
|
+
customerRef: decision.customerRef,
|
|
1003
|
+
limits: decision.limits,
|
|
1004
|
+
...forwardedExtra !== void 0 ? { extra: forwardedExtra } : {}
|
|
1005
|
+
};
|
|
1006
|
+
try {
|
|
1007
|
+
const result = await handler(args, handlerContext);
|
|
1008
|
+
const latencyMs = Date.now() - startTime;
|
|
1009
|
+
this.trackUsage(
|
|
1010
|
+
decision.customerRef,
|
|
1011
|
+
product,
|
|
1012
|
+
decision.limits.meterName || usageType,
|
|
1013
|
+
"success",
|
|
1014
|
+
requestId,
|
|
1015
|
+
latencyMs
|
|
1016
|
+
);
|
|
1017
|
+
return result;
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
if (error instanceof Error) {
|
|
1020
|
+
const errorType = error instanceof PaywallError ? "PaywallError" : "API Error";
|
|
1021
|
+
this.log(`\u274C Error in paywall [${errorType}]: ${error.message}`);
|
|
1022
|
+
} else {
|
|
1023
|
+
this.log(`\u274C Error in paywall:`, error);
|
|
1024
|
+
}
|
|
1025
|
+
if (!(error instanceof PaywallError)) {
|
|
859
1026
|
const latencyMs = Date.now() - startTime;
|
|
860
1027
|
this.trackUsage(
|
|
861
|
-
|
|
1028
|
+
decision.customerRef,
|
|
862
1029
|
product,
|
|
863
|
-
|
|
864
|
-
"
|
|
1030
|
+
decision.limits.meterName || usageType,
|
|
1031
|
+
"fail",
|
|
865
1032
|
requestId,
|
|
866
1033
|
latencyMs
|
|
867
1034
|
);
|
|
868
|
-
return result;
|
|
869
|
-
} catch (error) {
|
|
870
|
-
if (error instanceof Error) {
|
|
871
|
-
const errorType = error instanceof PaywallError ? "PaywallError" : "API Error";
|
|
872
|
-
this.log(`\u274C Error in paywall [${errorType}]: ${error.message}`);
|
|
873
|
-
} else {
|
|
874
|
-
this.log(`\u274C Error in paywall:`, error);
|
|
875
|
-
}
|
|
876
|
-
if (!(error instanceof PaywallError)) {
|
|
877
|
-
const latencyMs = Date.now() - startTime;
|
|
878
|
-
this.trackUsage(
|
|
879
|
-
backendCustomerRef,
|
|
880
|
-
product,
|
|
881
|
-
resolvedMeterName || usageType,
|
|
882
|
-
"fail",
|
|
883
|
-
requestId,
|
|
884
|
-
latencyMs
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
throw error;
|
|
888
1035
|
}
|
|
1036
|
+
throw error;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Core protection method - works for both MCP and HTTP
|
|
1041
|
+
*
|
|
1042
|
+
* The `handler` may optionally declare a second positional argument
|
|
1043
|
+
* of type `ProtectHandlerContext` to receive the resolved customer
|
|
1044
|
+
* ref, the pre-check `LimitResponseWithPlan`, and an opaque `extra`
|
|
1045
|
+
* bag threaded through from the adapter layer. One-arg handlers
|
|
1046
|
+
* ignore the second argument and continue to work unchanged.
|
|
1047
|
+
*
|
|
1048
|
+
* Implemented on top of `decide()`: pre-check runs through the same
|
|
1049
|
+
* decision routine, and gate outcomes are raised as a `PaywallError`
|
|
1050
|
+
* to preserve the legacy throw-based signal for consumers that
|
|
1051
|
+
* haven't migrated to adapter-level `formatGate` routing.
|
|
1052
|
+
*/
|
|
1053
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1054
|
+
async protect(handler, metadata = {}, getCustomerRef) {
|
|
1055
|
+
return async (args) => {
|
|
1056
|
+
const decision = await this.decide(args, metadata, getCustomerRef);
|
|
1057
|
+
if (decision.outcome === "gate") {
|
|
1058
|
+
const message = decision.gate.kind === "activation_required" ? "Activation required" : "Payment required";
|
|
1059
|
+
this.log(`\u274C Error in paywall [PaywallError]: ${message}`);
|
|
1060
|
+
throw new PaywallError(message, decision.gate);
|
|
1061
|
+
}
|
|
1062
|
+
return this.runAllow(decision, handler, metadata, args);
|
|
889
1063
|
};
|
|
890
1064
|
}
|
|
891
1065
|
/**
|
|
@@ -988,6 +1162,16 @@ var SolvaPayPaywall = class {
|
|
|
988
1162
|
this.log(
|
|
989
1163
|
`\u26A0\uFE0F Resolved customer ${customerRef} by email after conflict; using existing customer ${byEmail.customerRef}`
|
|
990
1164
|
);
|
|
1165
|
+
if (!byEmail.externalRef && this.apiClient.updateCustomer) {
|
|
1166
|
+
try {
|
|
1167
|
+
await this.apiClient.updateCustomer(byEmail.customerRef, { externalRef });
|
|
1168
|
+
} catch (backfillError) {
|
|
1169
|
+
this.log(
|
|
1170
|
+
`\u26A0\uFE0F Failed to backfill externalRef on ${byEmail.customerRef}:`,
|
|
1171
|
+
backfillError instanceof Error ? backfillError.message : backfillError
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
991
1175
|
return byEmail.customerRef;
|
|
992
1176
|
}
|
|
993
1177
|
} catch (emailLookupError) {
|
|
@@ -1064,6 +1248,7 @@ var SolvaPayPaywall = class {
|
|
|
1064
1248
|
};
|
|
1065
1249
|
|
|
1066
1250
|
// src/adapters/base.ts
|
|
1251
|
+
var EXTRA_FORWARD_KEY2 = "__solvapayExtra";
|
|
1067
1252
|
var AdapterUtils = class {
|
|
1068
1253
|
/**
|
|
1069
1254
|
* Ensure customer reference is properly formatted
|
|
@@ -1079,7 +1264,7 @@ var AdapterUtils = class {
|
|
|
1079
1264
|
*/
|
|
1080
1265
|
static async extractFromJWT(token, options) {
|
|
1081
1266
|
try {
|
|
1082
|
-
const { jwtVerify } = await import("
|
|
1267
|
+
const { jwtVerify } = await import("jose");
|
|
1083
1268
|
const jwtSecret = new TextEncoder().encode(
|
|
1084
1269
|
options?.secret || process.env.OAUTH_JWKS_SECRET || "test-jwt-secret"
|
|
1085
1270
|
);
|
|
@@ -1095,23 +1280,45 @@ var AdapterUtils = class {
|
|
|
1095
1280
|
};
|
|
1096
1281
|
async function createAdapterHandler(adapter, paywall, metadata, businessLogic) {
|
|
1097
1282
|
const backendRefCache = /* @__PURE__ */ new Map();
|
|
1098
|
-
const getCustomerRef = (args) => args.auth?.customer_ref || "anonymous";
|
|
1099
|
-
const protectedHandler = await paywall.protect(businessLogic, metadata, getCustomerRef);
|
|
1100
1283
|
return async (context, extra) => {
|
|
1284
|
+
let args;
|
|
1285
|
+
let customerRef;
|
|
1101
1286
|
try {
|
|
1102
|
-
|
|
1103
|
-
|
|
1287
|
+
args = await adapter.extractArgs(context);
|
|
1288
|
+
customerRef = await adapter.getCustomerRef(context, extra);
|
|
1104
1289
|
let backendRef = backendRefCache.get(customerRef);
|
|
1105
1290
|
if (!backendRef) {
|
|
1106
1291
|
backendRef = await paywall.ensureCustomer(customerRef, customerRef);
|
|
1107
1292
|
backendRefCache.set(customerRef, backendRef);
|
|
1108
1293
|
}
|
|
1109
1294
|
args.auth = { customer_ref: backendRef };
|
|
1110
|
-
const result = await protectedHandler(args);
|
|
1111
|
-
return adapter.formatResponse(result, context);
|
|
1112
1295
|
} catch (error) {
|
|
1113
1296
|
return adapter.formatError(error, context);
|
|
1114
1297
|
}
|
|
1298
|
+
const decideGetCustomerRef = (args2) => args2.auth?.customer_ref || "anonymous";
|
|
1299
|
+
try {
|
|
1300
|
+
const decision = await paywall.decide(args, metadata, decideGetCustomerRef);
|
|
1301
|
+
if (decision.outcome === "gate") {
|
|
1302
|
+
return adapter.formatGate(decision.gate, context);
|
|
1303
|
+
}
|
|
1304
|
+
if (extra !== void 0) {
|
|
1305
|
+
;
|
|
1306
|
+
args[EXTRA_FORWARD_KEY2] = extra;
|
|
1307
|
+
}
|
|
1308
|
+
try {
|
|
1309
|
+
const result = await paywall.runAllow(decision, businessLogic, metadata, args);
|
|
1310
|
+
return adapter.formatResponse(result, context);
|
|
1311
|
+
} finally {
|
|
1312
|
+
if (EXTRA_FORWARD_KEY2 in args) {
|
|
1313
|
+
delete args[EXTRA_FORWARD_KEY2];
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
if (error instanceof PaywallError) {
|
|
1318
|
+
return adapter.formatGate(error.structuredContent, context);
|
|
1319
|
+
}
|
|
1320
|
+
return adapter.formatError(error, context);
|
|
1321
|
+
}
|
|
1115
1322
|
};
|
|
1116
1323
|
}
|
|
1117
1324
|
|
|
@@ -1159,24 +1366,25 @@ var HttpAdapter = class {
|
|
|
1159
1366
|
}
|
|
1160
1367
|
return result;
|
|
1161
1368
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
}
|
|
1178
|
-
return errorResponse2;
|
|
1369
|
+
/**
|
|
1370
|
+
* Emit a 402 Payment Required response with the same JSON body shape
|
|
1371
|
+
* REST consumers have always received (`{success:false, error, product,
|
|
1372
|
+
* checkoutUrl, message, ...}`). The shape is reused via
|
|
1373
|
+
* `paywallErrorToClientPayload` so HTTP / Next / hosted-proxy clients
|
|
1374
|
+
* don't have to branch on an SDK version.
|
|
1375
|
+
*/
|
|
1376
|
+
formatGate(gate, [_req, reply]) {
|
|
1377
|
+
const errorResponse = paywallErrorToClientPayload(new PaywallError(gate.message, gate));
|
|
1378
|
+
if (reply && reply.status && typeof reply.json === "function") {
|
|
1379
|
+
reply.status(402).json(errorResponse);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (reply && reply.code) {
|
|
1383
|
+
reply.code(402);
|
|
1179
1384
|
}
|
|
1385
|
+
return errorResponse;
|
|
1386
|
+
}
|
|
1387
|
+
formatError(error, [_req, reply]) {
|
|
1180
1388
|
const errorResponse = {
|
|
1181
1389
|
success: false,
|
|
1182
1390
|
error: error instanceof Error ? error.message : "Internal server error"
|
|
@@ -1254,22 +1462,22 @@ var NextAdapter = class {
|
|
|
1254
1462
|
headers: { "Content-Type": "application/json" }
|
|
1255
1463
|
});
|
|
1256
1464
|
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Emit a 402 Payment Required `Response` with the same JSON body
|
|
1467
|
+
* REST consumers have always received. Reuses
|
|
1468
|
+
* `paywallErrorToClientPayload` so HTTP / Next / hosted-proxy
|
|
1469
|
+
* clients don't have to branch on an SDK version.
|
|
1470
|
+
*/
|
|
1471
|
+
formatGate(gate, _context) {
|
|
1472
|
+
return new Response(
|
|
1473
|
+
JSON.stringify(paywallErrorToClientPayload(new PaywallError(gate.message, gate))),
|
|
1474
|
+
{
|
|
1475
|
+
status: 402,
|
|
1476
|
+
headers: { "Content-Type": "application/json" }
|
|
1477
|
+
}
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1257
1480
|
formatError(error, _context) {
|
|
1258
|
-
if (error instanceof PaywallError) {
|
|
1259
|
-
return new Response(
|
|
1260
|
-
JSON.stringify({
|
|
1261
|
-
success: false,
|
|
1262
|
-
error: "Payment required",
|
|
1263
|
-
product: error.structuredContent.product,
|
|
1264
|
-
checkoutUrl: error.structuredContent.checkoutUrl,
|
|
1265
|
-
message: error.structuredContent.message
|
|
1266
|
-
}),
|
|
1267
|
-
{
|
|
1268
|
-
status: 402,
|
|
1269
|
-
headers: { "Content-Type": "application/json" }
|
|
1270
|
-
}
|
|
1271
|
-
);
|
|
1272
|
-
}
|
|
1273
1481
|
return new Response(
|
|
1274
1482
|
JSON.stringify({
|
|
1275
1483
|
success: false,
|
|
@@ -1317,19 +1525,27 @@ var McpAdapter = class {
|
|
|
1317
1525
|
}
|
|
1318
1526
|
return response;
|
|
1319
1527
|
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Emit a plain-narration paywall response — `content[0].text` carries
|
|
1530
|
+
* the gate's human message (LLM-actionable), `structuredContent`
|
|
1531
|
+
* carries the machine-readable gate payload, and `isError` stays
|
|
1532
|
+
* `false` per the MCP spec's own `isError` definition (paywall is
|
|
1533
|
+
* not a self-correctable tool execution error; it is a user-facing
|
|
1534
|
+
* control transfer to the UI).
|
|
1535
|
+
*
|
|
1536
|
+
* Hosts that read widget metadata from `tools/list` or tool-result
|
|
1537
|
+
* `_meta.ui` open the paywall iframe on top of this response;
|
|
1538
|
+
* `buildPayableHandler` stamps the `_meta.ui.resourceUri` envelope
|
|
1539
|
+
* before returning.
|
|
1540
|
+
*/
|
|
1541
|
+
formatGate(gate, _context) {
|
|
1542
|
+
return {
|
|
1543
|
+
content: [{ type: "text", text: gate.message }],
|
|
1544
|
+
isError: false,
|
|
1545
|
+
structuredContent: gate
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1320
1548
|
formatError(error, _context) {
|
|
1321
|
-
if (error instanceof PaywallError) {
|
|
1322
|
-
return {
|
|
1323
|
-
content: [
|
|
1324
|
-
{
|
|
1325
|
-
type: "text",
|
|
1326
|
-
text: JSON.stringify(paywallErrorToClientPayload(error), null, 2)
|
|
1327
|
-
}
|
|
1328
|
-
],
|
|
1329
|
-
isError: true,
|
|
1330
|
-
structuredContent: error.structuredContent
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
1549
|
return {
|
|
1334
1550
|
content: [
|
|
1335
1551
|
{
|
|
@@ -1735,551 +1951,162 @@ function createSolvaPay(config) {
|
|
|
1735
1951
|
};
|
|
1736
1952
|
}
|
|
1737
1953
|
|
|
1738
|
-
// src/
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
this.name = "McpBearerAuthError";
|
|
1743
|
-
}
|
|
1744
|
-
};
|
|
1745
|
-
function base64UrlDecode(input) {
|
|
1746
|
-
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1747
|
-
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
1748
|
-
const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
1749
|
-
return new TextDecoder().decode(bytes);
|
|
1750
|
-
}
|
|
1751
|
-
function extractBearerToken(authorization) {
|
|
1752
|
-
if (!authorization) return null;
|
|
1753
|
-
if (!authorization.startsWith("Bearer ")) return null;
|
|
1754
|
-
return authorization.slice(7).trim() || null;
|
|
1755
|
-
}
|
|
1756
|
-
function decodeJwtPayload(token) {
|
|
1757
|
-
const parts = token.split(".");
|
|
1758
|
-
if (parts.length < 2) {
|
|
1759
|
-
throw new McpBearerAuthError("Invalid JWT format");
|
|
1760
|
-
}
|
|
1761
|
-
try {
|
|
1762
|
-
const payloadText = base64UrlDecode(parts[1]);
|
|
1763
|
-
const payload = JSON.parse(payloadText);
|
|
1764
|
-
return payload;
|
|
1765
|
-
} catch {
|
|
1766
|
-
throw new McpBearerAuthError("Invalid JWT payload");
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
function getCustomerRefFromJwtPayload(payload, options = {}) {
|
|
1770
|
-
const claimPriority = options.claimPriority || ["customerRef", "customer_ref", "sub"];
|
|
1771
|
-
for (const claim of claimPriority) {
|
|
1772
|
-
const value = payload[claim];
|
|
1773
|
-
if (typeof value === "string" && value.trim()) {
|
|
1774
|
-
return value.trim();
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
throw new McpBearerAuthError(
|
|
1778
|
-
`No customer reference claim found (checked: ${claimPriority.join(", ")})`
|
|
1779
|
-
);
|
|
1780
|
-
}
|
|
1781
|
-
function getCustomerRefFromBearerAuthHeader(authorization, options = {}) {
|
|
1782
|
-
const token = extractBearerToken(authorization);
|
|
1783
|
-
if (!token) {
|
|
1784
|
-
throw new McpBearerAuthError("Missing bearer token");
|
|
1954
|
+
// src/types/paywall.ts
|
|
1955
|
+
function isPaywallStructuredContent(value) {
|
|
1956
|
+
if (typeof value !== "object" || value === null || !("kind" in value)) {
|
|
1957
|
+
return false;
|
|
1785
1958
|
}
|
|
1786
|
-
const
|
|
1787
|
-
return
|
|
1959
|
+
const kind = value.kind;
|
|
1960
|
+
return kind === "payment_required" || kind === "activation_required";
|
|
1788
1961
|
}
|
|
1789
1962
|
|
|
1790
|
-
// src/
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
return payloadClientId || "solvapay-mcp-client";
|
|
1963
|
+
// src/helpers/error.ts
|
|
1964
|
+
import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
|
|
1965
|
+
function isErrorResult(result) {
|
|
1966
|
+
return typeof result === "object" && result !== null && "error" in result && "status" in result;
|
|
1795
1967
|
}
|
|
1796
|
-
function
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1968
|
+
function handleRouteError(error, operationName, defaultMessage) {
|
|
1969
|
+
console.error(`[${operationName}] Error:`, error);
|
|
1970
|
+
if (error instanceof SolvaPayError3) {
|
|
1971
|
+
const errorMessage2 = error.message;
|
|
1972
|
+
return {
|
|
1973
|
+
error: errorMessage2,
|
|
1974
|
+
status: 500,
|
|
1975
|
+
details: errorMessage2
|
|
1976
|
+
};
|
|
1802
1977
|
}
|
|
1803
|
-
|
|
1804
|
-
}
|
|
1805
|
-
function getExpiresAt(payload) {
|
|
1806
|
-
return typeof payload.exp === "number" ? payload.exp : void 0;
|
|
1807
|
-
}
|
|
1808
|
-
function buildAuthInfoFromBearer(authorization, options = {}) {
|
|
1809
|
-
const token = extractBearerToken(authorization);
|
|
1810
|
-
if (!token) return null;
|
|
1811
|
-
const payload = decodeJwtPayload(token);
|
|
1812
|
-
const customerRef = getCustomerRefFromJwtPayload(payload, options);
|
|
1813
|
-
const clientId = getClientId(payload, options.clientId);
|
|
1814
|
-
const scopes = getScopes(payload, options.defaultScopes || []);
|
|
1815
|
-
const expiresAt = getExpiresAt(payload);
|
|
1978
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1979
|
+
const message = defaultMessage || `${operationName} failed`;
|
|
1816
1980
|
return {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
expiresAt,
|
|
1821
|
-
extra: {
|
|
1822
|
-
customer_ref: customerRef,
|
|
1823
|
-
...options.includePayload ? { payload } : {}
|
|
1824
|
-
}
|
|
1981
|
+
error: message,
|
|
1982
|
+
status: 500,
|
|
1983
|
+
details: errorMessage
|
|
1825
1984
|
};
|
|
1826
1985
|
}
|
|
1827
1986
|
|
|
1828
|
-
// src/
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
const header = req.headers?.authorization;
|
|
1844
|
-
if (typeof header === "string") return header;
|
|
1845
|
-
if (Array.isArray(header)) return header[0] || null;
|
|
1846
|
-
return null;
|
|
1847
|
-
}
|
|
1848
|
-
function getHeader(req, name) {
|
|
1849
|
-
const header = req.headers?.[name.toLowerCase()];
|
|
1850
|
-
if (typeof header === "string") return header;
|
|
1851
|
-
if (Array.isArray(header)) return header[0] || null;
|
|
1852
|
-
return null;
|
|
1853
|
-
}
|
|
1854
|
-
function getRequestJsonRpcId(body) {
|
|
1855
|
-
if (body && typeof body === "object" && "id" in body) {
|
|
1856
|
-
const id = body.id;
|
|
1857
|
-
return id ?? null;
|
|
1858
|
-
}
|
|
1859
|
-
return null;
|
|
1860
|
-
}
|
|
1861
|
-
function makeUnauthorizedJsonRpc(id) {
|
|
1862
|
-
return {
|
|
1863
|
-
jsonrpc: "2.0",
|
|
1864
|
-
id,
|
|
1865
|
-
error: {
|
|
1866
|
-
code: -32001,
|
|
1867
|
-
message: "Unauthorized"
|
|
1987
|
+
// src/helpers/auth.ts
|
|
1988
|
+
function base64UrlDecode(input) {
|
|
1989
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1990
|
+
const padding = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
|
|
1991
|
+
const base64 = padded + padding;
|
|
1992
|
+
if (typeof atob === "function") {
|
|
1993
|
+
const binary = atob(base64);
|
|
1994
|
+
let result = "";
|
|
1995
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1996
|
+
result += String.fromCharCode(binary.charCodeAt(i));
|
|
1997
|
+
}
|
|
1998
|
+
try {
|
|
1999
|
+
return decodeURIComponent(escape(result));
|
|
2000
|
+
} catch {
|
|
2001
|
+
return result;
|
|
1868
2002
|
}
|
|
1869
|
-
};
|
|
1870
|
-
}
|
|
1871
|
-
function setMcpChallengeHeader(res, publicBaseUrl, protectedResourcePath) {
|
|
1872
|
-
res.setHeader(
|
|
1873
|
-
"WWW-Authenticate",
|
|
1874
|
-
`Bearer resource_metadata="${withoutTrailingSlash(publicBaseUrl)}${protectedResourcePath}"`
|
|
1875
|
-
);
|
|
1876
|
-
}
|
|
1877
|
-
function getRequestQuery(req) {
|
|
1878
|
-
const raw = req.url ?? req.path ?? "";
|
|
1879
|
-
const qIndex = raw.indexOf("?");
|
|
1880
|
-
return qIndex === -1 ? "" : raw.slice(qIndex);
|
|
1881
|
-
}
|
|
1882
|
-
function isNativeClientOrigin(origin) {
|
|
1883
|
-
try {
|
|
1884
|
-
const url = new URL(origin);
|
|
1885
|
-
return NATIVE_CLIENT_ORIGIN_SCHEMES.includes(
|
|
1886
|
-
url.protocol
|
|
1887
|
-
);
|
|
1888
|
-
} catch {
|
|
1889
|
-
return false;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
function applyCorsHeaders(req, res) {
|
|
1893
|
-
const origin = getHeader(req, "origin");
|
|
1894
|
-
if (!origin) return;
|
|
1895
|
-
if (isNativeClientOrigin(origin)) {
|
|
1896
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1897
|
-
res.setHeader("Vary", "Origin");
|
|
1898
2003
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const requestedMethod = getHeader(req, "access-control-request-method") ?? "POST";
|
|
1903
|
-
const requestedHeaders = getHeader(req, "access-control-request-headers") ?? "authorization, content-type";
|
|
1904
|
-
res.setHeader("Access-Control-Allow-Methods", `${requestedMethod}, OPTIONS`);
|
|
1905
|
-
res.setHeader("Access-Control-Allow-Headers", requestedHeaders);
|
|
1906
|
-
res.setHeader("Access-Control-Max-Age", "600");
|
|
1907
|
-
res.status(204);
|
|
1908
|
-
if (typeof res.end === "function") {
|
|
1909
|
-
res.end();
|
|
1910
|
-
} else {
|
|
1911
|
-
res.json({});
|
|
2004
|
+
const BufferCtor = globalThis.Buffer;
|
|
2005
|
+
if (BufferCtor) {
|
|
2006
|
+
return BufferCtor.from(base64, "base64").toString("utf-8");
|
|
1912
2007
|
}
|
|
2008
|
+
throw new Error("No base64 decoder available in this runtime");
|
|
1913
2009
|
}
|
|
1914
|
-
|
|
1915
|
-
const
|
|
1916
|
-
if (
|
|
2010
|
+
function decodeJwtUnverified(token) {
|
|
2011
|
+
const parts = token.split(".");
|
|
2012
|
+
if (parts.length !== 3) return null;
|
|
1917
2013
|
try {
|
|
1918
|
-
|
|
2014
|
+
const json = base64UrlDecode(parts[1]);
|
|
2015
|
+
const payload = JSON.parse(json);
|
|
2016
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
2017
|
+
return payload;
|
|
1919
2018
|
} catch {
|
|
1920
|
-
return
|
|
2019
|
+
return null;
|
|
1921
2020
|
}
|
|
1922
2021
|
}
|
|
1923
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
if (text === "" && upstream.status === 204) {
|
|
1929
|
-
if (typeof res.end === "function") {
|
|
1930
|
-
res.end();
|
|
1931
|
-
return;
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
res.json(body);
|
|
2022
|
+
function extractBearerToken(request) {
|
|
2023
|
+
const authHeader = request.headers.get("authorization");
|
|
2024
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith("bearer ")) return null;
|
|
2025
|
+
const token = authHeader.slice(7).trim();
|
|
2026
|
+
return token.length > 0 ? token : null;
|
|
1935
2027
|
}
|
|
1936
|
-
function
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
res.json({ error: "upstream_unreachable" });
|
|
2028
|
+
function readEnv(name) {
|
|
2029
|
+
const proc = globalThis.process;
|
|
2030
|
+
return proc?.env?.[name];
|
|
1940
2031
|
}
|
|
1941
|
-
function
|
|
1942
|
-
|
|
1943
|
-
if (body instanceof Uint8Array) return Buffer.from(body).toString("utf8");
|
|
1944
|
-
return JSON.stringify(body ?? {});
|
|
2032
|
+
function isStrictMode() {
|
|
2033
|
+
return readEnv("SOLVAPAY_AUTH_STRICT") === "true";
|
|
1945
2034
|
}
|
|
1946
|
-
function
|
|
1947
|
-
|
|
1948
|
-
if (body instanceof Uint8Array) return Buffer.from(body).toString("utf8");
|
|
1949
|
-
if (body && typeof body === "object") {
|
|
1950
|
-
const params = new URLSearchParams();
|
|
1951
|
-
for (const [key, value] of Object.entries(body)) {
|
|
1952
|
-
if (value === void 0 || value === null) continue;
|
|
1953
|
-
if (Array.isArray(value)) {
|
|
1954
|
-
for (const entry of value) params.append(key, String(entry));
|
|
1955
|
-
} else {
|
|
1956
|
-
params.append(key, String(value));
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
return params.toString();
|
|
1960
|
-
}
|
|
1961
|
-
return "";
|
|
2035
|
+
function getConfiguredSecret() {
|
|
2036
|
+
return readEnv("SOLVAPAY_JWT_SECRET") || readEnv("SUPABASE_JWT_SECRET");
|
|
1962
2037
|
}
|
|
1963
|
-
function
|
|
1964
|
-
|
|
1965
|
-
|
|
2038
|
+
async function verifyHs256(token, secret) {
|
|
2039
|
+
try {
|
|
2040
|
+
const { jwtVerify } = await import("jose");
|
|
2041
|
+
const key = new TextEncoder().encode(secret);
|
|
2042
|
+
const { payload } = await jwtVerify(token, key, { algorithms: ["HS256"] });
|
|
2043
|
+
return payload;
|
|
2044
|
+
} catch {
|
|
2045
|
+
return null;
|
|
1966
2046
|
}
|
|
1967
|
-
if (typeof body === "string") return body;
|
|
1968
|
-
if (body instanceof Uint8Array) return Buffer.from(body).toString("utf8");
|
|
1969
|
-
return JSON.stringify(body ?? {});
|
|
1970
|
-
}
|
|
1971
|
-
function getOAuthProtectedResourceResponse(publicBaseUrl) {
|
|
1972
|
-
const resource = withoutTrailingSlash(publicBaseUrl);
|
|
1973
|
-
return {
|
|
1974
|
-
resource,
|
|
1975
|
-
authorization_servers: [resource],
|
|
1976
|
-
scopes_supported: ["openid", "profile", "email"]
|
|
1977
|
-
};
|
|
1978
|
-
}
|
|
1979
|
-
function getOAuthAuthorizationServerResponse({
|
|
1980
|
-
publicBaseUrl,
|
|
1981
|
-
paths
|
|
1982
|
-
}) {
|
|
1983
|
-
const base = withoutTrailingSlash(publicBaseUrl);
|
|
1984
|
-
const p = resolvePaths(paths);
|
|
1985
|
-
return {
|
|
1986
|
-
issuer: base,
|
|
1987
|
-
authorization_endpoint: `${base}${p.authorize}`,
|
|
1988
|
-
token_endpoint: `${base}${p.token}`,
|
|
1989
|
-
registration_endpoint: `${base}${p.register}`,
|
|
1990
|
-
revocation_endpoint: `${base}${p.revoke}`,
|
|
1991
|
-
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
|
|
1992
|
-
response_types_supported: ["code"],
|
|
1993
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
1994
|
-
scopes_supported: ["openid", "profile", "email"],
|
|
1995
|
-
code_challenge_methods_supported: ["S256"]
|
|
1996
|
-
};
|
|
1997
2047
|
}
|
|
1998
|
-
function
|
|
1999
|
-
const
|
|
2000
|
-
const
|
|
2001
|
-
const
|
|
2002
|
-
return
|
|
2003
|
-
if (req.path !== path) {
|
|
2004
|
-
next();
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
if (req.method === "OPTIONS") {
|
|
2008
|
-
handlePreflight(req, res);
|
|
2009
|
-
return;
|
|
2010
|
-
}
|
|
2011
|
-
if (req.method !== "POST") {
|
|
2012
|
-
next();
|
|
2013
|
-
return;
|
|
2014
|
-
}
|
|
2015
|
-
try {
|
|
2016
|
-
const response = await fetch(upstream, {
|
|
2017
|
-
method: "POST",
|
|
2018
|
-
headers: { "content-type": "application/json" },
|
|
2019
|
-
body: serializeRegisterBody(req.body)
|
|
2020
|
-
});
|
|
2021
|
-
applyCorsHeaders(req, res);
|
|
2022
|
-
await relayJsonResponse(response, res);
|
|
2023
|
-
} catch (error) {
|
|
2024
|
-
applyCorsHeaders(req, res);
|
|
2025
|
-
sendUpstreamError(res, error);
|
|
2026
|
-
}
|
|
2027
|
-
};
|
|
2048
|
+
function pickName(payload) {
|
|
2049
|
+
const metadataFullName = typeof payload.user_metadata?.full_name === "string" ? payload.user_metadata.full_name : null;
|
|
2050
|
+
const metadataName = typeof payload.user_metadata?.name === "string" ? payload.user_metadata.name : null;
|
|
2051
|
+
const claimName = typeof payload.name === "string" ? payload.name : null;
|
|
2052
|
+
return metadataFullName || metadataName || claimName || null;
|
|
2028
2053
|
}
|
|
2029
|
-
function
|
|
2030
|
-
|
|
2031
|
-
const api = withoutTrailingSlash(options.apiBaseUrl);
|
|
2032
|
-
return (req, res, next) => {
|
|
2033
|
-
if (req.path !== path) {
|
|
2034
|
-
next();
|
|
2035
|
-
return;
|
|
2036
|
-
}
|
|
2037
|
-
if (req.method === "OPTIONS") {
|
|
2038
|
-
handlePreflight(req, res);
|
|
2039
|
-
return;
|
|
2040
|
-
}
|
|
2041
|
-
if (req.method !== "GET") {
|
|
2042
|
-
next();
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
const query = getRequestQuery(req);
|
|
2046
|
-
const location = `${api}/v1/customer/auth/authorize${query}`;
|
|
2047
|
-
res.setHeader("Location", location);
|
|
2048
|
-
res.status(302);
|
|
2049
|
-
if (typeof res.end === "function") {
|
|
2050
|
-
res.end();
|
|
2051
|
-
return;
|
|
2052
|
-
}
|
|
2053
|
-
res.json({});
|
|
2054
|
-
};
|
|
2054
|
+
function pickEmail(payload) {
|
|
2055
|
+
return typeof payload.email === "string" ? payload.email : null;
|
|
2055
2056
|
}
|
|
2056
|
-
function
|
|
2057
|
-
|
|
2058
|
-
const api = withoutTrailingSlash(options.apiBaseUrl);
|
|
2059
|
-
const upstream = `${api}/v1/customer/auth/token`;
|
|
2060
|
-
return async (req, res, next) => {
|
|
2061
|
-
if (req.path !== path) {
|
|
2062
|
-
next();
|
|
2063
|
-
return;
|
|
2064
|
-
}
|
|
2065
|
-
if (req.method === "OPTIONS") {
|
|
2066
|
-
handlePreflight(req, res);
|
|
2067
|
-
return;
|
|
2068
|
-
}
|
|
2069
|
-
if (req.method !== "POST") {
|
|
2070
|
-
next();
|
|
2071
|
-
return;
|
|
2072
|
-
}
|
|
2073
|
-
const contentType = getHeader(req, "content-type") ?? "application/x-www-form-urlencoded";
|
|
2074
|
-
const authorization = getHeader(req, "authorization");
|
|
2075
|
-
const headers = { "content-type": contentType };
|
|
2076
|
-
if (authorization) headers["authorization"] = authorization;
|
|
2077
|
-
try {
|
|
2078
|
-
const response = await fetch(upstream, {
|
|
2079
|
-
method: "POST",
|
|
2080
|
-
headers,
|
|
2081
|
-
body: serializeRequestBody(contentType, req.body)
|
|
2082
|
-
});
|
|
2083
|
-
applyCorsHeaders(req, res);
|
|
2084
|
-
await relayJsonResponse(response, res);
|
|
2085
|
-
} catch (error) {
|
|
2086
|
-
applyCorsHeaders(req, res);
|
|
2087
|
-
sendUpstreamError(res, error);
|
|
2088
|
-
}
|
|
2089
|
-
};
|
|
2057
|
+
function unauthorized(details) {
|
|
2058
|
+
return { error: "Unauthorized", status: 401, details };
|
|
2090
2059
|
}
|
|
2091
|
-
function
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
const authorization = getHeader(req, "authorization");
|
|
2110
|
-
const headers = { "content-type": contentType };
|
|
2111
|
-
if (authorization) headers["authorization"] = authorization;
|
|
2112
|
-
try {
|
|
2113
|
-
const response = await fetch(upstream, {
|
|
2114
|
-
method: "POST",
|
|
2115
|
-
headers,
|
|
2116
|
-
body: serializeRequestBody(contentType, req.body)
|
|
2117
|
-
});
|
|
2118
|
-
applyCorsHeaders(req, res);
|
|
2119
|
-
await relayJsonResponse(response, res);
|
|
2120
|
-
} catch (error) {
|
|
2121
|
-
applyCorsHeaders(req, res);
|
|
2122
|
-
sendUpstreamError(res, error);
|
|
2123
|
-
}
|
|
2124
|
-
};
|
|
2125
|
-
}
|
|
2126
|
-
function createMcpOAuthBridge(options) {
|
|
2127
|
-
const {
|
|
2128
|
-
publicBaseUrl,
|
|
2129
|
-
apiBaseUrl,
|
|
2130
|
-
productRef,
|
|
2131
|
-
mcpPath = "/mcp",
|
|
2132
|
-
requireAuth = true,
|
|
2133
|
-
authInfo,
|
|
2134
|
-
protectedResourcePath = "/.well-known/oauth-protected-resource",
|
|
2135
|
-
authorizationServerPath = "/.well-known/oauth-authorization-server",
|
|
2136
|
-
oauthPaths
|
|
2137
|
-
} = options;
|
|
2138
|
-
const paths = resolvePaths(oauthPaths);
|
|
2139
|
-
const openidDiscoveryMiddleware = (req, res, next) => {
|
|
2140
|
-
if (req.method !== "GET" || req.path !== "/.well-known/openid-configuration") {
|
|
2141
|
-
next();
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
applyCorsHeaders(req, res);
|
|
2145
|
-
res.status(404);
|
|
2146
|
-
if (typeof res.end === "function") {
|
|
2147
|
-
res.end();
|
|
2148
|
-
} else {
|
|
2149
|
-
res.json({ error: "not_found" });
|
|
2150
|
-
}
|
|
2151
|
-
};
|
|
2152
|
-
const protectedResourceMiddleware = (req, res, next) => {
|
|
2153
|
-
if (req.method !== "GET" || req.path !== protectedResourcePath) {
|
|
2154
|
-
next();
|
|
2155
|
-
return;
|
|
2156
|
-
}
|
|
2157
|
-
res.json(getOAuthProtectedResourceResponse(publicBaseUrl));
|
|
2158
|
-
};
|
|
2159
|
-
const authorizationServerMiddleware = (req, res, next) => {
|
|
2160
|
-
if (req.method !== "GET" || req.path !== authorizationServerPath) {
|
|
2161
|
-
next();
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
if (!productRef) {
|
|
2165
|
-
res.status(500).json({ error: "SOLVAPAY_PRODUCT_REF missing" });
|
|
2166
|
-
return;
|
|
2167
|
-
}
|
|
2168
|
-
res.json(
|
|
2169
|
-
getOAuthAuthorizationServerResponse({
|
|
2170
|
-
publicBaseUrl,
|
|
2171
|
-
paths
|
|
2172
|
-
})
|
|
2173
|
-
);
|
|
2174
|
-
};
|
|
2175
|
-
const registerMiddleware = createOAuthRegisterHandler({
|
|
2176
|
-
apiBaseUrl,
|
|
2177
|
-
productRef,
|
|
2178
|
-
path: paths.register
|
|
2179
|
-
});
|
|
2180
|
-
const authorizeMiddleware = createOAuthAuthorizeHandler({
|
|
2181
|
-
apiBaseUrl,
|
|
2182
|
-
path: paths.authorize
|
|
2183
|
-
});
|
|
2184
|
-
const tokenMiddleware = createOAuthTokenHandler({ apiBaseUrl, path: paths.token });
|
|
2185
|
-
const revokeMiddleware = createOAuthRevokeHandler({ apiBaseUrl, path: paths.revoke });
|
|
2186
|
-
const mcpAuthMiddleware = (req, res, next) => {
|
|
2187
|
-
if (req.path !== mcpPath) {
|
|
2188
|
-
next();
|
|
2189
|
-
return;
|
|
2190
|
-
}
|
|
2191
|
-
if (req.method && req.method !== "POST" && req.method !== "OPTIONS") {
|
|
2192
|
-
applyCorsHeaders(req, res);
|
|
2193
|
-
res.setHeader("Allow", "POST, OPTIONS");
|
|
2194
|
-
res.status(405);
|
|
2195
|
-
if (typeof res.end === "function") {
|
|
2196
|
-
res.end();
|
|
2197
|
-
} else {
|
|
2198
|
-
res.json({ error: "method_not_allowed" });
|
|
2060
|
+
async function getAuthenticatedUserCore(request, options = {}) {
|
|
2061
|
+
try {
|
|
2062
|
+
const includeEmail = options.includeEmail !== false;
|
|
2063
|
+
const includeName = options.includeName !== false;
|
|
2064
|
+
const headerUserId = request.headers.get("x-user-id");
|
|
2065
|
+
if (headerUserId) {
|
|
2066
|
+
let email = null;
|
|
2067
|
+
let name = null;
|
|
2068
|
+
if (includeEmail || includeName) {
|
|
2069
|
+
const token2 = extractBearerToken(request);
|
|
2070
|
+
if (token2) {
|
|
2071
|
+
const secret2 = getConfiguredSecret();
|
|
2072
|
+
const payload2 = secret2 ? await verifyHs256(token2, secret2) : isStrictMode() ? null : decodeJwtUnverified(token2);
|
|
2073
|
+
if (payload2) {
|
|
2074
|
+
if (includeEmail) email = pickEmail(payload2);
|
|
2075
|
+
if (includeName) name = pickName(payload2);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2199
2078
|
}
|
|
2200
|
-
return;
|
|
2079
|
+
return { userId: headerUserId, email, name };
|
|
2201
2080
|
}
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
next();
|
|
2206
|
-
return;
|
|
2081
|
+
const token = extractBearerToken(request);
|
|
2082
|
+
if (!token) {
|
|
2083
|
+
return unauthorized("User ID not found. Ensure middleware is configured.");
|
|
2207
2084
|
}
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2085
|
+
const secret = getConfiguredSecret();
|
|
2086
|
+
let payload = null;
|
|
2087
|
+
if (secret) {
|
|
2088
|
+
payload = await verifyHs256(token, secret);
|
|
2089
|
+
if (!payload) {
|
|
2090
|
+
return unauthorized("Invalid or expired authentication token");
|
|
2212
2091
|
}
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
if (
|
|
2220
|
-
|
|
2221
|
-
return;
|
|
2092
|
+
} else if (isStrictMode()) {
|
|
2093
|
+
return unauthorized(
|
|
2094
|
+
"Strict auth mode is enabled but no JWT secret is configured. Set SOLVAPAY_JWT_SECRET or SUPABASE_JWT_SECRET."
|
|
2095
|
+
);
|
|
2096
|
+
} else {
|
|
2097
|
+
payload = decodeJwtUnverified(token);
|
|
2098
|
+
if (!payload) {
|
|
2099
|
+
return unauthorized("Malformed authentication token");
|
|
2222
2100
|
}
|
|
2223
|
-
res.status(401).json({ error: "Unauthorized" });
|
|
2224
2101
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
protectedResourceMiddleware,
|
|
2229
|
-
authorizationServerMiddleware,
|
|
2230
|
-
registerMiddleware,
|
|
2231
|
-
authorizeMiddleware,
|
|
2232
|
-
tokenMiddleware,
|
|
2233
|
-
revokeMiddleware,
|
|
2234
|
-
mcpAuthMiddleware
|
|
2235
|
-
];
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
// src/helpers/error.ts
|
|
2239
|
-
import { SolvaPayError as SolvaPayError3 } from "@solvapay/core";
|
|
2240
|
-
function isErrorResult(result) {
|
|
2241
|
-
return typeof result === "object" && result !== null && "error" in result && "status" in result;
|
|
2242
|
-
}
|
|
2243
|
-
function handleRouteError(error, operationName, defaultMessage) {
|
|
2244
|
-
console.error(`[${operationName}] Error:`, error);
|
|
2245
|
-
if (error instanceof SolvaPayError3) {
|
|
2246
|
-
const errorMessage2 = error.message;
|
|
2247
|
-
return {
|
|
2248
|
-
error: errorMessage2,
|
|
2249
|
-
status: 500,
|
|
2250
|
-
details: errorMessage2
|
|
2251
|
-
};
|
|
2252
|
-
}
|
|
2253
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
2254
|
-
const message = defaultMessage || `${operationName} failed`;
|
|
2255
|
-
return {
|
|
2256
|
-
error: message,
|
|
2257
|
-
status: 500,
|
|
2258
|
-
details: errorMessage
|
|
2259
|
-
};
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
// src/helpers/auth.ts
|
|
2263
|
-
async function getAuthenticatedUserCore(request, options = {}) {
|
|
2264
|
-
try {
|
|
2265
|
-
const { requireUserId, getUserEmailFromRequest, getUserNameFromRequest } = await import("@solvapay/auth");
|
|
2266
|
-
const userIdOrError = requireUserId(request);
|
|
2267
|
-
if (userIdOrError instanceof Response) {
|
|
2268
|
-
const clonedResponse = userIdOrError.clone();
|
|
2269
|
-
const body = await clonedResponse.json().catch(() => ({ error: "Unauthorized" }));
|
|
2270
|
-
return {
|
|
2271
|
-
error: body.error || "Unauthorized",
|
|
2272
|
-
status: userIdOrError.status,
|
|
2273
|
-
details: body.error || "Unauthorized"
|
|
2274
|
-
};
|
|
2102
|
+
const userId = typeof payload.sub === "string" ? payload.sub : null;
|
|
2103
|
+
if (!userId) {
|
|
2104
|
+
return unauthorized("Authentication token missing subject (sub) claim");
|
|
2275
2105
|
}
|
|
2276
|
-
const userId = userIdOrError;
|
|
2277
|
-
const email = options.includeEmail !== false ? await getUserEmailFromRequest(request) : null;
|
|
2278
|
-
const name = options.includeName !== false ? await getUserNameFromRequest(request) : null;
|
|
2279
2106
|
return {
|
|
2280
2107
|
userId,
|
|
2281
|
-
email,
|
|
2282
|
-
name
|
|
2108
|
+
email: includeEmail ? pickEmail(payload) : null,
|
|
2109
|
+
name: includeName ? pickName(payload) : null
|
|
2283
2110
|
};
|
|
2284
2111
|
} catch (error) {
|
|
2285
2112
|
return handleRouteError(error, "Get authenticated user", "Authentication failed");
|
|
@@ -2672,6 +2499,31 @@ async function activatePlanCore(request, body, options = {}) {
|
|
|
2672
2499
|
}
|
|
2673
2500
|
}
|
|
2674
2501
|
|
|
2502
|
+
// src/helpers/payment-method.ts
|
|
2503
|
+
async function getPaymentMethodCore(request, options = {}) {
|
|
2504
|
+
try {
|
|
2505
|
+
const customerResult = await syncCustomerCore(request, {
|
|
2506
|
+
solvaPay: options.solvaPay,
|
|
2507
|
+
includeEmail: options.includeEmail,
|
|
2508
|
+
includeName: options.includeName
|
|
2509
|
+
});
|
|
2510
|
+
if (isErrorResult(customerResult)) {
|
|
2511
|
+
return customerResult;
|
|
2512
|
+
}
|
|
2513
|
+
const customerRef = customerResult;
|
|
2514
|
+
const solvaPay = options.solvaPay || createSolvaPay();
|
|
2515
|
+
if (!solvaPay.apiClient.getPaymentMethod) {
|
|
2516
|
+
return {
|
|
2517
|
+
error: "getPaymentMethod is not implemented on this API client",
|
|
2518
|
+
status: 500
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
return await solvaPay.apiClient.getPaymentMethod({ customerRef });
|
|
2522
|
+
} catch (error) {
|
|
2523
|
+
return handleRouteError(error, "Get payment method", "Failed to load payment method");
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2675
2527
|
// src/helpers/plans.ts
|
|
2676
2528
|
import { getSolvaPayConfig as getSolvaPayConfig2 } from "@solvapay/core";
|
|
2677
2529
|
async function listPlansCore(request, options = {}) {
|
|
@@ -2841,6 +2693,37 @@ async function checkPurchaseCore(request, options = {}) {
|
|
|
2841
2693
|
}
|
|
2842
2694
|
|
|
2843
2695
|
// src/helpers/usage.ts
|
|
2696
|
+
async function getUsageCore(request, options = {}) {
|
|
2697
|
+
const purchaseResult = await checkPurchaseCore(request, options);
|
|
2698
|
+
if (isErrorResult(purchaseResult)) return purchaseResult;
|
|
2699
|
+
const activePurchase = (purchaseResult.purchases ?? []).find((p) => p.status === "active");
|
|
2700
|
+
if (!activePurchase) {
|
|
2701
|
+
return {
|
|
2702
|
+
meterRef: null,
|
|
2703
|
+
total: null,
|
|
2704
|
+
used: 0,
|
|
2705
|
+
remaining: null,
|
|
2706
|
+
percentUsed: null
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
const snap = activePurchase.planSnapshot;
|
|
2710
|
+
const usage = activePurchase.usage;
|
|
2711
|
+
const meterRef = snap?.meterRef ?? snap?.meterId ?? null;
|
|
2712
|
+
const total = typeof snap?.limit === "number" ? snap.limit : null;
|
|
2713
|
+
const used = typeof usage?.used === "number" ? usage.used : 0;
|
|
2714
|
+
const remaining = total !== null ? Math.max(0, total - used) : null;
|
|
2715
|
+
const percentUsed = total !== null && total > 0 ? Math.min(100, Math.round(used / total * 1e4) / 100) : null;
|
|
2716
|
+
return {
|
|
2717
|
+
meterRef,
|
|
2718
|
+
total,
|
|
2719
|
+
used,
|
|
2720
|
+
remaining,
|
|
2721
|
+
percentUsed,
|
|
2722
|
+
...usage?.periodStart ? { periodStart: usage.periodStart } : {},
|
|
2723
|
+
...usage?.periodEnd ? { periodEnd: usage.periodEnd } : {},
|
|
2724
|
+
purchaseRef: activePurchase.reference
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2844
2727
|
async function trackUsageCore(request, body, options = {}) {
|
|
2845
2728
|
try {
|
|
2846
2729
|
const userResult = await getAuthenticatedUserCore(request);
|
|
@@ -2905,37 +2788,30 @@ function verifyWebhook({
|
|
|
2905
2788
|
}
|
|
2906
2789
|
}
|
|
2907
2790
|
export {
|
|
2908
|
-
McpBearerAuthError,
|
|
2909
2791
|
PaywallError,
|
|
2910
2792
|
VIRTUAL_TOOL_DEFINITIONS,
|
|
2911
2793
|
activatePlanCore,
|
|
2912
|
-
|
|
2794
|
+
buildGateMessage,
|
|
2795
|
+
buildNudgeMessage,
|
|
2913
2796
|
cancelPurchaseCore,
|
|
2914
2797
|
checkPurchaseCore,
|
|
2798
|
+
classifyPaywallState,
|
|
2915
2799
|
createCheckoutSessionCore,
|
|
2916
2800
|
createCustomerSessionCore,
|
|
2917
|
-
createMcpOAuthBridge,
|
|
2918
|
-
createOAuthAuthorizeHandler,
|
|
2919
|
-
createOAuthRegisterHandler,
|
|
2920
|
-
createOAuthRevokeHandler,
|
|
2921
|
-
createOAuthTokenHandler,
|
|
2922
2801
|
createPaymentIntentCore,
|
|
2923
2802
|
createSolvaPay,
|
|
2924
2803
|
createSolvaPayClient,
|
|
2925
2804
|
createTopupPaymentIntentCore,
|
|
2926
2805
|
createVirtualTools,
|
|
2927
|
-
decodeJwtPayload,
|
|
2928
|
-
extractBearerToken,
|
|
2929
2806
|
getAuthenticatedUserCore,
|
|
2930
2807
|
getCustomerBalanceCore,
|
|
2931
|
-
getCustomerRefFromBearerAuthHeader,
|
|
2932
|
-
getCustomerRefFromJwtPayload,
|
|
2933
2808
|
getMerchantCore,
|
|
2934
|
-
|
|
2935
|
-
getOAuthProtectedResourceResponse,
|
|
2809
|
+
getPaymentMethodCore,
|
|
2936
2810
|
getProductCore,
|
|
2811
|
+
getUsageCore,
|
|
2937
2812
|
handleRouteError,
|
|
2938
2813
|
isErrorResult,
|
|
2814
|
+
isPaywallStructuredContent,
|
|
2939
2815
|
jsonSchemaToZodRawShape,
|
|
2940
2816
|
listPlansCore,
|
|
2941
2817
|
paywallErrorToClientPayload,
|