@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.
@@ -177,6 +177,15 @@ function clearRefreshToken() {
177
177
  } catch (err) {
178
178
  }
179
179
  }
180
+ function addTokenListener(listener) {
181
+ if (typeof listener !== "function") {
182
+ throw new Error("Token listener must be a function");
183
+ }
184
+ listeners.add(listener);
185
+ return () => {
186
+ listeners.delete(listener);
187
+ };
188
+ }
180
189
  if (typeof window !== "undefined" && window.addEventListener) {
181
190
  window.addEventListener("storage", (event) => {
182
191
  if (event.key !== "authToken") return;
@@ -225,6 +234,13 @@ var config = {
225
234
  // ⚠️ In true production, set to false and rely on httpOnly cookies
226
235
  persistRefreshToken: false
227
236
  };
237
+ var RUNTIME_POLICY_DEFAULTS = {
238
+ tokenRefreshBuffer: 60,
239
+ sessionValidationInterval: 15 * 60 * 1e3,
240
+ enableSessionValidation: true,
241
+ enableProactiveRefresh: true,
242
+ validateOnVisibility: true
243
+ };
228
244
  function getConfig() {
229
245
  return { ...config };
230
246
  }
@@ -283,6 +299,28 @@ function acquireLoginLock(clientKey, redirectUri, ttlMs = 5e3) {
283
299
  }
284
300
  return true;
285
301
  }
302
+ async function reportClientDiagnostic(safe) {
303
+ if (typeof fetch !== "function" || typeof getToken !== "function") return;
304
+ if (safe.outcome === "SUCCESS" && !safe.event.startsWith("PROFILE_") && !safe.event.startsWith("SESSION_")) return;
305
+ const { authBaseUrl } = getConfig();
306
+ const token = getToken();
307
+ if (!authBaseUrl || !token) return;
308
+ try {
309
+ await fetch(`${authBaseUrl.replace(/\/+$/, "")}/client-telemetry`, {
310
+ method: "POST",
311
+ credentials: "include",
312
+ keepalive: true,
313
+ headers: {
314
+ "Content-Type": "application/json",
315
+ Authorization: `Bearer ${token}`,
316
+ "X-Correlation-ID": safe.correlationId,
317
+ "X-Request-ID": safe.correlationId
318
+ },
319
+ body: JSON.stringify(safe)
320
+ });
321
+ } catch {
322
+ }
323
+ }
286
324
  async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
287
325
  const context = getDiagnosticContext();
288
326
  const safe = {
@@ -299,12 +337,13 @@ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
299
337
  };
300
338
  const method = outcome === "FAILURE" ? "error" : outcome === "WARNING" ? "warn" : "info";
301
339
  console[method]("[auth-client]", safe);
340
+ void reportClientDiagnostic(safe);
302
341
  return safe;
303
342
  }
304
343
 
305
344
  // core.js
306
345
  var callbackProcessed = false;
307
- function login(clientKeyArg, redirectUriArg) {
346
+ function login(clientKeyArg, redirectUriArg, options = {}) {
308
347
  resetCallbackState();
309
348
  const {
310
349
  clientKey: defaultClientKey,
@@ -327,23 +366,24 @@ function login(clientKeyArg, redirectUriArg) {
327
366
  sessionStorage.setItem("originalApp", clientKey);
328
367
  sessionStorage.setItem("returnUrl", redirectUri);
329
368
  if (isRouterMode()) {
330
- return routerLogin(clientKey, redirectUri);
369
+ return routerLogin(clientKey, redirectUri, options);
331
370
  } else {
332
- return clientLogin(clientKey, redirectUri);
371
+ return clientLogin(clientKey, redirectUri, options);
333
372
  }
334
373
  }
335
- function routerLogin(clientKey, redirectUri) {
374
+ function routerLogin(clientKey, redirectUri, options = {}) {
336
375
  const { authBaseUrl } = getConfig();
337
376
  const params = new URLSearchParams();
338
377
  if (redirectUri) {
339
378
  params.append("redirect_uri", redirectUri);
340
379
  }
380
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
341
381
  params.append("correlation_id", getDiagnosticContext().correlationId);
342
382
  const query = params.toString();
343
383
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
344
384
  window.location.href = backendLoginUrl;
345
385
  }
346
- function clientLogin(clientKey, redirectUri) {
386
+ function clientLogin(clientKey, redirectUri, options = {}) {
347
387
  const { accountUiUrl } = getConfig();
348
388
  const params = new URLSearchParams({
349
389
  client: clientKey
@@ -351,6 +391,7 @@ function clientLogin(clientKey, redirectUri) {
351
391
  if (redirectUri) {
352
392
  params.append("redirect_uri", redirectUri);
353
393
  }
394
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
354
395
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
355
396
  window.location.href = centralizedLoginUrl;
356
397
  }
@@ -415,6 +456,28 @@ function resetCallbackState() {
415
456
  }
416
457
  var refreshInProgress = false;
417
458
  var refreshPromise = null;
459
+ async function withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest) {
460
+ var _a;
461
+ const lockName = `auth-refresh-${clientKey}`;
462
+ const run = async () => {
463
+ try {
464
+ const persistedToken = localStorage.getItem("authToken");
465
+ if (tokenBeforeRefresh && persistedToken && persistedToken !== tokenBeforeRefresh) {
466
+ setToken(persistedToken);
467
+ emitAuthDiagnostic("TOKEN_REFRESH_REUSED_CROSS_TAB", "SUCCESS", "CROSS_TAB_ROTATION", {
468
+ clientKey
469
+ });
470
+ return persistedToken;
471
+ }
472
+ } catch {
473
+ }
474
+ return refreshRequest();
475
+ };
476
+ if (typeof navigator !== "undefined" && ((_a = navigator.locks) == null ? void 0 : _a.request)) {
477
+ return navigator.locks.request(lockName, run);
478
+ }
479
+ return run();
480
+ }
418
481
  async function refreshToken() {
419
482
  const { clientKey, authBaseUrl } = getConfig();
420
483
  if (refreshInProgress && refreshPromise) {
@@ -423,70 +486,74 @@ async function refreshToken() {
423
486
  }
424
487
  refreshInProgress = true;
425
488
  refreshPromise = (async () => {
426
- var _a, _b, _c, _d, _e, _f;
427
- try {
428
- const storedRefreshToken = getRefreshToken();
429
- console.log("\u{1F504} Refreshing token:", {
430
- clientKey,
431
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
432
- hasStoredRefreshToken: !!storedRefreshToken
433
- });
434
- const requestOptions = {
435
- method: "POST",
436
- credentials: "include",
437
- // ✅ Include httpOnly cookies (for HTTPS)
438
- headers: {
439
- ...diagnosticHeaders(),
440
- "Content-Type": "application/json"
441
- }
442
- };
443
- if (storedRefreshToken) {
444
- requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
445
- console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
446
- }
447
- const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
448
- if (!response.ok) {
449
- const errorText = await response.text();
450
- let serverCode = null;
451
- try {
452
- serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
453
- } catch {
454
- }
455
- emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
489
+ const tokenBeforeRefresh = getToken();
490
+ const refreshRequest = async () => {
491
+ var _a, _b, _c, _d, _e, _f;
492
+ try {
493
+ const storedRefreshToken = getRefreshToken();
494
+ console.log("\u{1F504} Refreshing token:", {
456
495
  clientKey,
457
- status: response.status
496
+ mode: isRouterMode() ? "ROUTER" : "CLIENT",
497
+ hasStoredRefreshToken: !!storedRefreshToken
458
498
  });
459
- const refreshError = new Error(`Refresh failed: ${response.status}`);
460
- refreshError.code = serverCode || `HTTP_${response.status}`;
461
- refreshError.status = response.status;
462
- throw refreshError;
463
- }
464
- const data = await response.json();
465
- const { access_token, refresh_token: new_refresh_token } = data;
466
- if (!access_token) {
467
- throw new Error("No access token in refresh response");
468
- }
469
- setToken(access_token);
470
- if (new_refresh_token) {
471
- setRefreshToken(new_refresh_token);
472
- console.log("\u{1F504} New refresh token stored from rotation");
473
- }
474
- console.log("\u2705 Token refresh successful, listeners notified");
475
- emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
476
- return access_token;
477
- } catch (err) {
478
- console.error("\u274C Token refresh error:", err);
479
- 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"));
480
- if (isAuthRejection) {
481
- clearToken();
482
- clearRefreshToken();
499
+ const requestOptions = {
500
+ method: "POST",
501
+ credentials: "include",
502
+ // ✅ Include httpOnly cookies (for HTTPS)
503
+ headers: {
504
+ ...diagnosticHeaders(),
505
+ "Content-Type": "application/json"
506
+ }
507
+ };
508
+ if (storedRefreshToken) {
509
+ requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
510
+ console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
511
+ }
512
+ const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
513
+ if (!response.ok) {
514
+ const errorText = await response.text();
515
+ let serverCode = null;
516
+ try {
517
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
518
+ } catch {
519
+ }
520
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
521
+ clientKey,
522
+ status: response.status
523
+ });
524
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
525
+ refreshError.code = serverCode || `HTTP_${response.status}`;
526
+ refreshError.status = response.status;
527
+ throw refreshError;
528
+ }
529
+ const data = await response.json();
530
+ const { access_token, refresh_token: new_refresh_token } = data;
531
+ if (!access_token) {
532
+ throw new Error("No access token in refresh response");
533
+ }
534
+ setToken(access_token);
535
+ if (new_refresh_token) {
536
+ setRefreshToken(new_refresh_token);
537
+ console.log("\u{1F504} New refresh token stored from rotation");
538
+ }
539
+ console.log("\u2705 Token refresh successful, listeners notified");
540
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
541
+ return access_token;
542
+ } catch (err) {
543
+ console.error("\u274C Token refresh error:", err);
544
+ 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"));
545
+ if (isAuthRejection) {
546
+ clearToken();
547
+ clearRefreshToken();
548
+ }
549
+ throw err;
483
550
  }
484
- throw err;
485
- } finally {
486
- refreshInProgress = false;
487
- refreshPromise = null;
488
- }
489
- })();
551
+ };
552
+ return withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest);
553
+ })().finally(() => {
554
+ refreshInProgress = false;
555
+ refreshPromise = null;
556
+ });
490
557
  return refreshPromise;
491
558
  }
492
559
  async function validateCurrentSession() {
@@ -738,23 +805,72 @@ function stopSessionSecurity() {
738
805
 
739
806
  // react/AuthProvider.jsx
740
807
  var AuthContext = (0, import_react.createContext)();
741
- function AuthProvider({ children, onSessionExpired }) {
808
+ function AuthProvider({ children, onSessionExpired, manageSessionSecurity = true }) {
742
809
  const [token, setTokenState] = (0, import_react.useState)(getToken());
743
810
  const [user, setUser] = (0, import_react.useState)(null);
744
811
  const [loading, setLoading] = (0, import_react.useState)(!!token);
745
812
  const [sessionValid, setSessionValid] = (0, import_react.useState)(true);
746
813
  const sessionSecurityRef = (0, import_react.useRef)(null);
814
+ const onSessionExpiredRef = (0, import_react.useRef)(onSessionExpired);
815
+ const recoveryInFlightRef = (0, import_react.useRef)(null);
816
+ const profileRecoveryTokensRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
817
+ (0, import_react.useEffect)(() => {
818
+ onSessionExpiredRef.current = onSessionExpired;
819
+ }, [onSessionExpired]);
820
+ (0, import_react.useEffect)(() => {
821
+ const unsubscribe = addTokenListener((nextToken, previousToken) => {
822
+ if (nextToken === previousToken) return;
823
+ setTokenState(nextToken);
824
+ if (nextToken) {
825
+ setSessionValid(true);
826
+ setLoading(true);
827
+ } else {
828
+ setSessionValid(false);
829
+ setUser(null);
830
+ setLoading(false);
831
+ }
832
+ });
833
+ return unsubscribe;
834
+ }, []);
835
+ const invalidateLocalSession = () => {
836
+ clearToken();
837
+ setTokenState(null);
838
+ setUser(null);
839
+ setSessionValid(false);
840
+ setLoading(false);
841
+ };
747
842
  const handleSessionInvalid = (reason) => {
748
843
  console.log("\u{1F6A8} AuthProvider: Session invalidated -", reason);
749
- setSessionValid(false);
750
- setUser(null);
751
- setTokenState(null);
752
- if (onSessionExpired && typeof onSessionExpired === "function") {
753
- onSessionExpired(reason);
754
- }
844
+ if (recoveryInFlightRef.current) return recoveryInFlightRef.current;
845
+ const recover = async () => {
846
+ const callback = onSessionExpiredRef.current;
847
+ if (typeof callback === "function") {
848
+ try {
849
+ await callback(reason);
850
+ const recoveredToken = getToken();
851
+ if (recoveredToken) {
852
+ setTokenState(recoveredToken);
853
+ setSessionValid(true);
854
+ setLoading(true);
855
+ return true;
856
+ }
857
+ } catch (error) {
858
+ await emitAuthDiagnostic("SESSION_RECOVERY_FAILED", "FAILURE", reason, {
859
+ status: (error == null ? void 0 : error.status) || null,
860
+ errorCode: (error == null ? void 0 : error.code) || null
861
+ });
862
+ }
863
+ }
864
+ invalidateLocalSession();
865
+ return false;
866
+ };
867
+ recoveryInFlightRef.current = recover().finally(() => {
868
+ recoveryInFlightRef.current = null;
869
+ });
870
+ return recoveryInFlightRef.current;
755
871
  };
756
872
  (0, import_react.useEffect)(() => {
757
- if (token && !sessionSecurityRef.current) {
873
+ if (manageSessionSecurity && token && !sessionSecurityRef.current) {
758
874
  console.log("\u{1F510} AuthProvider: Starting session security");
759
875
  const unsubscribe = onSessionInvalid(handleSessionInvalid);
760
876
  sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);
@@ -766,11 +882,11 @@ function AuthProvider({ children, onSessionExpired }) {
766
882
  }
767
883
  };
768
884
  }
769
- if (!token && sessionSecurityRef.current) {
885
+ if ((!manageSessionSecurity || !token) && sessionSecurityRef.current) {
770
886
  sessionSecurityRef.current.stopAll();
771
887
  sessionSecurityRef.current = null;
772
888
  }
773
- }, [token]);
889
+ }, [manageSessionSecurity, token]);
774
890
  (0, import_react.useEffect)(() => {
775
891
  console.log("\u{1F50D} AuthProvider useEffect triggered:", {
776
892
  hasToken: !!token,
@@ -791,26 +907,79 @@ function AuthProvider({ children, onSessionExpired }) {
791
907
  authBaseUrl,
792
908
  tokenPreview: token.slice(0, 50) + "..."
793
909
  });
794
- fetch(`${authBaseUrl}/account/profile`, {
795
- headers: { Authorization: `Bearer ${token}` },
796
- credentials: "include"
797
- }).then((res) => {
798
- console.log("\u{1F4E5} Profile response status:", res.status);
799
- if (!res.ok) throw new Error("Failed to fetch user");
800
- return res.json();
801
- }).then((responseBody) => {
802
- const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
803
- console.log("\u2705 Profile fetched successfully:", userData.email);
804
- setUser(userData);
805
- setSessionValid(true);
806
- setLoading(false);
807
- }).catch((err) => {
808
- console.error("\u274C Fetch user error:", err);
809
- clearToken();
810
- setTokenState(null);
811
- setUser(null);
812
- setLoading(false);
813
- });
910
+ let cancelled = false;
911
+ const fetchProfile = async (accessToken2) => {
912
+ const response = await fetch(`${authBaseUrl}/account/profile`, {
913
+ headers: { Authorization: `Bearer ${accessToken2}` },
914
+ credentials: "include"
915
+ });
916
+ console.log("\u{1F4E5} Profile response status:", response.status);
917
+ if (!response.ok) {
918
+ const error = new Error(`Profile request failed: ${response.status}`);
919
+ error.status = response.status;
920
+ throw error;
921
+ }
922
+ return response.json();
923
+ };
924
+ const loadProfile = async () => {
925
+ try {
926
+ const responseBody = await fetchProfile(token);
927
+ if (cancelled) return;
928
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
929
+ console.log("\u2705 Profile fetched successfully:", userData.email);
930
+ setUser(userData);
931
+ setSessionValid(true);
932
+ setLoading(false);
933
+ } catch (error) {
934
+ if (cancelled) return;
935
+ const status = Number((error == null ? void 0 : error.status) || 0);
936
+ const isUnauthorized = status === 401;
937
+ const callback = onSessionExpiredRef.current;
938
+ await emitAuthDiagnostic(
939
+ "PROFILE_REQUEST_FAILED",
940
+ isUnauthorized ? "FAILURE" : "WARNING",
941
+ isUnauthorized ? "PROFILE_UNAUTHORIZED" : status ? `PROFILE_HTTP_${status}` : "PROFILE_NETWORK_ERROR",
942
+ { status, errorName: error == null ? void 0 : error.name, errorCode: error == null ? void 0 : error.code }
943
+ );
944
+ if (!isUnauthorized) {
945
+ console.warn("\u26A0\uFE0F Profile unavailable; retaining the current auth session", error);
946
+ setLoading(false);
947
+ return;
948
+ }
949
+ if (typeof callback === "function" && !profileRecoveryTokensRef.current.has(token)) {
950
+ profileRecoveryTokensRef.current.add(token);
951
+ try {
952
+ await callback("profile_unauthorized");
953
+ const recoveredToken = getToken();
954
+ if (recoveredToken && recoveredToken !== token) {
955
+ setTokenState(recoveredToken);
956
+ setSessionValid(true);
957
+ setLoading(true);
958
+ return;
959
+ }
960
+ if (recoveredToken) {
961
+ const responseBody = await fetchProfile(recoveredToken);
962
+ if (cancelled) return;
963
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
964
+ setUser(userData);
965
+ setSessionValid(true);
966
+ setLoading(false);
967
+ return;
968
+ }
969
+ } catch (recoveryError) {
970
+ await emitAuthDiagnostic("PROFILE_RECOVERY_FAILED", "FAILURE", "PROFILE_REFRESH_FAILED", {
971
+ status: (recoveryError == null ? void 0 : recoveryError.status) || null,
972
+ errorCode: (recoveryError == null ? void 0 : recoveryError.code) || null
973
+ });
974
+ }
975
+ }
976
+ invalidateLocalSession();
977
+ }
978
+ };
979
+ void loadProfile();
980
+ return () => {
981
+ cancelled = true;
982
+ };
814
983
  }, [token]);
815
984
  const login2 = (clientKey, redirectUri, state) => {
816
985
  login(clientKey, redirectUri, state);
@@ -839,6 +1008,7 @@ function AuthProvider({ children, onSessionExpired }) {
839
1008
  setToken(newToken);
840
1009
  setTokenState(newToken);
841
1010
  setSessionValid(true);
1011
+ if (newToken) setLoading(true);
842
1012
  },
843
1013
  clearToken: () => {
844
1014
  stopSessionSecurity();