@meistrari/auth-nuxt 3.18.0 → 3.18.1

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/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
3
  "configKey": "telaAuth",
4
- "version": "3.18.0",
4
+ "version": "3.18.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -60,15 +60,27 @@ export interface UseTelaApplicationAuthReturn {
60
60
  * for new access and refresh tokens. The server updates the cookies and returns
61
61
  * the user and organization data.
62
62
  *
63
- * @throws {RefreshTokenExpiredError} If no refresh token is available or refresh fails
63
+ * Concurrent calls (including the SDK plugin's background refresh) share
64
+ * one in-flight request, and transient failures are retried with backoff
65
+ * before this rejects.
66
+ *
67
+ * @throws {RefreshTokenExpiredError} If the refresh token is definitively expired or revoked (401/404)
68
+ * @throws {ApplicationError} If the refresh failed transiently (network failure, outage, rate limit) —
69
+ * the session is still valid and the call can be retried. Check `error.status` for the HTTP status,
70
+ * `undefined` meaning no HTTP response was received.
64
71
  */
65
72
  refreshToken: () => Promise<void>;
66
73
  /**
67
74
  * Retrieves the current access token.
68
75
  * If the token is expired, or close to expiry, it will be refreshed automatically.
69
76
  *
77
+ * Stale-while-revalidate: when a refresh fails transiently (network
78
+ * failure, outage) but the current token is still valid, the current
79
+ * token is returned instead of throwing.
80
+ *
70
81
  * @returns The current access token
71
- * @throws {RefreshTokenExpiredError} If the token is expired and cannot be refreshed
82
+ * @throws {RefreshTokenExpiredError} If the refresh token is definitively expired or revoked (401/404)
83
+ * @throws {ApplicationError} If the refresh failed transiently and the current token is hard-expired
72
84
  */
73
85
  getToken: () => Promise<string | null | undefined>;
74
86
  }
@@ -1,7 +1,9 @@
1
1
  import { navigateTo, useCookie, useRuntimeConfig } from "#app";
2
- import { AuthorizationFlowError, isTokenExpired, RefreshTokenExpiredError, UserNotLoggedInError } from "@meistrari/auth-core";
2
+ import { ApplicationError, AuthorizationFlowError, isTokenExpired, RefreshTokenExpiredError, UserNotLoggedInError } from "@meistrari/auth-core";
3
3
  import { useApplicationSessionState } from "../state.js";
4
- import { willTokenExpireIn } from "../../helpers/token.js";
4
+ import { ACCESS_TOKEN_COOKIE, readClientCookie } from "../../helpers/client-cookies.js";
5
+ import { useRefreshOrchestrator } from "../../helpers/refresh-deps.js";
6
+ import { extractHttpStatus } from "../../helpers/refresh-policy.js";
5
7
  import { useTelaOrganization } from "./organization.js";
6
8
  const FIFTEEN_MINUTES = 60 * 15;
7
9
  const ONE_MINUTE = 60 * 1e3;
@@ -52,18 +54,24 @@ export function useTelaApplicationAuth() {
52
54
  state.sessionAssurance.value = null;
53
55
  await $fetch("/auth/logout", { method: "POST" });
54
56
  }
