@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.
package/dist/index.cjs CHANGED
@@ -268,6 +268,44 @@ var config = {
268
268
  // ⚠️ In true production, set to false and rely on httpOnly cookies
269
269
  persistRefreshToken: false
270
270
  };
271
+ var RUNTIME_POLICY_DEFAULTS = {
272
+ tokenRefreshBuffer: 60,
273
+ sessionValidationInterval: 15 * 60 * 1e3,
274
+ enableSessionValidation: true,
275
+ enableProactiveRefresh: true,
276
+ validateOnVisibility: true
277
+ };
278
+ function validateRuntimePolicy(policy = {}) {
279
+ const numberInRange = (value, min, max) => Number.isInteger(value) && value >= min && value <= max;
280
+ const result = { ...RUNTIME_POLICY_DEFAULTS };
281
+ if (numberInRange(policy.tokenRefreshBuffer, 10, 300)) result.tokenRefreshBuffer = policy.tokenRefreshBuffer;
282
+ if (numberInRange(policy.sessionValidationInterval, 6e4, 864e5)) result.sessionValidationInterval = policy.sessionValidationInterval;
283
+ for (const key of ["enableSessionValidation", "enableProactiveRefresh", "validateOnVisibility"]) {
284
+ if (typeof policy[key] === "boolean") result[key] = policy[key];
285
+ }
286
+ return result;
287
+ }
288
+ function applyRuntimePolicy(policy = {}) {
289
+ config = { ...config, ...validateRuntimePolicy(policy) };
290
+ return getConfig();
291
+ }
292
+ async function loadRuntimePolicy() {
293
+ const { authBaseUrl, clientKey } = config;
294
+ if (!authBaseUrl || !clientKey || typeof fetch !== "function") return getConfig();
295
+ try {
296
+ const response = await fetch(`${authBaseUrl.replace(/\/+$/, "")}/clients/${encodeURIComponent(clientKey)}/config`, {
297
+ method: "GET",
298
+ credentials: "include",
299
+ headers: { Accept: "application/json" }
300
+ });
301
+ if (!response.ok) throw new Error(`runtime policy HTTP ${response.status}`);
302
+ const payload = await response.json();
303
+ return applyRuntimePolicy(payload.authentication_policy || {});
304
+ } catch (error) {
305
+ console.warn("[auth-client] Runtime policy unavailable; using safe defaults", error.message);
306
+ return applyRuntimePolicy({});
307
+ }
308
+ }
271
309
  function setConfig(customConfig = {}) {
272
310
  if (!customConfig.clientKey || !customConfig.authBaseUrl) {
273
311
  throw new Error("Missing required config: clientKey and authBaseUrl are required");
@@ -353,6 +391,28 @@ function clearLoginLock() {
353
391
  } catch {
354
392
  }
355
393
  }
394
+ async function reportClientDiagnostic(safe) {
395
+ if (typeof fetch !== "function" || typeof getToken !== "function") return;
396
+ if (safe.outcome === "SUCCESS" && !safe.event.startsWith("PROFILE_") && !safe.event.startsWith("SESSION_")) return;
397
+ const { authBaseUrl } = getConfig();
398
+ const token = getToken();
399
+ if (!authBaseUrl || !token) return;
400
+ try {
401
+ await fetch(`${authBaseUrl.replace(/\/+$/, "")}/client-telemetry`, {
402
+ method: "POST",
403
+ credentials: "include",
404
+ keepalive: true,
405
+ headers: {
406
+ "Content-Type": "application/json",
407
+ Authorization: `Bearer ${token}`,
408
+ "X-Correlation-ID": safe.correlationId,
409
+ "X-Request-ID": safe.correlationId
410
+ },
411
+ body: JSON.stringify(safe)
412
+ });
413
+ } catch {
414
+ }
415
+ }
356
416
  async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
357
417
  const context = getDiagnosticContext();
358
418
  const safe = {
@@ -369,12 +429,13 @@ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
369
429
  };
370
430
  const method = outcome === "FAILURE" ? "error" : outcome === "WARNING" ? "warn" : "info";
371
431
  console[method]("[auth-client]", safe);
432
+ void reportClientDiagnostic(safe);
372
433
  return safe;
373
434
  }
374
435
 
375
436
  // core.js
376
437
  var callbackProcessed = false;
