@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.js CHANGED
@@ -217,6 +217,44 @@ var config = {
217
217
  // ⚠️ In true production, set to false and rely on httpOnly cookies
218
218
  persistRefreshToken: false
219
219
  };
220
+ var RUNTIME_POLICY_DEFAULTS = {
221
+ tokenRefreshBuffer: 60,
222
+ sessionValidationInterval: 15 * 60 * 1e3,
223
+ enableSessionValidation: true,
224
+ enableProactiveRefresh: true,
225
+ validateOnVisibility: true
226
+ };
227
+ function validateRuntimePolicy(policy = {}) {
228
+ const numberInRange = (value, min, max) => Number.isInteger(value) && value >= min && value <= max;
229
+ const result = { ...RUNTIME_POLICY_DEFAULTS };
230
+ if (numberInRange(policy.tokenRefreshBuffer, 10, 300)) result.tokenRefreshBuffer = policy.tokenRefreshBuffer;
231
+ if (numberInRange(policy.sessionValidationInterval, 6e4, 864e5)) result.sessionValidationInterval = policy.sessionValidationInterval;
232
+ for (const key of ["enableSessionValidation", "enableProactiveRefresh", "validateOnVisibility"]) {
233
+ if (typeof policy[key] === "boolean") result[key] = policy[key];
234
+ }
235
+ return result;
236
+ }
237
+ function applyRuntimePolicy(policy = {}) {
238
+ config = { ...config, ...validateRuntimePolicy(policy) };
239
+ return getConfig();
240
+ }
241
+ async function loadRuntimePolicy() {
242
+ const { authBaseUrl, clientKey } = config;
243
+ if (!authBaseUrl || !clientKey || typeof fetch !== "function") return getConfig();
244
+ try {
245
+ const response = await fetch(`${authBaseUrl.replace(/\/+$/, "")}/clients/${encodeURIComponent(clientKey)}/config`, {
246
+ method: "GET",
247
+ credentials: "include",
248
+ headers: { Accept: "application/json" }
249
+ });
250
+ if (!response.ok) throw new Error(`runtime policy HTTP ${response.status}`);
251
+ const payload = await response.json();
252
+ return applyRuntimePolicy(payload.authentication_policy || {});
253
+ } catch (error) {
254
+ console.warn("[auth-client] Runtime policy unavailable; using safe defaults", error.message);
255
+ return applyRuntimePolicy({});
256
+ }
257
+ }
220
258
  function setConfig(customConfig = {}) {
221
259
  if (!customConfig.clientKey || !customConfig.authBaseUrl) {
222
260
  throw new Error("Missing required config: clientKey and authBaseUrl are required");
@@ -302,6 +340,28 @@ function clearLoginLock() {
302
340
  } catch {
303
341
  }
304
342
  }
343
+ async function reportClientDiagnostic(safe) {
344
+ if (typeof fetch !== "function" || typeof getToken !== "function") return;
345
+ if (safe.outcome === "SUCCESS" && !safe.event.startsWith("PROFILE_") && !safe.event.startsWith("SESSION_")) return;
346
+ const { authBaseUrl } = getConfig();
347
+ const token = getToken();
348
+ if (!authBaseUrl || !token) return;
349
+ try {
350
+ await fetch(`${authBaseUrl.replace(/\/+$/, "")}/client-telemetry`, {
351
+ method: "POST",
352
+ credentials: "include",
353
+ keepalive: true,
354
+ headers: {
355
+ "Content-Type": "application/json",
356
+ Authorization: `Bearer ${token}`,
357
+ "X-Correlation-ID": safe.correlationId,
358
+ "X-Request-ID": safe.correlationId
359
+ },
360
+ body: JSON.stringify(safe)
361
+ });
362
+ } catch {
363
+ }
364
+ }
305
365
  async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
306
366
  const context = getDiagnosticContext();
307
367
  const safe = {
@@ -318,12 +378,13 @@ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
318
378
  };
319
379
  const method = outcome === "FAILURE" ? "error" : outcome === "WARNING" ? "warn" : "info";
320
380
  console[method]("[auth-client]", safe);
381
+ void reportClientDiagnostic(safe);
321
382
  return safe;
322
383
  }
323
384
 
324
385
  // core.js
325
386
  var callbackProcessed = false;
