@lenne.tech/nuxt-extensions 1.8.4 → 1.10.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/CLAUDE.md +19 -7
- package/README.md +37 -0
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -3
- package/dist/runtime/composables/auth/use-lt-auth.js +14 -2
- package/dist/runtime/lib/auth-client.d.ts +14 -8
- package/dist/runtime/lib/passkey-stub.d.ts +23 -0
- package/dist/runtime/lib/passkey-stub.js +10 -0
- package/dist/runtime/types/auth.d.ts +58 -0
- package/package.json +28 -41
package/CLAUDE.md
CHANGED
|
@@ -13,7 +13,7 @@ src/
|
|
|
13
13
|
│ ├── lib/ # Library exports (auth-client, auth-state, ai)
|
|
14
14
|
│ ├── middleware/ # Route middleware (system setup)
|
|
15
15
|
│ ├── plugins/ # Nuxt plugins (auth interceptor, error translation, config check)
|
|
16
|
-
│ ├── server/ #
|
|
16
|
+
│ ├── server/ # server-side tsconfig scope (no runtime routes; auth talks to the backend directly)
|
|
17
17
|
│ ├── testing/ # Playwright test helpers
|
|
18
18
|
│ ├── types/ # TypeScript type definitions
|
|
19
19
|
│ └── utils/ # Auto-imported utility functions
|
|
@@ -85,17 +85,17 @@ The API base URL is resolved at runtime from `NUXT_PUBLIC_API_URL` (client + SSR
|
|
|
85
85
|
## Authentication (Better Auth)
|
|
86
86
|
|
|
87
87
|
- SSR-safe session management via `useLtAuth()`
|
|
88
|
-
- Auth
|
|
89
|
-
- Middleware `
|
|
88
|
+
- Auth requests go **directly** to the backend Better-Auth endpoints (`basePath: '/iam'`) via `ltAuthFetch` (Cookie/JWT dual-mode) — the module ships **no** local Nitro `/api/auth/**` proxy
|
|
89
|
+
- Protected-route gating is the consumer's own middleware (see "Custom Auth Middleware Pattern" below); the module only registers the optional `lt-system-setup` middleware
|
|
90
90
|
- Passkey (WebAuthn) support via `@better-auth/passkey` peer dependency
|
|
91
91
|
- 2FA (TOTP) support built-in
|
|
92
92
|
|
|
93
93
|
### Auth Flow
|
|
94
94
|
|
|
95
95
|
1. Frontend calls `useLtAuth().signIn()` / `.signUp()`
|
|
96
|
-
2.
|
|
97
|
-
3.
|
|
98
|
-
4.
|
|
96
|
+
2. The Better-Auth client sends the request straight to the backend Better-Auth endpoints (`basePath: '/iam'`), with the base URL resolved by `buildLtApiUrl` (`NUXT_PUBLIC_API_URL` / SSR `NUXT_API_URL`)
|
|
97
|
+
3. The backend sets the session as an `httpOnly` cookie (`iam.session_token`); `lt-auth-state` is the non-httpOnly client-side cache used by middleware
|
|
98
|
+
4. Subsequent authenticated requests use `ltAuthFetch` (Cookie mode by default, JWT fallback)
|
|
99
99
|
|
|
100
100
|
## AI Assistant Flow
|
|
101
101
|
|
|
@@ -137,9 +137,21 @@ The `useLtAuth()` composable manages the `lt-auth-state` cookie for authenticati
|
|
|
137
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
138
|
|
|
139
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.
|
|
140
|
+
- **Authorization fields are fail-closed.** `role`, `roles`, `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
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
142
|
|
|
143
|
+
### Admin Detection (`role` vs `roles`):
|
|
144
|
+
`useLtAuth().isAdmin` accepts BOTH user shapes, so one frontend works against either backend:
|
|
145
|
+
|
|
146
|
+
- **`@lenne.tech/nest-server`** registers `roles` as a *core* Better-Auth additionalField (`type: 'string[]'`, `defaultValue: []`). Its users carry `roles: ['admin']` and no singular `role`.
|
|
147
|
+
- **Better-Auth's admin plugin** uses the singular `role: 'admin'`.
|
|
148
|
+
|
|
149
|
+
`isAdmin` is `true` when `role === 'admin'` **or** `roles` contains `'admin'` (a union — either shape granting admin is sufficient). A malformed non-array `roles` (the `lt-auth-state` cookie is client-writable) degrades to `false` — it never throws and never fail-opens via `String.prototype.includes` substring matching. Matching is case-sensitive (`'admin'`, not `'Admin'`), identical to nest-server's own `hasRole(RoleEnum.ADMIN)`.
|
|
150
|
+
|
|
151
|
+
- `roles` is in `AUTHZ_KEYS`, so a backend-side admin revocation closes the admin UI on the next session re-validation instead of being masked by a stale cached array.
|
|
152
|
+
- The `auth.enableAdmin` module option toggles the Better-Auth **admin client plugin** (user-management calls like `admin.listUsers()`). It does NOT control `isAdmin` — role detection works regardless of that flag.
|
|
153
|
+
- **`isAdmin` is a UX gate, not an authorization boundary.** It reads the non-httpOnly `lt-auth-state` cache, which a user can edit. Render admin UI behind it; enforce admin rights server-side (`@Restricted(RoleEnum.ADMIN)`).
|
|
154
|
+
|
|
143
155
|
### Custom Auth Middleware Pattern:
|
|
144
156
|
```typescript
|
|
145
157
|
// CORRECT — read-only check, no cookie mutation
|
package/README.md
CHANGED
|
@@ -166,6 +166,7 @@ export default defineNuxtConfig({
|
|
|
166
166
|
const {
|
|
167
167
|
user,
|
|
168
168
|
isAuthenticated,
|
|
169
|
+
isAdmin,
|
|
169
170
|
isLoading,
|
|
170
171
|
signIn,
|
|
171
172
|
signOut,
|
|
@@ -199,6 +200,42 @@ async function handlePasskeyLogin() {
|
|
|
199
200
|
</template>
|
|
200
201
|
```
|
|
201
202
|
|
|
203
|
+
#### Admin detection (`role` vs `roles`)
|
|
204
|
+
|
|
205
|
+
`isAdmin` accepts **both** user shapes, so the same frontend works against either backend:
|
|
206
|
+
|
|
207
|
+
| Backend | User shape | `isAdmin` is `true` when |
|
|
208
|
+
|---------|------------|-------------------------|
|
|
209
|
+
| `@lenne.tech/nest-server` | `roles: ['admin']` — core Better-Auth additionalField (`type: 'string[]'`), no singular `role` | `roles` contains `'admin'` |
|
|
210
|
+
| Better-Auth admin plugin | `role: 'admin'` — single role | `role === 'admin'` |
|
|
211
|
+
|
|
212
|
+
```vue
|
|
213
|
+
<script setup>
|
|
214
|
+
const { isAdmin } = useLtAuth();
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<template>
|
|
218
|
+
<NuxtLink v-if="isAdmin" to="/admin">Admin</NuxtLink>
|
|
219
|
+
</template>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
> **`isAdmin` is a UX gate, not an authorization boundary.** It reads the client-side `lt-auth-state` cookie cache, which is not httpOnly and therefore user-writable. Use it to decide what to *render*; always enforce admin rights server-side (`@Restricted(RoleEnum.ADMIN)` in nest-server).
|
|
223
|
+
>
|
|
224
|
+
> The `auth.enableAdmin` option toggles the Better-Auth **admin client plugin** (user-management calls such as `admin.listUsers()`). It does **not** control `isAdmin` — role detection works regardless of that flag.
|
|
225
|
+
|
|
226
|
+
For roles other than admin, use `hasRole(role)` / `hasAnyRole(...roles)`. They apply the exact same both-shapes union and `Array.isArray` guard as `isAdmin` (which is itself just `hasRole('admin')`), so you never need the unguarded `user.value?.roles?.includes(...)`:
|
|
227
|
+
|
|
228
|
+
```vue
|
|
229
|
+
<script setup>
|
|
230
|
+
const { hasRole, hasAnyRole } = useLtAuth();
|
|
231
|
+
</script>
|
|
232
|
+
|
|
233
|
+
<template>
|
|
234
|
+
<button v-if="hasRole('editor')">Edit</button>
|
|
235
|
+
<NuxtLink v-if="hasAnyRole('admin', 'editor')" to="/manage">Manage</NuxtLink>
|
|
236
|
+
</template>
|
|
237
|
+
```
|
|
238
|
+
|
|
202
239
|
### Custom Better Auth Plugins
|
|
203
240
|
|
|
204
241
|
You can extend the auth client with additional [Better Auth plugins](https://www.better-auth.com/docs/plugins):
|
package/dist/module.d.mts
CHANGED
|
@@ -3,7 +3,7 @@ import { LtExtensionsModuleOptions } from '../dist/runtime/types/index.js';
|
|
|
3
3
|
export * from '../dist/runtime/types/index.js';
|
|
4
4
|
|
|
5
5
|
declare const name = "@lenne.tech/nuxt-extensions";
|
|
6
|
-
declare const version = "1.
|
|
6
|
+
declare const version = "1.10.0";
|
|
7
7
|
declare const configKey = "ltExtensions";
|
|
8
8
|
declare const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
|
|
9
9
|
declare const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { defineNuxtModule, createResolver, addImports, addComponent, addPlugin, addRouteMiddleware } from '@nuxt/kit';
|
|
1
|
+
import { defineNuxtModule, createResolver, tryResolveModule, addImports, addComponent, addPlugin, addRouteMiddleware } from '@nuxt/kit';
|
|
2
2
|
|
|
3
3
|
const name = "@lenne.tech/nuxt-extensions";
|
|
4
|
-
const version = "1.
|
|
4
|
+
const version = "1.10.0";
|
|
5
5
|
const configKey = "ltExtensions";
|
|
6
6
|
const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
|
|
7
7
|
const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
|
|
@@ -66,6 +66,19 @@ const module$1 = defineNuxtModule({
|
|
|
66
66
|
i18n: { ...defaultOptions.i18n, ...options.i18n },
|
|
67
67
|
tus: { ...defaultOptions.tus, ...options.tus }
|
|
68
68
|
};
|
|
69
|
+
const passkeyAvailable = !!await tryResolveModule("@better-auth/passkey/client", new URL(import.meta.url));
|
|
70
|
+
if (!passkeyAvailable) {
|
|
71
|
+
const passkeyStub = resolve("./runtime/lib/passkey-stub");
|
|
72
|
+
nuxt.options.alias["@better-auth/passkey/client"] = passkeyStub;
|
|
73
|
+
nuxt.options.nitro ??= {};
|
|
74
|
+
nuxt.options.nitro.alias = { ...nuxt.options.nitro.alias, "@better-auth/passkey/client": passkeyStub };
|
|
75
|
+
}
|
|
76
|
+
const enablePasskey = (resolvedOptions.auth?.enablePasskey ?? true) && passkeyAvailable;
|
|
77
|
+
if ((resolvedOptions.auth?.enablePasskey ?? true) && !passkeyAvailable) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`[${name}] Passkey support disabled: the optional peer dependency "@better-auth/passkey" is not installed. Install it to use passkeys, or set \`ltExtensions.auth.enablePasskey = false\` to silence this warning.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
69
82
|
const rc = nuxt.options.runtimeConfig;
|
|
70
83
|
if (!rc.apiUrl) {
|
|
71
84
|
rc.apiUrl = "";
|
|
@@ -90,7 +103,7 @@ const module$1 = defineNuxtModule({
|
|
|
90
103
|
},
|
|
91
104
|
enabled: resolvedOptions.auth?.enabled ?? true,
|
|
92
105
|
enableAdmin: resolvedOptions.auth?.enableAdmin ?? true,
|
|
93
|
-
enablePasskey
|
|
106
|
+
enablePasskey,
|
|
94
107
|
enableTwoFactor: resolvedOptions.auth?.enableTwoFactor ?? true,
|
|
95
108
|
interceptor: {
|
|
96
109
|
enabled: resolvedOptions.auth?.interceptor?.enabled ?? true,
|
|
@@ -46,7 +46,17 @@ export function useLtAuth() {
|
|
|
46
46
|
const isJwtMode = computed(() => authMode.value === "jwt");
|
|
47
47
|
const user = computed(() => resolvedAuthState.value?.user ?? null);
|
|
48
48
|
const isAuthenticated = computed(() => !!user.value);
|
|
49
|
-
|
|
49
|
+
function hasRole(role) {
|
|
50
|
+
const current = user.value;
|
|
51
|
+
if (!current) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return current.role === role || Array.isArray(current.roles) && current.roles.includes(role);
|
|
55
|
+
}
|
|
56
|
+
function hasAnyRole(...roles) {
|
|
57
|
+
return roles.some((role) => hasRole(role));
|
|
58
|
+
}
|
|
59
|
+
const isAdmin = computed(() => hasRole("admin"));
|
|
50
60
|
const is2FAEnabled = computed(() => user.value?.twoFactorEnabled ?? false);
|
|
51
61
|
const features = useState("lt-auth-features", () => ({}));
|
|
52
62
|
const featuresFetched = useState("lt-auth-features-fetched", () => false);
|
|
@@ -129,7 +139,7 @@ export function useLtAuth() {
|
|
|
129
139
|
}
|
|
130
140
|
return response;
|
|
131
141
|
}
|
|
132
|
-
const AUTHZ_KEYS = ["banExpires", "banReason", "banned", "emailVerified", "role", "twoFactorEnabled"];
|
|
142
|
+
const AUTHZ_KEYS = ["banExpires", "banReason", "banned", "emailVerified", "role", "roles", "twoFactorEnabled"];
|
|
133
143
|
function mergeSessionUser(sessionUser) {
|
|
134
144
|
const cached = resolvedAuthState.value?.user;
|
|
135
145
|
if (!cached?.id || !sessionUser?.id || cached.id !== sessionUser.id) {
|
|
@@ -457,6 +467,8 @@ export function useLtAuth() {
|
|
|
457
467
|
isLoading: computed(() => isLoading.value),
|
|
458
468
|
user,
|
|
459
469
|
// User properties
|
|
470
|
+
hasAnyRole,
|
|
471
|
+
hasRole,
|
|
460
472
|
is2FAEnabled,
|
|
461
473
|
isAdmin,
|
|
462
474
|
// Feature detection
|
|
@@ -229,11 +229,6 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
|
|
|
229
229
|
priority?: RequestPriority | undefined;
|
|
230
230
|
cache?: RequestCache | undefined;
|
|
231
231
|
credentials?: RequestCredentials;
|
|
232
|
-
headers?: (HeadersInit & (HeadersInit | {
|
|
233
|
-
accept: "application/json" | "text/plain" | "application/octet-stream";
|
|
234
|
-
"content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
|
|
235
|
-
authorization: "Bearer" | "Basic";
|
|
236
|
-
})) | undefined;
|
|
237
232
|
integrity?: string | undefined;
|
|
238
233
|
keepalive?: boolean | undefined;
|
|
239
234
|
method: string;
|
|
@@ -263,6 +258,12 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
|
|
|
263
258
|
prefix: string | (() => string | undefined) | undefined;
|
|
264
259
|
value: string | (() => string | undefined) | undefined;
|
|
265
260
|
}) | undefined;
|
|
261
|
+
headers?: {} | {
|
|
262
|
+
[x: string]: string | undefined;
|
|
263
|
+
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
264
|
+
"content-type"?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
265
|
+
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
266
|
+
} | undefined;
|
|
266
267
|
body?: any;
|
|
267
268
|
query?: any;
|
|
268
269
|
params?: any;
|
|
@@ -703,9 +704,14 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
|
|
|
703
704
|
code?: string | undefined;
|
|
704
705
|
message?: string | undefined;
|
|
705
706
|
}, FetchOptions["throw"] extends true ? true : false>>;
|
|
706
|
-
updateSession: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<Partial<{}>> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<
|
|
707
|
-
|
|
708
|
-
|
|
707
|
+
updateSession: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<Partial<{}>> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<import("better-auth/client").InferSessionUpdateCtx<{
|
|
708
|
+
basePath: string;
|
|
709
|
+
baseURL: any;
|
|
710
|
+
fetchOptions: {
|
|
711
|
+
customFetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
712
|
+
};
|
|
713
|
+
plugins: any[];
|
|
714
|
+
}, FetchOptions>> | undefined, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
|
|
709
715
|
session: {
|
|
710
716
|
id: string;
|
|
711
717
|
createdAt: Date;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time stand-in for the OPTIONAL `@better-auth/passkey` peer dependency.
|
|
3
|
+
*
|
|
4
|
+
* `auth-client.ts` imports `passkeyClient` as a static top-level import, so a
|
|
5
|
+
* bundler has to resolve the specifier even in projects that never enable
|
|
6
|
+
* passkeys. Without a resolvable module the build dies on an unresolved
|
|
7
|
+
* optional peer, which contradicts `peerDependenciesMeta.optional: true`.
|
|
8
|
+
*
|
|
9
|
+
* The module aliases `@better-auth/passkey/client` onto this file when the real
|
|
10
|
+
* package is absent, and forces `auth.enablePasskey` to `false` in that case —
|
|
11
|
+
* so the factory below is normally never called. It stays a no-op (rather than
|
|
12
|
+
* throwing) to keep the direct `createLtAuthClient()` entry point working, whose
|
|
13
|
+
* own `enablePasskey` default is `true`.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* No-op replacement for the real `passkeyClient()` Better-Auth client plugin.
|
|
17
|
+
*
|
|
18
|
+
* Returns a plugin with a deliberately distinct `id`: claiming `'passkey'` would
|
|
19
|
+
* make Better-Auth expose passkey actions that cannot work without the package.
|
|
20
|
+
*/
|
|
21
|
+
export declare function passkeyClient(): {
|
|
22
|
+
id: "lt-passkey-unavailable";
|
|
23
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
let warned = false;
|
|
2
|
+
export function passkeyClient() {
|
|
3
|
+
if (!warned) {
|
|
4
|
+
warned = true;
|
|
5
|
+
console.warn(
|
|
6
|
+
'[@lenne.tech/nuxt-extensions] Passkey support is unavailable: the optional peer dependency "@better-auth/passkey" is not installed. Install it to use passkeys, or pass `enablePasskey: false` to createLtAuthClient() to silence this warning.'
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
return { id: "lt-passkey-unavailable" };
|
|
10
|
+
}
|
|
@@ -12,7 +12,22 @@ export interface LtUser {
|
|
|
12
12
|
id: string;
|
|
13
13
|
image?: string;
|
|
14
14
|
name?: string;
|
|
15
|
+
/** Single-role shape (Better-Auth admin plugin). See also {@link LtUser.roles}. */
|
|
15
16
|
role?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Multi-role shape, and the canonical home for this fact: `@lenne.tech/nest-server`
|
|
19
|
+
* registers `roles` as a core Better-Auth additionalField (`type: 'string[]'`,
|
|
20
|
+
* `defaultValue: []`), so its users carry `roles: ['admin']` and usually NO
|
|
21
|
+
* singular `role`.
|
|
22
|
+
*
|
|
23
|
+
* NOT a source of authorization truth: this lives in the non-httpOnly
|
|
24
|
+
* `lt-auth-state` cookie and is therefore client-writable. It is kept
|
|
25
|
+
* fail-closed on session merge (see `AUTHZ_KEYS` in `use-lt-auth.ts`) so a
|
|
26
|
+
* backend downgrade is never masked by a stale value — that is a *staleness*
|
|
27
|
+
* guarantee, not a security guarantee. `isAdmin` reads it for UI gating;
|
|
28
|
+
* authorization is enforced server-side (`@Restricted(RoleEnum.ADMIN)`).
|
|
29
|
+
*/
|
|
30
|
+
roles?: string[];
|
|
16
31
|
twoFactorEnabled?: boolean;
|
|
17
32
|
}
|
|
18
33
|
/**
|
|
@@ -94,7 +109,50 @@ export interface UseLtAuthReturn {
|
|
|
94
109
|
isLoading: ComputedRef<boolean>;
|
|
95
110
|
jwtToken: Ref<string | null>;
|
|
96
111
|
user: ComputedRef<LtUser | null>;
|
|
112
|
+
/**
|
|
113
|
+
* Returns `true` when the current user has ANY of the given roles (union).
|
|
114
|
+
* Same shape-tolerance and UX-gate-only caveats as {@link UseLtAuthReturn.hasRole}.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* // <NuxtLink v-if="hasAnyRole('admin', 'editor')" to="/manage">Manage</NuxtLink>
|
|
118
|
+
*/
|
|
119
|
+
hasAnyRole: (...roles: string[]) => boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Returns `true` when the current user has `role` in EITHER supported shape:
|
|
122
|
+
* `role === role` (Better-Auth admin plugin) or `roles` containing it
|
|
123
|
+
* (`@lenne.tech/nest-server`; see {@link LtUser.roles}). A malformed non-array
|
|
124
|
+
* `roles` degrades to `false` — never throws, never fail-opens via
|
|
125
|
+
* `String.prototype.includes` substring matching. Reads `user` live, so call it
|
|
126
|
+
* from a template or `computed` for reactivity.
|
|
127
|
+
*
|
|
128
|
+
* Prefer this over the raw `user.value?.roles?.includes(x)`, which is unguarded
|
|
129
|
+
* against the string-`roles` substring-confusion this method closes.
|
|
130
|
+
*
|
|
131
|
+
* UX gate only — the `lt-auth-state` cache is client-writable; enforce rights
|
|
132
|
+
* server-side.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* // <button v-if="hasRole('editor')">Edit</button>
|
|
136
|
+
* @see {@link LtUser.role}
|
|
137
|
+
* @see {@link LtUser.roles}
|
|
138
|
+
*/
|
|
139
|
+
hasRole: (role: string) => boolean;
|
|
97
140
|
is2FAEnabled: ComputedRef<boolean>;
|
|
141
|
+
/**
|
|
142
|
+
* `true` when the current user is an admin in EITHER supported shape:
|
|
143
|
+
* `role === 'admin'` (Better-Auth admin plugin) or `roles` containing `'admin'`
|
|
144
|
+
* (`@lenne.tech/nest-server`; see {@link LtUser.roles}). Equivalent to
|
|
145
|
+
* `hasRole('admin')`. A malformed non-array `roles` degrades to `false` (never
|
|
146
|
+
* throws, never fail-opens). Independent of the `enableAdmin` module option,
|
|
147
|
+
* which only toggles the Better-Auth admin *client plugin*.
|
|
148
|
+
*
|
|
149
|
+
* UX gate only: the `lt-auth-state` cache it reads is client-writable, so treat
|
|
150
|
+
* this as "what to render", never as the gate itself. Enforce admin rights
|
|
151
|
+
* server-side.
|
|
152
|
+
*
|
|
153
|
+
* @see {@link UseLtAuthReturn.hasRole}
|
|
154
|
+
* @see {@link LtUser.roles}
|
|
155
|
+
*/
|
|
98
156
|
isAdmin: ComputedRef<boolean>;
|
|
99
157
|
features: ComputedRef<Record<string, boolean | number | string[]>>;
|
|
100
158
|
fetchFeatures: () => Promise<Record<string, boolean | number | string[]>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nuxt-extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
},
|
|
13
13
|
"author": "lenne.Tech <info@lenne.tech> (https://lenne.tech)",
|
|
14
14
|
"license": "MIT",
|
|
15
|
-
"packageManager": "pnpm@10.33.0",
|
|
16
15
|
"type": "module",
|
|
17
16
|
"sideEffects": false,
|
|
18
17
|
"exports": {
|
|
@@ -52,9 +51,9 @@
|
|
|
52
51
|
"prepack": "node scripts/sync-module-version.mjs && nuxt-module-build build",
|
|
53
52
|
"c": "pnpm run check",
|
|
54
53
|
"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",
|
|
56
|
-
"check:fix": "pnpm install && pnpm audit --fix && pnpm run format && pnpm run lint:fix && pnpm test:types && pnpm test && pnpm run build",
|
|
57
|
-
"check:naf": "pnpm install && pnpm run format && pnpm run lint:fix && pnpm test:types && pnpm test && pnpm run build",
|
|
54
|
+
"check:raw": "pnpm install --frozen-lockfile && pnpm audit && pnpm run format:check && pnpm run lint && pnpm run version:check && pnpm run dev:prepare && pnpm test:types && pnpm test && pnpm run build && pnpm run check:manifest",
|
|
55
|
+
"check:fix": "pnpm install && pnpm audit --fix && pnpm run format && pnpm run lint:fix && pnpm run version:sync && pnpm test:types && pnpm test && pnpm run build && pnpm run check:manifest",
|
|
56
|
+
"check:naf": "pnpm install && pnpm run format && pnpm run lint:fix && pnpm run version:sync && pnpm test:types && pnpm test && pnpm run build && pnpm run check:manifest",
|
|
58
57
|
"cf": "pnpm run check:fix",
|
|
59
58
|
"cnaf": "pnpm run check:naf",
|
|
60
59
|
"dev": "pnpm dev:prepare && nuxt dev playground",
|
|
@@ -65,20 +64,24 @@
|
|
|
65
64
|
"lint:fix": "oxlint --fix --fix-suggestions src/",
|
|
66
65
|
"format": "oxfmt --write src/",
|
|
67
66
|
"format:check": "oxfmt --check src/",
|
|
67
|
+
"version:sync": "node scripts/sync-module-version.mjs",
|
|
68
|
+
"version:check": "node scripts/sync-module-version.mjs --check",
|
|
68
69
|
"test": "vitest run",
|
|
69
70
|
"test:watch": "vitest watch",
|
|
70
71
|
"test:coverage": "vitest run --coverage",
|
|
71
72
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
|
|
72
|
-
"release": "pnpm format:check && pnpm lint && pnpm test:types && pnpm test && pnpm prepack"
|
|
73
|
+
"release": "pnpm format:check && pnpm lint && pnpm run version:check && pnpm test:types && pnpm test && pnpm run check:consumer && pnpm prepack",
|
|
74
|
+
"check:consumer": "node scripts/check-consumer-build.mjs",
|
|
75
|
+
"check:manifest": "node scripts/check-package-manifest.mjs"
|
|
73
76
|
},
|
|
74
77
|
"dependencies": {
|
|
75
|
-
"@nuxt/kit": "4.
|
|
78
|
+
"@nuxt/kit": "^4.0.0"
|
|
76
79
|
},
|
|
77
80
|
"peerDependencies": {
|
|
78
81
|
"@better-auth/passkey": ">=1.0.0",
|
|
79
82
|
"@playwright/test": ">=1.0.0",
|
|
80
83
|
"better-auth": ">=1.0.0",
|
|
81
|
-
"nuxt": "
|
|
84
|
+
"nuxt": "^4.0.0",
|
|
82
85
|
"tus-js-client": ">=4.0.0"
|
|
83
86
|
},
|
|
84
87
|
"peerDependenciesMeta": {
|
|
@@ -96,41 +99,21 @@
|
|
|
96
99
|
}
|
|
97
100
|
},
|
|
98
101
|
"devDependencies": {
|
|
99
|
-
"@better-auth/passkey": "1.6.
|
|
100
|
-
"@nuxt/devtools": "3.2.4",
|
|
102
|
+
"@better-auth/passkey": "1.6.23",
|
|
101
103
|
"@nuxt/module-builder": "1.0.2",
|
|
102
|
-
"@nuxt/schema": "4.4.
|
|
103
|
-
"@playwright/test": "1.
|
|
104
|
-
"@types/node": "
|
|
105
|
-
"@vitest/coverage-v8": "4.1.
|
|
106
|
-
"better-auth": "1.6.
|
|
107
|
-
"happy-dom": "20.
|
|
108
|
-
"nuxt": "4.4.
|
|
109
|
-
"oxfmt": "0.
|
|
110
|
-
"oxlint": "1.
|
|
104
|
+
"@nuxt/schema": "4.4.8",
|
|
105
|
+
"@playwright/test": "1.61.1",
|
|
106
|
+
"@types/node": "26.1.1",
|
|
107
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
108
|
+
"better-auth": "1.6.23",
|
|
109
|
+
"happy-dom": "20.10.6",
|
|
110
|
+
"nuxt": "4.4.8",
|
|
111
|
+
"oxfmt": "0.59.0",
|
|
112
|
+
"oxlint": "1.74.0",
|
|
111
113
|
"tus-js-client": "4.3.1",
|
|
112
114
|
"typescript": "5.9.3",
|
|
113
|
-
"vitest": "4.1.
|
|
114
|
-
"vue-tsc": "3.3.
|
|
115
|
-
},
|
|
116
|
-
"pnpm": {
|
|
117
|
-
"onlyBuiltDependencies": [
|
|
118
|
-
"@parcel/watcher",
|
|
119
|
-
"esbuild"
|
|
120
|
-
],
|
|
121
|
-
"overrides": {
|
|
122
|
-
"brace-expansion@>=5.0.0 <5.0.6": "5.0.6",
|
|
123
|
-
"devalue@>=5.6.3 <=5.8.0": "5.8.1",
|
|
124
|
-
"kysely@>=0.26.0 <0.28.17": "0.28.17",
|
|
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"
|
|
133
|
-
}
|
|
115
|
+
"vitest": "4.1.10",
|
|
116
|
+
"vue-tsc": "3.3.7"
|
|
134
117
|
},
|
|
135
118
|
"keywords": [
|
|
136
119
|
"nuxt",
|
|
@@ -144,5 +127,9 @@
|
|
|
144
127
|
"tus-upload",
|
|
145
128
|
"file-upload",
|
|
146
129
|
"lenne-tech"
|
|
147
|
-
]
|
|
130
|
+
],
|
|
131
|
+
"engines": {
|
|
132
|
+
"node": ">= 22",
|
|
133
|
+
"pnpm": "^11.0.0"
|
|
134
|
+
}
|
|
148
135
|
}
|