377
- function login(clientKeyArg, redirectUriArg) {
438
+ function login(clientKeyArg, redirectUriArg, options = {}) {
378
439
  resetCallbackState();
379
440
  const {
380
441
  clientKey: defaultClientKey,
@@ -397,23 +458,24 @@ function login(clientKeyArg, redirectUriArg) {
397
458
  sessionStorage.setItem("originalApp", clientKey);
398
459
  sessionStorage.setItem("returnUrl", redirectUri);
399
460
  if (isRouterMode()) {
400
- return routerLogin(clientKey, redirectUri);
461
+ return routerLogin(clientKey, redirectUri, options);
401
462
  } else {
402
- return clientLogin(clientKey, redirectUri);
463
+ return clientLogin(clientKey, redirectUri, options);
403
464
  }
404
465
  }
405
- function routerLogin(clientKey, redirectUri) {
466
+ function routerLogin(clientKey, redirectUri, options = {}) {
406
467
  const { authBaseUrl } = getConfig();
407
468
  const params = new URLSearchParams();
408
469
  if (redirectUri) {
409
470
  params.append("redirect_uri", redirectUri);
410
471
  }
472
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
411
473
  params.append("correlation_id", getDiagnosticContext().correlationId);
412
474
  const query = params.toString();
413
475
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
414
476
  window.location.href = backendLoginUrl;
415
477
  }
416
- function clientLogin(clientKey, redirectUri) {
478
+ function clientLogin(clientKey, redirectUri, options = {}) {
417
479
  const { accountUiUrl } = getConfig();
418
480
  const params = new URLSearchParams({
419
481
  client: clientKey
@@ -421,6 +483,7 @@ function clientLogin(clientKey, redirectUri) {
421
483
  if (redirectUri) {
422
484
  params.append("redirect_uri", redirectUri);
423
485
  }
486
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
424
487
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
425
488
  window.location.href = centralizedLoginUrl;
426
489
  }
@@ -546,6 +609,28 @@ function resetCallbackState() {
546
609
  }
547
610
  var refreshInProgress = false;
548
611
  var refreshPromise = null;
612
+ async function withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest) {
613
+ var _a;
614
+ const lockName = `auth-refresh-${clientKey}`;
615
+ const run = async () => {
616
+ try {
617
+ const persistedToken = localStorage.getItem("authToken");
618
+ if (tokenBeforeRefresh && persistedToken && persistedToken !== tokenBeforeRefresh) {
619
+ setToken(persistedToken);
620
+ emitAuthDiagnostic("TOKEN_REFRESH_REUSED_CROSS_TAB", "SUCCESS", "CROSS_TAB_ROTATION", {
621
+ clientKey
622
+ });
623
+ return persistedToken;
624
+ }
625
+ } catch {
626
+ }
627
+ return refreshRequest();
628
+ };
629
+ if (typeof navigator !== "undefined" && ((_a = navigator.locks) == null ? void 0 : _a.request)) {
630
+ return navigator.locks.request(lockName, run);
631
+ }
632
+ return run();
633
+ }
549
634
  async function refreshToken() {
550
635
  const { clientKey, authBaseUrl } = getConfig();
551
636
  if (refreshInProgress && refreshPromise) {
@@ -554,70 +639,74 @@ async function refreshToken() {
554
639
  }
555
640
  refreshInProgress = true;
556
641
  refreshPromise = (async () => {
557
- var _a, _b, _c, _d, _e, _f;
558
- try {
559
- const storedRefreshToken = getRefreshToken();
560
- console.log("\u{1F504} Refreshing token:", {
561
- clientKey,
562
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
563
- hasStoredRefreshToken: !!storedRefreshToken
564
- });
565
- const requestOptions = {
566
- method: "POST",
567
- credentials: "include",
568
- // ✅ Include httpOnly cookies (for HTTPS)
569
- headers: {
570
- ...diagnosticHeaders(),
571
- "Content-Type": "application/json"
572
- }
573
- };
574
- if (storedRefreshToken) {
575
- requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
576
- console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
577
- }
578
- const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
579
- if (!response.ok) {
580
- const errorText = await response.text();
581
- let serverCode = null;
582
- try {
583
- serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
584
- } catch {
585
- }
586
- emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
642
+ const tokenBeforeRefresh = getToken();
643
+ const refreshRequest = async () => {
644
+ var _a, _b, _c, _d, _e, _f;
645
+ try {
646
+ const storedRefreshToken = getRefreshToken();
647
+ console.log("\u{1F504} Refreshing token:", {
587
648
  clientKey,
588
- status: response.status
649
+ mode: isRouterMode() ? "ROUTER" : "CLIENT",
650
+ hasStoredRefreshToken: !!storedRefreshToken
589
651
  });
590
- const refreshError = new Error(`Refresh failed: ${response.status}`);
591
- refreshError.code = serverCode || `HTTP_${response.status}`;
592
- refreshError.status = response.status;
593
- throw refreshError;
594
- }
595
- const data = await response.json();
596
- const { access_token, refresh_token: new_refresh_token } = data;
597
- if (!access_token) {
598
- throw new Error("No access token in refresh response");
599
- }
600
- setToken(access_token);
601
- if (new_refresh_token) {
602
- setRefreshToken(new_refresh_token);
603
- console.log("\u{1F504} New refresh token stored from rotation");
604
- }
605
- console.log("\u2705 Token refresh successful, listeners notified");
606
- emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
607
- return access_token;
608
- } catch (err) {
609
- console.error("\u274C Token refresh error:", err);
610
- 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"));
611
- if (isAuthRejection) {
612
- clearToken();
613
- clearRefreshToken();
652
+ const requestOptions = {
653
+ method: "POST",
654
+ credentials: "include",
655
+ // ✅ Include httpOnly cookies (for HTTPS)
656
+ headers: {
657
+ ...diagnosticHeaders(),
658
+ "Content-Type": "application/json"
659
+ }
660
+ };
661
+ if (storedRefreshToken) {
662
+ requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
663
+ console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
664
+ }
665
+ const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
666
+ if (!response.ok) {
667
+ const errorText = await response.text();
668
+ let serverCode = null;
669
+ try {
670
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
671
+ } catch {
672
+ }
673
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
674
+ clientKey,
675
+ status: response.status
676
+ });
677
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
678
+ refreshError.code = serverCode || `HTTP_${response.status}`;
679
+ refreshError.status = response.status;
680
+ throw refreshError;
681
+ }
682
+ const data = await response.json();
683
+ const { access_token, refresh_token: new_refresh_token } = data;
684
+ if (!access_token) {
685
+ throw new Error("No access token in refresh response");
686
+ }
687
+ setToken(access_token);
688
+ if (new_refresh_token) {
689
+ setRefreshToken(new_refresh_token);
690
+ console.log("\u{1F504} New refresh token stored from rotation");
691
+ }
692
+ console.log("\u2705 Token refresh successful, listeners notified");
693
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
694
+ return access_token;
695
+ } catch (err) {
696
+ console.error("\u274C Token refresh error:", err);
697
+ 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"));
698
+ if (isAuthRejection) {
699
+ clearToken();
700
+ clearRefreshToken();
701
+ }
702
+ throw err;
614
703
  }
615
- throw err;
616
- } finally {
617
- refreshInProgress = false;
618
- refreshPromise = null;
619
- }
620
- })();
704
+ };
705
+ return withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest);
706
+ })().finally(() => {
707
+ refreshInProgress = false;
708
+ refreshPromise = null;
709
+ });
621
710
  return refreshPromise;
622
711
  }
623
712
  async function validateCurrentSession() {
@@ -1170,23 +1259,72 @@ var preferences = {
1170
1259
  // react/AuthProvider.jsx
1171
1260
  var import_react = __toESM(require("react"), 1);
1172
1261
  var AuthContext = (0, import_react.createContext)();
1173
- function AuthProvider({ children, onSessionExpired }) {
1262
+ function AuthProvider({ children, onSessionExpired, manageSessionSecurity = true }) {
1174
1263
  const [token, setTokenState] = (0, import_react.useState)(getToken());
1175
1264
  const [user, setUser] = (0, import_react.useState)(null);
1176
1265
  const [loading, setLoading] = (0, import_react.useState)(!!token);
1177
1266
  const [sessionValid, setSessionValid] = (0, import_react.useState)(true);
1178
1267
  const sessionSecurityRef = (0, import_react.useRef)(null);
1268
+ const onSessionExpiredRef = (0, import_react.useRef)(onSessionExpired);
1269
+ const recoveryInFlightRef = (0, import_react.useRef)(null);
1270
+ const profileRecoveryTokensRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
1271
+ (0, import_react.useEffect)(() => {
1272
+ onSessionExpiredRef.current = onSessionExpired;
1273
+ }, [onSessionExpired]);
1274
+ (0, import_react.useEffect)(() => {
1275
+ const unsubscribe = addTokenListener((nextToken, previousToken) => {
1276
+ if (nextToken === previousToken) return;
1277
+ setTokenState(nextToken);
1278
+ if (nextToken) {
1279
+ setSessionValid(true);
1280
+ setLoading(true);
1281
+ } else {
1282
+ setSessionValid(false);
1283
+ setUser(null);
1284
+ setLoading(false);
1285
+ }
1286
+ });
1287
+ return unsubscribe;
1288
+ }, []);
1289
+ const invalidateLocalSession = () => {
1290
+ clearToken();
1291
+ setTokenState(null);
1292
+ setUser(null);
1293
+ setSessionValid(false);
1294
+ setLoading(false);
1295
+ };
1179
1296
  const handleSessionInvalid = (reason) => {
1180
1297
  console.log("\u{1F6A8} AuthProvider: Session invalidated -", reason);
1181
- setSessionValid(false);
1182
- setUser(null);
1183
- setTokenState(null);
1184
- if (onSessionExpired && typeof onSessionExpired === "function") {
1185
- onSessionExpired(reason);
1186
- }
1298
+ if (recoveryInFlightRef.current) return recoveryInFlightRef.current;
1299
+ const recover = async () => {
1300
+ const callback = onSessionExpiredRef.current;
1301
+ if (typeof callback === "function") {
1302
+ try {
1303
+ await callback(reason);
1304
+ const recoveredToken = getToken();
1305
+ if (recoveredToken) {
1306
+ setTokenState(recoveredToken);
1307
+ setSessionValid(true);
1308
+ setLoading(true);
1309
+ return true;
1310
+ }
1311
+ } catch (error) {
1312
+ await emitAuthDiagnostic("SESSION_RECOVERY_FAILED", "FAILURE", reason, {
1313
+ status: (error == null ? void 0 : error.status) || null,
1314
+ errorCode: (error == null ? void 0 : error.code) || null
1315
+ });
1316
+ }
1317
+ }
1318
+ invalidateLocalSession();
1319
+ return false;
1320
+ };
1321
+ recoveryInFlightRef.current = recover().finally(() => {
1322
+ recoveryInFlightRef.current = null;
1323
+ });
1324
+ return recoveryInFlightRef.current;
1187
1325
  };
1188
1326
  (0, import_react.useEffect)(() => {
1189
- if (token && !sessionSecurityRef.current) {
1327
+ if (manageSessionSecurity && token && !sessionSecurityRef.current) {
1190
1328
  console.log("\u{1F510} AuthProvider: Starting session security");
1191
1329
  const unsubscribe = onSessionInvalid(handleSessionInvalid);
1192
1330
  sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);
@@ -1198,11 +1336,11 @@ function AuthProvider({ children, onSessionExpired }) {
1198
1336
  }
1199
1337
  };
1200
1338
  }
1201
- if (!token && sessionSecurityRef.current) {
1339
+ if ((!manageSessionSecurity || !token) && sessionSecurityRef.current) {
1202
1340
  sessionSecurityRef.current.stopAll();
1203
1341
  sessionSecurityRef.current = null;
1204
1342
  }
1205
- }, [token]);
1343
+ }, [manageSessionSecurity, token]);
1206
1344
  (0, import_react.useEffect)(() => {
1207
1345
  console.log("\u{1F50D} AuthProvider useEffect triggered:", {
1208
1346
  hasToken: !!token,
@@ -1223,26 +1361,79 @@ function AuthProvider({ children, onSessionExpired }) {
1223
1361
  authBaseUrl,
1224
1362
  tokenPreview: token.slice(0, 50) + "..."
1225
1363
  });
1226
- fetch(`${authBaseUrl}/account/profile`, {
1227
- headers: { Authorization: `Bearer ${token}` },
1228
- credentials: "include"
1229
- }).then((res) => {
1230
- console.log("\u{1F4E5} Profile response status:", res.status);
1231
- if (!res.ok) throw new Error("Failed to fetch user");
1232
- return res.json();
1233
- }).then((responseBody) => {
1234
- const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
1235
- console.log("\u2705 Profile fetched successfully:", userData.email);
1236
- setUser(userData);
1237
- setSessionValid(true);
1238
- setLoading(false);
1239
- }).catch((err) => {
1240
- console.error("\u274C Fetch user error:", err);
1241
- clearToken();
1242
- setTokenState(null);
1243
- setUser(null);
1244
- setLoading(false);
1245
- });
1364
+ let cancelled = false;
1365
+ const fetchProfile = async (accessToken2) => {
1366
+ const response = await fetch(`${authBaseUrl}/account/profile`, {
1367
+ headers: { Authorization: `Bearer ${accessToken2}` },
1368
+ credentials: "include"
1369
+ });
1370
+ console.log("\u{1F4E5} Profile response status:", response.status);
1371
+ if (!response.ok) {
1372
+ const error = new Error(`Profile request failed: ${response.status}`);
1373
+ error.status = response.status;
1374
+ throw error;
1375
+ }
1376
+ return response.json();
1377
+ };
1378
+ const loadProfile = async () => {
1379
+ try {
1380
+ const responseBody = await fetchProfile(token);
1381
+ if (cancelled) return;
1382
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
1383
+ console.log("\u2705 Profile fetched successfully:", userData.email);
1384
+ setUser(userData);
1385
+ setSessionValid(true);
1386
+ setLoading(false);
1387
+ } catch (error) {
1388
+ if (cancelled) return;
1389
+ const status = Number((error == null ? void 0 : error.status) || 0);
1390
+ const isUnauthorized = status === 401;
1391
+ const callback = onSessionExpiredRef.current;
1392
+ await emitAuthDiagnostic(
1393
+ "PROFILE_REQUEST_FAILED",
1394
+ isUnauthorized ? "FAILURE" : "WARNING",
1395
+ isUnauthorized ? "PROFILE_UNAUTHORIZED" : status ? `PROFILE_HTTP_${status}` : "PROFILE_NETWORK_ERROR",
1396
+ { status, errorName: error == null ? void 0 : error.name, errorCode: error == null ? void 0 : error.code }
1397
+ );
1398
+ if (!isUnauthorized) {
1399
+ console.warn("\u26A0\uFE0F Profile unavailable; retaining the current auth session", error);
1400
+ setLoading(false);
1401
+ return;
1402
+ }
1403
+ if (typeof callback === "function" && !profileRecoveryTokensRef.current.has(token)) {
1404
+ profileRecoveryTokensRef.current.add(token);
1405
+ try {
1406
+ await callback("profile_unauthorized");
1407
+ const recoveredToken = getToken();
1408
+ if (recoveredToken && recoveredToken !== token) {
1409
+ setTokenState(recoveredToken);
1410
+ setSessionValid(true);
1411
+ setLoading(true);
1412
+ return;
1413
+ }
1414
+ if (recoveredToken) {
1415
+ const responseBody = await fetchProfile(recoveredToken);
1416
+ if (cancelled) return;
1417
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
1418
+ setUser(userData);
1419
+ setSessionValid(true);
1420
+ setLoading(false);
1421
+ return;
1422
+ }
1423
+ } catch (recoveryError) {
1424
+ await emitAuthDiagnostic("PROFILE_RECOVERY_FAILED", "FAILURE", "PROFILE_REFRESH_FAILED", {
1425
+ status: (recoveryError == null ? void 0 : recoveryError.status) || null,
1426
+ errorCode: (recoveryError == null ? void 0 : recoveryError.code) || null
1427
+ });
1428
+ }
1429
+ }
1430
+ invalidateLocalSession();
1431
+ }
1432
+ };
1433
+ void loadProfile();
1434
+ return () => {
1435
+ cancelled = true;
1436
+ };
1246
1437
  }, [token]);
1247
1438
  const login2 = (clientKey, redirectUri, state) => {
1248
1439
  login(clientKey, redirectUri, state);
@@ -1271,6 +1462,7 @@ function AuthProvider({ children, onSessionExpired }) {
1271
1462
  setToken(newToken);
1272
1463
  setTokenState(newToken);
1273
1464
  setSessionValid(true);
1465
+ if (newToken) setLoading(true);
1274
1466
  },
1275
1467
  clearToken: () => {
1276
1468
  stopSessionSecurity();
@@ -1438,6 +1630,8 @@ function usePreferences() {
1438
1630
  var auth = {
1439
1631
  // 🔧 Config
1440
1632
  setConfig,
1633
+ applyRuntimePolicy,
1634
+ loadRuntimePolicy,
1441
1635
  getConfig,
1442
1636
  isRouterMode,
1443
1637
  // 🔐 Core flows