@lenne.tech/nuxt-extensions 1.8.1 → 1.8.3
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/CLAUDE.md +8 -1
- package/README.md +4 -2
- package/dist/module.mjs +1 -0
- package/dist/runtime/composables/auth/use-lt-auth.js +25 -4
- package/dist/runtime/lib/auth-state.d.ts +51 -0
- package/dist/runtime/lib/auth-state.js +43 -24
- package/dist/runtime/plugins/lt-config-check.d.ts +17 -0
- package/dist/runtime/plugins/lt-config-check.js +11 -0
- package/dist/runtime/types/auth.d.ts +1 -1
- package/dist/runtime/types/module.d.ts +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -12,7 +12,7 @@ src/
|
|
|
12
12
|
│ ├── components/ # Auto-imported components
|
|
13
13
|
│ ├── lib/ # Library exports (auth-client, auth-state, ai)
|
|
14
14
|
│ ├── middleware/ # Route middleware (system setup)
|
|
15
|
-
│ ├── plugins/ # Nuxt plugins (auth interceptor, error translation)
|
|
15
|
+
│ ├── plugins/ # Nuxt plugins (auth interceptor, error translation, config check)
|
|
16
16
|
│ ├── server/ # Nitro server routes (auth proxy)
|
|
17
17
|
│ ├── testing/ # Playwright test helpers
|
|
18
18
|
│ ├── types/ # TypeScript type definitions
|
|
@@ -133,6 +133,13 @@ The `useLtAuth()` composable manages the `lt-auth-state` cookie for authenticati
|
|
|
133
133
|
3. If `useLtAuth()` can't find a valid user in the cookie on the server, it initializes with `{user: null}` — this is intentional and correct
|
|
134
134
|
4. The `iam.session_token` httpOnly cookie (set by Better Auth) is the actual session identifier — `lt-auth-state` is a convenience cache for the client-side middleware
|
|
135
135
|
|
|
136
|
+
### Session User Merge (nest-server-only fields):
|
|
137
|
+
On session re-validation (`validateSession()` at app init / hard reload, and the passkey get-session fallback), `useLtAuth()` **merges** the Better-Auth session user onto the cached user of the SAME identity via the private `mergeSessionUser()`, instead of overwriting it. Consequences you must know:
|
|
138
|
+
|
|
139
|
+
- **nest-server-only fields persist across reloads.** Better-Auth's get-session returns only Better-Auth-owned fields (id/email/name + registered additionalFields). Fields it does not own (e.g. custom preferences like `leadTableColumns`) previously vanished on every reload; they now survive. If you built a workaround that re-fetched such fields after each reload, you can retire it.
|
|
140
|
+
- **Authorization fields are fail-closed.** `role`, `banned`, `banExpires`, `banReason`, `emailVerified`, `twoFactorEnabled` always reflect the session — any of these the session omits is dropped from the merge (never kept stale), so a backend downgrade is reflected client-side. If your project adds its OWN authorization-relevant user field, either register it as a Better-Auth additionalField (so get-session returns it) or add it to the `AUTHZ_KEYS` list in `use-lt-auth.ts` — otherwise it could persist stale in the client cache.
|
|
141
|
+
- **Keep the cached user lean.** `lt-auth-state` is a non-httpOnly cookie sent on every same-site request and capped at ~4 KB by browsers. Because the merge keeps preference fields across reloads, large blobs on the user object can push the cookie over the limit — the browser then silently rejects the write and the next reload reads as a logout. `setUser` emits a dev-mode warning above ~3.5 KB; move large preference data off the user object and fetch it from an authenticated API.
|
|
142
|
+
|
|
136
143
|
### Custom Auth Middleware Pattern:
|
|
137
144
|
```typescript
|
|
138
145
|
// CORRECT — read-only check, no cookie mutation
|
package/README.md
CHANGED
|
@@ -55,14 +55,16 @@ npm install tus-js-client
|
|
|
55
55
|
All environment variables are resolved **at runtime** (not build time). This means you can build a Docker image once and deploy it to different environments by changing env vars — no rebuild needed.
|
|
56
56
|
|
|
57
57
|
**SSR fallback chain:**
|
|
58
|
-
`NUXT_API_URL` → `NUXT_PUBLIC_API_URL` → `auth.baseURL` (from nuxt.config.ts) →
|
|
58
|
+
`NUXT_API_URL` → `NUXT_PUBLIC_API_URL` → `auth.baseURL` (from nuxt.config.ts) → *(unset)*
|
|
59
59
|
|
|
60
60
|
**Client fallback chain (no proxy):**
|
|
61
|
-
`NUXT_PUBLIC_API_URL` → `auth.baseURL` (from nuxt.config.ts) →
|
|
61
|
+
`NUXT_PUBLIC_API_URL` → `auth.baseURL` (from nuxt.config.ts) → *(unset)*
|
|
62
62
|
|
|
63
63
|
**Client with proxy (`NUXT_PUBLIC_API_PROXY=true`):**
|
|
64
64
|
All requests go to `/api/{path}` — the Vite dev proxy forwards them to the backend.
|
|
65
65
|
|
|
66
|
+
> **No implicit `localhost` default.** If none of the sources above is set, there is **no** built-in fallback URL: API paths stay relative (e.g. `/iam/token`) and resolve against the app origin, so auth and setup calls 404 unless the app is served behind a same-origin reverse proxy. The module logs a one-time warning (`[LtExtensions] No API URL configured…`) at app init and on the first API call. Always set `NUXT_PUBLIC_API_URL` (and optionally `NUXT_API_URL` for SSR).
|
|
67
|
+
|
|
66
68
|
> **Security:** `NUXT_API_URL` is never exposed to the client bundle. It stays in `runtimeConfig.apiUrl` (server only). This is important when using internal network addresses like `http://api.svc.cluster.local`.
|
|
67
69
|
|
|
68
70
|
### Deployment Scenarios
|
package/dist/module.mjs
CHANGED
|
@@ -170,6 +170,7 @@ const module$1 = defineNuxtModule({
|
|
|
170
170
|
filePath: resolve(`./runtime/components/transition/${componentName}.vue`)
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
|
+
addPlugin(resolve("./runtime/plugins/lt-config-check"));
|
|
173
174
|
if (resolvedOptions.auth?.enabled && resolvedOptions.auth?.interceptor?.enabled) {
|
|
174
175
|
addPlugin(resolve("./runtime/plugins/auth-interceptor.client"));
|
|
175
176
|
}
|
|
@@ -58,7 +58,13 @@ export function useLtAuth() {
|
|
|
58
58
|
if (import.meta.client) {
|
|
59
59
|
const maxAge = 60 * 60 * 24 * 7;
|
|
60
60
|
const secure = globalThis.location?.protocol === "https:" ? "; secure" : "";
|
|
61
|
-
|
|
61
|
+
const encoded = encodeURIComponent(JSON.stringify(newState));
|
|
62
|
+
if (import.meta.dev && encoded.length > 3500) {
|
|
63
|
+
console.warn(
|
|
64
|
+
`[lt-auth] lt-auth-state cookie is ${encoded.length} bytes (encoded), approaching the ~4KB browser limit \u2014 an oversized cookie is dropped on write and reads as a logout on reload. Keep the cached user lean: move large preference data off the user object.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
document.cookie = `${stateCookieName}=${encoded}; path=/; max-age=${maxAge}; samesite=lax${secure}`;
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
70
|
function clearUser() {
|
|
@@ -123,6 +129,20 @@ export function useLtAuth() {
|
|
|
123
129
|
}
|
|
124
130
|
return response;
|
|
125
131
|
}
|
|
132
|
+
const AUTHZ_KEYS = ["banExpires", "banReason", "banned", "emailVerified", "role", "twoFactorEnabled"];
|
|
133
|
+
function mergeSessionUser(sessionUser) {
|
|
134
|
+
const cached = resolvedAuthState.value?.user;
|
|
135
|
+
if (!cached?.id || !sessionUser?.id || cached.id !== sessionUser.id) {
|
|
136
|
+
return sessionUser;
|
|
137
|
+
}
|
|
138
|
+
const merged = { ...cached, ...sessionUser };
|
|
139
|
+
for (const key of AUTHZ_KEYS) {
|
|
140
|
+
if (!(key in sessionUser)) {
|
|
141
|
+
delete merged[key];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return merged;
|
|
145
|
+
}
|
|
126
146
|
async function validateSession() {
|
|
127
147
|
try {
|
|
128
148
|
const session = authClient.useSession();
|
|
@@ -141,7 +161,7 @@ export function useLtAuth() {
|
|
|
141
161
|
});
|
|
142
162
|
}
|
|
143
163
|
if (session.value.data?.user) {
|
|
144
|
-
setUser(session.value.data.user, "cookie");
|
|
164
|
+
setUser(mergeSessionUser(session.value.data.user), "cookie");
|
|
145
165
|
switchToJwtMode().catch(() => {
|
|
146
166
|
});
|
|
147
167
|
return true;
|
|
@@ -297,8 +317,9 @@ export function useLtAuth() {
|
|
|
297
317
|
if (sessionResponse.ok) {
|
|
298
318
|
const sessionData = await sessionResponse.json();
|
|
299
319
|
if (sessionData?.user) {
|
|
300
|
-
|
|
301
|
-
|
|
320
|
+
const mergedUser = mergeSessionUser(sessionData.user);
|
|
321
|
+
result.user = mergedUser;
|
|
322
|
+
setUser(mergedUser, "cookie");
|
|
302
323
|
switchToJwtMode().catch(() => {
|
|
303
324
|
});
|
|
304
325
|
}
|
|
@@ -90,6 +90,14 @@ export declare function getLtAuthCookieNames(): {
|
|
|
90
90
|
* unchanged by the duplicate-tolerant read; it only decides which twin wins.
|
|
91
91
|
*/
|
|
92
92
|
export declare function resolveLtAuthState(cookieString: string, stateCookieName?: string): LtAuthState | null;
|
|
93
|
+
/**
|
|
94
|
+
* Clear the one-shot warning state.
|
|
95
|
+
*
|
|
96
|
+
* INTERNAL — test-only. Module-scope state survives every test in a file that
|
|
97
|
+
* does not call `vi.resetModules()`, which would make assertions on these
|
|
98
|
+
* warnings depend on test order. Production code must never call this.
|
|
99
|
+
*/
|
|
100
|
+
export declare function resetLtWarnOnceState(): void;
|
|
93
101
|
/**
|
|
94
102
|
* Determines if HTTP requests should use the Nuxt Vite dev proxy.
|
|
95
103
|
*
|
|
@@ -124,6 +132,46 @@ export declare function resolveLtAuthState(cookieString: string, stateCookieName
|
|
|
124
132
|
* @see https://github.com/lenneTech/nuxt-base-starter — Vite proxy configuration
|
|
125
133
|
*/
|
|
126
134
|
export declare function isLocalDevApiProxy(): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Outcome of resolving the API base URL for the current render scope.
|
|
137
|
+
*
|
|
138
|
+
* INTERNAL — plumbing for {@link buildLtApiUrl} and the `lt-config-check` plugin.
|
|
139
|
+
* Not part of the public API (absent from the package barrels and auto-imports);
|
|
140
|
+
* reachable only via a deep path import. Shape may change without a major bump.
|
|
141
|
+
*/
|
|
142
|
+
export interface LtApiUrlResolution {
|
|
143
|
+
/** Base URL to prefix onto API paths. `'/api'` in proxy mode, `''` when unresolved. */
|
|
144
|
+
baseUrl: string;
|
|
145
|
+
/** `true` when no API URL is configured for {@link scope}. */
|
|
146
|
+
missing: boolean;
|
|
147
|
+
/** `true` when the Vite dev proxy prefix is in use (see {@link isLocalDevApiProxy}). */
|
|
148
|
+
proxy: boolean;
|
|
149
|
+
/** The render scope the resolution was performed for. */
|
|
150
|
+
scope: 'client' | 'server';
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the API base URL for the current render scope.
|
|
154
|
+
*
|
|
155
|
+
* INTERNAL — plumbing shared by {@link buildLtApiUrl} and the `lt-config-check`
|
|
156
|
+
* plugin; not part of the public API (absent from the barrels and auto-imports).
|
|
157
|
+
*
|
|
158
|
+
* Pure: it reads `runtimeConfig` and reports what it found, but never logs and
|
|
159
|
+
* never mutates. The `missing` flag lets callers decide how to surface the
|
|
160
|
+
* misconfiguration — {@link buildLtApiUrl} warns lazily, the `lt-config-check`
|
|
161
|
+
* plugin warns eagerly at app init. Both funnel through the same one-shot key,
|
|
162
|
+
* so the user sees exactly one warning either way.
|
|
163
|
+
*
|
|
164
|
+
* Throws when called outside a Nuxt runtime-config context; callers catch.
|
|
165
|
+
*/
|
|
166
|
+
export declare function resolveLtApiBaseUrl(): LtApiUrlResolution;
|
|
167
|
+
/**
|
|
168
|
+
* Warn — at most once per module instance — that no API URL is configured.
|
|
169
|
+
*
|
|
170
|
+
* INTERNAL — shared by {@link buildLtApiUrl} and the `lt-config-check` plugin so
|
|
171
|
+
* an eager boot-time check and a lazy first-URL check never double-report. Not
|
|
172
|
+
* part of the public API (absent from the barrels and auto-imports).
|
|
173
|
+
*/
|
|
174
|
+
export declare function warnMissingLtApiUrl(scope: 'client' | 'server'): void;
|
|
127
175
|
/**
|
|
128
176
|
* Build a full API URL for a given path, handling SSR, proxy, and direct modes.
|
|
129
177
|
*
|
|
@@ -157,6 +205,9 @@ export declare function isLocalDevApiProxy(): boolean;
|
|
|
157
205
|
*
|
|
158
206
|
* Trailing slashes on the base URL are automatically stripped.
|
|
159
207
|
*
|
|
208
|
+
* A missing API URL is reported at most once per module instance — the condition
|
|
209
|
+
* is constant for the process, so repeating it per call only drowns other output.
|
|
210
|
+
*
|
|
160
211
|
* @param path - The API path (e.g., `/system-setup/status`, `/i18n/errors/de`)
|
|
161
212
|
*/
|
|
162
213
|
export declare function buildLtApiUrl(path: string): string;
|
|
@@ -42,7 +42,17 @@ export function resolveLtAuthState(cookieString, stateCookieName) {
|
|
|
42
42
|
}
|
|
43
43
|
return fallback;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
46
|
+
function warnOnce(key, message) {
|
|
47
|
+
if (warnedKeys.has(key)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
warnedKeys.add(key);
|
|
51
|
+
console.warn(message);
|
|
52
|
+
}
|
|
53
|
+
export function resetLtWarnOnceState() {
|
|
54
|
+
warnedKeys.clear();
|
|
55
|
+
}
|
|
46
56
|
export function isLocalDevApiProxy() {
|
|
47
57
|
if (import.meta.server) {
|
|
48
58
|
return false;
|
|
@@ -55,12 +65,10 @@ export function isLocalDevApiProxy() {
|
|
|
55
65
|
}
|
|
56
66
|
const buildId = runtimeConfig?.app && runtimeConfig.app?.buildId;
|
|
57
67
|
if (buildId === "dev") {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
);
|
|
63
|
-
}
|
|
68
|
+
warnOnce(
|
|
69
|
+
"api-proxy-fallback",
|
|
70
|
+
"\n\u26A0\uFE0F [LtExtensions] API proxy activated implicitly because nuxt dev was detected.\n To make this explicit, add NUXT_PUBLIC_API_PROXY=true to your .env file.\n If you do NOT want the proxy, set NUXT_PUBLIC_API_PROXY=false.\n"
|
|
71
|
+
);
|
|
64
72
|
return true;
|
|
65
73
|
}
|
|
66
74
|
return false;
|
|
@@ -68,26 +76,37 @@ export function isLocalDevApiProxy() {
|
|
|
68
76
|
return false;
|
|
69
77
|
}
|
|
70
78
|
}
|
|
79
|
+
const MISSING_API_URL_WARNING = {
|
|
80
|
+
client: "[LtExtensions] No API URL configured. Set NUXT_PUBLIC_API_URL. API paths stay relative and resolve against the app origin, so auth and setup calls 404 unless the app is served behind a same-origin reverse proxy.",
|
|
81
|
+
server: "[LtExtensions] No API URL configured for SSR. Set NUXT_API_URL or NUXT_PUBLIC_API_URL. API paths stay relative, so server-side requests never reach the backend: $fetch resolves them against Nitro (404) and global fetch rejects them as an invalid URL."
|
|
82
|
+
};
|
|
83
|
+
function stripTrailingSlashes(url) {
|
|
84
|
+
return url.replace(/\/+$/, "");
|
|
85
|
+
}
|
|
86
|
+
export function resolveLtApiBaseUrl() {
|
|
87
|
+
const runtimeConfig = useRuntimeConfig();
|
|
88
|
+
const publicUrl = runtimeConfig.public.apiUrl || "";
|
|
89
|
+
const authBaseURL = runtimeConfig.public?.ltExtensions?.auth?.baseURL || "";
|
|
90
|
+
if (import.meta.server) {
|
|
91
|
+
const apiUrl2 = runtimeConfig.apiUrl || publicUrl || authBaseURL;
|
|
92
|
+
return { baseUrl: stripTrailingSlashes(apiUrl2), missing: !apiUrl2, proxy: false, scope: "server" };
|
|
93
|
+
}
|
|
94
|
+
if (isLocalDevApiProxy()) {
|
|
95
|
+
return { baseUrl: "/api", missing: false, proxy: true, scope: "client" };
|
|
96
|
+
}
|
|
97
|
+
const apiUrl = publicUrl || authBaseURL;
|
|
98
|
+
return { baseUrl: stripTrailingSlashes(apiUrl), missing: !apiUrl, proxy: false, scope: "client" };
|
|
99
|
+
}
|
|
100
|
+
export function warnMissingLtApiUrl(scope) {
|
|
101
|
+
warnOnce(`missing-api-url:${scope}`, MISSING_API_URL_WARNING[scope]);
|
|
102
|
+
}
|
|
71
103
|
export function buildLtApiUrl(path) {
|
|
72
104
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (import.meta.server) {
|
|
77
|
-
const apiUrl2 = runtimeConfig.apiUrl || publicUrl || authBaseURL;
|
|
78
|
-
if (!apiUrl2) {
|
|
79
|
-
console.warn("[LtExtensions] No API URL configured. Set NUXT_API_URL or NUXT_PUBLIC_API_URL.");
|
|
80
|
-
}
|
|
81
|
-
return `${(apiUrl2 || "").replace(/\/+$/, "")}${path}`;
|
|
82
|
-
}
|
|
83
|
-
if (isLocalDevApiProxy()) {
|
|
84
|
-
return `/api${path}`;
|
|
85
|
-
}
|
|
86
|
-
const apiUrl = publicUrl || authBaseURL;
|
|
87
|
-
if (!apiUrl) {
|
|
88
|
-
console.warn("[LtExtensions] No API URL configured. Set NUXT_PUBLIC_API_URL.");
|
|
105
|
+
const { baseUrl, missing, scope } = resolveLtApiBaseUrl();
|
|
106
|
+
if (missing) {
|
|
107
|
+
warnMissingLtApiUrl(scope);
|
|
89
108
|
}
|
|
90
|
-
return `${
|
|
109
|
+
return `${baseUrl}${path}`;
|
|
91
110
|
} catch {
|
|
92
111
|
return path;
|
|
93
112
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Check Plugin
|
|
3
|
+
*
|
|
4
|
+
* Reports a missing API URL once at app init instead of waiting for whichever
|
|
5
|
+
* code path happens to build the first URL. The condition is read from
|
|
6
|
+
* `runtimeConfig` and is fixed for the module instance, so it belongs in a
|
|
7
|
+
* single boot-time check rather than in the hot `buildLtApiUrl()` path.
|
|
8
|
+
*
|
|
9
|
+
* Universal on purpose: SSR and the browser resolve the base URL from different
|
|
10
|
+
* keys (`NUXT_API_URL` vs `NUXT_PUBLIC_API_URL`) and can be misconfigured
|
|
11
|
+
* independently. On the server the plugin runs per request, but
|
|
12
|
+
* `warnMissingLtApiUrl()` is one-shot, so the warning still appears exactly once
|
|
13
|
+
* per process — and `buildLtApiUrl()` shares the same key, so it never repeats.
|
|
14
|
+
*/
|
|
15
|
+
import type { ObjectPlugin, Plugin } from '#app';
|
|
16
|
+
declare const _default: ObjectPlugin & Plugin;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from "#imports";
|
|
2
|
+
import { resolveLtApiBaseUrl, warnMissingLtApiUrl } from "../lib/auth-state.js";
|
|
3
|
+
export default defineNuxtPlugin(() => {
|
|
4
|
+
try {
|
|
5
|
+
const { missing, scope } = resolveLtApiBaseUrl();
|
|
6
|
+
if (missing) {
|
|
7
|
+
warnMissingLtApiUrl(scope);
|
|
8
|
+
}
|
|
9
|
+
} catch {
|
|
10
|
+
}
|
|
11
|
+
});
|
|
@@ -33,7 +33,7 @@ export interface LtAuthState {
|
|
|
33
33
|
* All options have sensible defaults for nest-server compatibility
|
|
34
34
|
*/
|
|
35
35
|
export interface LtAuthClientConfig {
|
|
36
|
-
/** API base URL (default: from
|
|
36
|
+
/** API base URL (default: `''` — empty means requests use the current app origin; production passes an explicit URL from `NUXT_PUBLIC_API_URL`. No implicit `localhost` fallback) */
|
|
37
37
|
baseURL?: string;
|
|
38
38
|
/** Auth API base path (default: '/iam' - must match nest-server betterAuth.basePath) */
|
|
39
39
|
basePath?: string;
|
|
@@ -56,7 +56,7 @@ export interface LtAuthCookieNamesOptions {
|
|
|
56
56
|
export interface LtAuthModuleOptions {
|
|
57
57
|
/** Auth API base path (default: '/iam' - must match nest-server betterAuth.basePath) */
|
|
58
58
|
basePath?: string;
|
|
59
|
-
/** API base URL (default: from
|
|
59
|
+
/** API base URL (default: `''` — resolved at runtime from `NUXT_PUBLIC_API_URL` / `NUXT_API_URL`; no implicit `localhost` fallback, so an unset URL keeps API paths relative to the app origin) */
|
|
60
60
|
baseURL?: string;
|
|
61
61
|
/** Override the cookie names used for auth state and JWT storage */
|
|
62
62
|
cookieNames?: LtAuthCookieNamesOptions;
|