@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.
@@ -254,6 +254,44 @@ var config = {
254
254
  // ⚠️ In true production, set to false and rely on httpOnly cookies
255
255
  persistRefreshToken: false
256
256
  };
257
+ var RUNTIME_POLICY_DEFAULTS = {
258
+ tokenRefreshBuffer: 60,
259
+ sessionValidationInterval: 15 * 60 * 1e3,
260
+ enableSessionValidation: true,
261
+ enableProactiveRefresh: true,
262
+ validateOnVisibility: true
263
+ };
264
+ function validateRuntimePolicy(policy = {}) {
265
+ const numberInRange = (value, min, max) => Number.isInteger(value) && value >= min && value <= max;
266
+ const result = { ...RUNTIME_POLICY_DEFAULTS };
267
+ if (numberInRange(policy.tokenRefreshBuffer, 10, 300)) result.tokenRefreshBuffer = policy.tokenRefreshBuffer;
268
+ if (numberInRange(policy.sessionValidationInterval, 6e4, 864e5)) result.sessionValidationInterval = policy.sessionValidationInterval;
269
+ for (const key of ["enableSessionValidation", "enableProactiveRefresh", "validateOnVisibility"]) {
270
+ if (typeof policy[key] === "boolean") result[key] = policy[key];
271
+ }
272
+ return result;
273
+ }
274
+ function applyRuntimePolicy(policy = {}) {
275
+ config = { ...config, ...validateRuntimePolicy(policy) };
276
+ return getConfig();
277
+ }
278
+ async function loadRuntimePolicy() {
279
+ const { authBaseUrl, clientKey } = config;
280
+ if (!authBaseUrl || !clientKey || typeof fetch !== "function") return getConfig();
281
+ try {
282
+ const response = await fetch(`${authBaseUrl.replace(/\/+$/, "")}/clients/${encodeURIComponent(clientKey)}/config`, {
283
+ method: "GET",
284
+ credentials: "include",
285
+ headers: { Accept: "application/json" }
286
+ });
287
+ if (!response.ok) throw new Error(`runtime policy HTTP ${response.status}`);
288
+ const payload = await response.json();
289
+ return applyRuntimePolicy(payload.authentication_policy || {});
290
+ } catch (error) {
291
+ console.warn("[auth-client] Runtime policy unavailable; using safe defaults", error.message);
292
+ return applyRuntimePolicy({});
293
+ }
294
+ }
257
295
  function setConfig(customConfig = {}) {
258
296
  if (!customConfig.clientKey || !customConfig.authBaseUrl) {
259
297
  throw new Error("Missing required config: clientKey and authBaseUrl are required");
@@ -339,6 +377,28 @@ function clearLoginLock() {
339
377
  } catch {
340
378
  }
341
379
  }
380
+ async function reportClientDiagnostic(safe) {
381
+ if (typeof fetch !== "function" || typeof getToken !== "function") return;
382
+ if (safe.outcome === "SUCCESS" && !safe.event.startsWith("PROFILE_") && !safe.event.startsWith("SESSION_")) return;
383
+ const { authBaseUrl } = getConfig();
384
+ const token = getToken();
385
+ if (!authBaseUrl || !token) return;
386
+ try {
387
+ await fetch(`${authBaseUrl.replace(/\/+$/, "")}/client-telemetry`, {
388
+ method: "POST",
389
+ credentials: "include",
390
+ keepalive: true,
391
+ headers: {
392
+ "Content-Type": "application/json",
393
+ Authorization: `Bearer ${token}`,
394
+ "X-Correlation-ID": safe.correlationId,
395
+ "X-Request-ID": safe.correlationId
396
+ },
397
+ body: JSON.stringify(safe)
398
+ });
399
+ } catch {
400
+ }
401
+ }
342
402
  async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
343
403
  const context = getDiagnosticContext();
344
404
  const safe = {
@@ -355,12 +415,13 @@ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
355
415
  };
356
416
  const method = outcome === "FAILURE" ? "error" : outcome === "WARNING" ? "warn" : "info";
357
417
  console[method]("[auth-client]", safe);
418
+ void reportClientDiagnostic(safe);
358
419
  return safe;
359
420
  }
360
421
 
361
422
  // core.js
