@basictech/react 0.7.0-beta.4 → 0.7.0-beta.6

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.3";
254
+ var version = "0.7.0-beta.4";
255
255
 
256
256
  // src/updater/versionUpdater.ts
257
257
  var VersionUpdater = class {
@@ -395,6 +395,7 @@ var STORAGE_KEYS = {
395
395
  USER_INFO: "basic_user_info",
396
396
  AUTH_STATE: "basic_auth_state",
397
397
  REDIRECT_URI: "basic_redirect_uri",
398
+ SERVER_URL: "basic_server_url",
398
399
  DEBUG: "basic_debug"
399
400
  };
400
401
  function getCookie(name) {
@@ -593,8 +594,9 @@ async function validateAndCheckSchema(schema) {
593
594
  // src/AuthContext.tsx
594
595
  import { jsx, jsxs } from "react/jsx-runtime";
595
596
  var DEFAULT_AUTH_CONFIG = {
596
- scopes: "profile email app:admin",
597
- server_url: "https://api.basic.tech"
597
+ scopes: "profile,email,app:admin",
598
+ server_url: "https://api.basic.tech",
599
+ ws_url: "wss://pds.basic.id/ws"
598
600
  };
599
601
  var BasicContext = createContext({
600
602
  unicorn: "\u{1F984}",
@@ -633,9 +635,11 @@ function BasicProvider({
633
635
  const storageAdapter = storage || new LocalStorageAdapter();
634
636
  const authConfig = {
635
637
  scopes: auth?.scopes || DEFAULT_AUTH_CONFIG.scopes,
636
- server_url: auth?.server_url || DEFAULT_AUTH_CONFIG.server_url
638
+ server_url: auth?.server_url || DEFAULT_AUTH_CONFIG.server_url,
639
+ ws_url: auth?.ws_url || DEFAULT_AUTH_CONFIG.ws_url
637
640
  };
638
641
  const scopesString = Array.isArray(authConfig.scopes) ? authConfig.scopes.join(" ") : authConfig.scopes;
642
+ const refreshPromiseRef = useRef(null);
639
643
  const isDevMode = () => isDevelopment(debug);
640
644
  const cleanOAuthParams = () => cleanOAuthParamsFromUrl();
641
645
  useEffect(() => {
@@ -723,7 +727,10 @@ function BasicProvider({
723
727
  return;
724
728
  }
725
729
  log("connecting to db...");
726
- 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) => {
727
734
  log("error connecting to db", e);
728
735
  });
729
736
  }
@@ -733,6 +740,17 @@ function BasicProvider({
733
740
  useEffect(() => {
734
741
  const initializeAuth = async () => {
735
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);
736
754
  try {
737
755
  const versionUpdater = createVersionUpdater(storageAdapter, version, getMigrations());
738
756
  const updateResult = await versionUpdater.checkAndUpdate();
@@ -803,16 +821,21 @@ function BasicProvider({
803
821
  useEffect(() => {
804
822
  async function fetchUser(acc_token) {
805
823
  console.info("fetching user");
806
- const user2 = await fetch(`${authConfig.server_url}/auth/userInfo`, {
807
- method: "GET",
808
- headers: {
809
- "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}`);
810
838
  }
811
- }).then((response) => response.json()).catch((error2) => log("Error:", error2));
812
- if (user2.error) {
813
- log("error fetching user", user2.error);
814
- return;
815
- } else {
816
839
  if (token?.refresh_token) {
817
840
  await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token.refresh_token);
818
841
  }
@@ -823,6 +846,9 @@ function BasicProvider({
823
846
  setUser(user2);
824
847
  setIsSignedIn(true);
825
848
  setIsAuthReady(true);
849
+ } catch (error2) {
850
+ log("Failed to fetch user info:", error2);
851
+ setIsAuthReady(true);
826
852
  }
827
853
  }
828
854
  async function checkToken() {
@@ -832,7 +858,8 @@ function BasicProvider({
832
858
  return;
833
859
  }
834
860
  const decoded = jwtDecode(token?.access_token);
835
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3;
861
+ const expirationBuffer = 5;
862
+ const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
836
863
  if (isExpired) {
837
864
  log("token is expired - refreshing ...");
838
865
  try {
@@ -891,7 +918,9 @@ function BasicProvider({
891
918
  }
892
919
  const signInLink = await getSignInLink();
893
920
  log("Generated sign-in link:", signInLink);
894
- if (!signInLink || !signInLink.startsWith("https://")) {
921
+ try {
922
+ new URL(signInLink);
923
+ } catch {
895
924
  log("Error: Invalid sign-in link generated");
896
925
  throw new Error("Failed to generate valid sign-in URL");
897
926
  }
@@ -949,6 +978,7 @@ function BasicProvider({
949
978
  await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
950
979
  await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
951
980
  await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
981
+ await storageAdapter.remove(STORAGE_KEYS.SERVER_URL);
952
982
  if (syncRef.current) {
953
983
  (async () => {
954
984
  try {
@@ -968,6 +998,18 @@ function BasicProvider({
968
998
  const refreshToken = await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
969
999
  if (refreshToken) {
970
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
+ }
971
1013
  try {
972
1014
  const newToken = await fetchToken(refreshToken, true);
973
1015
  if (newToken?.access_token) {
@@ -990,9 +1032,24 @@ function BasicProvider({
990
1032
  throw new Error("no token found");
991
1033
  }
992
1034
  const decoded = jwtDecode(token?.access_token);
993
- const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3;
1035
+ const expirationBuffer = 5;
1036
+ const isExpired = decoded.exp && decoded.exp < Date.now() / 1e3 + expirationBuffer;
994
1037
  if (isExpired) {
995
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
+ }
996
1053
  const refreshToken = token?.refresh_token || await storageAdapter.get(STORAGE_KEYS.REFRESH_TOKEN);
997
1054
  if (refreshToken) {
998
1055
  try {
@@ -1013,98 +1070,117 @@ function BasicProvider({
1013
1070
  return token?.access_token || "";
1014
1071
  };
1015
1072
  const fetchToken = async (codeOrRefreshToken, isRefreshToken = false) => {
1016
- try {
1017
- if (!isOnline) {
1018
- log("Network is offline, marking refresh as pending");
1019
- setPendingRefresh(true);
1020
- throw new Error("Network offline - refresh will be retried when online");
1021
- }
1022
- let requestBody;
1023
- if (isRefreshToken) {
1024
- requestBody = {
1025
- grant_type: "refresh_token",
1026
- refresh_token: codeOrRefreshToken
1027
- };
1028
- if (project_id) {
1029
- requestBody.client_id = project_id;
1030
- }
1031
- } else {
1032
- requestBody = {
1033
- grant_type: "authorization_code",
1034
- code: codeOrRefreshToken
1035
- };
1036
- const storedRedirectUri = await storageAdapter.get(STORAGE_KEYS.REDIRECT_URI);
1037
- if (storedRedirectUri) {
1038
- requestBody.redirect_uri = storedRedirectUri;
1039
- log("Including redirect_uri in token exchange:", storedRedirectUri);
1040
- } else {
1041
- log("Warning: No redirect_uri found in storage for token exchange");
1042
- }
1043
- if (project_id) {
1044
- requestBody.client_id = project_id;
1045
- }
1046
- }
1047
- log("Token exchange request body:", { ...requestBody, refresh_token: isRefreshToken ? "[REDACTED]" : void 0, code: !isRefreshToken ? "[REDACTED]" : void 0 });
1048
- const token2 = await fetch(`${authConfig.server_url}/auth/token`, {
1049
- method: "POST",
1050
- headers: {
1051
- "Content-Type": "application/json"
1052
- },
1053
- body: JSON.stringify(requestBody)
1054
- }).then((response) => response.json()).catch((error2) => {
1055
- 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 {
1056
1079
  if (!isOnline) {
1080
+ log("Network is offline, marking refresh as pending");
1057
1081
  setPendingRefresh(true);
1058
1082
  throw new Error("Network offline - refresh will be retried when online");
1059
1083
  }
1060
- throw new Error("Network error during token refresh");
1061
- });
1062
- if (token2.error) {
1063
- log("error fetching token", token2.error);
1064
- if (token2.error.includes("network") || token2.error.includes("timeout")) {
1065
- setPendingRefresh(true);
1066
- 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
+ }
1067
1108
  }
1068
- await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1069
- await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1070
- await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1071
- clearCookie("basic_token");
1072
- clearCookie("basic_access_token");
1073
- setUser({});
1074
- setIsSignedIn(false);
1075
- setToken(null);
1076
- setIsAuthReady(true);
1077
- throw new Error(`Token refresh failed: ${token2.error}`);
1078
- } else {
1079
- setToken(token2);
1080
- setPendingRefresh(false);
1081
- if (token2.refresh_token) {
1082
- await storageAdapter.set(STORAGE_KEYS.REFRESH_TOKEN, token2.refresh_token);
1083
- 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");
1084
1155
  }
1085
- if (!isRefreshToken) {
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);
1086
1162
  await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1087
- log("Cleaned up redirect_uri from storage after successful exchange");
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);
1088
1170
  }
1089
- setCookie("basic_access_token", token2.access_token, { httpOnly: false });
1090
- log("Updated access token in cookie");
1171
+ throw error2;
1091
1172
  }
1092
- return token2;
1093
- } catch (error2) {
1094
- log("Token refresh error:", error2);
1095
- if (!error2.message.includes("offline") && !error2.message.includes("Network")) {
1096
- await storageAdapter.remove(STORAGE_KEYS.REFRESH_TOKEN);
1097
- await storageAdapter.remove(STORAGE_KEYS.USER_INFO);
1098
- await storageAdapter.remove(STORAGE_KEYS.REDIRECT_URI);
1099
- clearCookie("basic_token");
1100
- clearCookie("basic_access_token");
1101
- setUser({});
1102
- setIsSignedIn(false);
1103
- setToken(null);
1104
- setIsAuthReady(true);
1105
- }
1106
- 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
+ });
1107
1182
  }
1183
+ return refreshPromise;
1108
1184
  };
1109
1185
  const noDb = {
1110
1186
  collection: () => {