@lenne.tech/nuxt-extensions 1.7.1 → 1.8.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.mjs
CHANGED
|
@@ -73,6 +73,9 @@ const module$1 = defineNuxtModule({
|
|
|
73
73
|
if (!rc.public.apiUrl) {
|
|
74
74
|
rc.public.apiUrl = "";
|
|
75
75
|
}
|
|
76
|
+
if (!rc.public.cookiePrefix) {
|
|
77
|
+
rc.public.cookiePrefix = "";
|
|
78
|
+
}
|
|
76
79
|
nuxt.options.runtimeConfig.public.ltExtensions = {
|
|
77
80
|
ai: {
|
|
78
81
|
basePath: resolvedOptions.ai?.basePath || "/ai",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useNuxtApp, useCookie, useState, ref, computed, watch } from "#imports";
|
|
1
|
+
import { useNuxtApp, useCookie, useState, useRequestHeaders, ref, computed, watch } from "#imports";
|
|
2
2
|
import { ltArrayBufferToBase64Url, ltBase64UrlToUint8Array } from "../../utils/crypto.js";
|
|
3
|
-
import { clearLtAuthCookies, getLtApiBase, getLtAuthCookieNames } from "../../lib/auth-state.js";
|
|
3
|
+
import { clearLtAuthCookies, getLtApiBase, getLtAuthCookieNames, resolveLtAuthState } from "../../lib/auth-state.js";
|
|
4
4
|
import { useLtAuthClient } from "../use-lt-auth-client.js";
|
|
5
5
|
function useTranslation() {
|
|
6
6
|
const nuxtApp = useNuxtApp();
|
|
@@ -23,32 +23,28 @@ export function useLtAuth() {
|
|
|
23
23
|
});
|
|
24
24
|
if (import.meta.client) {
|
|
25
25
|
try {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join("=")) : "";
|
|
30
|
-
if (value) {
|
|
31
|
-
const parsed = JSON.parse(value);
|
|
32
|
-
if (parsed?.user && !authState.value?.user) {
|
|
33
|
-
authState.value = parsed;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
26
|
+
const resolved = resolveLtAuthState(document.cookie, stateCookieName);
|
|
27
|
+
if (resolved?.user && !authState.value?.user) {
|
|
28
|
+
authState.value = resolved;
|
|
36
29
|
}
|
|
37
30
|
} catch {
|
|
38
31
|
}
|
|
39
32
|
}
|
|
40
|
-
|
|
41
|
-
authState.value = { user: null, authMode: "cookie" };
|
|
42
|
-
}
|
|
33
|
+
const ssrCookieHeader = import.meta.server ? useRequestHeaders(["cookie"]).cookie || "" : "";
|
|
43
34
|
const jwtToken = useCookie(tokenCookieName, {
|
|
44
35
|
maxAge: 60 * 60 * 24 * 7,
|
|
45
36
|
// 7 days
|
|
46
37
|
sameSite: "lax"
|
|
47
38
|
});
|
|
48
39
|
const isLoading = ref(false);
|
|
49
|
-
const
|
|
40
|
+
const resolvedAuthState = computed(() => {
|
|
41
|
+
const refVal = authState.value;
|
|
42
|
+
const cookieString = import.meta.client ? document.cookie : ssrCookieHeader;
|
|
43
|
+
return resolveLtAuthState(cookieString, stateCookieName) ?? refVal ?? null;
|
|
44
|
+
});
|
|
45
|
+
const authMode = computed(() => resolvedAuthState.value?.authMode || "cookie");
|
|
50
46
|
const isJwtMode = computed(() => authMode.value === "jwt");
|
|
51
|
-
const user = computed(() =>
|
|
47
|
+
const user = computed(() => resolvedAuthState.value?.user ?? null);
|
|
52
48
|
const isAuthenticated = computed(() => !!user.value);
|
|
53
49
|
const isAdmin = computed(() => user.value?.role === "admin");
|
|
54
50
|
const is2FAEnabled = computed(() => user.value?.twoFactorEnabled ?? false);
|
|
@@ -56,7 +52,9 @@ export function useLtAuth() {
|
|
|
56
52
|
const featuresFetched = useState("lt-auth-features-fetched", () => false);
|
|
57
53
|
function setUser(userData, mode = "cookie") {
|
|
58
54
|
const newState = { user: userData, authMode: mode };
|
|
59
|
-
|
|
55
|
+
if (import.meta.client || userData) {
|
|
56
|
+
authState.value = newState;
|
|
57
|
+
}
|
|
60
58
|
if (import.meta.client) {
|
|
61
59
|
const maxAge = 60 * 60 * 24 * 7;
|
|
62
60
|
const secure = globalThis.location?.protocol === "https:" ? "; secure" : "";
|
|
@@ -64,10 +62,9 @@ export function useLtAuth() {
|
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
64
|
function clearUser() {
|
|
67
|
-
const clearedState = { user: null, authMode: "cookie" };
|
|
68
|
-
authState.value = clearedState;
|
|
69
|
-
jwtToken.value = null;
|
|
70
65
|
if (import.meta.client) {
|
|
66
|
+
authState.value = { user: null, authMode: "cookie" };
|
|
67
|
+
jwtToken.value = null;
|
|
71
68
|
clearLtAuthCookies();
|
|
72
69
|
}
|
|
73
70
|
}
|
|
@@ -11,21 +11,85 @@
|
|
|
11
11
|
*
|
|
12
12
|
* The state is persisted in cookies for SSR compatibility.
|
|
13
13
|
*/
|
|
14
|
-
import type { LtAuthMode } from '../types/index.js';
|
|
14
|
+
import type { LtAuthMode, LtAuthState } from '../types/index.js';
|
|
15
15
|
/** Default name of the auth-state cookie. Used when the module config has not been resolved yet. */
|
|
16
16
|
export declare const LT_AUTH_STATE_COOKIE_DEFAULT = "lt-auth-state";
|
|
17
17
|
/** Default name of the JWT-token cookie. Used when the module config has not been resolved yet. */
|
|
18
18
|
export declare const LT_JWT_TOKEN_COOKIE_DEFAULT = "lt-jwt-token";
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the cookie prefix that drives the auth cookie names.
|
|
21
|
+
*
|
|
22
|
+
* A single, dedicated, OPT-IN knob with a safe default:
|
|
23
|
+
* - **`cookiePrefix`** (`NUXT_PUBLIC_COOKIE_PREFIX` →
|
|
24
|
+
* `runtimeConfig.public.cookiePrefix`) → `<prefix>-auth-state` /
|
|
25
|
+
* `<prefix>-jwt-token`. Lets a project run with its own cookie namespace —
|
|
26
|
+
* e.g. several lenne.tech apps on a shared host during development, where
|
|
27
|
+
* cookies collide by host (not port) and would otherwise read each other's
|
|
28
|
+
* `lt-auth-state` (a "ghost" user from the other project).
|
|
29
|
+
* - otherwise `''` → the default `lt-auth-state` / `lt-jwt-token`.
|
|
30
|
+
*
|
|
31
|
+
* `storagePrefix` deliberately does NOT influence the cookie name. It is a
|
|
32
|
+
* localStorage-namespacing convention; coupling it to cookies would silently
|
|
33
|
+
* rename auth cookies on a mere upgrade (logging every user out, bouncing valid
|
|
34
|
+
* sessions in custom middleware that reads `lt-auth-state` directly) and force
|
|
35
|
+
* frontend/backend to be deployed in lockstep. Cookie naming is therefore
|
|
36
|
+
* controlled ONLY by this explicit, opt-in knob → fully backward compatible.
|
|
37
|
+
*
|
|
38
|
+
* IMPORTANT: when you set `cookiePrefix`, mirror it on the backend
|
|
39
|
+
* (`COOKIE_PREFIX` env — see nest-server `resolveBetterAuthCookiePrefix`) so
|
|
40
|
+
* both sides always agree on the cookie name.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveLtCookiePrefix(pub: null | Record<string, any> | undefined): string;
|
|
19
43
|
/**
|
|
20
44
|
* Resolve the configured auth cookie names from runtime config.
|
|
21
45
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
46
|
+
* **Per-project isolation (collision avoidance).** Cookies are scoped by
|
|
47
|
+
* host+path, NOT by port. Two lenne.tech apps sharing a host during development
|
|
48
|
+
* (e.g. both on `localhost`) would otherwise read each other's `lt-auth-state`
|
|
49
|
+
* cookie — surfacing a "ghost" user from the other project. The cookie name is
|
|
50
|
+
* therefore derived from a per-project prefix (see {@link resolveLtCookiePrefix}).
|
|
51
|
+
*
|
|
52
|
+
* Resolution order:
|
|
53
|
+
* 1. an explicitly configured `cookieNames.state/token` (exact name) always wins;
|
|
54
|
+
* 2. otherwise `<prefix>-auth-state` / `<prefix>-jwt-token` where `prefix`
|
|
55
|
+
* comes from {@link resolveLtCookiePrefix} (the opt-in `cookiePrefix`);
|
|
56
|
+
* 3. otherwise fall back to the legacy `lt-auth-state` / `lt-jwt-token`
|
|
57
|
+
* (backward compatible — no behaviour change for apps without a prefix).
|
|
58
|
+
*
|
|
59
|
+
* Falls back to the defaults so callers keep working even outside a Nuxt
|
|
60
|
+
* context (tests, edge SSR boots).
|
|
24
61
|
*/
|
|
25
62
|
export declare function getLtAuthCookieNames(): {
|
|
26
63
|
state: string;
|
|
27
64
|
token: string;
|
|
28
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the authoritative auth state from a raw `Cookie` header /
|
|
68
|
+
* `document.cookie` string, tolerating **multiple** auth-state cookies with the
|
|
69
|
+
* same name.
|
|
70
|
+
*
|
|
71
|
+
* A deployed setup can end up with TWO auth-state cookies in parallel — a
|
|
72
|
+
* host-only one (written by `useCookie` / `setUser`) and a domain-scoped one
|
|
73
|
+
* (e.g. set by a backend SAML callback with `Domain=<appHost>` so it is
|
|
74
|
+
* readable across `app` and `api.app`). They can disagree: one carries the
|
|
75
|
+
* signed-in user, the other a stale `{ user: null }` left behind by a partial
|
|
76
|
+
* clear. A naive single-value read (`useCookie`, or `document.cookie.find`)
|
|
77
|
+
* may pick the stale twin and wrongly report the user as logged out — which
|
|
78
|
+
* makes SSR auth guards bounce a perfectly valid session to the login page.
|
|
79
|
+
*
|
|
80
|
+
* This scans ALL matching entries and prefers the one that actually carries a
|
|
81
|
+
* `user`; only when none do does it return the (user-less) fallback state.
|
|
82
|
+
*
|
|
83
|
+
* SECURITY NOTE: preferring the user-bearing twin is intentional and required —
|
|
84
|
+
* preferring the `{ user: null }` twin is exactly what caused the random logout.
|
|
85
|
+
* `lt-auth-state` is a NON-authoritative client convenience cache for the app
|
|
86
|
+
* shell, NOT the session identifier (that is the httpOnly session cookie, e.g.
|
|
87
|
+
* `iam.session_token`). A forged/injected user-bearing cookie therefore yields
|
|
88
|
+
* at most UI spoofing ("looks logged in") — every real API/server call still
|
|
89
|
+
* authenticates against the httpOnly session and 401s. This trust boundary is
|
|
90
|
+
* unchanged by the duplicate-tolerant read; it only decides which twin wins.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveLtAuthState(cookieString: string, stateCookieName?: string): LtAuthState | null;
|
|
29
93
|
/**
|
|
30
94
|
* Determines if HTTP requests should use the Nuxt Vite dev proxy.
|
|
31
95
|
*
|
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
import { useRuntimeConfig } from "#imports";
|
|
2
2
|
export const LT_AUTH_STATE_COOKIE_DEFAULT = "lt-auth-state";
|
|
3
3
|
export const LT_JWT_TOKEN_COOKIE_DEFAULT = "lt-jwt-token";
|
|
4
|
+
function sanitizeCookiePrefix(raw) {
|
|
5
|
+
if (typeof raw !== "string") {
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
return raw.trim().replace(/[^A-Za-z0-9._-]/g, "");
|
|
9
|
+
}
|
|
10
|
+
export function resolveLtCookiePrefix(pub) {
|
|
11
|
+
return sanitizeCookiePrefix(pub?.cookiePrefix);
|
|
12
|
+
}
|
|
4
13
|
export function getLtAuthCookieNames() {
|
|
5
14
|
try {
|
|
6
15
|
const runtimeConfig = useRuntimeConfig();
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
16
|
+
const pub = runtimeConfig.public;
|
|
17
|
+
const configured = pub?.ltExtensions?.auth?.cookieNames;
|
|
18
|
+
const prefix = resolveLtCookiePrefix(pub);
|
|
19
|
+
const state = configured?.state && configured.state !== LT_AUTH_STATE_COOKIE_DEFAULT ? configured.state : prefix ? `${prefix}-auth-state` : LT_AUTH_STATE_COOKIE_DEFAULT;
|
|
20
|
+
const token = configured?.token && configured.token !== LT_JWT_TOKEN_COOKIE_DEFAULT ? configured.token : prefix ? `${prefix}-jwt-token` : LT_JWT_TOKEN_COOKIE_DEFAULT;
|
|
21
|
+
return { state, token };
|
|
12
22
|
} catch {
|
|
13
23
|
return { state: LT_AUTH_STATE_COOKIE_DEFAULT, token: LT_JWT_TOKEN_COOKIE_DEFAULT };
|
|
14
24
|
}
|
|
15
25
|
}
|
|
26
|
+
export function resolveLtAuthState(cookieString, stateCookieName) {
|
|
27
|
+
const name = stateCookieName || getLtAuthCookieNames().state;
|
|
28
|
+
const prefix = `${name}=`;
|
|
29
|
+
let fallback = null;
|
|
30
|
+
for (const entry of (cookieString || "").split("; ")) {
|
|
31
|
+
if (!entry.startsWith(prefix)) continue;
|
|
32
|
+
try {
|
|
33
|
+
const state = JSON.parse(decodeURIComponent(entry.slice(prefix.length)));
|
|
34
|
+
if (state?.user) {
|
|
35
|
+
return state;
|
|
36
|
+
}
|
|
37
|
+
if (state) {
|
|
38
|
+
fallback = state;
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
16
45
|
let _proxyFallbackWarned = false;
|
|
17
46
|
export function isLocalDevApiProxy() {
|
|
18
47
|
if (import.meta.server) {
|
|
@@ -65,31 +94,20 @@ export function buildLtApiUrl(path) {
|
|
|
65
94
|
}
|
|
66
95
|
export function getLtAuthMode() {
|
|
67
96
|
if (import.meta.server) return "cookie";
|
|
68
|
-
|
|
69
|
-
const { state: stateCookieName } = getLtAuthCookieNames();
|
|
70
|
-
const cookie = document.cookie.split("; ").find((row) => row.startsWith(`${stateCookieName}=`));
|
|
71
|
-
if (cookie) {
|
|
72
|
-
const parts = cookie.split("=");
|
|
73
|
-
const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join("=")) : "";
|
|
74
|
-
const state = JSON.parse(value);
|
|
75
|
-
return state?.authMode || "cookie";
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
}
|
|
79
|
-
return "cookie";
|
|
97
|
+
return resolveLtAuthState(document.cookie)?.authMode || "cookie";
|
|
80
98
|
}
|
|
81
99
|
export function getLtJwtToken() {
|
|
82
100
|
if (import.meta.server) return null;
|
|
83
101
|
try {
|
|
84
102
|
const { token: tokenCookieName } = getLtAuthCookieNames();
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
const cookiePrefix = `${tokenCookieName}=`;
|
|
104
|
+
for (const row of document.cookie.split("; ")) {
|
|
105
|
+
if (!row.startsWith(cookiePrefix)) continue;
|
|
106
|
+
const raw = decodeURIComponent(row.slice(cookiePrefix.length));
|
|
107
|
+
const value = raw.startsWith('"') && raw.endsWith('"') ? JSON.parse(raw) : raw;
|
|
108
|
+
if (value) {
|
|
109
|
+
return value;
|
|
91
110
|
}
|
|
92
|
-
return value || null;
|
|
93
111
|
}
|
|
94
112
|
} catch {
|
|
95
113
|
}
|
|
@@ -110,27 +128,32 @@ export function clearLtAuthCookies() {
|
|
|
110
128
|
if (import.meta.server) return;
|
|
111
129
|
const { state: stateCookieName, token: tokenCookieName } = getLtAuthCookieNames();
|
|
112
130
|
const secure = globalThis.location?.protocol === "https:" ? "; secure" : "";
|
|
131
|
+
const host = globalThis.location?.hostname;
|
|
132
|
+
const domainAttr = host ? `; domain=${host}` : "";
|
|
113
133
|
document.cookie = `${stateCookieName}=; path=/; max-age=0; samesite=lax${secure}`;
|
|
134
|
+
if (domainAttr) {
|
|
135
|
+
document.cookie = `${stateCookieName}=; path=/; max-age=0; samesite=lax${secure}${domainAttr}`;
|
|
136
|
+
}
|
|
114
137
|
document.cookie = `${tokenCookieName}=; path=/; max-age=0; samesite=lax${secure}`;
|
|
138
|
+
if (domainAttr) {
|
|
139
|
+
document.cookie = `${tokenCookieName}=; path=/; max-age=0; samesite=lax${secure}${domainAttr}`;
|
|
140
|
+
}
|
|
115
141
|
const sessionCookieNames = ["better-auth.session_token", "better-auth.session", "__Secure-better-auth.session_token", "session_token", "session"];
|
|
116
142
|
for (const name of sessionCookieNames) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
for (const path of ["/", "/api", "/api/iam", "/iam"]) {
|
|
144
|
+
document.cookie = `${name}=; path=${path}; max-age=0`;
|
|
145
|
+
if (domainAttr) {
|
|
146
|
+
document.cookie = `${name}=; path=${path}; max-age=0${domainAttr}`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
121
149
|
}
|
|
122
150
|
}
|
|
123
151
|
export function setLtAuthMode(mode) {
|
|
124
152
|
if (import.meta.server) return;
|
|
125
153
|
try {
|
|
126
154
|
const { state: stateCookieName } = getLtAuthCookieNames();
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
if (cookie) {
|
|
130
|
-
const parts = cookie.split("=");
|
|
131
|
-
const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join("=")) : "";
|
|
132
|
-
state = { ...JSON.parse(value), authMode: mode };
|
|
133
|
-
}
|
|
155
|
+
const existing = resolveLtAuthState(document.cookie, stateCookieName);
|
|
156
|
+
const state = { ...existing ?? { user: null }, authMode: mode };
|
|
134
157
|
const maxAge = 60 * 60 * 24 * 7;
|
|
135
158
|
const secure = globalThis.location?.protocol === "https:" ? "; secure" : "";
|
|
136
159
|
document.cookie = `${stateCookieName}=${encodeURIComponent(JSON.stringify(state))}; path=/; max-age=${maxAge}; samesite=lax${secure}`;
|
|
@@ -170,18 +193,7 @@ export async function attemptLtJwtSwitch(basePath = "/iam") {
|
|
|
170
193
|
}
|
|
171
194
|
export function isLtAuthenticated() {
|
|
172
195
|
if (import.meta.server) return false;
|
|
173
|
-
|
|
174
|
-
const { state: stateCookieName } = getLtAuthCookieNames();
|
|
175
|
-
const cookie = document.cookie.split("; ").find((row) => row.startsWith(`${stateCookieName}=`));
|
|
176
|
-
if (cookie) {
|
|
177
|
-
const parts = cookie.split("=");
|
|
178
|
-
const value = parts.length > 1 ? decodeURIComponent(parts.slice(1).join("=")) : "";
|
|
179
|
-
const state = JSON.parse(value);
|
|
180
|
-
return !!state?.user;
|
|
181
|
-
}
|
|
182
|
-
} catch {
|
|
183
|
-
}
|
|
184
|
-
return false;
|
|
196
|
+
return !!resolveLtAuthState(document.cookie)?.user;
|
|
185
197
|
}
|
|
186
198
|
export function createLtAuthFetch(basePath = "/iam") {
|
|
187
199
|
return async function ltAuthFetch2(input, init) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nuxt-extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"prepare": "git config core.hooksPath .githooks",
|
|
52
52
|
"prepack": "node scripts/sync-module-version.mjs && nuxt-module-build build",
|
|
53
53
|
"c": "pnpm run check",
|
|
54
|
-
"check": "
|
|
54
|
+
"check": "node scripts/check.mjs",
|
|
55
|
+
"check:raw": "pnpm audit && pnpm run format:check && pnpm run lint && pnpm run dev:prepare && pnpm test:types && pnpm test && pnpm run build",
|
|
55
56
|
"check:fix": "pnpm install && pnpm audit --fix && pnpm run format && pnpm run lint:fix && pnpm test:types && pnpm test && pnpm run build",
|
|
56
57
|
"check:naf": "pnpm install && pnpm run format && pnpm run lint:fix && pnpm test:types && pnpm test && pnpm run build",
|
|
57
58
|
"cf": "pnpm run check:fix",
|
|
@@ -121,7 +122,14 @@
|
|
|
121
122
|
"brace-expansion@>=5.0.0 <5.0.6": "5.0.6",
|
|
122
123
|
"devalue@>=5.6.3 <=5.8.0": "5.8.1",
|
|
123
124
|
"kysely@>=0.26.0 <0.28.17": "0.28.17",
|
|
124
|
-
"ws@>=8.0.0 <8.20.1": "8.21.0"
|
|
125
|
+
"ws@>=8.0.0 <8.20.1": "8.21.0",
|
|
126
|
+
"shell-quote@<1.8.4": "1.8.4",
|
|
127
|
+
"nuxt@<4.4.7": "4.4.7",
|
|
128
|
+
"vite@>=7.0.0 <7.3.5": "7.3.5",
|
|
129
|
+
"tar@<7.5.16": "7.5.16",
|
|
130
|
+
"launch-editor@<2.14.1": "2.14.1",
|
|
131
|
+
"esbuild@<0.28.1": "0.28.1",
|
|
132
|
+
"@babel/core@<7.29.6": "7.29.6"
|
|
125
133
|
}
|
|
126
134
|
},
|
|
127
135
|
"keywords": [
|