@stackbe/sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -39,9 +39,62 @@ var StackBEError = class extends Error {
39
39
  this.statusCode = statusCode;
40
40
  this.code = code;
41
41
  }
42
+ /** Check if this is a specific error type */
43
+ is(code) {
44
+ return this.code === code;
45
+ }
46
+ /** Check if this is an auth-related error */
47
+ isAuthError() {
48
+ return [
49
+ "TOKEN_EXPIRED",
50
+ "TOKEN_ALREADY_USED",
51
+ "TOKEN_INVALID",
52
+ "SESSION_EXPIRED",
53
+ "SESSION_INVALID",
54
+ "UNAUTHORIZED"
55
+ ].includes(this.code);
56
+ }
57
+ /** Check if this is a "not found" error */
58
+ isNotFoundError() {
59
+ return [
60
+ "NOT_FOUND",
61
+ "CUSTOMER_NOT_FOUND",
62
+ "SUBSCRIPTION_NOT_FOUND",
63
+ "PLAN_NOT_FOUND",
64
+ "APP_NOT_FOUND"
65
+ ].includes(this.code);
66
+ }
42
67
  };
43
68
 
44
69
  // src/http.ts
70
+ function mapErrorCode(status, message, errorType) {
71
+ if (errorType && errorType !== "Error") {
72
+ const codeFromError = errorType.toUpperCase().replace(/\s+/g, "_");
73
+ if (codeFromError.includes("NOT_FOUND")) return "NOT_FOUND";
74
+ if (codeFromError.includes("UNAUTHORIZED")) return "UNAUTHORIZED";
75
+ }
76
+ const lowerMessage = message.toLowerCase();
77
+ if (lowerMessage.includes("token") && lowerMessage.includes("expired")) return "TOKEN_EXPIRED";
78
+ if (lowerMessage.includes("already been used")) return "TOKEN_ALREADY_USED";
79
+ if (lowerMessage.includes("invalid") && lowerMessage.includes("token")) return "TOKEN_INVALID";
80
+ if (lowerMessage.includes("session") && lowerMessage.includes("expired")) return "SESSION_EXPIRED";
81
+ if (lowerMessage.includes("session") && lowerMessage.includes("invalid")) return "SESSION_INVALID";
82
+ if (lowerMessage.includes("customer") && lowerMessage.includes("not found")) return "CUSTOMER_NOT_FOUND";
83
+ if (lowerMessage.includes("subscription") && lowerMessage.includes("not found")) return "SUBSCRIPTION_NOT_FOUND";
84
+ if (lowerMessage.includes("plan") && lowerMessage.includes("not found")) return "PLAN_NOT_FOUND";
85
+ if (lowerMessage.includes("app") && lowerMessage.includes("not found")) return "APP_NOT_FOUND";
86
+ if (lowerMessage.includes("not found")) return "NOT_FOUND";
87
+ if (lowerMessage.includes("limit") && lowerMessage.includes("exceeded")) return "USAGE_LIMIT_EXCEEDED";
88
+ if (lowerMessage.includes("metric") && lowerMessage.includes("not found")) return "METRIC_NOT_FOUND";
89
+ if (lowerMessage.includes("feature") && lowerMessage.includes("not available")) return "FEATURE_NOT_AVAILABLE";
90
+ if (lowerMessage.includes("no active subscription")) return "NO_ACTIVE_SUBSCRIPTION";
91
+ if (lowerMessage.includes("required")) return "MISSING_REQUIRED_FIELD";
92
+ if (lowerMessage.includes("invalid") && lowerMessage.includes("email")) return "INVALID_EMAIL";
93
+ if (status === 400) return "VALIDATION_ERROR";
94
+ if (status === 401) return "UNAUTHORIZED";
95
+ if (status === 404) return "NOT_FOUND";
96
+ return "UNKNOWN_ERROR";
97
+ }
45
98
  var HttpClient = class {
46
99
  constructor(config) {
47
100
  this.config = config;
@@ -79,11 +132,9 @@ var HttpClient = class {
79
132
  const data = await response.json();
80
133
  if (!response.ok) {
81
134
  const errorData = data;
82
- throw new StackBEError(
83
- errorData.message || "Unknown error",
84
- errorData.statusCode || response.status,
85
- errorData.error || "UNKNOWN_ERROR"
86
- );
135
+ const message = errorData.message || "Unknown error";
136
+ const code = errorData.code || mapErrorCode(response.status, message, errorData.error || "");
137
+ throw new StackBEError(message, errorData.statusCode || response.status, code);
87
138
  }
88
139
  return data;
89
140
  } catch (error) {
@@ -635,14 +686,51 @@ var SubscriptionsClient = class {
635
686
  };
636
687
 
637
688
  // src/auth.ts
689
+ function decodeJwt(token) {
690
+ try {
691
+ const parts = token.split(".");
692
+ if (parts.length !== 3) return null;
693
+ const payload = Buffer.from(parts[1], "base64").toString("utf-8");
694
+ return JSON.parse(payload);
695
+ } catch {
696
+ return null;
697
+ }
698
+ }
638
699
  var AuthClient = class {
639
- constructor(http, appId) {
700
+ constructor(http, appId, options = {}) {
640
701
  this.http = http;
641
702
  this.appId = appId;
703
+ this.sessionCache = /* @__PURE__ */ new Map();
704
+ this.sessionCacheTTL = (options.sessionCacheTTL ?? 0) * 1e3;
705
+ this.devCallbackUrl = options.devCallbackUrl;
706
+ }
707
+ /**
708
+ * Check if we're in a development environment.
709
+ */
710
+ isDev() {
711
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
712
+ return true;
713
+ }
714
+ return false;
715
+ }
716
+ /**
717
+ * Clear the session cache (useful for testing or logout).
718
+ */
719
+ clearCache() {
720
+ this.sessionCache.clear();
721
+ }
722
+ /**
723
+ * Remove a specific session from the cache.
724
+ */
725
+ invalidateSession(sessionToken) {
726
+ this.sessionCache.delete(sessionToken);
642
727
  }
643
728
  /**
644
729
  * Send a magic link email to a customer for passwordless authentication.
645
730
  *
731
+ * If `devCallbackUrl` is configured and we're in a dev environment (NODE_ENV !== 'production'),
732
+ * the dev callback URL will be used automatically.
733
+ *
646
734
  * @example
647
735
  * ```typescript
648
736
  * // Send magic link
@@ -653,19 +741,26 @@ var AuthClient = class {
653
741
  * redirectUrl: 'https://myapp.com/dashboard',
654
742
  * });
655
743
  *
656
- * // For localhost development
744
+ * // For localhost development (auto-detected if devCallbackUrl is configured)
657
745
  * await stackbe.auth.sendMagicLink('user@example.com', {
658
746
  * useDev: true,
659
747
  * });
660
748
  * ```
661
749
  */
662
750
  async sendMagicLink(email, options = {}) {
751
+ let useDev = options.useDev;
752
+ let redirectUrl = options.redirectUrl;
753
+ if (!redirectUrl && this.devCallbackUrl) {
754
+ if (useDev || this.isDev()) {
755
+ redirectUrl = this.devCallbackUrl;
756
+ }
757
+ }
663
758
  return this.http.post(
664
759
  `/v1/apps/${this.appId}/auth/magic-link`,
665
760
  {
666
761
  email,
667
- redirectUrl: options.redirectUrl,
668
- useDev: options.useDev
762
+ redirectUrl,
763
+ useDev
669
764
  }
670
765
  );
671
766
  }
@@ -673,32 +768,83 @@ var AuthClient = class {
673
768
  * Verify a magic link token and get a session token.
674
769
  * Call this when the user clicks the magic link.
675
770
  *
771
+ * Returns the session token along with tenant and organization context.
772
+ *
676
773
  * @example
677
774
  * ```typescript
678
775
  * // In your /verify route handler
679
776
  * const { token } = req.query;
680
777
  *
681
- * const result = await stackbe.auth.verifyToken(token);
778
+ * try {
779
+ * const result = await stackbe.auth.verifyToken(token);
682
780
  *
683
- * if (result.valid) {
684
- * // Set session cookie
685
- * res.cookie('session', result.token, { httpOnly: true });
781
+ * // result includes: customerId, email, sessionToken, tenantId, organizationId
782
+ * res.cookie('session', result.sessionToken, { httpOnly: true });
686
783
  * res.redirect('/dashboard');
687
- * } else {
688
- * res.redirect('/login?error=invalid_token');
784
+ * } catch (error) {
785
+ * if (error instanceof StackBEError) {
786
+ * if (error.code === 'TOKEN_EXPIRED') {
787
+ * res.redirect('/login?error=expired');
788
+ * } else if (error.code === 'TOKEN_ALREADY_USED') {
789
+ * res.redirect('/login?error=used');
790
+ * }
791
+ * }
689
792
  * }
690
793
  * ```
691
794
  */
692
795
  async verifyToken(token) {
693
- return this.http.post(
694
- `/v1/apps/${this.appId}/auth/verify`,
695
- { token }
796
+ const response = await fetch(
797
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/verify?token=${encodeURIComponent(token)}`,
798
+ {
799
+ headers: {
800
+ "Content-Type": "application/json"
801
+ }
802
+ }
696
803
  );
804
+ if (!response.ok) {
805
+ const errorData = await response.json().catch(() => ({}));
806
+ const message = errorData.message || "Token verification failed";
807
+ let code = "TOKEN_INVALID";
808
+ if (message.includes("expired")) {
809
+ code = "TOKEN_EXPIRED";
810
+ } else if (message.includes("already been used")) {
811
+ code = "TOKEN_ALREADY_USED";
812
+ }
813
+ throw new StackBEError(message, response.status, code);
814
+ }
815
+ const data = await response.json();
816
+ let tenantId;
817
+ let organizationId;
818
+ let orgRole;
819
+ if (data.sessionToken) {
820
+ const payload = decodeJwt(data.sessionToken);
821
+ if (payload) {
822
+ tenantId = payload.tenantId;
823
+ organizationId = payload.organizationId;
824
+ orgRole = payload.orgRole;
825
+ }
826
+ }
827
+ return {
828
+ success: data.success ?? true,
829
+ valid: true,
830
+ customerId: data.customerId,
831
+ email: data.email,
832
+ sessionToken: data.sessionToken,
833
+ tenantId,
834
+ organizationId,
835
+ orgRole,
836
+ redirectUrl: data.redirectUrl
837
+ };
697
838
  }
698
839
  /**
699
840
  * Get the current session for an authenticated customer.
700
841
  * Use this to validate session tokens and get customer data.
701
842
  *
843
+ * If `sessionCacheTTL` is configured, results are cached to reduce API calls.
844
+ * Use `invalidateSession()` or `clearCache()` to manually clear cached sessions.
845
+ *
846
+ * Returns session info including tenant and organization context extracted from the JWT.
847
+ *
702
848
  * @example
703
849
  * ```typescript
704
850
  * // Validate session on each request
@@ -707,16 +853,27 @@ var AuthClient = class {
707
853
  * const session = await stackbe.auth.getSession(sessionToken);
708
854
  *
709
855
  * if (session) {
710
- * req.customer = session.customer;
711
- * req.subscription = session.subscription;
712
- * req.entitlements = session.entitlements;
856
+ * console.log(session.customerId);
857
+ * console.log(session.tenantId); // Tenant context
858
+ * console.log(session.organizationId); // Org context (if applicable)
859
+ * console.log(session.subscription);
860
+ * console.log(session.entitlements);
713
861
  * }
714
862
  * ```
715
863
  */
716
864
  async getSession(sessionToken) {
865
+ if (this.sessionCacheTTL > 0) {
866
+ const cached = this.sessionCache.get(sessionToken);
867
+ if (cached && cached.expiresAt > Date.now()) {
868
+ return cached.session;
869
+ }
870
+ if (cached) {
871
+ this.sessionCache.delete(sessionToken);
872
+ }
873
+ }
717
874
  try {
718
875
  const response = await fetch(
719
- `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session?appId=${this.appId}`,
876
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session`,
720
877
  {
721
878
  headers: {
722
879
  "Authorization": `Bearer ${sessionToken}`,
@@ -726,12 +883,44 @@ var AuthClient = class {
726
883
  );
727
884
  if (!response.ok) {
728
885
  if (response.status === 401) {
886
+ this.sessionCache.delete(sessionToken);
729
887
  return null;
730
888
  }
731
- throw new Error("Failed to get session");
889
+ throw new StackBEError("Failed to get session", response.status, "SESSION_INVALID");
890
+ }
891
+ const data = await response.json();
892
+ let tenantId;
893
+ let organizationId;
894
+ let orgRole;
895
+ const payload = decodeJwt(sessionToken);
896
+ if (payload) {
897
+ tenantId = payload.tenantId;
898
+ organizationId = payload.organizationId;
899
+ orgRole = payload.orgRole;
900
+ }
901
+ const session = {
902
+ valid: data.valid ?? true,
903
+ customerId: data.customerId,
904
+ email: data.email,
905
+ expiresAt: data.expiresAt,
906
+ tenantId: tenantId ?? data.tenantId,
907
+ organizationId: data.organizationId ?? organizationId,
908
+ orgRole: data.orgRole ?? orgRole,
909
+ customer: data.customer,
910
+ subscription: data.subscription,
911
+ entitlements: data.entitlements
912
+ };
913
+ if (this.sessionCacheTTL > 0) {
914
+ this.sessionCache.set(sessionToken, {
915
+ session,
916
+ expiresAt: Date.now() + this.sessionCacheTTL
917
+ });
918
+ }
919
+ return session;
920
+ } catch (error) {
921
+ if (error instanceof StackBEError) {
922
+ throw error;
732
923
  }
733
- return await response.json();
734
- } catch {
735
924
  return null;
736
925
  }
737
926
  }
@@ -843,7 +1032,10 @@ var StackBE = class {
843
1032
  this.customers = new CustomersClient(this.http);
844
1033
  this.checkout = new CheckoutClient(this.http, config.appId);
845
1034
  this.subscriptions = new SubscriptionsClient(this.http);
846
- this.auth = new AuthClient(this.http, config.appId);
1035
+ this.auth = new AuthClient(this.http, config.appId, {
1036
+ sessionCacheTTL: config.sessionCacheTTL,
1037
+ devCallbackUrl: config.devCallbackUrl
1038
+ });
847
1039
  }
848
1040
  /**
849
1041
  * Create a middleware for Express that tracks usage automatically.
package/dist/index.mjs CHANGED
@@ -6,9 +6,62 @@ var StackBEError = class extends Error {
6
6
  this.statusCode = statusCode;
7
7
  this.code = code;
8
8
  }
9
+ /** Check if this is a specific error type */
10
+ is(code) {
11
+ return this.code === code;
12
+ }
13
+ /** Check if this is an auth-related error */
14
+ isAuthError() {
15
+ return [
16
+ "TOKEN_EXPIRED",
17
+ "TOKEN_ALREADY_USED",
18
+ "TOKEN_INVALID",
19
+ "SESSION_EXPIRED",
20
+ "SESSION_INVALID",
21
+ "UNAUTHORIZED"
22
+ ].includes(this.code);
23
+ }
24
+ /** Check if this is a "not found" error */
25
+ isNotFoundError() {
26
+ return [
27
+ "NOT_FOUND",
28
+ "CUSTOMER_NOT_FOUND",
29
+ "SUBSCRIPTION_NOT_FOUND",
30
+ "PLAN_NOT_FOUND",
31
+ "APP_NOT_FOUND"
32
+ ].includes(this.code);
33
+ }
9
34
  };
10
35
 
11
36
  // src/http.ts
37
+ function mapErrorCode(status, message, errorType) {
38
+ if (errorType && errorType !== "Error") {
39
+ const codeFromError = errorType.toUpperCase().replace(/\s+/g, "_");
40
+ if (codeFromError.includes("NOT_FOUND")) return "NOT_FOUND";
41
+ if (codeFromError.includes("UNAUTHORIZED")) return "UNAUTHORIZED";
42
+ }
43
+ const lowerMessage = message.toLowerCase();
44
+ if (lowerMessage.includes("token") && lowerMessage.includes("expired")) return "TOKEN_EXPIRED";
45
+ if (lowerMessage.includes("already been used")) return "TOKEN_ALREADY_USED";
46
+ if (lowerMessage.includes("invalid") && lowerMessage.includes("token")) return "TOKEN_INVALID";
47
+ if (lowerMessage.includes("session") && lowerMessage.includes("expired")) return "SESSION_EXPIRED";
48
+ if (lowerMessage.includes("session") && lowerMessage.includes("invalid")) return "SESSION_INVALID";
49
+ if (lowerMessage.includes("customer") && lowerMessage.includes("not found")) return "CUSTOMER_NOT_FOUND";
50
+ if (lowerMessage.includes("subscription") && lowerMessage.includes("not found")) return "SUBSCRIPTION_NOT_FOUND";
51
+ if (lowerMessage.includes("plan") && lowerMessage.includes("not found")) return "PLAN_NOT_FOUND";
52
+ if (lowerMessage.includes("app") && lowerMessage.includes("not found")) return "APP_NOT_FOUND";
53
+ if (lowerMessage.includes("not found")) return "NOT_FOUND";
54
+ if (lowerMessage.includes("limit") && lowerMessage.includes("exceeded")) return "USAGE_LIMIT_EXCEEDED";
55
+ if (lowerMessage.includes("metric") && lowerMessage.includes("not found")) return "METRIC_NOT_FOUND";
56
+ if (lowerMessage.includes("feature") && lowerMessage.includes("not available")) return "FEATURE_NOT_AVAILABLE";
57
+ if (lowerMessage.includes("no active subscription")) return "NO_ACTIVE_SUBSCRIPTION";
58
+ if (lowerMessage.includes("required")) return "MISSING_REQUIRED_FIELD";
59
+ if (lowerMessage.includes("invalid") && lowerMessage.includes("email")) return "INVALID_EMAIL";
60
+ if (status === 400) return "VALIDATION_ERROR";
61
+ if (status === 401) return "UNAUTHORIZED";
62
+ if (status === 404) return "NOT_FOUND";
63
+ return "UNKNOWN_ERROR";
64
+ }
12
65
  var HttpClient = class {
13
66
  constructor(config) {
14
67
  this.config = config;
@@ -46,11 +99,9 @@ var HttpClient = class {
46
99
  const data = await response.json();
47
100
  if (!response.ok) {
48
101
  const errorData = data;
49
- throw new StackBEError(
50
- errorData.message || "Unknown error",
51
- errorData.statusCode || response.status,
52
- errorData.error || "UNKNOWN_ERROR"
53
- );
102
+ const message = errorData.message || "Unknown error";
103
+ const code = errorData.code || mapErrorCode(response.status, message, errorData.error || "");
104
+ throw new StackBEError(message, errorData.statusCode || response.status, code);
54
105
  }
55
106
  return data;
56
107
  } catch (error) {
@@ -602,14 +653,51 @@ var SubscriptionsClient = class {
602
653
  };
603
654
 
604
655
  // src/auth.ts
656
+ function decodeJwt(token) {
657
+ try {
658
+ const parts = token.split(".");
659
+ if (parts.length !== 3) return null;
660
+ const payload = Buffer.from(parts[1], "base64").toString("utf-8");
661
+ return JSON.parse(payload);
662
+ } catch {
663
+ return null;
664
+ }
665
+ }
605
666
  var AuthClient = class {
606
- constructor(http, appId) {
667
+ constructor(http, appId, options = {}) {
607
668
  this.http = http;
608
669
  this.appId = appId;
670
+ this.sessionCache = /* @__PURE__ */ new Map();
671
+ this.sessionCacheTTL = (options.sessionCacheTTL ?? 0) * 1e3;
672
+ this.devCallbackUrl = options.devCallbackUrl;
673
+ }
674
+ /**
675
+ * Check if we're in a development environment.
676
+ */
677
+ isDev() {
678
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
679
+ return true;
680
+ }
681
+ return false;
682
+ }
683
+ /**
684
+ * Clear the session cache (useful for testing or logout).
685
+ */
686
+ clearCache() {
687
+ this.sessionCache.clear();
688
+ }
689
+ /**
690
+ * Remove a specific session from the cache.
691
+ */
692
+ invalidateSession(sessionToken) {
693
+ this.sessionCache.delete(sessionToken);
609
694
  }
610
695
  /**
611
696
  * Send a magic link email to a customer for passwordless authentication.
612
697
  *
698
+ * If `devCallbackUrl` is configured and we're in a dev environment (NODE_ENV !== 'production'),
699
+ * the dev callback URL will be used automatically.
700
+ *
613
701
  * @example
614
702
  * ```typescript
615
703
  * // Send magic link
@@ -620,19 +708,26 @@ var AuthClient = class {
620
708
  * redirectUrl: 'https://myapp.com/dashboard',
621
709
  * });
622
710
  *
623
- * // For localhost development
711
+ * // For localhost development (auto-detected if devCallbackUrl is configured)
624
712
  * await stackbe.auth.sendMagicLink('user@example.com', {
625
713
  * useDev: true,
626
714
  * });
627
715
  * ```
628
716
  */
629
717
  async sendMagicLink(email, options = {}) {
718
+ let useDev = options.useDev;
719
+ let redirectUrl = options.redirectUrl;
720
+ if (!redirectUrl && this.devCallbackUrl) {
721
+ if (useDev || this.isDev()) {
722
+ redirectUrl = this.devCallbackUrl;
723
+ }
724
+ }
630
725
  return this.http.post(
631
726
  `/v1/apps/${this.appId}/auth/magic-link`,
632
727
  {
633
728
  email,
634
- redirectUrl: options.redirectUrl,
635
- useDev: options.useDev
729
+ redirectUrl,
730
+ useDev
636
731
  }
637
732
  );
638
733
  }
@@ -640,32 +735,83 @@ var AuthClient = class {
640
735
  * Verify a magic link token and get a session token.
641
736
  * Call this when the user clicks the magic link.
642
737
  *
738
+ * Returns the session token along with tenant and organization context.
739
+ *
643
740
  * @example
644
741
  * ```typescript
645
742
  * // In your /verify route handler
646
743
  * const { token } = req.query;
647
744
  *
648
- * const result = await stackbe.auth.verifyToken(token);
745
+ * try {
746
+ * const result = await stackbe.auth.verifyToken(token);
649
747
  *
650
- * if (result.valid) {
651
- * // Set session cookie
652
- * res.cookie('session', result.token, { httpOnly: true });
748
+ * // result includes: customerId, email, sessionToken, tenantId, organizationId
749
+ * res.cookie('session', result.sessionToken, { httpOnly: true });
653
750
  * res.redirect('/dashboard');
654
- * } else {
655
- * res.redirect('/login?error=invalid_token');
751
+ * } catch (error) {
752
+ * if (error instanceof StackBEError) {
753
+ * if (error.code === 'TOKEN_EXPIRED') {
754
+ * res.redirect('/login?error=expired');
755
+ * } else if (error.code === 'TOKEN_ALREADY_USED') {
756
+ * res.redirect('/login?error=used');
757
+ * }
758
+ * }
656
759
  * }
657
760
  * ```
658
761
  */
659
762
  async verifyToken(token) {
660
- return this.http.post(
661
- `/v1/apps/${this.appId}/auth/verify`,
662
- { token }
763
+ const response = await fetch(
764
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/verify?token=${encodeURIComponent(token)}`,
765
+ {
766
+ headers: {
767
+ "Content-Type": "application/json"
768
+ }
769
+ }
663
770
  );
771
+ if (!response.ok) {
772
+ const errorData = await response.json().catch(() => ({}));
773
+ const message = errorData.message || "Token verification failed";
774
+ let code = "TOKEN_INVALID";
775
+ if (message.includes("expired")) {
776
+ code = "TOKEN_EXPIRED";
777
+ } else if (message.includes("already been used")) {
778
+ code = "TOKEN_ALREADY_USED";
779
+ }
780
+ throw new StackBEError(message, response.status, code);
781
+ }
782
+ const data = await response.json();
783
+ let tenantId;
784
+ let organizationId;
785
+ let orgRole;
786
+ if (data.sessionToken) {
787
+ const payload = decodeJwt(data.sessionToken);
788
+ if (payload) {
789
+ tenantId = payload.tenantId;
790
+ organizationId = payload.organizationId;
791
+ orgRole = payload.orgRole;
792
+ }
793
+ }
794
+ return {
795
+ success: data.success ?? true,
796
+ valid: true,
797
+ customerId: data.customerId,
798
+ email: data.email,
799
+ sessionToken: data.sessionToken,
800
+ tenantId,
801
+ organizationId,
802
+ orgRole,
803
+ redirectUrl: data.redirectUrl
804
+ };
664
805
  }
665
806
  /**
666
807
  * Get the current session for an authenticated customer.
667
808
  * Use this to validate session tokens and get customer data.
668
809
  *
810
+ * If `sessionCacheTTL` is configured, results are cached to reduce API calls.
811
+ * Use `invalidateSession()` or `clearCache()` to manually clear cached sessions.
812
+ *
813
+ * Returns session info including tenant and organization context extracted from the JWT.
814
+ *
669
815
  * @example
670
816
  * ```typescript
671
817
  * // Validate session on each request
@@ -674,16 +820,27 @@ var AuthClient = class {
674
820
  * const session = await stackbe.auth.getSession(sessionToken);
675
821
  *
676
822
  * if (session) {
677
- * req.customer = session.customer;
678
- * req.subscription = session.subscription;
679
- * req.entitlements = session.entitlements;
823
+ * console.log(session.customerId);
824
+ * console.log(session.tenantId); // Tenant context
825
+ * console.log(session.organizationId); // Org context (if applicable)
826
+ * console.log(session.subscription);
827
+ * console.log(session.entitlements);
680
828
  * }
681
829
  * ```
682
830
  */
683
831
  async getSession(sessionToken) {
832
+ if (this.sessionCacheTTL > 0) {
833
+ const cached = this.sessionCache.get(sessionToken);
834
+ if (cached && cached.expiresAt > Date.now()) {
835
+ return cached.session;
836
+ }
837
+ if (cached) {
838
+ this.sessionCache.delete(sessionToken);
839
+ }
840
+ }
684
841
  try {
685
842
  const response = await fetch(
686
- `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session?appId=${this.appId}`,
843
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session`,
687
844
  {
688
845
  headers: {
689
846
  "Authorization": `Bearer ${sessionToken}`,
@@ -693,12 +850,44 @@ var AuthClient = class {
693
850
  );
694
851
  if (!response.ok) {
695
852
  if (response.status === 401) {
853
+ this.sessionCache.delete(sessionToken);
696
854
  return null;
697
855
  }
698
- throw new Error("Failed to get session");
856
+ throw new StackBEError("Failed to get session", response.status, "SESSION_INVALID");
857
+ }
858
+ const data = await response.json();
859
+ let tenantId;
860
+ let organizationId;
861
+ let orgRole;
862
+ const payload = decodeJwt(sessionToken);
863
+ if (payload) {
864
+ tenantId = payload.tenantId;
865
+ organizationId = payload.organizationId;
866
+ orgRole = payload.orgRole;
867
+ }
868
+ const session = {
869
+ valid: data.valid ?? true,
870
+ customerId: data.customerId,
871
+ email: data.email,
872
+ expiresAt: data.expiresAt,
873
+ tenantId: tenantId ?? data.tenantId,
874
+ organizationId: data.organizationId ?? organizationId,
875
+ orgRole: data.orgRole ?? orgRole,
876
+ customer: data.customer,
877
+ subscription: data.subscription,
878
+ entitlements: data.entitlements
879
+ };
880
+ if (this.sessionCacheTTL > 0) {
881
+ this.sessionCache.set(sessionToken, {
882
+ session,
883
+ expiresAt: Date.now() + this.sessionCacheTTL
884
+ });
885
+ }
886
+ return session;
887
+ } catch (error) {
888
+ if (error instanceof StackBEError) {
889
+ throw error;
699
890
  }
700
- return await response.json();
701
- } catch {
702
891
  return null;
703
892
  }
704
893
  }
@@ -810,7 +999,10 @@ var StackBE = class {
810
999
  this.customers = new CustomersClient(this.http);
811
1000
  this.checkout = new CheckoutClient(this.http, config.appId);
812
1001
  this.subscriptions = new SubscriptionsClient(this.http);
813
- this.auth = new AuthClient(this.http, config.appId);
1002
+ this.auth = new AuthClient(this.http, config.appId, {
1003
+ sessionCacheTTL: config.sessionCacheTTL,
1004
+ devCallbackUrl: config.devCallbackUrl
1005
+ });
814
1006
  }
815
1007
  /**
816
1008
  * Create a middleware for Express that tracks usage automatically.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbe/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Official JavaScript/TypeScript SDK for StackBE - the billing backend for your side project",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",