362
423
  var callbackProcessed = false;
363
- function login(clientKeyArg, redirectUriArg) {
424
+ function login(clientKeyArg, redirectUriArg, options = {}) {
364
425
  resetCallbackState();
365
426
  const {
366
427
  clientKey: defaultClientKey,
@@ -383,23 +444,24 @@ function login(clientKeyArg, redirectUriArg) {
383
444
  sessionStorage.setItem("originalApp", clientKey);
384
445
  sessionStorage.setItem("returnUrl", redirectUri);
385
446
  if (isRouterMode()) {
386
- return routerLogin(clientKey, redirectUri);
447
+ return routerLogin(clientKey, redirectUri, options);
387
448
  } else {
388
- return clientLogin(clientKey, redirectUri);
449
+ return clientLogin(clientKey, redirectUri, options);
389
450
  }
390
451
  }
391
- function routerLogin(clientKey, redirectUri) {
452
+ function routerLogin(clientKey, redirectUri, options = {}) {
392
453
  const { authBaseUrl } = getConfig();
393
454
  const params = new URLSearchParams();
394
455
  if (redirectUri) {
395
456
  params.append("redirect_uri", redirectUri);
396
457
  }
458
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
397
459
  params.append("correlation_id", getDiagnosticContext().correlationId);
398
460
  const query = params.toString();
399
461
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
400
462
  window.location.href = backendLoginUrl;
401
463
  }
402
- function clientLogin(clientKey, redirectUri) {
464
+ function clientLogin(clientKey, redirectUri, options = {}) {
403
465
  const { accountUiUrl } = getConfig();
404
466
  const params = new URLSearchParams({
405
467
  client: clientKey
@@ -407,6 +469,7 @@ function clientLogin(clientKey, redirectUri) {
407
469
  if (redirectUri) {
408
470
  params.append("redirect_uri", redirectUri);
409
471
  }
472
+ if (options.switchAccount || options.switch_account) params.append("switch_account", "true");
410
473
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
411
474
  window.location.href = centralizedLoginUrl;
412
475
  }
@@ -532,6 +595,28 @@ function resetCallbackState() {
532
595
  }
533
596
  var refreshInProgress = false;
534
597
  var refreshPromise = null;
598
+ async function withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest) {
599
+ var _a;
600
+ const lockName = `auth-refresh-${clientKey}`;
601
+ const run = async () => {
602
+ try {
603
+ const persistedToken = localStorage.getItem("authToken");
604
+ if (tokenBeforeRefresh && persistedToken && persistedToken !== tokenBeforeRefresh) {
605
+ setToken(persistedToken);
606
+ emitAuthDiagnostic("TOKEN_REFRESH_REUSED_CROSS_TAB", "SUCCESS", "CROSS_TAB_ROTATION", {
607
+ clientKey
608
+ });
609
+ return persistedToken;
610
+ }
611
+ } catch {
612
+ }
613
+ return refreshRequest();
614
+ };
615
+ if (typeof navigator !== "undefined" && ((_a = navigator.locks) == null ? void 0 : _a.request)) {
616
+ return navigator.locks.request(lockName, run);
617
+ }
618
+ return run();
619
+ }
535
620
  async function refreshToken() {
536
621
  const { clientKey, authBaseUrl } = getConfig();
537
622
  if (refreshInProgress && refreshPromise) {
@@ -540,70 +625,74 @@ async function refreshToken() {
540
625
  }
541
626
  refreshInProgress = true;
542
627
  refreshPromise = (async () => {
543
- var _a, _b, _c, _d, _e, _f;
544
- try {
545
- const storedRefreshToken = getRefreshToken();
546
- console.log("\u{1F504} Refreshing token:", {
547
- clientKey,
548
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
549
- hasStoredRefreshToken: !!storedRefreshToken
550
- });
551
- const requestOptions = {
552
- method: "POST",
553
- credentials: "include",
554
- // ✅ Include httpOnly cookies (for HTTPS)
555
- headers: {
556
- ...diagnosticHeaders(),
557
- "Content-Type": "application/json"
558
- }
559
- };
560
- if (storedRefreshToken) {
561
- requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
562
- console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
563
- }
564
- const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
565
- if (!response.ok) {
566
- const errorText = await response.text();
567
- let serverCode = null;
568
- try {
569
- serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
570
- } catch {
571
- }
572
- emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
628
+ const tokenBeforeRefresh = getToken();
629
+ const refreshRequest = async () => {
630
+ var _a, _b, _c, _d, _e, _f;
631
+ try {
632
+ const storedRefreshToken = getRefreshToken();
633
+ console.log("\u{1F504} Refreshing token:", {
573
634
  clientKey,
574
- status: response.status
635
+ mode: isRouterMode() ? "ROUTER" : "CLIENT",
636
+ hasStoredRefreshToken: !!storedRefreshToken
575
637
  });
576
- const refreshError = new Error(`Refresh failed: ${response.status}`);
577
- refreshError.code = serverCode || `HTTP_${response.status}`;
578
- refreshError.status = response.status;
579
- throw refreshError;
580
- }
581
- const data = await response.json();
582
- const { access_token, refresh_token: new_refresh_token } = data;
583
- if (!access_token) {
584
- throw new Error("No access token in refresh response");
585
- }
586
- setToken(access_token);
587
- if (new_refresh_token) {
588
- setRefreshToken(new_refresh_token);
589
- console.log("\u{1F504} New refresh token stored from rotation");
590
- }
591
- console.log("\u2705 Token refresh successful, listeners notified");
592
- emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
593
- return access_token;
594
- } catch (err) {
595
- console.error("\u274C Token refresh error:", err);
596
- 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"));
597
- if (isAuthRejection) {
598
- clearToken();
599
- clearRefreshToken();
638
+ const requestOptions = {
639
+ method: "POST",
640
+ credentials: "include",
641
+ // ✅ Include httpOnly cookies (for HTTPS)
642
+ headers: {
643
+ ...diagnosticHeaders(),
644
+ "Content-Type": "application/json"
645
+ }
646
+ };
647
+ if (storedRefreshToken) {
648
+ requestOptions.body = JSON.stringify({ refreshToken: storedRefreshToken });
649
+ console.log("\u{1F4E6} Sending refresh token in body only (Header skipped) v3.0.2");
650
+ }
651
+ const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
652
+ if (!response.ok) {
653
+ const errorText = await response.text();
654
+ let serverCode = null;
655
+ try {
656
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
657
+ } catch {
658
+ }
659
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
660
+ clientKey,
661
+ status: response.status
662
+ });
663
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
664
+ refreshError.code = serverCode || `HTTP_${response.status}`;
665
+ refreshError.status = response.status;
666
+ throw refreshError;
667
+ }
668
+ const data = await response.json();
669
+ const { access_token, refresh_token: new_refresh_token } = data;
670
+ if (!access_token) {
671
+ throw new Error("No access token in refresh response");
672
+ }
673
+ setToken(access_token);
674
+ if (new_refresh_token) {
675
+ setRefreshToken(new_refresh_token);
676
+ console.log("\u{1F504} New refresh token stored from rotation");
677
+ }
678
+ console.log("\u2705 Token refresh successful, listeners notified");
679
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
680
+ return access_token;
681
+ } catch (err) {
682
+ console.error("\u274C Token refresh error:", err);
683
+ 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"));
684
+ if (isAuthRejection) {
685
+ clearToken();
686
+ clearRefreshToken();
687
+ }
688
+ throw err;
600
689
  }
601
- throw err;
602
- } finally {
603
- refreshInProgress = false;
604
- refreshPromise = null;
605
- }
606
- })();
690
+ };
691
+ return withCrossTabRefreshLock(clientKey, tokenBeforeRefresh, refreshRequest);
692
+ })().finally(() => {
693
+ refreshInProgress = false;
694
+ refreshPromise = null;
695
+ });
607
696
  return refreshPromise;
608
697
  }
609
698
  async function validateCurrentSession() {
@@ -1168,6 +1257,8 @@ var PreferencesContext = (0, import_react3.createContext)(null);
1168
1257
  var auth = {
1169
1258
  // 🔧 Config
1170
1259
  setConfig,
1260
+ applyRuntimePolicy,
1261
+ loadRuntimePolicy,
1171
1262
  getConfig,
1172
1263
  isRouterMode,
1173
1264
  // 🔐 Core flows