@basictech/react 0.7.0-beta.3 → 0.7.0-beta.5

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.mjs CHANGED
@@ -139,15 +139,15 @@ var BasicSync = class extends Dexie2 {
139
139
  this.version(2).stores({});
140
140
  this.Collection.prototype.get = this.Collection.prototype.toArray;
141
141
  }
142
- async connect({ access_token }) {
143
- const WS_URL = `wss://pds.basic.id/ws`;
142
+ async connect({ access_token, ws_url }) {
143
+ const WS_URL = ws_url || "wss://pds.basic.id/ws";
144
144
  log("Connecting to", WS_URL);
145
145
  await this.updateSyncNodes();
146
146
  log("Starting connection...");
147
147
  return this.syncable.connect("websocket", WS_URL, { authToken: access_token, schema: this.basic_schema });
148
148
  }
149
- async disconnect() {
150
- const WS_URL = `wss://pds.basic.id/ws`;
149
+ async disconnect({ ws_url } = {}) {
150
+ const WS_URL = ws_url || "wss://pds.basic.id/ws";
151
151
  return this.syncable.disconnect(WS_URL);
152
152
  }
153
153
  async updateSyncNodes() {
@@ -251,7 +251,7 @@ var BasicSync = class extends Dexie2 {
251
251
  };
252
252
 
253
253
  // package.json
254
- var version = "0.7.0-beta.2";
254
+ var version = "0.7.0-beta.4";
255
255
 
256
256
  // src/updater/versionUpdater.ts
257
257
  var VersionUpdater = class {
@@ -394,6 +394,8 @@ var STORAGE_KEYS = {
394
394
  REFRESH_TOKEN: "basic_refresh_token",
395
395
  USER_INFO: "basic_user_info",
396
396
  AUTH_STATE: "basic_auth_state",
397
+ REDIRECT_URI: "basic_redirect_uri",
398
+ SERVER_URL: "basic_server_url",
397
399
  DEBUG: "basic_debug"
398
400
  };
399
401
  function getCookie(name) {
@@ -591,6 +593,11 @@ async function validateAndCheckSchema(schema) {
591
593
 
592
594
  // src/AuthContext.tsx
593
595
  import { jsx, jsxs } from "react/jsx-runtime";
596
+ var DEFAULT_AUTH_CONFIG = {
597
+ scopes: "profile,email,app:admin",
598
+ server_url: "https://api.basic.tech",
599
+ ws_url: "wss://pds.basic.id/ws"
600
+ };
594
601
  var BasicContext = createContext({
595
602
  unicorn: "\u{1F984}",
596
603
  isAuthReady: false,
@@ -611,7 +618,8 @@ function BasicProvider({
611
618
  project_id,
612
619
  schema,
613
620
  debug = false,
614
- storage
621
+ storage,
622
+ auth
615
623
  }) {
616
624
  const [isAuthReady, setIsAuthReady] = useState(false);
617
625
  const [isSignedIn, setIsSignedIn] = useState(false);
@@ -625,6 +633,13 @@ function BasicProvider({
625
633
  const [pendingRefresh, setPendingRefresh] = useState(false);
626
634
  const syncRef = useRef(null);
627
635
  const storageAdapter = storage || new LocalStorageAdapter();
636
+ const authConfig = {
637
+ scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
638
+ server_url: auth?.server_url || DEFAULT_AUTH_CONFIG.server_url,
639
+ ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url
640
+ };
641
+ const scopesString = Array.isArray(authConfig.scopes) ? authConfig.scopes.join(" ") : authConfig.scopes;
642
+ const refreshPromiseRef = useRef(null);
628
643
  const isDevMode = () => isDevelopment(debug);
629
644
  const cleanOAuthParams = () => cleanOAuthParamsFromUrl();
630
645
  useEffect(() => {
@@ -712,7 +727,10 @@ function BasicProvider({
712
727
  return;
713
728
  }
714
729
  log("connecting to db...");
715
- syncRef.current?.connect({ access_token: tok }).catch((e) => {
730
+ syncRef.current?.connect({
731
+ access_token: tok,
732
+ ws_url: authConfig.ws_url
733
+ }).catch((e) => {
716
734
  log("error connecting to db", e);
717
735
  });
718
736
  }
@@ -722,6 +740,17 @@ function BasicProvider({
722
740
  useEffect(() => {
723
741
  const initializeAuth = async () => {
724
742
  await storageAdapter.set(STORAGE_KEYS.DEBUG, debug ? "true" : "false");
743
+ const storedServerUrl = await storageAdapter.get(STORAGE_KEYS.SERVER_URL);
744
+ if (storedServerUrl && storedServerUrl !== authConfig.server_url) {
745
+ log("Server URL changed, clearing stored tokens");
746
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
747
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
748
+ await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
749
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
750
+ clearCookie("basic_token");
751
+ clearCookie("basic_access_token");
752
+ }
753
+ await storageAdapter.set(STORAGE_KEYS.SERVER_URL, authConfig.server_url);
725
754
  try {
726
755
  const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
727
756
  const updateResult = await versionUpdater.checkAndUpdate();
@@ -792,16 +821,21 @@ function BasicProvider({
792
821
  useEffect(() => {
793
822
  async function fetchUser(acc_token) {
794
823
  console.info("fetching user");
795
- const user2 = await fetch("https://api.basic.tech/auth/userInfo", {
796
- method: "GET",
797
- headers: {
798
- "Authorization": `Bearer ${acc_token}`
824
+ try {
825
+ const response = await fetch(`${authConfig.server_url}/auth/userInfo`, {
826
+ method: "GET",
827
+ headers: {
828
+ "Authorization": `Bearer ${acc_token}`
829
+ }
830
+ });
831
+ if (!response.ok) {
832
+ throw new Error(`Failed to fetch user info: ${response.status}`);
833
+ }
834
+ const user2 = await response.json();
835
+ if (user2.error) {
836
+ log("error fetching user", user2.error);
837
+ throw new Error(`User info error: ${user2.error}`);
799
838
  }
800
- }).then((response) => response.json()).catch((error2) => log("Error:", error2));
801
- if (user2.error) {
802
- log("error fetching user", user2.error);
803
- return;
804
- } else {
805
839
  if (token?.refresh_token) {
806
840
  await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
807
841
  }
@@ -812,6 +846,9 @@ function BasicProvider({
812
846
  setUser(user2);
813
847
  setIsSignedIn(true);
814
848
  setIsAuthReady(true);
849
+ } catch (error2) {
850
+ log("Failed to fetch user info:", error2);
851
+ setIsAuthReady(true);
815
852
  }
816
853
  }
817
854
  async function checkToken() {
@@ -821,7 +858,8 @@ function BasicProvider({
821
858
  return;
822
859
  }
823
860
  const decoded = jwtDecode(token?.access_token);
824
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3;
861
+ const expirationBuffer = 5;
862
+ const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
825
863
  if (isExpired) {
826
864
  log("token is expired - refreshing ...");
827
865
  try {
@@ -856,13 +894,15 @@ function BasicProvider({
856
894
  if (!redirectUrl || !redirectUrl.startsWith("http://") && !redirectUrl.startsWith("https://")) {
857
895
  throw new Error("Invalid redirect URI provided");
858
896
  }
859
- let baseUrl = "https://api.basic.tech/auth/authorize";
897
+ await storageAdapter.set(STORAGE_KEYS.REDIRECT_URI, redirectUrl);
898
+ log("Stored redirect_uri for token exchange:", redirectUrl);
899
+ let baseUrl = `${authConfig.server_url}/auth/authorize`;
860
900
  baseUrl += `?client_id=${project_id}`;
861
901
  baseUrl += `&redirect_uri=${encodeURIComponent(redirectUrl)}`;
862
902
  baseUrl += `&response_type=code`;
863
- baseUrl += `&scope=profile`;
903
+ baseUrl += `&scope=${encodeURIComponent(scopesString)}`;
864
904
  baseUrl += `&state=${randomState}`;
865
- log("Generated sign-in link successfully");
905
+ log("Generated sign-in link successfully with scopes:", scopesString);
866
906
  return baseUrl;
867
907
  } catch (error2) {
868
908
  log("Error generating sign-in link:", error2);
@@ -878,7 +918,9 @@ function BasicProvider({
878
918
  }
879
919
  const signInLink = await getSignInLink();
880
920
  log("Generated sign-in link:", signInLink);
881
- if (!signInLink || !signInLink.startsWith("https://")) {
921
+ try {
922
+ new URL(signInLink);
923
+ } catch {
882
924
  log("Error: Invalid sign-in link generated");
883
925
  throw new Error("Failed to generate valid sign-in URL");
884
926
  }
@@ -935,6 +977,8 @@ function BasicProvider({
935
977
  await storageAdapter.remove(STORAGE_KEYS.AUTH_STATE);
936
978
  await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
937
979
  await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
980
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
981
+ await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
938
982
  if (syncRef.current) {
939
983
  (async () => {
940
984
  try {
@@ -954,6 +998,18 @@ function BasicProvider({
954
998
  const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
955
999
  if (refreshToken) {
956
1000
  log("No token in memory, attempting to refresh from storage");
1001
+ if (refreshPromiseRef.current) {
1002
+ log("Token refresh already in progress, waiting...");
1003
+ try {
1004
+ const newToken = await refreshPromiseRef.current;
1005
+ if (newToken?.access_token) {
1006
+ return newToken.access_token;
1007
+ }
1008
+ } catch (error2) {
1009
+ log("In-flight refresh failed:", error2);
1010
+ throw error2;
1011
+ }
1012
+ }
957
1013
  try {
958
1014
  const newToken = await fetchToken(refreshToken, true);
959
1015
  if (newToken?.access_token) {
@@ -976,9 +1032,24 @@ function BasicProvider({
976
1032
  throw new Error("no token found");
977
1033
  }
978
1034
  const decoded = jwtDecode(token?.access_token);
979
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3;
1035
+ const expirationBuffer = 5;
1036
+ const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
980
1037
  if (isExpired) {
981
1038
  log("token is expired - refreshing ...");
1039
+ if (refreshPromiseRef.current) {
1040
+ log("Token refresh already in progress, waiting...");
1041
+ try {
1042
+ const newToken = await refreshPromiseRef.current;
1043
+ return newToken?.access_token || "";
1044
+ } catch (error2) {
1045
+ log("In-flight refresh failed:", error2);
1046
+ if (error2.message.includes("offline") || error2.message.includes("Network")) {
1047
+ log("Network issue - using expired token until network is restored");
1048
+ return token.access_token;
1049
+ }
1050
+ throw error2;
1051
+ }
1052
+ }
982
1053
  const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
983
1054
  if (refreshToken) {
984
1055
  try {
@@ -999,73 +1070,117 @@ function BasicProvider({
999
1070
  return token?.access_token || "";
1000
1071
  };
1001
1072
  const fetchToken = async (codeOrRefreshToken, isRefreshToken = false) => {
1002
- try {
1003
- if (!isOnline) {
1004
- log("Network is offline, marking refresh as pending");
1005
- setPendingRefresh(true);
1006
- throw new Error("Network offline - refresh will be retried when online");
1007
- }
1008
- const requestBody = isRefreshToken ? {
1009
- grant_type: "refresh_token",
1010
- refresh_token: codeOrRefreshToken
1011
- } : {
1012
- grant_type: "authorization_code",
1013
- code: codeOrRefreshToken
1014
- };
1015
- const token2 = await fetch("https://api.basic.tech/auth/token", {
1016
- method: "POST",
1017
- headers: {
1018
- "Content-Type": "application/json"
1019
- },
1020
- body: JSON.stringify(requestBody)
1021
- }).then((response) => response.json()).catch((error2) => {
1022
- log("Network error fetching token:", error2);
1073
+ if (isRefreshToken && refreshPromiseRef.current) {
1074
+ log("Reusing in-flight refresh token request");
1075
+ return refreshPromiseRef.current;
1076
+ }
1077
+ const refreshPromise = (async () => {
1078
+ try {
1023
1079
  if (!isOnline) {
1080
+ log("Network is offline, marking refresh as pending");
1024
1081
  setPendingRefresh(true);
1025
1082
  throw new Error("Network offline - refresh will be retried when online");
1026
1083
  }
1027
- throw new Error("Network error during token refresh");
1028
- });
1029
- if (token2.error) {
1030
- log("error fetching token", token2.error);
1031
- if (token2.error.includes("network") || token2.error.includes("timeout")) {
1032
- setPendingRefresh(true);
1033
- throw new Error("Network issue - refresh will be retried when online");
1084
+ let requestBody;
1085
+ if (isRefreshToken) {
1086
+ requestBody = {
1087
+ grant_type: "refresh_token",
1088
+ refresh_token: codeOrRefreshToken
1089
+ };
1090
+ if (project_id) {
1091
+ requestBody.client_id = project_id;
1092
+ }
1093
+ } else {
1094
+ requestBody = {
1095
+ grant_type: "authorization_code",
1096
+ code: codeOrRefreshToken
1097
+ };
1098
+ const storedRedirectUri = await storageAdapter.get(STORAGE_KEYS.REDIRECT_URI);
1099
+ if (storedRedirectUri) {
1100
+ requestBody.redirect_uri = storedRedirectUri;
1101
+ log("Including redirect_uri in token exchange:", storedRedirectUri);
1102
+ } else {
1103
+ log("Warning: No redirect_uri found in storage for token exchange");
1104
+ }
1105
+ if (project_id) {
1106
+ requestBody.client_id = project_id;
1107
+ }
1034
1108
  }
1035
- await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1036
- await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1037
- clearCookie("basic_token");
1038
- clearCookie("basic_access_token");
1039
- setUser({});
1040
- setIsSignedIn(false);
1041
- setToken(null);
1042
- setIsAuthReady(true);
1043
- throw new Error(`Token refresh failed: ${token2.error}`);
1044
- } else {
1045
- setToken(token2);
1046
- setPendingRefresh(false);
1047
- if (token2.refresh_token) {
1048
- await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token2.refresh_token);
1049
- log("Updated refresh token in storage");
1109
+ log("Token exchange request body:", { ...requestBody, refresh_token: isRefreshToken ? "[REDACTED]" : void 0, code: !isRefreshToken ? "[REDACTED]" : void 0 });
1110
+ const token2 = await fetch(`${authConfig.server_url}/auth/token`, {
1111
+ method: "POST",
1112
+ headers: {
1113
+ "Content-Type": "application/json"
1114
+ },
1115
+ body: JSON.stringify(requestBody)
1116
+ }).then((response) => response.json()).catch((error2) => {
1117
+ log("Network error fetching token:", error2);
1118
+ if (!isOnline) {
1119
+ setPendingRefresh(true);
1120
+ throw new Error("Network offline - refresh will be retried when online");
1121
+ }
1122
+ throw new Error("Network error during token refresh");
1123
+ });
1124
+ if (token2.error) {
1125
+ log("error fetching token", token2.error);
1126
+ if (token2.error.includes("network") || token2.error.includes("timeout")) {
1127
+ setPendingRefresh(true);
1128
+ throw new Error("Network issue - refresh will be retried when online");
1129
+ }
1130
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1131
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1132
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1133
+ await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
1134
+ clearCookie("basic_token");
1135
+ clearCookie("basic_access_token");
1136
+ setUser({});
1137
+ setIsSignedIn(false);
1138
+ setToken(null);
1139
+ setIsAuthReady(true);
1140
+ throw new Error(`Token refresh failed: ${token2.error}`);
1141
+ } else {
1142
+ setToken(token2);
1143
+ setPendingRefresh(false);
1144
+ if (token2.refresh_token) {
1145
+ await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token2.refresh_token);
1146
+ log("Updated refresh token in storage");
1147
+ }
1148
+ if (!isRefreshToken) {
1149
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1150
+ log("Cleaned up redirect_uri from storage after successful exchange");
1151
+ }
1152
+ setCookie("basic_access_token", token2.access_token, { httpOnly: false });
1153
+ setCookie("basic_token", JSON.stringify(token2));
1154
+ log("Updated access token and full token in cookies");
1050
1155
  }
1051
- setCookie("basic_access_token", token2.access_token, { httpOnly: false });
1052
- log("Updated access token in cookie");
1053
- }
1054
- return token2;
1055
- } catch (error2) {
1056
- log("Token refresh error:", error2);
1057
- if (!error2.message.includes("offline") && !error2.message.includes("Network")) {
1058
- await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1059
- await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1060
- clearCookie("basic_token");
1061
- clearCookie("basic_access_token");
1062
- setUser({});
1063
- setIsSignedIn(false);
1064
- setToken(null);
1065
- setIsAuthReady(true);
1156
+ return token2;
1157
+ } catch (error2) {
1158
+ log("Token refresh error:", error2);
1159
+ if (!error2.message.includes("offline") && !error2.message.includes("Network")) {
1160
+ await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1161
+ await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1162
+ await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1163
+ await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
1164
+ clearCookie("basic_token");
1165
+ clearCookie("basic_access_token");
1166
+ setUser({});
1167
+ setIsSignedIn(false);
1168
+ setToken(null);
1169
+ setIsAuthReady(true);
1170
+ }
1171
+ throw error2;
1066
1172
  }
1067
- throw error2;
1173
+ })();
1174
+ if (isRefreshToken) {
1175
+ refreshPromiseRef.current = refreshPromise;
1176
+ refreshPromise.finally(() => {
1177
+ if (refreshPromiseRef.current === refreshPromise) {
1178
+ refreshPromiseRef.current = null;
1179
+ log("Cleared refresh promise reference");
1180
+ }
1181
+ });
1068
1182
  }
1183
+ return refreshPromise;
1069
1184
  };
1070
1185
  const noDb = {
1071
1186
  collection: () => {