@basictech/react 0.7.0-beta.4 → 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/changelog.md +6 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +179 -103
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +179 -103
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/AuthContext.tsx +220 -120
- package/src/sync/index.ts +4 -4
- package/src/utils/storage.ts +1 -0
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 =
|
|
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 =
|
|
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.
|
|
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
|
|
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({
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
1069
|
-
await
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1090
|
-
log("Updated access token in cookie");
|
|
1171
|
+
throw error2;
|
|
1091
1172
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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: () => {
|