@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.
- package/dist/module.json +1 -1
- package/dist/runtime/composables/admin.d.ts +68 -3
- package/dist/runtime/composables/admin.js +45 -1
- package/dist/runtime/composables/application/auth.d.ts +14 -2
- package/dist/runtime/composables/application/auth.js +35 -16
- package/dist/runtime/helpers/client-cookies.d.ts +23 -0
- package/dist/runtime/helpers/client-cookies.js +28 -0
- package/dist/runtime/helpers/refresh-deps.d.ts +37 -0
- package/dist/runtime/helpers/refresh-deps.js +35 -0
- package/dist/runtime/helpers/refresh-orchestrator.d.ts +64 -0
- package/dist/runtime/helpers/refresh-orchestrator.js +85 -0
- package/dist/runtime/helpers/refresh-policy.d.ts +54 -0
- package/dist/runtime/helpers/refresh-policy.js +32 -0
- package/dist/runtime/helpers/refresh-scheduler.d.ts +39 -0
- package/dist/runtime/helpers/refresh-scheduler.js +90 -0
- package/dist/runtime/helpers/worker-timer.d.ts +27 -0
- package/dist/runtime/helpers/worker-timer.js +68 -0
- package/dist/runtime/plugins/application-token-refresh.js +58 -99
- package/dist/runtime/plugins/handshake.js +103 -31
- package/dist/runtime/server/routes/auth/refresh.d.ts +6 -0
- package/dist/runtime/server/routes/auth/refresh.js +55 -35
- package/dist/runtime/server/utils/refresh-upstream.d.ts +28 -0
- package/dist/runtime/server/utils/refresh-upstream.js +46 -0
- package/package.json +3 -3
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
77
|
+
clearLocalSession();
|
|
139
78
|
return;
|
|
140
79
|
}
|
|
141
80
|
try {
|
|
142
|
-
await
|
|
143
|
-
} catch (
|
|
144
|
-
|
|
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
|
|
152
|
-
} catch (
|
|
153
|
-
|
|
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
|
-
|
|
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"
|
|
175
|
-
|
|
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,
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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;
|