@stackbe/sdk 0.2.0 → 0.3.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,6 +686,16 @@ 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
700
  constructor(http, appId) {
640
701
  this.http = http;
@@ -673,32 +734,80 @@ var AuthClient = class {
673
734
  * Verify a magic link token and get a session token.
674
735
  * Call this when the user clicks the magic link.
675
736
  *
737
+ * Returns the session token along with tenant and organization context.
738
+ *
676
739
  * @example
677
740
  * ```typescript
678
741
  * // In your /verify route handler
679
742
  * const { token } = req.query;
680
743
  *
681
- * const result = await stackbe.auth.verifyToken(token);
744
+ * try {
745
+ * const result = await stackbe.auth.verifyToken(token);
682
746
  *
683
- * if (result.valid) {
684
- * // Set session cookie
685
- * res.cookie('session', result.token, { httpOnly: true });
747
+ * // result includes: customerId, email, sessionToken, tenantId, organizationId
748
+ * res.cookie('session', result.sessionToken, { httpOnly: true });
686
749
  * res.redirect('/dashboard');
687
- * } else {
688
- * res.redirect('/login?error=invalid_token');
750
+ * } catch (error) {
751
+ * if (error instanceof StackBEError) {
752
+ * if (error.code === 'TOKEN_EXPIRED') {
753
+ * res.redirect('/login?error=expired');
754
+ * } else if (error.code === 'TOKEN_ALREADY_USED') {
755
+ * res.redirect('/login?error=used');
756
+ * }
757
+ * }
689
758
  * }
690
759
  * ```
691
760
  */
692
761
  async verifyToken(token) {
693
- return this.http.post(
694
- `/v1/apps/${this.appId}/auth/verify`,
695
- { token }
762
+ const response = await fetch(
763
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/verify?token=${encodeURIComponent(token)}`,
764
+ {
765
+ headers: {
766
+ "Content-Type": "application/json"
767
+ }
768
+ }
696
769
  );
770
+ if (!response.ok) {
771
+ const errorData = await response.json().catch(() => ({}));
772
+ const message = errorData.message || "Token verification failed";
773
+ let code = "TOKEN_INVALID";
774
+ if (message.includes("expired")) {
775
+ code = "TOKEN_EXPIRED";
776
+ } else if (message.includes("already been used")) {
777
+ code = "TOKEN_ALREADY_USED";
778
+ }
779
+ throw new StackBEError(message, response.status, code);
780
+ }
781
+ const data = await response.json();
782
+ let tenantId;
783
+ let organizationId;
784
+ let orgRole;
785
+ if (data.sessionToken) {
786
+ const payload = decodeJwt(data.sessionToken);
787
+ if (payload) {
788
+ tenantId = payload.tenantId;
789
+ organizationId = payload.organizationId;
790
+ orgRole = payload.orgRole;
791
+ }
792
+ }
793
+ return {
794
+ success: data.success ?? true,
795
+ valid: true,
796
+ customerId: data.customerId,
797
+ email: data.email,
798
+ sessionToken: data.sessionToken,
799
+ tenantId,
800
+ organizationId,
801
+ orgRole,
802
+ redirectUrl: data.redirectUrl
803
+ };
697
804
  }
698
805
  /**
699
806
  * Get the current session for an authenticated customer.
700
807
  * Use this to validate session tokens and get customer data.
701
808
  *
809
+ * Returns session info including tenant and organization context extracted from the JWT.
810
+ *
702
811
  * @example
703
812
  * ```typescript
704
813
  * // Validate session on each request
@@ -707,16 +816,18 @@ var AuthClient = class {
707
816
  * const session = await stackbe.auth.getSession(sessionToken);
708
817
  *
709
818
  * if (session) {
710
- * req.customer = session.customer;
711
- * req.subscription = session.subscription;
712
- * req.entitlements = session.entitlements;
819
+ * console.log(session.customerId);
820
+ * console.log(session.tenantId); // Tenant context
821
+ * console.log(session.organizationId); // Org context (if applicable)
822
+ * console.log(session.subscription);
823
+ * console.log(session.entitlements);
713
824
  * }
714
825
  * ```
715
826
  */
716
827
  async getSession(sessionToken) {
717
828
  try {
718
829
  const response = await fetch(
719
- `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session?appId=${this.appId}`,
830
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session`,
720
831
  {
721
832
  headers: {
722
833
  "Authorization": `Bearer ${sessionToken}`,
@@ -728,10 +839,34 @@ var AuthClient = class {
728
839
  if (response.status === 401) {
729
840
  return null;
730
841
  }
731
- throw new Error("Failed to get session");
842
+ throw new StackBEError("Failed to get session", response.status, "SESSION_INVALID");
843
+ }
844
+ const data = await response.json();
845
+ let tenantId;
846
+ let organizationId;
847
+ let orgRole;
848
+ const payload = decodeJwt(sessionToken);
849
+ if (payload) {
850
+ tenantId = payload.tenantId;
851
+ organizationId = payload.organizationId;
852
+ orgRole = payload.orgRole;
853
+ }
854
+ return {
855
+ valid: data.valid ?? true,
856
+ customerId: data.customerId,
857
+ email: data.email,
858
+ expiresAt: data.expiresAt,
859
+ tenantId: tenantId ?? data.tenantId,
860
+ organizationId: data.organizationId ?? organizationId,
861
+ orgRole: data.orgRole ?? orgRole,
862
+ customer: data.customer,
863
+ subscription: data.subscription,
864
+ entitlements: data.entitlements
865
+ };
866
+ } catch (error) {
867
+ if (error instanceof StackBEError) {
868
+ throw error;
732
869
  }
733
- return await response.json();
734
- } catch {
735
870
  return null;
736
871
  }
737
872
  }
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,6 +653,16 @@ 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
667
  constructor(http, appId) {
607
668
  this.http = http;
@@ -640,32 +701,80 @@ var AuthClient = class {
640
701
  * Verify a magic link token and get a session token.
641
702
  * Call this when the user clicks the magic link.
642
703
  *
704
+ * Returns the session token along with tenant and organization context.
705
+ *
643
706
  * @example
644
707
  * ```typescript
645
708
  * // In your /verify route handler
646
709
  * const { token } = req.query;
647
710
  *
648
- * const result = await stackbe.auth.verifyToken(token);
711
+ * try {
712
+ * const result = await stackbe.auth.verifyToken(token);
649
713
  *
650
- * if (result.valid) {
651
- * // Set session cookie
652
- * res.cookie('session', result.token, { httpOnly: true });
714
+ * // result includes: customerId, email, sessionToken, tenantId, organizationId
715
+ * res.cookie('session', result.sessionToken, { httpOnly: true });
653
716
  * res.redirect('/dashboard');
654
- * } else {
655
- * res.redirect('/login?error=invalid_token');
717
+ * } catch (error) {
718
+ * if (error instanceof StackBEError) {
719
+ * if (error.code === 'TOKEN_EXPIRED') {
720
+ * res.redirect('/login?error=expired');
721
+ * } else if (error.code === 'TOKEN_ALREADY_USED') {
722
+ * res.redirect('/login?error=used');
723
+ * }
724
+ * }
656
725
  * }
657
726
  * ```
658
727
  */
659
728
  async verifyToken(token) {
660
- return this.http.post(
661
- `/v1/apps/${this.appId}/auth/verify`,
662
- { token }
729
+ const response = await fetch(
730
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/verify?token=${encodeURIComponent(token)}`,
731
+ {
732
+ headers: {
733
+ "Content-Type": "application/json"
734
+ }
735
+ }
663
736
  );
737
+ if (!response.ok) {
738
+ const errorData = await response.json().catch(() => ({}));
739
+ const message = errorData.message || "Token verification failed";
740
+ let code = "TOKEN_INVALID";
741
+ if (message.includes("expired")) {
742
+ code = "TOKEN_EXPIRED";
743
+ } else if (message.includes("already been used")) {
744
+ code = "TOKEN_ALREADY_USED";
745
+ }
746
+ throw new StackBEError(message, response.status, code);
747
+ }
748
+ const data = await response.json();
749
+ let tenantId;
750
+ let organizationId;
751
+ let orgRole;
752
+ if (data.sessionToken) {
753
+ const payload = decodeJwt(data.sessionToken);
754
+ if (payload) {
755
+ tenantId = payload.tenantId;
756
+ organizationId = payload.organizationId;
757
+ orgRole = payload.orgRole;
758
+ }
759
+ }
760
+ return {
761
+ success: data.success ?? true,
762
+ valid: true,
763
+ customerId: data.customerId,
764
+ email: data.email,
765
+ sessionToken: data.sessionToken,
766
+ tenantId,
767
+ organizationId,
768
+ orgRole,
769
+ redirectUrl: data.redirectUrl
770
+ };
664
771
  }
665
772
  /**
666
773
  * Get the current session for an authenticated customer.
667
774
  * Use this to validate session tokens and get customer data.
668
775
  *
776
+ * Returns session info including tenant and organization context extracted from the JWT.
777
+ *
669
778
  * @example
670
779
  * ```typescript
671
780
  * // Validate session on each request
@@ -674,16 +783,18 @@ var AuthClient = class {
674
783
  * const session = await stackbe.auth.getSession(sessionToken);
675
784
  *
676
785
  * if (session) {
677
- * req.customer = session.customer;
678
- * req.subscription = session.subscription;
679
- * req.entitlements = session.entitlements;
786
+ * console.log(session.customerId);
787
+ * console.log(session.tenantId); // Tenant context
788
+ * console.log(session.organizationId); // Org context (if applicable)
789
+ * console.log(session.subscription);
790
+ * console.log(session.entitlements);
680
791
  * }
681
792
  * ```
682
793
  */
683
794
  async getSession(sessionToken) {
684
795
  try {
685
796
  const response = await fetch(
686
- `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session?appId=${this.appId}`,
797
+ `${this.http.baseUrl}/v1/apps/${this.appId}/auth/session`,
687
798
  {
688
799
  headers: {
689
800
  "Authorization": `Bearer ${sessionToken}`,
@@ -695,10 +806,34 @@ var AuthClient = class {
695
806
  if (response.status === 401) {
696
807
  return null;
697
808
  }
698
- throw new Error("Failed to get session");
809
+ throw new StackBEError("Failed to get session", response.status, "SESSION_INVALID");
810
+ }
811
+ const data = await response.json();
812
+ let tenantId;
813
+ let organizationId;
814
+ let orgRole;
815
+ const payload = decodeJwt(sessionToken);
816
+ if (payload) {
817
+ tenantId = payload.tenantId;
818
+ organizationId = payload.organizationId;
819
+ orgRole = payload.orgRole;
820
+ }
821
+ return {
822
+ valid: data.valid ?? true,
823
+ customerId: data.customerId,
824
+ email: data.email,
825
+ expiresAt: data.expiresAt,
826
+ tenantId: tenantId ?? data.tenantId,
827
+ organizationId: data.organizationId ?? organizationId,
828
+ orgRole: data.orgRole ?? orgRole,
829
+ customer: data.customer,
830
+ subscription: data.subscription,
831
+ entitlements: data.entitlements
832
+ };
833
+ } catch (error) {
834
+ if (error instanceof StackBEError) {
835
+ throw error;
699
836
  }
700
- return await response.json();
701
- } catch {
702
837
  return null;
703
838
  }
704
839
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbe/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",