@spidy092/auth-client 3.1.2 → 3.1.4

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.
@@ -143,6 +143,15 @@ function clearRefreshToken() {
143
143
  } catch (err) {
144
144
  }
145
145
  }
146
+ function addTokenListener(listener) {
147
+ if (typeof listener !== "function") {
148
+ throw new Error("Token listener must be a function");
149
+ }
150
+ listeners.add(listener);
151
+ return () => {
152
+ listeners.delete(listener);
153
+ };
154
+ }
146
155
  if (typeof window !== "undefined" && window.addEventListener) {
147
156
  window.addEventListener("storage", (event) => {
148
157
  if (event.key !== "authToken") return;
@@ -191,6 +200,13 @@ var config = {
191
200
  // ⚠️ In true production, set to false and rely on httpOnly cookies
192
201
  persistRefreshToken: false
193
202
  };
203
+ var RUNTIME_POLICY_DEFAULTS = {
204
+ tokenRefreshBuffer: 60,
205
+ sessionValidationInterval: 15 * 60 * 1e3,
206
+ enableSessionValidation: true,
207
+ enableProactiveRefresh: true,
208
+ validateOnVisibility: true
209
+ };
194
210
  function getConfig() {
195
211
  return { ...config };
196
212
  }
@@ -249,6 +265,28 @@ function acquireLoginLock(clientKey, redirectUri, ttlMs = 5e3) {
249
265
  }
250
266
  return true;
251
267
  }
268
+ async function reportClientDiagnostic(safe) {
269
+ if (typeof fetch !== "function" || typeof getToken !== "function") return;
270
+ if (safe.outcome === "SUCCESS" && !safe.event.startsWith("PROFILE_") && !safe.event.startsWith("SESSION_")) return;
271
+ const { authBaseUrl } = getConfig();
272
+ const token = getToken();
273
+ if (!authBaseUrl || !token) return;
274
+ try {
275
+ await fetch(`${authBaseUrl.replace(/\/+$/, "")}/client-telemetry`, {
276
+ method: "POST",
277
+ credentials: "include",
278
+ keepalive: true,
279
+ headers: {
280
+ "Content-Type": "application/json",
281
+ Authorization: `Bearer ${token}`,
282
+ "X-Correlation-ID": safe.correlationId,
283
+ "X-Request-ID": safe.correlationId
284
+ },
285
+ body: JSON.stringify(safe)
286
+ });
287
+ } catch {
288
+ }
289
+ }
252
290
  async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
253
291
  const context = getDiagnosticContext();
254
292
  const safe = {
@@ -265,12 +303,13 @@ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
265
303
  };
266
304
  const method = outcome === "FAILURE" ? "error" : outcome === "WARNING" ? "warn" : "info";
267
305
  console[method]("[auth-client]", safe);
306
+ void reportClientDiagnostic(safe);
268
307
  return safe;
269
308
  }
270
309
 
271
310
  // core.js
272
311
  var callbackProcessed = false;
273
- function login(clientKeyArg, redirectUriArg) {
312
+ function login(clientKeyArg, redirectUriArg, options = {}) {
274
313
  resetCallbackState();
275
314
  const {
276
315
  clientKey: defaultClientKey,
@@ -293,23 +332,24 @@ function login(clientKeyArg, redirectUriArg) {
293
332
  sessionStorage.setItem("originalApp", clientKey);
294
333
  sessionStorage.setItem("returnUrl", redirectUri);
295
334
  if (isRouterMode()) {
296
- return routerLogin(clientKey, redirectUri);
335
+ return routerLogin(clientKey, redirectUri, options);
297
336
  } else {
298
- return clientLogin(clientKey, redirectUri);
337
+ return clientLogin(clientKey, redirectUri, options);
299
338
  }
300
339
  }
301
- function routerLogin(clientKey, redirectUri) {
340
+ function routerLogin(clientKey, redirectUri, options = {}) {
302
341
  const { authBaseUrl } = getConfig();
303
342
  const params = new URLSearchParams();
304
343
  if (redirectUri) {
305
344
  params.append("redirect_uri", redirectUri);
306
345
  }
346
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
307
347
  params.append("correlation_id", getDiagnosticContext().correlationId);
308
348
  const query = params.toString();
309
349
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
310
350
  window.location.href = backendLoginUrl;
311
351
  }
312
- function clientLogin(clientKey, redirectUri) {
352
+ function clientLogin(clientKey, redirectUri, options = {}) {
313
353
  const { accountUiUrl } = getConfig();
314
354
  const params = new URLSearchParams({
315
355
  client: clientKey
@@ -317,6 +357,7 @@ function clientLogin(clientKey, redirectUri) {
317
357
  if (redirectUri) {
318
358
  params.append("redirect_uri", redirectUri);
319
359
  }
360
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
320
361
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
321
362
  window.location.href = centralizedLoginUrl;
322
363
  }
@@ -381,6 +422,28 @@ function resetCallbackState() {
381
422
  }
382
423
  var refreshInProgress = false;
383
424
  var refreshPromise = null;
425
+ async function withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest) {
426
+ var _a;
427
+ const lockName = `auth-refresh-${clientKey}`;
428
+ const run = async () => {
429
+ try {
430
+ const persistedToken = localStorage.getItem("authToken");
431
+ if (tokenBeforeRefresh && persistedToken && persistedToken !== tokenBeforeRefresh) {
432
+ setToken(persistedToken);
433
+ emitAuthDiagnostic("TOKEN_REFRESH_REUSED_CROSS_TAB", "SUCCESS", "CROSS_TAB_ROTATION", {
434
+ clientKey
435
+ });
436
+ return persistedToken;
437
+ }
438
+ } catch {
439
+ }
440
+ return refreshRequest();
441
+ };
442
+ if (typeof navigator !== "undefined" && ((_a = navigator.locks) == null ? void 0 : _a.request)) {
443
+ return navigator.locks.request(lockName, run);
444
+ }
445
+ return run();
446
+ }
384
447
  async function refreshToken() {
385
448
  const { clientKey, authBaseUrl } = getConfig();
386
449
  if (refreshInProgress && refreshPromise) {
@@ -389,70 +452,74 @@ async function refreshToken() {
389
452
  }
390
453
  refreshInProgress = true;
391
454
  refreshPromise = (async () => {
392
- var _a, _b, _c, _d, _e, _f;
393
- try {
394
- const storedRefreshToken = getRefreshToken();
395
- console.log("\u{1F504} Refreshing token:", {
396
- clientKey,
397
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
398
- hasStoredRefreshToken: !!storedRefreshToken
399
- });
400
- const requestOptions = {
401
- method: "POST",
402
- credentials: "include",
403
- // ✅ Include httpOnly cookies (for HTTPS)
404
- headers: {
405
- ...diagnosticHeaders(),
406
- "Content-Type": "application/json"
407
- }
408
- };
409
- if (storedRefreshToken) {
410
- requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
411
- console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
412
- }
413
- const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
414
- if (!response.ok) {
415
- const errorText = await response.text();
416
- let serverCode = null;
417
- try {
418
- serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
419
- } catch {
420
- }
421
- emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
455
+ const tokenBeforeRefresh = getToken();
456
+ const refreshRequest = async () => {
457
+ var _a, _b, _c, _d, _e, _f;
458
+ try {
459
+ const storedRefreshToken = getRefreshToken();
460
+ console.log("\u{1F504} Refreshing token:", {
422
461
  clientKey,
423
- status: response.status
462
+ mode: isRouterMode() ? "ROUTER" : "CLIENT",
463
+ hasStoredRefreshToken: !!storedRefreshToken
424
464
  });
425
- const refreshError = new Error(`Refresh failed: ${response.status}`);
426
- refreshError.code = serverCode || `HTTP_${response.status}`;
427
- refreshError.status = response.status;
428
- throw refreshError;
429
- }
430
- const data = await response.json();
431
- const { access_token, refresh_token: new_refresh_token } = data;
432
- if (!access_token) {
433
- throw new Error("No access token in refresh response");
434
- }
435
- setToken(access_token);
436
- if (new_refresh_token) {
437
- setRefreshToken(new_refresh_token);
438
- console.log("\u{1F504} New refresh token stored from rotation");
439
- }
440
- console.log("\u2705 Token refresh successful, listeners notified");
441
- emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
442
- return access_token;
443
- } catch (err) {
444
- console.error("\u274C Token refresh error:", err);
445
- const isAuthRejection = ((_c = err.message) == null ? void 0 : _c.includes("401")) || ((_d = err.message) == null ? void 0 : _d.includes("403")) || ((_e = err.message) == null ? void 0 : _e.includes("invalid_grant")) || ((_f = err.message) == null ? void 0 : _f.includes("Refresh failed: 4"));
446
- if (isAuthRejection) {
447
- clearToken();
448
- clearRefreshToken();
465
+ const requestOptions = {
466
+ method: "POST",
467
+ credentials: "include",
468
+ // ✅ Include httpOnly cookies (for HTTPS)
469
+ headers: {
470
+ ...diagnosticHeaders(),
471
+ "Content-Type": "application/json"
472
+ }
473
+ };
474
+ if (storedRefreshToken) {
475
+ requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
476
+ console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
477
+ }
478
+ const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
479
+ if (!response.ok) {
480
+ const errorText = await response.text();
481
+ let serverCode = null;
482
+ try {
483
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
484
+ } catch {
485
+ }
486
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
487
+ clientKey,
488
+ status: response.status
489
+ });
490
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
491
+ refreshError.code = serverCode || `HTTP_${response.status}`;
492
+ refreshError.status = response.status;
493
+ throw refreshError;
494
+ }
495
+ const data = await response.json();
496
+ const { access_token, refresh_token: new_refresh_token } = data;
497
+ if (!access_token) {
498
+ throw new Error("No access token in refresh response");
499
+ }
500
+ setToken(access_token);
501
+ if (new_refresh_token) {
502
+ setRefreshToken(new_refresh_token);
503
+ console.log("\u{1F504} New refresh token stored from rotation");
504
+ }
505
+ console.log("\u2705 Token refresh successful, listeners notified");
506
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
507
+ return access_token;
508
+ } catch (err) {
509
+ console.error("\u274C Token refresh error:", err);
510
+ const isAuthRejection = ((_c = err.message) == null ? void 0 : _c.includes("401")) || ((_d = err.message) == null ? void 0 : _d.includes("403")) || ((_e = err.message) == null ? void 0 : _e.includes("invalid_grant")) || ((_f = err.message) == null ? void 0 : _f.includes("Refresh failed: 4"));
511
+ if (isAuthRejection) {
512
+ clearToken();
513
+ clearRefreshToken();
514
+ }
515
+ throw err;
449
516
  }
450
- throw err;
451
- } finally {
452
- refreshInProgress = false;
453
- refreshPromise = null;
454
- }
455
- })();
517
+ };
518
+ return withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest);
519
+ })().finally(() => {
520
+ refreshInProgress = false;
521
+ refreshPromise = null;
522
+ });
456
523
  return refreshPromise;
457
524
  }
458
525
  async function validateCurrentSession() {
@@ -704,23 +771,72 @@ function stopSessionSecurity() {
704
771
 
705
772
  // react/AuthProvider.jsx
706
773
  var AuthContext = createContext();
707
- function AuthProvider({ children, onSessionExpired }) {
774
+ function AuthProvider({ children, onSessionExpired, manageSessionSecurity = true }) {
708
775
  const [token, setTokenState] = useState(getToken());
709
776
  const [user, setUser] = useState(null);
710
777
  const [loading, setLoading] = useState(!!token);
711
778
  const [sessionValid, setSessionValid] = useState(true);
712
779
  const sessionSecurityRef = useRef(null);
780
+ const onSessionExpiredRef = useRef(onSessionExpired);
781
+ const recoveryInFlightRef = useRef(null);
782
+ const profileRecoveryTokensRef = useRef(/* @__PURE__ */ new Set());
783
+ useEffect(() => {
784
+ onSessionExpiredRef.current = onSessionExpired;
785
+ }, [onSessionExpired]);
786
+ useEffect(() => {
787
+ const unsubscribe = addTokenListener((nextToken, previousToken) => {
788
+ if (nextToken === previousToken) return;
789
+ setTokenState(nextToken);
790
+ if (nextToken) {
791
+ setSessionValid(true);
792
+ setLoading(true);
793
+ } else {
794
+ setSessionValid(false);
795
+ setUser(null);
796
+ setLoading(false);
797
+ }
798
+ });
799
+ return unsubscribe;
800
+ }, []);
801
+ const invalidateLocalSession = () => {
802
+ clearToken();
803
+ setTokenState(null);
804
+ setUser(null);
805
+ setSessionValid(false);
806
+ setLoading(false);
807
+ };
713
808
  const handleSessionInvalid = (reason) => {
714
809
  console.log("\u{1F6A8} AuthProvider: Session invalidated -", reason);
715
- setSessionValid(false);
716
- setUser(null);
717
- setTokenState(null);
718
- if (onSessionExpired && typeof onSessionExpired === "function") {
719
- onSessionExpired(reason);
720
- }
810
+ if (recoveryInFlightRef.current) return recoveryInFlightRef.current;
811
+ const recover = async () => {
812
+ const callback = onSessionExpiredRef.current;
813
+ if (typeof callback === "function") {
814
+ try {
815
+ await callback(reason);
816
+ const recoveredToken = getToken();
817
+ if (recoveredToken) {
818
+ setTokenState(recoveredToken);
819
+ setSessionValid(true);
820
+ setLoading(true);
821
+ return true;
822
+ }
823
+ } catch (error) {
824
+ await emitAuthDiagnostic("SESSION_RECOVERY_FAILED", "FAILURE", reason, {
825
+ status: (error == null ? void 0 : error.status) || null,
826
+ errorCode: (error == null ? void 0 : error.code) || null
827
+ });
828
+ }
829
+ }
830
+ invalidateLocalSession();
831
+ return false;
832
+ };
833
+ recoveryInFlightRef.current = recover().finally(() => {
834
+ recoveryInFlightRef.current = null;
835
+ });
836
+ return recoveryInFlightRef.current;
721
837
  };
722
838
  useEffect(() => {
723
- if (token && !sessionSecurityRef.current) {
839
+ if (manageSessionSecurity && token && !sessionSecurityRef.current) {
724
840
  console.log("\u{1F510} AuthProvider: Starting session security");
725
841
  const unsubscribe = onSessionInvalid(handleSessionInvalid);
726
842
  sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);
@@ -732,11 +848,11 @@ function AuthProvider({ children, onSessionExpired }) {
732
848
  }
733
849
  };
734
850
  }
735
- if (!token && sessionSecurityRef.current) {
851
+ if ((!manageSessionSecurity || !token) && sessionSecurityRef.current) {
736
852
  sessionSecurityRef.current.stopAll();
737
853
  sessionSecurityRef.current = null;
738
854
  }
739
- }, [token]);
855
+ }, [manageSessionSecurity, token]);
740
856
  useEffect(() => {
741
857
  console.log("\u{1F50D} AuthProvider useEffect triggered:", {
742
858
  hasToken: !!token,
@@ -757,26 +873,79 @@ function AuthProvider({ children, onSessionExpired }) {
757
873
  authBaseUrl,
758
874
  tokenPreview: token.slice(0, 50) + "..."
759
875
  });
760
- fetch(`${authBaseUrl}/account/profile`, {
761
- headers: { Authorization: `Bearer ${token}` },
762
- credentials: "include"
763
- }).then((res) => {
764
- console.log("\u{1F4E5} Profile response status:", res.status);
765
- if (!res.ok) throw new Error("Failed to fetch user");
766
- return res.json();
767
- }).then((responseBody) => {
768
- const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
769
- console.log("\u2705 Profile fetched successfully:", userData.email);
770
- setUser(userData);
771
- setSessionValid(true);
772
- setLoading(false);
773
- }).catch((err) => {
774
- console.error("\u274C Fetch user error:", err);
775
- clearToken();
776
- setTokenState(null);
777
- setUser(null);
778
- setLoading(false);
779
- });
876
+ let cancelled = false;
877
+ const fetchProfile = async (accessToken2) => {
878
+ const response = await fetch(`${authBaseUrl}/account/profile`, {
879
+ headers: { Authorization: `Bearer ${accessToken2}` },
880
+ credentials: "include"
881
+ });
882
+ console.log("\u{1F4E5} Profile response status:", response.status);
883
+ if (!response.ok) {
884
+ const error = new Error(`Profile request failed: ${response.status}`);
885
+ error.status = response.status;
886
+ throw error;
887
+ }
888
+ return response.json();
889
+ };
890
+ const loadProfile = async () => {
891
+ try {
892
+ const responseBody = await fetchProfile(token);
893
+ if (cancelled) return;
894
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
895
+ console.log("\u2705 Profile fetched successfully:", userData.email);
896
+ setUser(userData);
897
+ setSessionValid(true);
898
+ setLoading(false);
899
+ } catch (error) {
900
+ if (cancelled) return;
901
+ const status = Number((error == null ? void 0 : error.status) || 0);
902
+ const isUnauthorized = status === 401;
903
+ const callback = onSessionExpiredRef.current;
904
+ await emitAuthDiagnostic(
905
+ "PROFILE_REQUEST_FAILED",
906
+ isUnauthorized ? "FAILURE" : "WARNING",
907
+ isUnauthorized ? "PROFILE_UNAUTHORIZED" : status ? `PROFILE_HTTP_${status}` : "PROFILE_NETWORK_ERROR",
908
+ { status, errorName: error == null ? void 0 : error.name, errorCode: error == null ? void 0 : error.code }
909
+ );
910
+ if (!isUnauthorized) {
911
+ console.warn("\u26A0\uFE0F Profile unavailable; retaining the current auth session", error);
912
+ setLoading(false);
913
+ return;
914
+ }
915
+ if (typeof callback === "function" && !profileRecoveryTokensRef.current.has(token)) {
916
+ profileRecoveryTokensRef.current.add(token);
917
+ try {
918
+ await callback("profile_unauthorized");
919
+ const recoveredToken = getToken();
920
+ if (recoveredToken && recoveredToken !== token) {
921
+ setTokenState(recoveredToken);
922
+ setSessionValid(true);
923
+ setLoading(true);
924
+ return;
925
+ }
926
+ if (recoveredToken) {
927
+ const responseBody = await fetchProfile(recoveredToken);
928
+ if (cancelled) return;
929
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
930
+ setUser(userData);
931
+ setSessionValid(true);
932
+ setLoading(false);
933
+ return;
934
+ }
935
+ } catch (recoveryError) {
936
+ await emitAuthDiagnostic("PROFILE_RECOVERY_FAILED", "FAILURE", "PROFILE_REFRESH_FAILED", {
937
+ status: (recoveryError == null ? void 0 : recoveryError.status) || null,
938
+ errorCode: (recoveryError == null ? void 0 : recoveryError.code) || null
939
+ });
940
+ }
941
+ }
942
+ invalidateLocalSession();
943
+ }
944
+ };
945
+ void loadProfile();
946
+ return () => {
947
+ cancelled = true;
948
+ };
780
949
  }, [token]);
781
950
  const login2 = (clientKey, redirectUri, state) => {
782
951
  login(clientKey, redirectUri, state);
@@ -805,6 +974,7 @@ function AuthProvider({ children, onSessionExpired }) {
805
974
  setToken(newToken);
806
975
  setTokenState(newToken);
807
976
  setSessionValid(true);
977
+ if (newToken) setLoading(true);
808
978
  },
809
979
  clearToken: () => {
810
980
  stopSessionSecurity();