@lenne.tech/nuxt-extensions 1.8.2 → 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
CHANGED
|
@@ -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
|
|
@@ -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
|
}
|