@lenne.tech/nuxt-extensions 1.24.0 → 1.25.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 +29 -2
- package/README.md +46 -1
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/composables/auth/use-system-setup.d.ts +18 -4
- package/dist/runtime/composables/auth/use-system-setup.js +32 -7
- package/dist/runtime/lib/sync-hints.js +3 -3
- package/dist/runtime/middleware/setup.d.ts +8 -1
- package/dist/runtime/middleware/setup.js +2 -2
- package/package.json +3 -3
package/CLAUDE.md
CHANGED
|
@@ -27,7 +27,7 @@ src/
|
|
|
27
27
|
|-----------|---------|
|
|
28
28
|
| `useLtAuth()` | Authentication state, login, logout, session management, password change + reset |
|
|
29
29
|
| `useLtAuthClient()` | Raw Better Auth client access (`twoFactor.*`, `admin.*`, `passkey`) |
|
|
30
|
-
| `useSystemSetup()` | First-admin setup flow |
|
|
30
|
+
| `useSystemSetup()` | First-admin setup flow: status, HTTP-setup release + setup token (nest-server >= 12.4.0) |
|
|
31
31
|
| `useLtTusUpload()` | TUS resumable file uploads |
|
|
32
32
|
| `useLtFile()` | File utilities (size formatting, URLs) |
|
|
33
33
|
| `useLtShare()` | Web Share API with clipboard fallback |
|
|
@@ -311,7 +311,8 @@ This is a consequence of client-side hashing, not a bug in either backend, and t
|
|
|
311
311
|
### `admin.*` is passed through UNHASHED — an armed trap, not a live one
|
|
312
312
|
|
|
313
313
|
`admin.createUser` (`password`) and `admin.setUserPassword` (`newPassword`) are the only
|
|
314
|
-
|
|
314
|
+
passwords this library does **not** hash. (The setup token of `useSystemSetup().initSetup()` is
|
|
315
|
+
unhashed too, by design — see "First Admin Setup" below.)
|
|
315
316
|
|
|
316
317
|
Today that is harmless: `@lenne.tech/nest-server` does not register better-auth's `admin()`
|
|
317
318
|
plugin and has no option to enable it, so those routes 404. But `auth.enableAdmin` defaults to
|
|
@@ -333,6 +334,28 @@ replaces ONE field and forwards the rest — `{ ...params, password: hashed }`.
|
|
|
333
334
|
shipped defects), and never `{ password: hashed, ...params }`, which looks almost identical and
|
|
334
335
|
puts the **plaintext** back on the wire.
|
|
335
336
|
|
|
337
|
+
## First Admin Setup: the HTTP init is closed on deployed stages
|
|
338
|
+
|
|
339
|
+
Since nest-server 12.4.0 the anonymous `POST /system-setup/init` is open only in `local`, `ci` and
|
|
340
|
+
`e2e`. A deployed API refuses it with 403 (`LTNS_0053`) unless `systemSetup.httpInit` releases it,
|
|
341
|
+
optionally behind a setup token (`LTNS_0054` when wrong). `GET /system-setup/status` reports
|
|
342
|
+
`httpInitEnabled` and `httpInitTokenRequired`; `useSystemSetup()` exposes both.
|
|
343
|
+
|
|
344
|
+
- **The middleware does not redirect when `httpInitEnabled === false`.** That is deliberate, not a
|
|
345
|
+
bug: a redirect would hold every visitor on a page whose only action fails, and the login would be
|
|
346
|
+
unreachable. The setup page itself must show the notice ("first admin comes from the
|
|
347
|
+
configuration") instead of the form.
|
|
348
|
+
- **The token is a header (`X-System-Setup-Token`), unhashed, never a body field.** Unlike the
|
|
349
|
+
passwords, it is not hashed: the server compares it as given. Do not add it to the SHA256
|
|
350
|
+
wrappers.
|
|
351
|
+
- **A missing field means an API before 12.4.0**, which accepts the HTTP setup: `httpInitEnabled`
|
|
352
|
+
reads `true`. The server's 403 is the boundary, the flag is UX. Only a 200 without the field
|
|
353
|
+
counts as old; a failed status read leaves both values unchanged (`null` initially).
|
|
354
|
+
- **After a 403 from `initSetup()` the composable re-reads the status** before rethrowing, so the
|
|
355
|
+
page switches to the notice or the token field when the API was restarted with another
|
|
356
|
+
configuration. Pass the error itself to `translateError(error)`: the `#LTNS_` code is in the
|
|
357
|
+
body, `error.message` holds only the transport line.
|
|
358
|
+
|
|
336
359
|
## Form Label Repair
|
|
337
360
|
|
|
338
361
|
**The defect:** Nuxt UI's `FormField` derives the label's `for` and the control's `id` from one
|
|
@@ -433,6 +456,10 @@ fields at `app:beforeMount` and restores them at `app:mounted`, dispatching a sy
|
|
|
433
456
|
relative `extends` points at nothing, and `oxlint --type-aware` then stops every vendored project's
|
|
434
457
|
`check` with `Invalid tsconfig`. Nitro code is typechecked through the root `tsconfig.server.json`.
|
|
435
458
|
No gate here runs in vendor mode, so `test/vendored-core.test.ts` holds this.
|
|
459
|
+
- **nuxt-base-starter lints `app/core/**` with a subset of the plugins in `.oxlintrc.json`, never
|
|
460
|
+
more**, and `lint` here runs with `--deny-warnings`. A plugin only the starter applies reports its
|
|
461
|
+
warnings in every generated project after the release; the 1.24.0 smoke test found three. A plugin
|
|
462
|
+
the starter wants for `app/core/**` is added here first.
|
|
436
463
|
- **The `nuxt/schema` blocks in `runtime/types/module.ts` augment `PublicRuntimeConfig` and nothing
|
|
437
464
|
else.** Vendoring turns them into project source, where they cause TS2310, and the cli removes a
|
|
438
465
|
block only while it holds that one interface. Type server-only config where it is read.
|
package/README.md
CHANGED
|
@@ -567,6 +567,51 @@ against the value it receives, and because this library hashes first, that value
|
|
|
567
567
|
64-character hex string — the check passes on every path. The form is the only place the rule can
|
|
568
568
|
live.
|
|
569
569
|
|
|
570
|
+
### First Admin Setup
|
|
571
|
+
|
|
572
|
+
With `auth.systemSetup.enabled`, a global middleware sends visitors to `setupPath` while the API
|
|
573
|
+
has no user yet, and `useSystemSetup()` creates the first admin.
|
|
574
|
+
|
|
575
|
+
Since `@lenne.tech/nest-server` 12.4.0 the anonymous `POST /system-setup/init` is open only in
|
|
576
|
+
`local`, `ci` and `e2e`. A deployed API refuses it with 403 (`LTNS_0053`) unless the deployment
|
|
577
|
+
releases it (`systemSetup.httpInit`), optionally behind a setup token. A deployment usually creates
|
|
578
|
+
its first admin from its configuration instead (`systemSetup.initialAdmin`).
|
|
579
|
+
`GET /system-setup/status` reports which applies, and the composable exposes it:
|
|
580
|
+
|
|
581
|
+
```vue
|
|
582
|
+
<script setup lang="ts">
|
|
583
|
+
const { httpInitEnabled, httpInitTokenRequired, initSetup } = useSystemSetup();
|
|
584
|
+
const { translateError } = useLtErrorTranslation();
|
|
585
|
+
const toast = useToast();
|
|
586
|
+
|
|
587
|
+
async function onSubmit(data: { email: string; name: string; password: string; token?: string }) {
|
|
588
|
+
try {
|
|
589
|
+
// `token` goes out as the X-System-Setup-Token header, unhashed; omit it when not required.
|
|
590
|
+
await initSetup(data);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
// Pass the error itself: the #LTNS_ code is in the response body, not in `error.message`.
|
|
593
|
+
toast.add({ color: 'error', description: translateError(error), title: 'Setup failed' });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
</script>
|
|
597
|
+
|
|
598
|
+
<template>
|
|
599
|
+
<!-- `false`: the deployment creates its first admin from its configuration — no form. -->
|
|
600
|
+
<SetupNotice v-if="httpInitEnabled === false" />
|
|
601
|
+
<SetupForm v-else :with-token="httpInitTokenRequired === true" @submit="onSubmit" />
|
|
602
|
+
</template>
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
- **Without the HTTP setup the middleware redirects nowhere.** A redirect would hold every visitor
|
|
606
|
+
on a page whose only action fails. The setup page stays reachable, so it must show the notice
|
|
607
|
+
itself.
|
|
608
|
+
- **An API before 12.4.0** reports neither field and accepts the HTTP setup, so `httpInitEnabled`
|
|
609
|
+
reads `true` and `httpInitTokenRequired` `false` — the behaviour before this release.
|
|
610
|
+
- **After a 403 from `initSetup()`** the composable re-reads the status before rethrowing, so a page
|
|
611
|
+
whose API was restarted with another configuration switches to the notice (`LTNS_0053`) or to the
|
|
612
|
+
token field (`LTNS_0054`) without a reload.
|
|
613
|
+
- `null` means the status was not read yet, or reading it failed.
|
|
614
|
+
|
|
570
615
|
### Custom Better Auth Plugins
|
|
571
616
|
|
|
572
617
|
You can extend the auth client with additional [Better Auth plugins](https://www.better-auth.com/docs/plugins):
|
|
@@ -975,7 +1020,7 @@ The package works **with or without** `@nuxtjs/i18n`:
|
|
|
975
1020
|
| `useLtTusUpload()` | TUS protocol file uploads with pause/resume |
|
|
976
1021
|
| `useLtFile()` | File utilities (size formatting, URLs) |
|
|
977
1022
|
| `useLtShare()` | Web Share API with clipboard fallback |
|
|
978
|
-
| `useSystemSetup()` |
|
|
1023
|
+
| `useSystemSetup()` | First admin creation: setup status, HTTP-setup release + token (nest-server >= 12.4.0) |
|
|
979
1024
|
| `useLtAi()` | One-shot `prompt()` + streaming `promptStream()` for the nest-server AI module |
|
|
980
1025
|
| `useLtAiChat()` | Multi-turn chat state, streaming, budget summary, confirmation gate, `maxMessages` cap |
|
|
981
1026
|
| `useLtAiConnections()` | User self-service connection list + `select()` |
|
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.25.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
|
@@ -3,7 +3,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
3
3
|
import { SECURITY_TXT_DEFAULT_VALID_DAYS, SECURITY_TXT_DEFAULT_LANGUAGES, SECURITY_TXT_DEFAULT_CONTACT, SECURITY_TXT_PATH } from '../dist/runtime/server/lib/security-txt.js';
|
|
4
4
|
|
|
5
5
|
const name = "@lenne.tech/nuxt-extensions";
|
|
6
|
-
const version = "1.
|
|
6
|
+
const version = "1.25.0";
|
|
7
7
|
const configKey = "ltExtensions";
|
|
8
8
|
const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
|
|
9
9
|
const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
|
|
@@ -12,13 +12,27 @@ import type { ComputedRef } from 'vue';
|
|
|
12
12
|
export interface UseSystemSetupReturn {
|
|
13
13
|
/** Whether the system needs initial setup (null = not checked yet) */
|
|
14
14
|
needsSetup: ComputedRef<boolean | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the API accepts the setup over HTTP (null = not checked yet). `false` means the
|
|
17
|
+
* deployment creates its first admin from its configuration, and `initSetup()` would be
|
|
18
|
+
* refused with 403 (`LTNS_0053`): show a notice instead of the form. An API before
|
|
19
|
+
* nest-server 12.4.0 does not report it and accepts the HTTP setup, so it reads as `true`.
|
|
20
|
+
*/
|
|
21
|
+
httpInitEnabled: ComputedRef<boolean | null>;
|
|
22
|
+
/** Whether `initSetup()` needs a `token` (null = not checked yet). Only an explicit `true` counts. */
|
|
23
|
+
httpInitTokenRequired: ComputedRef<boolean | null>;
|
|
15
24
|
/** Check setup status from the backend */
|
|
16
25
|
checkSetupStatus: () => Promise<boolean>;
|
|
17
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the system with the first admin user. `token` is the setup token, required when
|
|
28
|
+
* `httpInitTokenRequired` is true. A refusal is rethrown unchanged, so `useLtErrorTranslation()`
|
|
29
|
+
* reads its code (`LTNS_0053` HTTP setup disabled, `LTNS_0054` wrong token) from the body.
|
|
30
|
+
*/
|
|
18
31
|
initSetup: (params: {
|
|
19
32
|
email: string;
|
|
20
33
|
password: string;
|
|
21
34
|
name: string;
|
|
35
|
+
token?: string;
|
|
22
36
|
}) => Promise<void>;
|
|
23
37
|
}
|
|
24
38
|
/**
|
|
@@ -26,11 +40,11 @@ export interface UseSystemSetupReturn {
|
|
|
26
40
|
*
|
|
27
41
|
* @example
|
|
28
42
|
* ```typescript
|
|
29
|
-
* const { needsSetup, checkSetupStatus, initSetup } = useSystemSetup();
|
|
43
|
+
* const { needsSetup, httpInitEnabled, httpInitTokenRequired, checkSetupStatus, initSetup } = useSystemSetup();
|
|
30
44
|
*
|
|
31
45
|
* await checkSetupStatus();
|
|
32
|
-
* if (needsSetup.value) {
|
|
33
|
-
* await initSetup({ email: 'admin@example.com', password: 'secret', name: 'Admin' });
|
|
46
|
+
* if (needsSetup.value && httpInitEnabled.value) {
|
|
47
|
+
* await initSetup({ email: 'admin@example.com', password: 'secret', name: 'Admin', token });
|
|
34
48
|
* }
|
|
35
49
|
* ```
|
|
36
50
|
*/
|
|
@@ -8,9 +8,17 @@ function isApiUrlMissing() {
|
|
|
8
8
|
return true;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
const SETUP_TOKEN_HEADER = "X-System-Setup-Token";
|
|
12
|
+
function isRefusal(error) {
|
|
13
|
+
const { status, statusCode } = error ?? {};
|
|
14
|
+
return (statusCode ?? status) === 403;
|
|
15
|
+
}
|
|
11
16
|
export function useSystemSetup() {
|
|
12
17
|
const needsSetupState = useState("lt-needs-setup", () => null);
|
|
18
|
+
const httpInitState = useState("lt-system-setup-http-init", () => null);
|
|
13
19
|
const needsSetup = computed(() => needsSetupState.value);
|
|
20
|
+
const httpInitEnabled = computed(() => httpInitState.value?.enabled ?? null);
|
|
21
|
+
const httpInitTokenRequired = computed(() => httpInitState.value?.tokenRequired ?? null);
|
|
14
22
|
async function checkSetupStatus() {
|
|
15
23
|
if (import.meta.server && isApiUrlMissing()) {
|
|
16
24
|
warnMissingLtApiUrl("server");
|
|
@@ -19,6 +27,12 @@ export function useSystemSetup() {
|
|
|
19
27
|
try {
|
|
20
28
|
const url = buildLtApiUrl("/system-setup/status");
|
|
21
29
|
const data = await $fetch(url);
|
|
30
|
+
httpInitState.value = {
|
|
31
|
+
// Missing or malformed reads as released: an API before 12.4.0 accepts the HTTP setup,
|
|
32
|
+
// and the server's 403 is the boundary, not this flag.
|
|
33
|
+
enabled: typeof data.httpInitEnabled === "boolean" ? data.httpInitEnabled : true,
|
|
34
|
+
tokenRequired: data.httpInitTokenRequired === true
|
|
35
|
+
};
|
|
22
36
|
needsSetupState.value = data.needsSetup;
|
|
23
37
|
return data.needsSetup;
|
|
24
38
|
} catch {
|
|
@@ -29,18 +43,29 @@ export function useSystemSetup() {
|
|
|
29
43
|
async function initSetup(params) {
|
|
30
44
|
const url = buildLtApiUrl("/system-setup/init");
|
|
31
45
|
const hashedPassword = await ltSha256(params.password);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
const token = params.token?.trim();
|
|
47
|
+
try {
|
|
48
|
+
await $fetch(url, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
body: {
|
|
51
|
+
email: params.email,
|
|
52
|
+
password: hashedPassword,
|
|
53
|
+
name: params.name
|
|
54
|
+
},
|
|
55
|
+
...token ? { headers: { [SETUP_TOKEN_HEADER]: token } } : {}
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (isRefusal(error)) {
|
|
59
|
+
await checkSetupStatus();
|
|
38
60
|
}
|
|
39
|
-
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
40
63
|
needsSetupState.value = false;
|
|
41
64
|
}
|
|
42
65
|
return {
|
|
43
66
|
needsSetup,
|
|
67
|
+
httpInitEnabled,
|
|
68
|
+
httpInitTokenRequired,
|
|
44
69
|
checkSetupStatus,
|
|
45
70
|
initSetup
|
|
46
71
|
};
|
|
@@ -74,15 +74,15 @@ function connectHub(key, hub) {
|
|
|
74
74
|
hub.close = openLtSyncHints({
|
|
75
75
|
models: models.split(","),
|
|
76
76
|
onConnect: () => {
|
|
77
|
-
for (const subscriber of
|
|
77
|
+
for (const subscriber of Array.from(hub.subscribers)) subscriber.onConnect();
|
|
78
78
|
},
|
|
79
79
|
onHint: (model) => {
|
|
80
|
-
for (const subscriber of
|
|
80
|
+
for (const subscriber of Array.from(hub.subscribers)) if (subscriber.model === model) subscriber.onHint();
|
|
81
81
|
},
|
|
82
82
|
onStop: (reason) => {
|
|
83
83
|
hub.close = void 0;
|
|
84
84
|
hub.models = "";
|
|
85
|
-
for (const subscriber of
|
|
85
|
+
for (const subscriber of Array.from(hub.subscribers)) subscriber.onStop?.(reason);
|
|
86
86
|
},
|
|
87
87
|
tenantId: key || void 0
|
|
88
88
|
});
|
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* needs initial setup (no admin user exists yet).
|
|
6
6
|
*
|
|
7
7
|
* - needsSetup === null -> calls checkSetupStatus()
|
|
8
|
-
* - needsSetup === true AND route != setupPath -> redirect to setupPath
|
|
8
|
+
* - needsSetup === true AND HTTP init released AND route != setupPath -> redirect to setupPath
|
|
9
|
+
* - needsSetup === true AND HTTP init NOT released -> no redirect
|
|
9
10
|
* - needsSetup === false AND route === setupPath -> redirect to loginPath
|
|
11
|
+
*
|
|
12
|
+
* Without the HTTP init (nest-server >= 12.4.0 closes it outside `local`, `ci` and `e2e`), the
|
|
13
|
+
* API refuses `POST /system-setup/init` with 403 and the deployment creates its first admin from
|
|
14
|
+
* its configuration. A redirect would then hold every visitor on a page whose only action fails,
|
|
15
|
+
* with the login out of reach. So nothing is redirected; the setup page stays reachable and shows
|
|
16
|
+
* its notice there (`useSystemSetup().httpInitEnabled === false`).
|
|
10
17
|
*/
|
|
11
18
|
import type { RouteMiddleware } from '#app';
|
|
12
19
|
declare const _default: RouteMiddleware;
|
|
@@ -5,12 +5,12 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|
|
5
5
|
const authConfig = runtimeConfig.public?.ltExtensions?.auth;
|
|
6
6
|
const setupPath = authConfig?.systemSetup?.setupPath || "/auth/setup";
|
|
7
7
|
const loginPath = authConfig?.loginPath || "/auth/login";
|
|
8
|
-
const { needsSetup, checkSetupStatus } = useSystemSetup();
|
|
8
|
+
const { needsSetup, httpInitEnabled, checkSetupStatus } = useSystemSetup();
|
|
9
9
|
try {
|
|
10
10
|
if (needsSetup.value === null) {
|
|
11
11
|
await checkSetupStatus();
|
|
12
12
|
}
|
|
13
|
-
if (needsSetup.value === true && to.path !== setupPath) {
|
|
13
|
+
if (needsSetup.value === true && httpInitEnabled.value !== false && to.path !== setupPath) {
|
|
14
14
|
return navigateTo(setupPath);
|
|
15
15
|
}
|
|
16
16
|
if (needsSetup.value === false && to.path === setupPath) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nuxt-extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"dev:build": "nuxt build playground",
|
|
61
61
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
62
62
|
"build": "nuxt-module-build build",
|
|
63
|
-
"lint": "oxlint --type-aware src/ scripts/",
|
|
64
|
-
"lint:fix": "oxlint --type-aware --fix src/ scripts/",
|
|
63
|
+
"lint": "oxlint --type-aware --deny-warnings src/ scripts/",
|
|
64
|
+
"lint:fix": "oxlint --type-aware --deny-warnings --fix src/ scripts/",
|
|
65
65
|
"format": "oxfmt --write src/",
|
|
66
66
|
"format:check": "oxfmt --check src/",
|
|
67
67
|
"version:sync": "node scripts/sync-module-version.mjs",
|