326
- function login(clientKeyArg, redirectUriArg) {
387
+ function login(clientKeyArg, redirectUriArg, options = {}) {
327
388
  resetCallbackState();
328
389
  const {
329
390
  clientKey: defaultClientKey,
@@ -346,23 +407,24 @@ function login(clientKeyArg, redirectUriArg) {
346
407
  sessionStorage.setItem("originalApp", clientKey);
347
408
  sessionStorage.setItem("returnUrl", redirectUri);
348
409
  if (isRouterMode()) {
349
- return routerLogin(clientKey, redirectUri);
410
+ return routerLogin(clientKey, redirectUri, options);
350
411
  } else {
351
- return clientLogin(clientKey, redirectUri);
412
+ return clientLogin(clientKey, redirectUri, options);
352
413
  }
353
414
  }
354
- function routerLogin(clientKey, redirectUri) {
415
+ function routerLogin(clientKey, redirectUri, options = {}) {
355
416
  const { authBaseUrl } = getConfig();
356
417
  const params = new URLSearchParams();
357
418
  if (redirectUri) {
358
419
  params.append("redirect_uri", redirectUri);
359
420
  }
421
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
360
422
  params.append("correlation_id", getDiagnosticContext().correlationId);
361
423
  const query = params.toString();
362
424
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
363
425
  window.location.href = backendLoginUrl;
364
426
  }
365
- function clientLogin(clientKey, redirectUri) {
427
+ function clientLogin(clientKey, redirectUri, options = {}) {
366
428
  const { accountUiUrl } = getConfig();
367
429
  const params = new URLSearchParams({
368
430
  client: clientKey
@@ -370,6 +432,7 @@ function clientLogin(clientKey, redirectUri) {
370
432
  if (redirectUri) {
371
433
  params.append("redirect_uri", redirectUri);
372
434
  }
435
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
373
436
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
374
437
  window.location.href = centralizedLoginUrl;
375
438
  }
@@ -495,6 +558,28 @@ function resetCallbackState() {
495
558
  }
496
559
  var refreshInProgress = false;
497
560
  var refreshPromise = null;
561
+ async function withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest) {
562
+ var _a;
563
+ const lockName = `auth-refresh-${clientKey}`;
564
+ const run = async () => {
565
+ try {
566
+ const persistedToken = localStorage.getItem("authToken");
567
+ if (tokenBeforeRefresh && persistedToken && persistedToken !== tokenBeforeRefresh) {
568
+ setToken(persistedToken);
569
+ emitAuthDiagnostic("TOKEN_REFRESH_REUSED_CROSS_TAB", "SUCCESS", "CROSS_TAB_ROTATION", {
570
+ clientKey
571
+ });
572
+ return persistedToken;
573
+ }
574
+ } catch {
575
+ }
576
+ return refreshRequest();
577
+ };
578
+ if (typeof navigator !== "undefined" && ((_a = navigator.locks) == null ? void 0 : _a.request)) {
579
+ return navigator.locks.request(lockName, run);
580
+ }
581
+ return run();
582
+ }
498
583
  async function refreshToken() {
499
584
  const { clientKey, authBaseUrl } = getConfig();
500
585
  if (refreshInProgress && refreshPromise) {
@@ -503,70 +588,74 @@ async function refreshToken() {
503
588
  }
504
589
  refreshInProgress = true;
505
590
  refreshPromise = (async () => {
506
- var _a, _b, _c, _d, _e, _f;
507
- try {
508
- const storedRefreshToken = getRefreshToken();
509
- console.log("\u{1F504} Refreshing token:", {
510
- clientKey,
511
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
512
- hasStoredRefreshToken: !!storedRefreshToken
513
- });
514
- const requestOptions = {
515
- method: "POST",
516
- credentials: "include",
517
- // ✅ Include httpOnly cookies (for HTTPS)
518
- headers: {
519
- ...diagnosticHeaders(),
520
- "Content-Type": "application/json"
521
- }
522
- };
523
- if (storedRefreshToken) {
524
- requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
525
- console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
526
- }
527
- const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
528
- if (!response.ok) {
529
- const errorText = await response.text();
530
- let serverCode = null;
531
- try {
532
- serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
533
- } catch {
534
- }
535
- emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
591
+ const tokenBeforeRefresh = getToken();
592
+ const refreshRequest = async () => {
593
+ var _a, _b, _c, _d, _e, _f;
594
+ try {
595
+ const storedRefreshToken = getRefreshToken();
596
+ console.log("\u{1F504} Refreshing token:", {
536
597
  clientKey,
537
- status: response.status
598
+ mode: isRouterMode() ? "ROUTER" : "CLIENT",
599
+ hasStoredRefreshToken: !!storedRefreshToken
538
600
  });
539
- const refreshError = new Error(`Refresh failed: ${response.status}`);
540
- refreshError.code = serverCode || `HTTP_${response.status}`;
541
- refreshError.status = response.status;
542
- throw refreshError;
543
- }
544
- const data = await response.json();
545
- const { access_token, refresh_token: new_refresh_token } = data;
546
- if (!access_token) {
547
- throw new Error("No access token in refresh response");
548
- }
549
- setToken(access_token);
550
- if (new_refresh_token) {
551
- setRefreshToken(new_refresh_token);
552
- console.log("\u{1F504} New refresh token stored from rotation");
553
- }
554
- console.log("\u2705 Token refresh successful, listeners notified");
555
- emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
556
- return access_token;
557
- } catch (err) {
558
- console.error("\u274C Token refresh error:", err);
559
- 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"));
560
- if (isAuthRejection) {
561
- clearToken();
562
- clearRefreshToken();
601
+ const requestOptions = {
602
+ method: "POST",
603
+ credentials: "include",
604
+ // ✅ Include httpOnly cookies (for HTTPS)
605
+ headers: {
606
+ ...diagnosticHeaders(),
607
+ "Content-Type": "application/json"
608
+ }
609
+ };
610
+ if (storedRefreshToken) {
611
+ requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
612
+ console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
613
+ }
614
+ const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
615
+ if (!response.ok) {
616
+ const errorText = await response.text();
617
+ let serverCode = null;
618
+ try {
619
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
620
+ } catch {
621
+ }
622
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
623
+ clientKey,
624
+ status: response.status
625
+ });
626
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
627
+ refreshError.code = serverCode || `HTTP_${response.status}`;
628
+ refreshError.status = response.status;
629
+ throw refreshError;
630
+ }
631
+ const data = await response.json();
632
+ const { access_token, refresh_token: new_refresh_token } = data;
633
+ if (!access_token) {
634
+ throw new Error("No access token in refresh response");
635
+ }
636
+ setToken(access_token);
637
+ if (new_refresh_token) {
638
+ setRefreshToken(new_refresh_token);
639
+ console.log("\u{1F504} New refresh token stored from rotation");
640
+ }
641
+ console.log("\u2705 Token refresh successful, listeners notified");
642
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
643
+ return access_token;
644
+ } catch (err) {
645
+ console.error("\u274C Token refresh error:", err);
646
+ 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"));
647
+ if (isAuthRejection) {
648
+ clearToken();
649
+ clearRefreshToken();
650
+ }
651
+ throw err;
563
652
  }
564
- throw err;
565
- } finally {
566
- refreshInProgress = false;
567
- refreshPromise = null;
568
- }
569
- })();
653
+ };
654
+ return withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest);
655
+ })().finally(() => {
656
+ refreshInProgress = false;
657
+ refreshPromise = null;
658
+ });
570
659
  return refreshPromise;
571
660
  }
572
661
  async function validateCurrentSession() {
@@ -1119,23 +1208,72 @@ var preferences = {
1119
1208
  // react/AuthProvider.jsx
1120
1209
  import React, { createContext, useState, useEffect, useRef } from "react";
1121
1210
  var AuthContext = createContext();
1122
- function AuthProvider({ children, onSessionExpired }) {
1211
+ function AuthProvider({ children, onSessionExpired, manageSessionSecurity = true }) {
1123
1212
  const [token, setTokenState] = useState(getToken());
1124
1213
  const [user, setUser] = useState(null);
1125
1214
  const [loading, setLoading] = useState(!!token);
1126
1215
  const [sessionValid, setSessionValid] = useState(true);
1127
1216
  const sessionSecurityRef = useRef(null);
1217
+ const onSessionExpiredRef = useRef(onSessionExpired);
1218
+ const recoveryInFlightRef = useRef(null);
1219
+ const profileRecoveryTokensRef = useRef(/* @__PURE__ */ new Set());
1220
+ useEffect(() => {
1221
+ onSessionExpiredRef.current = onSessionExpired;
1222
+ }, [onSessionExpired]);
1223
+ useEffect(() => {
1224
+ const unsubscribe = addTokenListener((nextToken, previousToken) => {
1225
+ if (nextToken === previousToken) return;
1226
+ setTokenState(nextToken);
1227
+ if (nextToken) {
1228
+ setSessionValid(true);
1229
+ setLoading(true);
1230
+ } else {
1231
+ setSessionValid(false);
1232
+ setUser(null);
1233
+ setLoading(false);
1234
+ }
1235
+ });
1236
+ return unsubscribe;
1237
+ }, []);
1238
+ const invalidateLocalSession = () => {
1239
+ clearToken();
1240
+ setTokenState(null);
1241
+ setUser(null);
1242
+ setSessionValid(false);
1243
+ setLoading(false);
1244
+ };
1128
1245
  const handleSessionInvalid = (reason) => {
1129
1246
  console.log("\u{1F6A8} AuthProvider: Session invalidated -", reason);
1130
- setSessionValid(false);
1131
- setUser(null);
1132
- setTokenState(null);
1133
- if (onSessionExpired && typeof onSessionExpired === "function") {
1134
- onSessionExpired(reason);
1135
- }
1247
+ if (recoveryInFlightRef.current) return recoveryInFlightRef.current;
1248
+ const recover = async () => {
1249
+ const callback = onSessionExpiredRef.current;
1250
+ if (typeof callback === "function") {
1251
+ try {
1252
+ await callback(reason);
1253
+ const recoveredToken = getToken();
1254
+ if (recoveredToken) {
1255
+ setTokenState(recoveredToken);
1256
+ setSessionValid(true);
1257
+ setLoading(true);
1258
+ return true;
1259
+ }
1260
+ } catch (error) {
1261
+ await emitAuthDiagnostic("SESSION_RECOVERY_FAILED", "FAILURE", reason, {
1262
+ status: (error == null ? void 0 : error.status) || null,
1263
+ errorCode: (error == null ? void 0 : error.code) || null
1264
+ });
1265
+ }
1266
+ }
1267
+ invalidateLocalSession();
1268
+ return false;
1269
+ };
1270
+ recoveryInFlightRef.current = recover().finally(() => {
1271
+ recoveryInFlightRef.current = null;
1272
+ });
1273
+ return recoveryInFlightRef.current;
1136
1274
  };
1137
1275
  useEffect(() => {
1138
- if (token && !sessionSecurityRef.current) {
1276
+ if (manageSessionSecurity && token && !sessionSecurityRef.current) {
1139
1277
  console.log("\u{1F510} AuthProvider: Starting session security");
1140
1278
  const unsubscribe = onSessionInvalid(handleSessionInvalid);
1141
1279
  sessionSecurityRef.current = startSessionSecurity(handleSessionInvalid);
@@ -1147,11 +1285,11 @@ function AuthProvider({ children, onSessionExpired }) {
1147
1285
  }
1148
1286
  };
1149
1287
  }
1150
- if (!token && sessionSecurityRef.current) {
1288
+ if ((!manageSessionSecurity || !token) && sessionSecurityRef.current) {
1151
1289
  sessionSecurityRef.current.stopAll();
1152
1290
  sessionSecurityRef.current = null;
1153
1291
  }
1154
- }, [token]);
1292
+ }, [manageSessionSecurity, token]);
1155
1293
  useEffect(() => {
1156
1294
  console.log("\u{1F50D} AuthProvider useEffect triggered:", {
1157
1295
  hasToken: !!token,
@@ -1172,26 +1310,79 @@ function AuthProvider({ children, onSessionExpired }) {
1172
1310
  authBaseUrl,
1173
1311
  tokenPreview: token.slice(0, 50) + "..."
1174
1312
  });
1175
- fetch(`${authBaseUrl}/account/profile`, {
1176
- headers: { Authorization: `Bearer ${token}` },
1177
- credentials: "include"
1178
- }).then((res) => {
1179
- console.log("\u{1F4E5} Profile response status:", res.status);
1180
- if (!res.ok) throw new Error("Failed to fetch user");
1181
- return res.json();
1182
- }).then((responseBody) => {
1183
- const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
1184
- console.log("\u2705 Profile fetched successfully:", userData.email);
1185
- setUser(userData);
1186
- setSessionValid(true);
1187
- setLoading(false);
1188
- }).catch((err) => {
1189
- console.error("\u274C Fetch user error:", err);
1190
- clearToken();
1191
- setTokenState(null);
1192
- setUser(null);
1193
- setLoading(false);
1194
- });
1313
+ let cancelled = false;
1314
+ const fetchProfile = async (accessToken2) => {
1315
+ const response = await fetch(`${authBaseUrl}/account/profile`, {
1316
+ headers: { Authorization: `Bearer ${accessToken2}` },
1317
+ credentials: "include"
1318
+ });
1319
+ console.log("\u{1F4E5} Profile response status:", response.status);
1320
+ if (!response.ok) {
1321
+ const error = new Error(`Profile request failed: ${response.status}`);
1322
+ error.status = response.status;
1323
+ throw error;
1324
+ }
1325
+ return response.json();
1326
+ };
1327
+ const loadProfile = async () => {
1328
+ try {
1329
+ const responseBody = await fetchProfile(token);
1330
+ if (cancelled) return;
1331
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
1332
+ console.log("\u2705 Profile fetched successfully:", userData.email);
1333
+ setUser(userData);
1334
+ setSessionValid(true);
1335
+ setLoading(false);
1336
+ } catch (error) {
1337
+ if (cancelled) return;
1338
+ const status = Number((error == null ? void 0 : error.status) || 0);
1339
+ const isUnauthorized = status === 401;
1340
+ const callback = onSessionExpiredRef.current;
1341
+ await emitAuthDiagnostic(
1342
+ "PROFILE_REQUEST_FAILED",
1343
+ isUnauthorized ? "FAILURE" : "WARNING",
1344
+ isUnauthorized ? "PROFILE_UNAUTHORIZED" : status ? `PROFILE_HTTP_${status}` : "PROFILE_NETWORK_ERROR",
1345
+ { status, errorName: error == null ? void 0 : error.name, errorCode: error == null ? void 0 : error.code }
1346
+ );
1347
+ if (!isUnauthorized) {
1348
+ console.warn("\u26A0\uFE0F Profile unavailable; retaining the current auth session", error);
1349
+ setLoading(false);
1350
+ return;
1351
+ }
1352
+ if (typeof callback === "function" && !profileRecoveryTokensRef.current.has(token)) {
1353
+ profileRecoveryTokensRef.current.add(token);
1354
+ try {
1355
+ await callback("profile_unauthorized");
1356
+ const recoveredToken = getToken();
1357
+ if (recoveredToken && recoveredToken !== token) {
1358
+ setTokenState(recoveredToken);
1359
+ setSessionValid(true);
1360
+ setLoading(true);
1361
+ return;
1362
+ }
1363
+ if (recoveredToken) {
1364
+ const responseBody = await fetchProfile(recoveredToken);
1365
+ if (cancelled) return;
1366
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
1367
+ setUser(userData);
1368
+ setSessionValid(true);
1369
+ setLoading(false);
1370
+ return;
1371
+ }
1372
+ } catch (recoveryError) {
1373
+ await emitAuthDiagnostic("PROFILE_RECOVERY_FAILED", "FAILURE", "PROFILE_REFRESH_FAILED", {
1374
+ status: (recoveryError == null ? void 0 : recoveryError.status) || null,
1375
+ errorCode: (recoveryError == null ? void 0 : recoveryError.code) || null
1376
+ });
1377
+ }
1378
+ }
1379
+ invalidateLocalSession();
1380
+ }
1381
+ };
1382
+ void loadProfile();
1383
+ return () => {
1384
+ cancelled = true;
1385
+ };
1195
1386
  }, [token]);
1196
1387
  const login2 = (clientKey, redirectUri, state) => {
1197
1388
  login(clientKey, redirectUri, state);
@@ -1220,6 +1411,7 @@ function AuthProvider({ children, onSessionExpired }) {
1220
1411
  setToken(newToken);
1221
1412
  setTokenState(newToken);
1222
1413
  setSessionValid(true);
1414
+ if (newToken) setLoading(true);
1223
1415
  },
1224
1416
  clearToken: () => {
1225
1417
  stopSessionSecurity();
@@ -1387,6 +1579,8 @@ function usePreferences() {
1387
1579
  var auth = {
1388
1580
  // 🔧 Config
1389
1581
  setConfig,
1582
+ applyRuntimePolicy,
1583
+ loadRuntimePolicy,
1390
1584
  getConfig,
1391
1585
  isRouterMode,
1392
1586
  // 🔐 Core flows