@meistrari/auth-nuxt 3.18.0 → 3.19.0

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.
@@ -0,0 +1,39 @@
1
+ import type { RefreshOutcome } from './refresh-orchestrator.js';
2
+ import type { RefreshPolicy } from './refresh-policy.js';
3
+ /** Timer surface the scheduler drives (see `worker-timer.ts`) */
4
+ export interface SchedulerTimer {
5
+ set: (delayMs: number, callback: () => void) => void;
6
+ clear: () => void;
7
+ }
8
+ export interface RefreshSchedulerDeps {
9
+ ensureFreshToken: (options?: {
10
+ force?: boolean;
11
+ }) => Promise<RefreshOutcome>;
12
+ /** Reads the live access token — must not be a stale captured ref */
13
+ readAccessToken: () => string | null;
14
+ parseTokenExpiry: (token: string) => number | null;
15
+ policy: RefreshPolicy;
16
+ timer: SchedulerTimer;
17
+ /**
18
+ * Invoked exactly once, only on a definitive auth failure (401/404).
19
+ * This is the scheduler's single exit; every other outcome reschedules
20
+ */
21
+ onAuthFailure: (error: unknown) => void | Promise<void>;
22
+ now?: () => number;
23
+ random?: () => number;
24
+ /** Optional sink for unexpected tick errors (defaults to console.error) */
25
+ onUnexpectedError?: (error: unknown) => void;
26
+ }
27
+ export interface RefreshScheduler {
28
+ /** Starts the loop with an immediate tick */
29
+ start: () => void;
30
+ /** Stops the loop and clears any pending timer */
31
+ stop: () => void;
32
+ /**
33
+ * Runs a tick now, replacing any pending timer. Safe to spam from
34
+ * `visibilitychange`/`online`/`focus` listeners: in-flight dedup and
35
+ * the orchestrator's freshness pre-check make redundant kicks free.
36
+ */
37
+ kick: () => void;
38
+ }
39
+ export declare function createRefreshScheduler(deps: RefreshSchedulerDeps): RefreshScheduler;
@@ -0,0 +1,90 @@
1
+ import { computeBackoffDelay, computeNextRefreshDelay } from "./refresh-policy.js";
2
+ export function createRefreshScheduler(deps) {
3
+ const {
4
+ ensureFreshToken,
5
+ readAccessToken,
6
+ parseTokenExpiry,
7
+ policy,
8
+ timer,
9
+ onAuthFailure,
10
+ now = () => Date.now(),
11
+ random = Math.random,
12
+ onUnexpectedError = (error) => console.error("[Tela Auth SDK] Unexpected token refresh scheduler error:", error)
13
+ } = deps;
14
+ let stopped = true;
15
+ let authFailed = false;
16
+ let running = false;
17
+ let kickPending = false;
18
+ let consecutiveFailures = 0;
19
+ function nextBackoffDelay() {
20
+ const delay = Math.max(
21
+ computeBackoffDelay(consecutiveFailures, policy, random),
22
+ policy.minRefreshIntervalMs
23
+ );
24
+ consecutiveFailures = Math.min(consecutiveFailures + 1, 10);
25
+ return delay;
26
+ }
27
+ async function tick() {
28
+ if (stopped || authFailed || running) {
29
+ return;
30
+ }
31
+ running = true;
32
+ let delay;
33
+ try {
34
+ const outcome = await ensureFreshToken();
35
+ if (outcome.status === "auth-failed") {
36
+ authFailed = true;
37
+ timer.clear();
38
+ await onAuthFailure(outcome.error);
39
+ return;
40
+ }
41
+ if (outcome.status === "ok") {
42
+ consecutiveFailures = 0;
43
+ const token = readAccessToken();
44
+ const expiry = token ? parseTokenExpiry(token) : null;
45
+ delay = computeNextRefreshDelay(expiry, now(), policy);
46
+ } else {
47
+ delay = nextBackoffDelay();
48
+ }
49
+ } catch (error) {
50
+ onUnexpectedError(error);
51
+ delay = nextBackoffDelay();
52
+ } finally {
53
+ running = false;
54
+ }
55
+ if (stopped || authFailed) {
56
+ return;
57
+ }
58
+ if (kickPending) {
59
+ kickPending = false;
60
+ void tick();
61
+ return;
62
+ }
63
+ timer.set(delay, () => void tick());
64
+ }
65
+ return {
66
+ start() {
67
+ if (authFailed) {
68
+ return;
69
+ }
70
+ stopped = false;
71
+ void tick();
72
+ },
73
+ stop() {
74
+ stopped = true;
75
+ kickPending = false;
76
+ timer.clear();
77
+ },
78
+ kick() {
79
+ if (stopped || authFailed) {
80
+ return;
81
+ }
82
+ if (running) {
83
+ kickPending = true;
84
+ return;
85
+ }
86
+ timer.clear();
87
+ void tick();
88
+ }
89
+ };
90
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A single-slot timer backed by an inline Web Worker
3
+ *
4
+ * Unlike main-thread timers, Worker timers are NOT throttled when the tab
5
+ * is in the background, so scheduled refreshes fire reliably. Falls back
6
+ * to `setTimeout` where Workers are unavailable, tracking the fallback id
7
+ * so a re-`set()` can never stack duplicate timer loops
8
+ */
9
+ export interface WorkerLike {
10
+ onmessage: ((event: unknown) => void) | null;
11
+ onerror: ((event: unknown) => void) | null;
12
+ postMessage: (message: unknown) => void;
13
+ terminate: () => void;
14
+ }
15
+ export interface WorkerTimerDeps {
16
+ /** Creates the timer worker; injectable for tests. Throw to force fallback */
17
+ createWorker?: () => WorkerLike;
18
+ setTimeoutFn?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
19
+ clearTimeoutFn?: (id: ReturnType<typeof setTimeout>) => void;
20
+ }
21
+ export interface WorkerTimer {
22
+ /** Schedules `callback` after `delayMs`, replacing any pending timer */
23
+ set: (delayMs: number, callback: () => void) => void;
24
+ /** Cancels the pending timer, whichever mode it is running in */
25
+ clear: () => void;
26
+ }
27
+ export declare function createWorkerTimer(deps?: WorkerTimerDeps): WorkerTimer;
@@ -0,0 +1,68 @@
1
+ const WORKER_SCRIPT = "self.onmessage=function(e){setTimeout(function(){postMessage('tick')},e.data)}";
2
+ function createBlobWorker() {
3
+ const blob = new Blob([WORKER_SCRIPT], { type: "application/javascript" });
4
+ const blobUrl = URL.createObjectURL(blob);
5
+ try {
6
+ return new Worker(blobUrl);
7
+ } finally {
8
+ URL.revokeObjectURL(blobUrl);
9
+ }
10
+ }
11
+ export function createWorkerTimer(deps = {}) {
12
+ const {
13
+ createWorker = createBlobWorker,
14
+ setTimeoutFn = (callback, delayMs) => setTimeout(callback, delayMs),
15
+ clearTimeoutFn = (id) => clearTimeout(id)
16
+ } = deps;
17
+ let worker = null;
18
+ let fallbackId = null;
19
+ let workerAvailable = true;
20
+ function clear() {
21
+ if (worker) {
22
+ worker.terminate();
23
+ worker = null;
24
+ }
25
+ if (fallbackId !== null) {
26
+ clearTimeoutFn(fallbackId);
27
+ fallbackId = null;
28
+ }
29
+ }
30
+ function scheduleFallback(delayMs, callback) {
31
+ fallbackId = setTimeoutFn(() => {
32
+ fallbackId = null;
33
+ callback();
34
+ }, delayMs);
35
+ }
36
+ function set(delayMs, callback) {
37
+ clear();
38
+ if (!workerAvailable) {
39
+ scheduleFallback(delayMs, callback);
40
+ return;
41
+ }
42
+ try {
43
+ const created = createWorker();
44
+ created.onmessage = () => {
45
+ if (worker !== created) {
46
+ return;
47
+ }
48
+ worker = null;
49
+ created.terminate();
50
+ callback();
51
+ };
52
+ created.onerror = () => {
53
+ if (worker === created) {
54
+ worker = null;
55
+ }
56
+ created.terminate();
57
+ workerAvailable = false;
58
+ scheduleFallback(delayMs, callback);
59
+ };
60
+ created.postMessage(delayMs);
61
+ worker = created;
62
+ } catch {
63
+ workerAvailable = false;
64
+ scheduleFallback(delayMs, callback);
65
+ }
66
+ }
67
+ return { set, clear };
68
+ }
@@ -1,12 +1,16 @@
1
1
  import { defineNuxtPlugin, navigateTo, useCookie, useRoute, useRuntimeConfig } from "#app";
2
- import { isTokenExpired } from "@meistrari/auth-core";
2
+ import { RefreshTokenExpiredError } from "@meistrari/auth-core";
3
3
  import { useTelaApplicationAuth } from "../composables/application/auth.js";
4
4
  import { useApplicationSessionState } from "../composables/state.js";
5
5
  import { createNuxtAuthClient } from "../shared.js";
6
+ import { ACCESS_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE, readClientCookie } from "../helpers/client-cookies.js";
7
+ import { useRefreshOrchestrator } from "../helpers/refresh-deps.js";
8
+ import { classifyRefreshStatus, DEFAULT_REFRESH_POLICY, extractHttpStatus } from "../helpers/refresh-policy.js";
9
+ import { createRefreshScheduler } from "../helpers/refresh-scheduler.js";
10
+ import { createWorkerTimer } from "../helpers/worker-timer.js";
6
11
  import { parseTokenExpiry } from "../helpers/token.js";
7
12
  const SEVEN_DAYS = 60 * 60 * 24 * 7;
8
13
  const FIFTEEN_MINUTES = 60 * 15;
9
- const TWO_MINUTES = 2 * 60 * 1e3;
10
14
  export default defineNuxtPlugin({
11
15
  name: "tela-application-token-refresh",
12
16
  enforce: "post",
@@ -21,108 +25,43 @@ export default defineNuxtPlugin({
21
25
  const loginPath = appConfig.application?.loginPath ?? "/login";
22
26
  const unauthorizedPath = appConfig.application?.unauthorizedPath ?? "/unauthorized";
23
27
  const exemptPaths = [loginPath, unauthorizedPath];
24
- const accessTokenCookie = useCookie("tela-access-token", {
28
+ const accessTokenCookie = useCookie(ACCESS_TOKEN_COOKIE, {
25
29
  secure: !import.meta.dev,
26
30
  sameSite: "lax",
27
31
  maxAge: FIFTEEN_MINUTES
28
32
  });
29
- const refreshTokenCookie = useCookie("tela-refresh-token", {
33
+ const refreshTokenCookie = useCookie(REFRESH_TOKEN_COOKIE, {
30
34
  httpOnly: true,
31
35
  secure: !import.meta.dev,
32
36
  sameSite: "lax",
33
37
  maxAge: SEVEN_DAYS
34
38
  });
35
- const authClient = createNuxtAuthClient(appConfig.apiUrl, () => null, () => refreshTokenCookie.value ?? null);
36
- let isRefreshing = false;
37
- let refreshWorker = null;
38
- let workerAvailable = true;
39
- async function refreshToken() {
40
- if (isRefreshing) {
41
- return;
42
- }
43
- isRefreshing = true;
44
- try {
45
- if (import.meta.server) {
46
- const { accessToken, refreshToken: refreshToken2, user: user2, organization: organization2, assurance: assurance2 } = await authClient.application.refreshAccessToken(refreshTokenCookie.value ?? "");
47
- accessTokenCookie.value = accessToken;
48
- refreshTokenCookie.value = refreshToken2;
49
- state.user.value = user2;
50
- state.activeOrganization.value = organization2;
51
- state.sessionAssurance.value = assurance2;
52
- return true;
53
- }
54
- const { user, organization, assurance } = await $fetch("/auth/refresh", {
55
- method: "POST"
56
- });
57
- state.user.value = user;
58
- state.activeOrganization.value = organization;
59
- state.sessionAssurance.value = assurance;
60
- return true;
61
- } catch {
62
- await sdkLogout();
63
- if (import.meta.client) {
64
- const currentPath = decodeURI(route.path);
65
- if (!exemptPaths.includes(currentPath)) {
66
- await navigateTo({
67
- path: loginPath,
68
- query: { returnTo: route.fullPath }
69
- });
70
- }
71
- }
72
- } finally {
73
- isRefreshing = false;
74
- }
75
- }
76
- function logout() {
39
+ function clearLocalSession() {
77
40
  accessTokenCookie.value = null;
78
41
  state.user.value = null;
79
42
  state.activeOrganization.value = null;
80
43
  state.sessionAssurance.value = null;
81
44
  }
82
- function scheduleWorkerTimeout(delayMs, callback) {
83
- if (refreshWorker) {
84
- refreshWorker.terminate();
85
- refreshWorker = null;
86
- }
87
- try {
88
- const workerScript = `self.onmessage=function(e){setTimeout(function(){postMessage('tick')},e.data)}`;
89
- const blob = new Blob([workerScript], { type: "application/javascript" });
90
- const blobUrl = URL.createObjectURL(blob);
91
- const worker = new Worker(blobUrl);
92
- URL.revokeObjectURL(blobUrl);
93
- worker.onmessage = () => {
94
- worker.terminate();
95
- refreshWorker = null;
96
- callback();
97
- };
98
- worker.onerror = () => {
99
- worker.terminate();
100
- refreshWorker = null;
101
- workerAvailable = false;
102
- window.setTimeout(callback, delayMs);
103
- };
104
- worker.postMessage(delayMs);
105
- refreshWorker = worker;
106
- } catch {
107
- workerAvailable = false;
108
- window.setTimeout(callback, delayMs);
109
- }
110
- }
111
- async function scheduleTokenRefresh() {
112
- if (!accessTokenCookie.value || isTokenExpired(accessTokenCookie.value, TWO_MINUTES)) {
113
- const result = await refreshToken();
114
- if (!result) {
45
+ if (import.meta.server) {
46
+ let handleServerRefreshFailure = function(error) {
47
+ const isAuthFailure = error instanceof RefreshTokenExpiredError || classifyRefreshStatus(extractHttpStatus(error)) === "auth";
48
+ if (isAuthFailure) {
49
+ console.error("[Tela Auth SDK] Refresh token rejected during SSR, clearing session:", error?.message);
50
+ refreshTokenCookie.value = null;
51
+ clearLocalSession();
115
52
  return;
116
53
  }
54
+ console.error("[Tela Auth SDK] Transient token refresh failure during SSR, leaving session for client recovery:", error?.message);
55
+ };
56
+ const authClient = createNuxtAuthClient(appConfig.apiUrl, () => null, () => refreshTokenCookie.value ?? null);
57
+ async function refreshOnServer() {
58
+ const { accessToken, refreshToken, user, organization, assurance } = await authClient.application.refreshAccessToken(refreshTokenCookie.value ?? "");
59
+ accessTokenCookie.value = accessToken;
60
+ refreshTokenCookie.value = refreshToken;
61
+ state.user.value = user;
62
+ state.activeOrganization.value = organization;
63
+ state.sessionAssurance.value = assurance;
117
64
  }
118
- const expiry = parseTokenExpiry(accessTokenCookie.value);
119
- if (!expiry) {
120
- return;
121
- }
122
- const nextRefresh = Math.max(expiry - TWO_MINUTES - Date.now(), 0);
123
- scheduleWorkerTimeout(nextRefresh, () => void scheduleTokenRefresh());
124
- }
125
- if (import.meta.server) {
126
65
  if (accessTokenCookie.value) {
127
66
  try {
128
67
  const data = await authClient.application.whoAmI(accessTokenCookie.value, {
@@ -135,23 +74,21 @@ export default defineNuxtPlugin({
135
74
  console.error("[Tela Auth SDK] Failed to get user and organization:", error.message);
136
75
  if (!refreshTokenCookie.value) {
137
76
  console.error("[Tela Auth SDK] Missing refresh token, logging out...");
138
- logout();
77
+ clearLocalSession();
139
78
  return;
140
79
  }
141
80
  try {
142
- await refreshToken();
143
- } catch (error2) {
144
- console.error("[Tela Auth SDK] Failed to refresh token:", error2.message);
145
- logout();
81
+ await refreshOnServer();
82
+ } catch (refreshError) {
83
+ handleServerRefreshFailure(refreshError);
146
84
  }
147
85
  }
148
86
  }
149
87
  if (!accessTokenCookie.value && refreshTokenCookie.value) {
150
88
  try {
151
- await refreshToken();
152
- } catch (error) {
153
- console.error("[Tela Auth SDK] Failed to refresh token:", error.message);
154
- logout();
89
+ await refreshOnServer();
90
+ } catch (refreshError) {
91
+ handleServerRefreshFailure(refreshError);
155
92
  }
156
93
  }
157
94
  return;
@@ -169,12 +106,34 @@ export default defineNuxtPlugin({
169
106
  console.error("[Tela Auth SDK] Failed to load user info on client startup:", error);
170
107
  }
171
108
  }
172
- void scheduleTokenRefresh();
109
+ const orchestrator = useRefreshOrchestrator(state);
110
+ const scheduler = createRefreshScheduler({
111
+ ensureFreshToken: orchestrator.ensureFreshToken,
112
+ readAccessToken: () => readClientCookie(ACCESS_TOKEN_COOKIE),
113
+ parseTokenExpiry,
114
+ policy: DEFAULT_REFRESH_POLICY,
115
+ timer: createWorkerTimer(),
116
+ // Reachable only from a definitive 401/404 — every other
117
+ // failure keeps the session and reschedules.
118
+ onAuthFailure: async () => {
119
+ await sdkLogout();
120
+ const currentPath = decodeURI(route.path);
121
+ if (!exemptPaths.includes(currentPath)) {
122
+ await navigateTo({
123
+ path: loginPath,
124
+ query: { returnTo: route.fullPath }
125
+ });
126
+ }
127
+ }
128
+ });
129
+ scheduler.start();
173
130
  document.addEventListener("visibilitychange", () => {
174
- if (document.visibilityState === "visible" && !workerAvailable) {
175
- void scheduleTokenRefresh();
131
+ if (document.visibilityState === "visible") {
132
+ scheduler.kick();
176
133
  }
177
134
  });
135
+ window.addEventListener("online", () => scheduler.kick());
136
+ window.addEventListener("focus", () => scheduler.kick());
178
137
  }
179
138
  }
180
139
  });
@@ -2,7 +2,15 @@ import { defineNuxtPlugin, useCookie, useRequestURL, useRuntimeConfig } from "#a
2
2
  import { navigateTo, useRoute } from "#imports";
3
3
  import { watch } from "vue";
4
4
  import { useTelaSession } from "../composables/session.js";
5
+ import { useOrganizationState } from "../composables/state.js";
6
+ import { readClientCookie } from "../helpers/client-cookies.js";
7
+ import { createRefreshOrchestrator } from "../helpers/refresh-orchestrator.js";
8
+ import { DEFAULT_REFRESH_POLICY } from "../helpers/refresh-policy.js";
9
+ import { createRefreshScheduler } from "../helpers/refresh-scheduler.js";
10
+ import { parseTokenExpiry } from "../helpers/token.js";
11
+ import { createWorkerTimer } from "../helpers/worker-timer.js";
5
12
  import { createNuxtAuthClient } from "../shared.js";
13
+ const TOKEN_FETCH_TIMEOUT_MS = 15e3;
6
14
  export default defineNuxtPlugin({
7
15
  name: "tela-auth-handshake",
8
16
  enforce: "pre",
@@ -11,13 +19,23 @@ export default defineNuxtPlugin({
11
19
  },
12
20
  async setup() {
13
21
  const authConfig = useRuntimeConfig().public.telaAuth;
14
- const { session, user, getToken } = useTelaSession();
22
+ const { session, user, signOut } = useTelaSession();
15
23
  const tokenCookie = useCookie(authConfig.jwtCookieName);
16
24
  tokenCookie.value = null;
17
25
  const authClient = createNuxtAuthClient(authConfig.apiUrl, () => tokenCookie.value ?? null);
18
26
  const query = useRoute().query;
19
27
  const nonce = query.nonce;
20
28
  const origin = useRequestURL();
29
+ async function initializeSession() {
30
+ if (session.value && user.value)
31
+ return;
32
+ const sessionData = await authClient.session.getSession();
33
+ if (!sessionData) {
34
+ return;
35
+ }
36
+ user.value = sessionData.user;
37
+ session.value = sessionData.session;
38
+ }
21
39
  if (import.meta.server) {
22
40
  if (nonce && typeof nonce === "string") {
23
41
  if (nonce !== "null") {
@@ -39,43 +57,97 @@ export default defineNuxtPlugin({
39
57
  });
40
58
  return;
41
59
  }
42
- } else if (import.meta.client && !session.value) {
43
- const token = await getToken();
44
- if (token) {
45
- tokenCookie.value = token;
60
+ if (tokenCookie.value) {
61
+ await initializeSession();
46
62
  }
47
- }
48
- async function initializeSession() {
49
- if (session.value && user.value)
50
- return;
51
- const sessionData = await authClient.session.getSession();
52
- if (!sessionData) {
53
- return;
54
- }
55
- user.value = sessionData.user;
56
- session.value = sessionData.session;
57
- }
58
- if (tokenCookie.value) {
59
- await initializeSession();
63
+ return;
60
64
  }
61
65
  if (import.meta.client) {
62
- let tokenRefreshInterval = null;
63
- watch(session, async (newVal) => {
64
- if (tokenRefreshInterval) {
65
- clearTimeout(tokenRefreshInterval);
66
- tokenRefreshInterval = null;
66
+ const readAccessToken = () => readClientCookie(authConfig.jwtCookieName) ?? tokenCookie.value ?? null;
67
+ async function signOutSafely() {
68
+ try {
69
+ await signOut();
70
+ } catch {
71
+ const { activeOrganization, activeMember } = useOrganizationState();
72
+ tokenCookie.value = null;
73
+ user.value = null;
74
+ session.value = null;
75
+ activeOrganization.value = null;
76
+ activeMember.value = null;
77
+ }
78
+ }
79
+ async function fetchToken() {
80
+ let timeoutId;
81
+ const timeout = new Promise((_, reject) => {
82
+ timeoutId = setTimeout(() => reject(new Error("Token fetch timed out")), TOKEN_FETCH_TIMEOUT_MS);
83
+ });
84
+ try {
85
+ const { token } = await Promise.race([authClient.session.getToken(), timeout]);
86
+ return token;
87
+ } finally {
88
+ clearTimeout(timeoutId);
89
+ }
90
+ }
91
+ const orchestrator = createRefreshOrchestrator({
92
+ performRefresh: async () => {
93
+ tokenCookie.value = await fetchToken();
94
+ },
95
+ readAccessToken,
96
+ parseTokenExpiry,
97
+ policy: DEFAULT_REFRESH_POLICY,
98
+ lockName: "tela-auth-handshake-token-refresh"
99
+ });
100
+ let startupRefreshFailed = false;
101
+ if (!session.value) {
102
+ const outcome = await orchestrator.ensureFreshToken({ force: true });
103
+ if (outcome.status === "auth-failed") {
104
+ await signOutSafely();
105
+ }
106
+ startupRefreshFailed = outcome.status === "transient-failed" || outcome.status === "skipped-offline";
107
+ }
108
+ if (tokenCookie.value) {
109
+ await initializeSession();
110
+ }
111
+ const scheduler = createRefreshScheduler({
112
+ ensureFreshToken: orchestrator.ensureFreshToken,
113
+ readAccessToken,
114
+ parseTokenExpiry,
115
+ policy: DEFAULT_REFRESH_POLICY,
116
+ timer: createWorkerTimer(),
117
+ // Reachable only from a definitive 401/404 — every other
118
+ // failure keeps the session and reschedules. Middleware
119
+ // handles the redirect to the login page after sign-out.
120
+ onAuthFailure: async () => {
121
+ await signOutSafely();
67
122
  }
123
+ });
124
+ watch(session, (newVal) => {
68
125
  if (newVal) {
69
- async function createTokenRefreshInterval() {
70
- const token = await getToken();
71
- if (token) {
72
- tokenCookie.value = token;
73
- }
74
- tokenRefreshInterval = window.setTimeout(() => void createTokenRefreshInterval(), 6e4);
75
- }
76
- await createTokenRefreshInterval();
126
+ scheduler.start();
127
+ } else {
128
+ scheduler.stop();
77
129
  }
78
130
  }, { immediate: true });
131
+ if (startupRefreshFailed) {
132
+ const stopTokenWatch = watch(tokenCookie, async (token) => {
133
+ if (!token || session.value) {
134
+ return;
135
+ }
136
+ try {
137
+ await initializeSession();
138
+ stopTokenWatch();
139
+ } catch {
140
+ }
141
+ });
142
+ scheduler.start();
143
+ }
144
+ document.addEventListener("visibilitychange", () => {
145
+ if (document.visibilityState === "visible") {
146
+ scheduler.kick();
147
+ }
148
+ });
149
+ window.addEventListener("online", () => scheduler.kick());
150
+ window.addEventListener("focus", () => scheduler.kick());
79
151
  }
80
152
  },
81
153
  hooks: {
@@ -9,11 +9,17 @@ import type { FullOrganization, JWTPayloadAssurance, User } from '@meistrari/aut
9
9
  * 4. Returns the user and organization data
10
10
  *
11
11
  * This keeps the refresh token secure by never exposing it to the client.
12
+ *
13
+ * Failures are differentiated so the client can react correctly:
14
+ * - Definitive auth failure → 401 `REFRESH_TOKEN_EXPIRED`, cookies deleted
15
+ * - Upstream rate limit → 429, cookies kept
16
+ * - Anything else (outage, network) → 503, cookies kept
12
17
  */
13
18
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
14
19
  success: boolean;
15
20
  user: User;
16
21
  organization: FullOrganization;
17
22
  assurance: JWTPayloadAssurance;
23
+ accessTokenExpiresAt: number | null;
18
24
  }>>;
19
25
  export default _default;