57
+ const orchestrator = useRefreshOrchestrator(state);
58
+ function readAccessToken() {
59
+ return readClientCookie(ACCESS_TOKEN_COOKIE) ?? accessTokenCookie.value ?? null;
60
+ }
55
61
  async function refreshToken() {
56
- try {
57
- const result = await $fetch("/auth/refresh", {
58
- method: "POST"
59
- });
60
- state.user.value = result.user;
61
- state.activeOrganization.value = result.organization;
62
- state.sessionAssurance.value = result.assurance;
63
- } catch (error) {
64
- console.error("[Auth Refresh] Failed to refresh token:", error);
65
- throw new RefreshTokenExpiredError();
62
+ const outcome = await orchestrator.ensureFreshToken({ force: true });
63
+ if (outcome.status === "ok") {
64
+ return;
65
+ }
66
+ if (outcome.status === "auth-failed") {
67
+ console.error("[Auth Refresh] Refresh token expired or revoked:", outcome.error);
68
+ throw new RefreshTokenExpiredError({ cause: outcome.error, status: extractHttpStatus(outcome.error) });
66
69
  }
70
+ console.error("[Auth Refresh] Transient token refresh failure:", "error" in outcome ? outcome.error : "offline");
71
+ throw new ApplicationError("Failed to refresh access token", {
72
+ cause: "error" in outcome ? outcome.error : void 0,
73
+ status: "error" in outcome ? extractHttpStatus(outcome.error) : void 0
74
+ });
67
75
  }
68
76
  async function initSession() {
69
77
  if (!accessTokenCookie.value) {
@@ -81,11 +89,22 @@ export function useTelaApplicationAuth() {
81
89
  await useTelaOrganization().setActiveOrganization(organizationId);
82
90
  }
83
91
  async function getToken() {
84
- const shouldRefresh = accessTokenCookie.value ? willTokenExpireIn(accessTokenCookie.value, ONE_MINUTE * 2) : true;
85
- if (shouldRefresh) {
86
- await refreshToken();
92
+ const outcome = await orchestrator.ensureFreshToken();
93
+ if (outcome.status === "ok") {
94
+ return readAccessToken();
95
+ }
96
+ if (outcome.status === "auth-failed") {
97
+ console.error("[Auth Refresh] Refresh token expired or revoked:", outcome.error);
98
+ throw new RefreshTokenExpiredError({ cause: outcome.error, status: extractHttpStatus(outcome.error) });
99
+ }
100
+ const currentToken = readAccessToken();
101
+ if (currentToken && !isTokenExpired(currentToken)) {
102
+ return currentToken;
87
103
  }
88
- return accessTokenCookie.value;
104
+ throw new ApplicationError("Failed to refresh access token", {
105
+ cause: "error" in outcome ? outcome.error : void 0,
106
+ status: "error" in outcome ? extractHttpStatus(outcome.error) : void 0
107
+ });
89
108
  }
90
109
  return {
91
110
  user: state.user,
@@ -0,0 +1,23 @@
1
+ /** Cookie holding the application access token (readable JWT) */
2
+ export declare const ACCESS_TOKEN_COOKIE = "tela-access-token";
3
+ /** Cookie holding the application refresh token (httpOnly — invisible to JS) */
4
+ export declare const REFRESH_TOKEN_COOKIE = "tela-refresh-token";
5
+ /**
6
+ * Reads a cookie value straight from `document.cookie`
7
+ *
8
+ * This is the source of truth for all token freshness math: `useCookie`
9
+ * refs captured at setup time can go stale after the server rotates
10
+ * cookies via `Set-Cookie`, which is exactly when the scheduling math
11
+ * must not be wrong
12
+ *
13
+ * @returns The decoded cookie value, or `null` when absent or on the server
14
+ */
15
+ export declare function readClientCookie(name: string): string | null;
16
+ /**
17
+ * Re-syncs the `useCookie` refs for both auth cookies after the server
18
+ * rotated them, so reactive app code observes the new values.
19
+ *
20
+ * Reactive sugar only — freshness logic never depends on it, it always
21
+ * reads `document.cookie` via {@link readClientCookie}
22
+ */
23
+ export declare function syncAuthCookieRefs(): void;
@@ -0,0 +1,28 @@
1
+ import { refreshCookie } from "#app";
2
+ export const ACCESS_TOKEN_COOKIE = "tela-access-token";
3
+ export const REFRESH_TOKEN_COOKIE = "tela-refresh-token";
4
+ export function readClientCookie(name) {
5
+ if (typeof document === "undefined") {
6
+ return null;
7
+ }
8
+ const prefix = `${name}=`;
9
+ for (const part of document.cookie.split(";")) {
10
+ const trimmed = part.trim();
11
+ if (trimmed.startsWith(prefix)) {
12
+ const value = trimmed.slice(prefix.length);
13
+ try {
14
+ return decodeURIComponent(value);
15
+ } catch {
16
+ return value;
17
+ }
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ export function syncAuthCookieRefs() {
23
+ if (import.meta.server) {
24
+ return;
25
+ }
26
+ refreshCookie(ACCESS_TOKEN_COOKIE);
27
+ refreshCookie(REFRESH_TOKEN_COOKIE);
28
+ }
@@ -0,0 +1,37 @@
1
+ import type { Ref } from 'vue';
2
+ import type { FullOrganization, JWTPayload, User } from '@meistrari/auth-core';
3
+ import type { RefreshOrchestrator, RefreshOrchestratorDeps } from './refresh-orchestrator.js';
4
+ /** Success body of the `/auth/refresh` Nitro route */
5
+ export interface RefreshResponseBody {
6
+ success: boolean;
7
+ user: User;
8
+ organization: FullOrganization;
9
+ assurance: JWTPayload['assurance'];
10
+ /** Epoch ms expiry of the freshly issued access token */
11
+ accessTokenExpiresAt: number | null;
12
+ }
13
+ interface ApplicationSessionState {
14
+ user: Ref<User | null>;
15
+ activeOrganization: Ref<FullOrganization | null>;
16
+ sessionAssurance: Ref<JWTPayload['assurance'] | null>;
17
+ }
18
+ /**
19
+ * Builds the orchestrator dependencies used by both the token-refresh
20
+ * plugin and `useTelaApplicationAuth`, so the two call sites can't drift
21
+ *
22
+ * `performRefresh` hits the Nitro `/auth/refresh` route with a bounded
23
+ * timeout (so a held Web Lock always releases), then updates session
24
+ * state and re-syncs the reactive cookie refs. Cookies themselves are
25
+ * rotated by the route via `Set-Cookie`
26
+ */
27
+ export declare function buildRefreshDeps(state: ApplicationSessionState): RefreshOrchestratorDeps;
28
+ /**
29
+ * Returns the refresh orchestrator for the current environment.
30
+ *
31
+ * On the client this is a singleton shared by the plugin and the
32
+ * composable, so their refreshes dedupe into one in-flight request.
33
+ * On the server each caller gets a throwaway instance — no state may
34
+ * leak across requests
35
+ */
36
+ export declare function useRefreshOrchestrator(state: ApplicationSessionState): RefreshOrchestrator;
37
+ export {};
@@ -0,0 +1,35 @@
1
+ import { useCookie } from "#app";
2
+ import { ACCESS_TOKEN_COOKIE, readClientCookie, syncAuthCookieRefs } from "./client-cookies.js";
3
+ import { createRefreshOrchestrator } from "./refresh-orchestrator.js";
4
+ import { DEFAULT_REFRESH_POLICY } from "./refresh-policy.js";
5
+ import { parseTokenExpiry } from "./token.js";
6
+ export function buildRefreshDeps(state) {
7
+ const accessTokenCookie = useCookie(ACCESS_TOKEN_COOKIE);
8
+ return {
9
+ performRefresh: async () => {
10
+ const result = await $fetch("/auth/refresh", {
11
+ method: "POST",
12
+ timeout: 15e3
13
+ });
14
+ state.user.value = result.user;
15
+ state.activeOrganization.value = result.organization;
16
+ state.sessionAssurance.value = result.assurance;
17
+ syncAuthCookieRefs();
18
+ },
19
+ // document.cookie is the live source of truth on the client; the
20
+ // useCookie ref covers SSR, where document is unavailable.
21
+ readAccessToken: () => readClientCookie(ACCESS_TOKEN_COOKIE) ?? accessTokenCookie.value ?? null,
22
+ parseTokenExpiry,
23
+ policy: DEFAULT_REFRESH_POLICY
24
+ };
25
+ }
26
+ let sharedOrchestrator = null;
27
+ export function useRefreshOrchestrator(state) {
28
+ if (import.meta.server) {
29
+ return createRefreshOrchestrator(buildRefreshDeps(state));
30
+ }
31
+ if (!sharedOrchestrator) {
32
+ sharedOrchestrator = createRefreshOrchestrator(buildRefreshDeps(state));
33
+ }
34
+ return sharedOrchestrator;
35
+ }
@@ -0,0 +1,64 @@
1
+ import type { RefreshPolicy } from './refresh-policy.js';
2
+ /**
3
+ * Result of an {@link RefreshOrchestrator.ensureFreshToken} call
4
+ *
5
+ * Always a concrete outcome — the orchestrator never throws and never
6
+ * resolves to `undefined`, so callers (the scheduler in particular)
7
+ * can't mistake a concurrent call for a failure
8
+ */
9
+ export type RefreshOutcome = {
10
+ status: 'ok';
11
+ refreshed: boolean;
12
+ } | {
13
+ status: 'skipped-offline';
14
+ } | {
15
+ status: 'auth-failed';
16
+ error: unknown;
17
+ } | {
18
+ status: 'transient-failed';
19
+ error: unknown;
20
+ };
21
+ /** Minimal Web Locks surface used by the orchestrator. */
22
+ export interface RefreshLockManager {
23
+ request: <T>(name: string, callback: () => Promise<T>) => Promise<T>;
24
+ }
25
+ export interface RefreshOrchestratorDeps {
26
+ /**
27
+ * Performs one actual refresh: the network call plus any state/cookie
28
+ * updates. Must reject on failure; the rejection is classified via
29
+ * `extractHttpStatus`. Must be bounded (pass a fetch timeout) so a
30
+ * held Web Lock always releases
31
+ */
32
+ performRefresh: () => Promise<void>;
33
+ /** Reads the current access token from the live cookie source of truth */
34
+ readAccessToken: () => string | null;
35
+ /** Parses a JWT expiry to epoch ms, `null` when undecodable */
36
+ parseTokenExpiry: (token: string) => number | null;
37
+ policy: RefreshPolicy;
38
+ now?: () => number;
39
+ sleep?: (ms: number) => Promise<void>;
40
+ random?: () => number;
41
+ /** Returns `false` only when the browser is known to be offline */
42
+ isOnline?: () => boolean;
43
+ /** Web Locks manager; omit/`null` where unavailable (older browsers, SSR) */
44
+ locks?: RefreshLockManager | null;
45
+ lockName?: string;
46
+ }
47
+ export interface RefreshOrchestrator {
48
+ /**
49
+ * Ensures the access token is fresh, refreshing it if needed
50
+ *
51
+ * Concurrent callers share one in-flight promise (a single network
52
+ * request). Cross-tab callers are serialized by a Web Lock, and a
53
+ * caller that waited on the lock re-reads the cookie afterwards so it
54
+ * rides a sibling tab's rotation without its own network call.
55
+ * Transient failures back off and retry up to the policy cap;
56
+ * only a definitive 401/404 yields `auth-failed`
57
+ *
58
+ * @param options.force - Skip freshness checks and always hit the network
59
+ */
60
+ ensureFreshToken: (options?: {
61
+ force?: boolean;
62
+ }) => Promise<RefreshOutcome>;
63
+ }
64
+ export declare function createRefreshOrchestrator(deps: RefreshOrchestratorDeps): RefreshOrchestrator;
@@ -0,0 +1,85 @@
1
+ import { classifyRefreshStatus, computeBackoffDelay, extractHttpStatus } from "./refresh-policy.js";
2
+ const DEFAULT_LOCK_NAME = "tela-auth-token-refresh";
3
+ export function createRefreshOrchestrator(deps) {
4
+ const {
5
+ performRefresh,
6
+ readAccessToken,
7
+ parseTokenExpiry,
8
+ policy,
9
+ now = () => Date.now(),
10
+ sleep = async (ms) => await new Promise((resolve) => setTimeout(resolve, ms)),
11
+ random = Math.random,
12
+ isOnline = () => typeof navigator === "undefined" || navigator.onLine !== false,
13
+ locks = typeof navigator !== "undefined" && "locks" in navigator ? navigator.locks : null,
14
+ lockName = DEFAULT_LOCK_NAME
15
+ } = deps;
16
+ let inFlight = null;
17
+ function isTokenFresh() {
18
+ const token = readAccessToken();
19
+ if (!token) {
20
+ return false;
21
+ }
22
+ const expiry = parseTokenExpiry(token);
23
+ if (expiry === null) {
24
+ return false;
25
+ }
26
+ return expiry - now() > policy.refreshThresholdMs;
27
+ }
28
+ async function attemptWithinLock(force) {
29
+ if (!force && isTokenFresh()) {
30
+ return { kind: "ok", refreshed: false };
31
+ }
32
+ try {
33
+ await performRefresh();
34
+ return { kind: "ok", refreshed: true };
35
+ } catch (error) {
36
+ return { kind: "error", error };
37
+ }
38
+ }
39
+ async function attempt(force) {
40
+ if (locks) {
41
+ try {
42
+ return await locks.request(lockName, async () => await attemptWithinLock(force));
43
+ } catch {
44
+ }
45
+ }
46
+ return await attemptWithinLock(force);
47
+ }
48
+ async function run(force) {
49
+ try {
50
+ for (let retry = 0; ; retry++) {
51
+ if (!isOnline()) {
52
+ return { status: "skipped-offline" };
53
+ }
54
+ const result = await attempt(force);
55
+ if (result.kind === "ok") {
56
+ return { status: "ok", refreshed: result.refreshed };
57
+ }
58
+ const kind = classifyRefreshStatus(extractHttpStatus(result.error));
59
+ if (kind === "auth") {
60
+ return { status: "auth-failed", error: result.error };
61
+ }
62
+ if (retry >= policy.maxImmediateRetries) {
63
+ return { status: "transient-failed", error: result.error };
64
+ }
65
+ await sleep(computeBackoffDelay(retry, policy, random));
66
+ }
67
+ } catch (error) {
68
+ return { status: "transient-failed", error };
69
+ }
70
+ }
71
+ async function ensureFreshToken(options = {}) {
72
+ if (inFlight) {
73
+ return await inFlight;
74
+ }
75
+ const force = options.force ?? false;
76
+ if (!force && isTokenFresh()) {
77
+ return { status: "ok", refreshed: false };
78
+ }
79
+ inFlight = run(force).finally(() => {
80
+ inFlight = null;
81
+ });
82
+ return await inFlight;
83
+ }
84
+ return { ensureFreshToken };
85
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Pure refresh policy: failure classification, status extraction,
3
+ * backoff math, and schedule math
4
+ */
5
+ /** How a refresh failure should be handled */
6
+ export type RefreshFailureKind = 'auth' | 'transient';
7
+ /** Timing knobs shared by the orchestrator and the scheduler */
8
+ export interface RefreshPolicy {
9
+ /** Refresh this long before the access token expires (ms) */
10
+ refreshThresholdMs: number;
11
+ /** How many immediate in-place retries a single refresh attempt gets */
12
+ maxImmediateRetries: number;
13
+ /** Base delay for exponential backoff between retries (ms) */
14
+ retryBaseDelayMs: number;
15
+ /** Upper bound for any backoff delay (ms) */
16
+ retryMaxDelayMs: number;
17
+ /** Hard floor between two scheduled refreshes (ms) */
18
+ minRefreshIntervalMs: number;
19
+ }
20
+ /** Default timing policy */
21
+ export declare const DEFAULT_REFRESH_POLICY: RefreshPolicy;
22
+ /**
23
+ * Classifies a refresh failure by HTTP status
24
+ *
25
+ * Only a definitive 401/404 means the session is gone. Everything else —
26
+ * 429, 5xx, other 4xx, or no status at all (network failure) — is
27
+ * transient and must never log the user out
28
+ */
29
+ export declare function classifyRefreshStatus(status: number | undefined): RefreshFailureKind;
30
+ /**
31
+ * Extracts an HTTP status code from an unknown error shape
32
+ *
33
+ * Checks, in order: `status`, `statusCode`, `response.status`, then the
34
+ * same fields on `cause` (recursively, so wrapped SDK errors still yield
35
+ * the upstream status). Returns `undefined` for pure transport failures
36
+ */
37
+ export declare function extractHttpStatus(error: unknown, depth?: number): number | undefined;
38
+ /**
39
+ * Computes a full-jitter exponential backoff delay
40
+ *
41
+ * @param attempt - Zero-based retry attempt number
42
+ * @param policy - Timing policy providing base and max delays
43
+ * @param random - Random source in [0, 1); injectable for deterministic tests
44
+ */
45
+ export declare function computeBackoffDelay(attempt: number, policy: Pick<RefreshPolicy, 'retryBaseDelayMs' | 'retryMaxDelayMs'>, random?: () => number): number;
46
+ /**
47
+ * Computes the delay until the next scheduled refresh from the access
48
+ * token's expiry, clamped to a hard minimum interval
49
+ *
50
+ * A `null` expiry (missing or undecodable token) and an already-passed
51
+ * expiry both return `minRefreshIntervalMs` — never 0 — so a stale
52
+ * cookie read can never spin the scheduler into a hot loop
53
+ */
54
+ export declare function computeNextRefreshDelay(expiryMs: number | null, nowMs: number, policy: Pick<RefreshPolicy, 'refreshThresholdMs' | 'minRefreshIntervalMs'>): number;
@@ -0,0 +1,32 @@
1
+ export const DEFAULT_REFRESH_POLICY = {
2
+ refreshThresholdMs: 2 * 60 * 1e3,
3
+ maxImmediateRetries: 3,
4
+ retryBaseDelayMs: 1e3,
5
+ retryMaxDelayMs: 3e4,
6
+ minRefreshIntervalMs: 5e3
7
+ };
8
+ export function classifyRefreshStatus(status) {
9
+ return status === 401 || status === 404 ? "auth" : "transient";
10
+ }
11
+ export function extractHttpStatus(error, depth = 0) {
12
+ if (!error || typeof error !== "object" || depth > 4) {
13
+ return void 0;
14
+ }
15
+ const candidate = error;
16
+ for (const value of [candidate.status, candidate.statusCode, candidate.response?.status]) {
17
+ if (typeof value === "number" && Number.isFinite(value)) {
18
+ return value;
19
+ }
20
+ }
21
+ return extractHttpStatus(candidate.cause, depth + 1);
22
+ }
23
+ export function computeBackoffDelay(attempt, policy, random = Math.random) {
24
+ const exponential = Math.min(policy.retryMaxDelayMs, policy.retryBaseDelayMs * 2 ** Math.max(attempt, 0));
25
+ return Math.floor(random() * exponential);
26
+ }
27
+ export function computeNextRefreshDelay(expiryMs, nowMs, policy) {
28
+ if (expiryMs === null) {
29
+ return policy.minRefreshIntervalMs;
30
+ }
31
+ return Math.max(expiryMs - policy.refreshThresholdMs - nowMs, policy.minRefreshIntervalMs);
32
+ }
@@ -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
  });
@@ -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;
@@ -1,6 +1,8 @@
1
1
  import { createError, useRuntimeConfig } from "#imports";
2
2
  import { defineEventHandler, deleteCookie, getCookie, setCookie } from "h3";
3
+ import { parseTokenExpiry } from "../../../helpers/token.js";
3
4
  import { createNuxtAuthClient } from "../../../shared.js";
5
+ import { planRefreshFailure, shouldWriteTokens } from "../../utils/refresh-upstream.js";
4
6
  export default defineEventHandler(async (event) => {
5
7
  const config = useRuntimeConfig();
6
8
  const authConfig = config.public.telaAuth;
@@ -12,44 +14,62 @@ export default defineEventHandler(async (event) => {
12
14
  message: "No refresh token found"
13
15
  });
14
16
  }
17
+ const authClient = createNuxtAuthClient(
18
+ authConfig.apiUrl,
19
+ () => null,
20
+ () => refreshToken
21
+ );
22
+ let accessToken;
23
+ let newRefreshToken;
24
+ let user;
25
+ let organization;
26
+ let assurance;
15
27
  try {
16
- const authClient = createNuxtAuthClient(
17
- authConfig.apiUrl,
18
- () => null,
19
- () => refreshToken
20
- );
21
- const { accessToken, refreshToken: newRefreshToken, user, organization, assurance } = await authClient.application.refreshAccessToken(refreshToken);
22
- setCookie(event, "tela-access-token", accessToken, {
23
- secure: !import.meta.dev,
24
- sameSite: "lax",
25
- maxAge: 60 * 15,
26
- // 15 minutes
27
- priority: "high",
28
- path: "/"
29
- });
30
- setCookie(event, "tela-refresh-token", newRefreshToken, {
31
- secure: !import.meta.dev,
32
- sameSite: "lax",
33
- httpOnly: true,
34
- maxAge: 60 * 60 * 24 * 7,
35
- // 7 days
36
- priority: "high",
37
- path: "/"
38
- });
39
- return {
40
- success: true,
41
- user,
42
- organization,
43
- assurance
44
- };
28
+ ({ accessToken, refreshToken: newRefreshToken, user, organization, assurance } = await authClient.application.refreshAccessToken(refreshToken));
45
29
  } catch (error) {
46
- console.error("[Auth Refresh] Token refresh error:", error);
47
- deleteCookie(event, "tela-access-token", { path: "/" });
48
- deleteCookie(event, "tela-refresh-token", { path: "/" });
30
+ const plan = planRefreshFailure(error);
31
+ console.error(`[Auth Refresh] Token refresh error (responding ${plan.statusCode}):`, error);
32
+ if (plan.deleteCookies) {
33
+ deleteCookie(event, "tela-access-token", { path: "/" });
34
+ deleteCookie(event, "tela-refresh-token", { path: "/" });
35
+ }
49
36
  throw createError({
50
- statusCode: 401,
51
- statusMessage: "Unauthorized",
52
- message: "Failed to refresh access token"
37
+ statusCode: plan.statusCode,
38
+ statusMessage: plan.statusMessage,
39
+ message: plan.message
40
+ });
41
+ }
42
+ const currentAccessToken = getCookie(event, "tela-access-token");
43
+ if (!shouldWriteTokens({ accessToken, refreshToken: newRefreshToken, currentAccessToken })) {
44
+ console.error("[Auth Refresh] Upstream returned unusable tokens, refusing to overwrite cookies");
45
+ throw createError({
46
+ statusCode: 503,
47
+ statusMessage: "Service Unavailable",
48
+ message: "REFRESH_UPSTREAM_UNAVAILABLE"
53
49
  });
54
50
  }
51
+ setCookie(event, "tela-access-token", accessToken, {
52
+ secure: !import.meta.dev,
53
+ sameSite: "lax",
54
+ maxAge: 60 * 15,
55
+ // 15 minutes
56
+ priority: "high",
57
+ path: "/"
58
+ });
59
+ setCookie(event, "tela-refresh-token", newRefreshToken, {
60
+ secure: !import.meta.dev,
61
+ sameSite: "lax",
62
+ httpOnly: true,
63
+ maxAge: 60 * 60 * 24 * 7,
64
+ // 7 days
65
+ priority: "high",
66
+ path: "/"
67
+ });
68
+ return {
69
+ success: true,
70
+ user,
71
+ organization,
72
+ assurance,
73
+ accessTokenExpiresAt: parseTokenExpiry(accessToken)
74
+ };
55
75
  });
@@ -0,0 +1,28 @@
1
+ /** How the `/auth/refresh` route should respond to an upstream failure */
2
+ export interface RefreshFailurePlan {
3
+ statusCode: number;
4
+ statusMessage: string;
5
+ message: string;
6
+ /** Only a definitive auth failure may destroy the session cookies */
7
+ deleteCookies: boolean;
8
+ }
9
+ /**
10
+ * Maps an upstream refresh failure to the HTTP response the route should
11
+ * send, so the browser can distinguish "session revoked" (401, cookies
12
+ * deleted) from "API briefly down" (429/503, cookies kept).
13
+ */
14
+ export declare function planRefreshFailure(error: unknown): RefreshFailurePlan;
15
+ /**
16
+ * Guards cookie writes after a successful upstream refresh
17
+ *
18
+ * Rejects empty, undecodable, and already-expired tokens, plus access
19
+ * tokens strictly older than the one currently in the cookie — a delayed
20
+ * response must not overwrite a newer rotation. Grace-window reissues
21
+ * return the same token (equal `exp`), which stays writable.
22
+ */
23
+ export declare function shouldWriteTokens(input: {
24
+ accessToken: string | null | undefined;
25
+ refreshToken: string | null | undefined;
26
+ currentAccessToken?: string | null;
27
+ nowMs?: number;
28
+ }): boolean;
@@ -0,0 +1,46 @@
1
+ import { RefreshTokenExpiredError } from "@meistrari/auth-core";
2
+ import { classifyRefreshStatus, extractHttpStatus } from "../../helpers/refresh-policy.js";
3
+ import { parseTokenExpiry } from "../../helpers/token.js";
4
+ export function planRefreshFailure(error) {
5
+ const status = extractHttpStatus(error);
6
+ const isAuthFailure = error instanceof RefreshTokenExpiredError || error?.code === "REFRESH_TOKEN_EXPIRED" || status !== void 0 && classifyRefreshStatus(status) === "auth";
7
+ if (isAuthFailure) {
8
+ return {
9
+ statusCode: 401,
10
+ statusMessage: "Unauthorized",
11
+ message: "REFRESH_TOKEN_EXPIRED",
12
+ deleteCookies: true
13
+ };
14
+ }
15
+ if (status === 429) {
16
+ return {
17
+ statusCode: 429,
18
+ statusMessage: "Too Many Requests",
19
+ message: "REFRESH_RATE_LIMITED",
20
+ deleteCookies: false
21
+ };
22
+ }
23
+ return {
24
+ statusCode: 503,
25
+ statusMessage: "Service Unavailable",
26
+ message: "REFRESH_UPSTREAM_UNAVAILABLE",
27
+ deleteCookies: false
28
+ };
29
+ }
30
+ export function shouldWriteTokens(input) {
31
+ const { accessToken, refreshToken, currentAccessToken, nowMs = Date.now() } = input;
32
+ if (!accessToken || !refreshToken) {
33
+ return false;
34
+ }
35
+ const expiry = parseTokenExpiry(accessToken);
36
+ if (expiry === null || expiry <= nowMs) {
37
+ return false;
38
+ }
39
+ if (currentAccessToken) {
40
+ const currentExpiry = parseTokenExpiry(currentAccessToken);
41
+ if (currentExpiry !== null && expiry < currentExpiry) {
42
+ return false;
43
+ }
44
+ }
45
+ return true;
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/auth-nuxt",
3
- "version": "3.18.0",
3
+ "version": "3.18.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -36,11 +36,11 @@
36
36
  "docs": "nuxt-module-build prepare && typedoc"
37
37
  },
38
38
  "dependencies": {
39
- "@meistrari/auth-core": "1.28.0",
39
+ "@meistrari/auth-core": "1.28.1",
40
40
  "jose": "6.1.3"
41
41
  },
42
42
  "peerDependencies": {
43
- "nuxt": "^3.0.0 || ^4.0.0",
43
+ "nuxt": "^3.10.0 || ^4.0.0",
44
44
  "vue": "^3.0.0"
45
45
  },
46
46
  "devDependencies": {