@lenne.tech/nuxt-extensions 1.21.0 → 1.23.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 +3 -1
- package/README.md +105 -15
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +3 -1
- package/dist/runtime/composables/index.d.ts +2 -0
- package/dist/runtime/composables/index.js +2 -0
- package/dist/runtime/composables/use-lt-ai-settings.d.ts +12 -0
- package/dist/runtime/composables/use-lt-ai-settings.js +32 -0
- package/dist/runtime/composables/use-lt-ai-status.d.ts +16 -0
- package/dist/runtime/composables/use-lt-ai-status.js +57 -0
- package/dist/runtime/lib/auth-client.d.ts +25 -1
- package/dist/runtime/lib/passkey-stub.d.ts +4 -0
- package/dist/runtime/lib/passkey-stub.js +4 -0
- package/dist/runtime/types/ai.d.ts +42 -0
- package/dist/runtime/types/index.d.ts +1 -1
- package/package.json +9 -9
package/CLAUDE.md
CHANGED
|
@@ -40,6 +40,8 @@ src/
|
|
|
40
40
|
| `useLtAi()` | One-shot `prompt()` + streaming `promptStream()` (POST `/ai/prompt` / `/ai/stream`) |
|
|
41
41
|
| `useLtAiChat()` | Multi-turn chat: streaming, budget summary, confirmation gate, `stop()`/`clear()`, optional `maxMessages` cap, auto-stop on unmount |
|
|
42
42
|
| `useLtAiConnections()` | User-facing list of available connections + `select()` |
|
|
43
|
+
| `useLtAiSettings()` | The user's own AI settings: MCP confirmation switch (`GET/PUT /ai/settings`, `enforced` → show disabled) |
|
|
44
|
+
| `useLtAiStatus()` | Whether the API's AI module is on (`GET /ai/status`, public; 404 = off): `enabled`, `mcp`, shared `useState`, one request per app start, fails closed |
|
|
43
45
|
| `useLtAiUsage()` | Full token/prompt usage breakdown (`GET /ai/usage`) |
|
|
44
46
|
| `useLtAiPrompts()` | User-facing CRUD for re-usable prompt snippets ("Vorlagen", `scope: 'user'` / `'tenant'`) |
|
|
45
47
|
| `useLtAiPlaceholders()` | Loads the backend's `{{placeholder}}` registry for editor sidebars |
|
|
@@ -411,7 +413,7 @@ fields at `app:beforeMount` and restores them at `app:mounted`, dispatching a sy
|
|
|
411
413
|
Handling" below).
|
|
412
414
|
3. **Check existing composables** before creating new ones — this module may already provide what you need
|
|
413
415
|
4. **Peer dependencies** (`better-auth`, `@better-auth/passkey`, `tus-js-client`) must be installed in the consuming project.
|
|
414
|
-
`better-auth` and `@better-auth/passkey` are pinned to **`>=1.7.
|
|
416
|
+
`better-auth` and `@better-auth/passkey` are pinned to **`>=1.7.16 <1.8.0`** — deliberately not a caret.
|
|
415
417
|
**They move in lock-step with `@lenne.tech/nest-server`.** better-auth is one protocol with two ends;
|
|
416
418
|
this module is the client end, nest-server the server end, and both declare the *same* narrow range.
|
|
417
419
|
Raising it means raising it in **both** framework repos and pinning the new version in **both** starters,
|
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.t
|
|
|
29
29
|
|---|---|---|
|
|
30
30
|
| Node | `>= 22` | |
|
|
31
31
|
| Nuxt | `^4.0.0` | |
|
|
32
|
-
| `better-auth` | `>=1.7.
|
|
33
|
-
| `@better-auth/passkey` | `>=1.7.
|
|
32
|
+
| `better-auth` | `>=1.7.16 <1.8.0` | **required peer** — pin it, see below |
|
|
33
|
+
| `@better-auth/passkey` | `>=1.7.16 <1.8.0` | optional peer, passkey/WebAuthn only |
|
|
34
34
|
| `tus-js-client` | `>=4.0.0` | optional peer, resumable uploads only |
|
|
35
35
|
| `@lenne.tech/nest-server` | `>= 11.37.0` | **only if you use the backend** — see the lock-step rule |
|
|
36
36
|
|
|
@@ -38,7 +38,7 @@ Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.t
|
|
|
38
38
|
better-auth is one protocol with two ends. This module is the client end and
|
|
39
39
|
`@lenne.tech/nest-server` is the server end, and they must resolve the **same**
|
|
40
40
|
better-auth minor. better-auth breaks in *minor* releases — 1.7 removed subpath
|
|
41
|
-
exports and changed the 2FA response shape — so a caret (`^1.7.
|
|
41
|
+
exports and changed the 2FA response shape — so a caret (`^1.7.16`) would re-open the
|
|
42
42
|
hole at 1.8. If the api and the app drift apart, 2FA activation fails at runtime with a
|
|
43
43
|
generic client error while **both** projects' checks stay green: each is internally
|
|
44
44
|
consistent, and only the assembled workspace has both halves.
|
|
@@ -46,21 +46,35 @@ consistent, and only the assembled workspace has both halves.
|
|
|
46
46
|
Raising better-auth therefore means raising it in `@lenne.tech/nest-server` **and**
|
|
47
47
|
`@lenne.tech/nuxt-extensions` in one step.
|
|
48
48
|
|
|
49
|
-
**Why the floor is 1.7.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
(
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
**Why the floor is 1.7.16:** better-auth 1.7.16 closes twelve advisories published on
|
|
50
|
+
2026-11-03. Among those that reach a typical lt project: a social sign-in can treat an unverified
|
|
51
|
+
provider email as verified
|
|
52
|
+
([GHSA-9hm7-xhfp-cx58](https://github.com/better-auth/better-auth/security/advisories/GHSA-9hm7-xhfp-cx58)),
|
|
53
|
+
revoked sessions can stay valid with secondary storage or the cookie cache
|
|
54
|
+
([GHSA-fvxx-76fc-893x](https://github.com/better-auth/better-auth/security/advisories/GHSA-fvxx-76fc-893x)),
|
|
55
|
+
email-change links stay valid after the address changes hands
|
|
56
|
+
([GHSA-hwq2-v29r-p69r](https://github.com/better-auth/better-auth/security/advisories/GHSA-hwq2-v29r-p69r)),
|
|
57
|
+
and passkey verification does not enforce origin or user verification
|
|
58
|
+
([GHSA-gv75-j3v8-3crm](https://github.com/better-auth/better-auth/security/advisories/GHSA-gv75-j3v8-3crm)).
|
|
59
|
+
It also pins better-call 1.4.5, which closes
|
|
60
|
+
[GHSA-23w8-wx9r-vm57](https://github.com/better-auth/better-call/security/advisories/GHSA-23w8-wx9r-vm57)
|
|
61
|
+
(high, shared router context). That one is a second layer only: better-auth's own handler builds a
|
|
62
|
+
fresh router per request, so the flaw is not reachable through it. Older flaws stay behind the
|
|
63
|
+
floor too: before
|
|
64
|
+
1.7.12 better-auth verifies JWTs with the algorithm named in the token's own header
|
|
65
|
+
([#11925](https://github.com/better-auth/better-auth/pull/11925)), and below 1.7.7 the OAuth
|
|
66
|
+
state can be used as a magic link to sign in as another user
|
|
67
|
+
([GHSA-965c-763c-88jm](https://github.com/better-auth/better-auth/security/advisories/GHSA-965c-763c-88jm),
|
|
68
|
+
critical). All of these are in the server end, so your API must run 1.7.16 too
|
|
69
|
+
(`@lenne.tech/nest-server` 12.3.1 declares the same range), and the pin below keeps both halves
|
|
70
|
+
on it.
|
|
57
71
|
|
|
58
72
|
## Installation
|
|
59
73
|
|
|
60
74
|
```bash
|
|
61
|
-
npm install @lenne.tech/nuxt-extensions better-auth@1.7.
|
|
75
|
+
npm install @lenne.tech/nuxt-extensions better-auth@1.7.16
|
|
62
76
|
# Optional: For passkey support
|
|
63
|
-
npm install @better-auth/passkey@1.7.
|
|
77
|
+
npm install @better-auth/passkey@1.7.16
|
|
64
78
|
# Optional: For TUS file uploads
|
|
65
79
|
npm install tus-js-client
|
|
66
80
|
```
|
|
@@ -70,8 +84,9 @@ In a fullstack monorepo, pin it workspace-wide so the api and the app cannot dri
|
|
|
70
84
|
```yaml
|
|
71
85
|
# pnpm-workspace.yaml
|
|
72
86
|
overrides:
|
|
73
|
-
better-auth: 1.7.
|
|
74
|
-
'@better-auth/passkey': 1.7.
|
|
87
|
+
better-auth: 1.7.16
|
|
88
|
+
'@better-auth/passkey': 1.7.16
|
|
89
|
+
'@better-auth/core': 1.7.16 # the api of nest-server-starter projects depends on it directly
|
|
75
90
|
```
|
|
76
91
|
|
|
77
92
|
## Features
|
|
@@ -709,6 +724,77 @@ await load();
|
|
|
709
724
|
is private to the owner; `scope: 'tenant'` is shared across the owner's tenant. The list
|
|
710
725
|
returned by `load()` is already server-filtered to what the user is allowed to see.
|
|
711
726
|
|
|
727
|
+
### AI status
|
|
728
|
+
|
|
729
|
+
`useLtAiStatus()` says whether the API's AI module is on, so AI pages and navigation entries can
|
|
730
|
+
be shown only then. It reads the public `GET /ai/status` of nest-server: 200 `{ mcp }` while the
|
|
731
|
+
module is on, 404 while it is off.
|
|
732
|
+
|
|
733
|
+
```typescript
|
|
734
|
+
// middleware/ai.ts
|
|
735
|
+
export default defineNuxtRouteMiddleware(async () => {
|
|
736
|
+
const status = useLtAiStatus();
|
|
737
|
+
await status.load();
|
|
738
|
+
if (!status.enabled.value) {
|
|
739
|
+
return navigateTo('/app');
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
- One request per app start. The state is shared (`useState`) and travels from SSR to the
|
|
745
|
+
client, and callers that load at the same time share one request. `load({ force: true })` asks
|
|
746
|
+
again.
|
|
747
|
+
- Sent without credentials; the endpoint is public.
|
|
748
|
+
- Fails closed. A 404 means "AI is off" and sets no `error`. A network error, a timeout (5 s),
|
|
749
|
+
any other status or a malformed body leaves `enabled` false and sets `error`.
|
|
750
|
+
- `GET /ai/status` exists from nest-server 12.3.1. An older API answers 404 as well, so AI reads
|
|
751
|
+
as off there. Gate on this only once your API runs 12.3.1 or later.
|
|
752
|
+
- `mcp` says whether the API serves MCP. Show the MCP confirmation switch below only then.
|
|
753
|
+
|
|
754
|
+
### MCP confirmation switch
|
|
755
|
+
|
|
756
|
+
Over MCP (Claude Desktop, Claude Code and other clients), nest-server 12.3.0 or later asks the
|
|
757
|
+
user before a destructive tool runs, and before a mutating one if the server is configured that
|
|
758
|
+
way. Each user may switch the question off for themselves, like a "bypass permissions" mode.
|
|
759
|
+
`useLtAiSettings()` reads and changes that switch (`GET/PUT /ai/settings`):
|
|
760
|
+
|
|
761
|
+
```vue
|
|
762
|
+
<script setup lang="ts">
|
|
763
|
+
const { enforced, load, settings, update } = useLtAiSettings();
|
|
764
|
+
await load();
|
|
765
|
+
|
|
766
|
+
async function toggle(value: boolean) {
|
|
767
|
+
try {
|
|
768
|
+
await update({ mcpConfirmation: value });
|
|
769
|
+
} catch {
|
|
770
|
+
// `error` holds the message; `settings` keeps the previous value, so the switch snaps back
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
</script>
|
|
774
|
+
|
|
775
|
+
<template>
|
|
776
|
+
<USwitch
|
|
777
|
+
v-if="settings"
|
|
778
|
+
:model-value="settings.mcpConfirmation"
|
|
779
|
+
:disabled="enforced"
|
|
780
|
+
label="Ask before destructive AI actions over MCP"
|
|
781
|
+
@update:model-value="toggle"
|
|
782
|
+
/>
|
|
783
|
+
</template>
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
- Mount the switch only once `settings` is loaded (`v-if`). reka-ui's `Switch` decides at mount
|
|
787
|
+
whether it is controlled: mounted with `undefined`, for example while `onMounted(load)` is still
|
|
788
|
+
running, it keeps its own state, and a refused change then stays flipped although `settings`
|
|
789
|
+
kept the old value.
|
|
790
|
+
- `settings` is always the value as it takes effect, never just the stored choice. `update()`
|
|
791
|
+
adopts the server's answer instead of the input.
|
|
792
|
+
- `enforced` is true when the server fixed the value (`ai.mcp.confirmation.enforced`). Show the
|
|
793
|
+
switch disabled. A change is refused with 403 `#LTNS_0609`, which `update()` throws.
|
|
794
|
+
- Switching it off lets an MCP client run destructive tools in this user's name without asking.
|
|
795
|
+
Say so next to the switch. The tool's own `authorize()` check and the admin's `deny` / `ask`
|
|
796
|
+
rules still apply, and other users and the in-app chat are not affected.
|
|
797
|
+
|
|
712
798
|
### Lower-level + supporting composables
|
|
713
799
|
|
|
714
800
|
| Composable | Purpose |
|
|
@@ -716,6 +802,8 @@ returned by `load()` is already server-filtered to what the user is allowed to s
|
|
|
716
802
|
| `useLtAi()` | One-shot `prompt(input)` and streaming `promptStream(input, handlers)` (POST `/ai/stream`) |
|
|
717
803
|
| `useLtAiChat()` | Multi-turn chat state, streaming, budget, confirmation, `stop()`/`clear()`, optional `maxMessages` cap, auto-stop on component unmount |
|
|
718
804
|
| `useLtAiConnections()` | User self-service: available connections + `select()` (`selected`/`locked`) |
|
|
805
|
+
| `useLtAiSettings()` | The user's own AI settings: the MCP confirmation switch + `update()` (`enforced`) |
|
|
806
|
+
| `useLtAiStatus()` | Whether the API's AI module is on (`enabled`, `mcp`), shared state, one request per app start |
|
|
719
807
|
| `useLtAiUsage()` | Full token/prompt usage breakdown (`GET /ai/usage`) |
|
|
720
808
|
| `useLtAiPrompts()` | User-facing CRUD for re-usable prompt snippets ("Vorlagen", `scope: 'user'` / `'tenant'`) |
|
|
721
809
|
| `useLtAiPlaceholders()` | Loads `{{placeholder}}` registry from the backend so slot / prompt editors render a dynamic helper sidebar |
|
|
@@ -829,6 +917,8 @@ The package works **with or without** `@nuxtjs/i18n`:
|
|
|
829
917
|
| `useLtAi()` | One-shot `prompt()` + streaming `promptStream()` for the nest-server AI module |
|
|
830
918
|
| `useLtAiChat()` | Multi-turn chat state, streaming, budget summary, confirmation gate, `maxMessages` cap |
|
|
831
919
|
| `useLtAiConnections()` | User self-service connection list + `select()` |
|
|
920
|
+
| `useLtAiSettings()` | The user's own AI settings (MCP confirmation switch, nest-server >= 12.3.0) |
|
|
921
|
+
| `useLtAiStatus()` | Whether the API's AI module is on and serves MCP (public `GET /ai/status`) |
|
|
832
922
|
| `useLtAiUsage()` | Token/prompt usage breakdown per user / tenant |
|
|
833
923
|
| `useLtAiPrompts()` | User-facing prompt snippet CRUD (`'user'` / `'tenant'` scope) |
|
|
834
924
|
| `useLtAiPlaceholders()` | Loads the backend's `{{placeholder}}` registry |
|
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.23.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
|
@@ -2,7 +2,7 @@ import { defineNuxtModule, createResolver, tryResolveModule, addImports, addComp
|
|
|
2
2
|
import { pathToFileURL } from 'node:url';
|
|
3
3
|
|
|
4
4
|
const name = "@lenne.tech/nuxt-extensions";
|
|
5
|
-
const version = "1.
|
|
5
|
+
const version = "1.23.0";
|
|
6
6
|
const configKey = "ltExtensions";
|
|
7
7
|
const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
|
|
8
8
|
const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
|
|
@@ -204,6 +204,8 @@ const module$1 = defineNuxtModule({
|
|
|
204
204
|
{ name: "useLtAi", from: resolve("./runtime/composables/use-lt-ai") },
|
|
205
205
|
{ name: "useLtAiChat", from: resolve("./runtime/composables/use-lt-ai-chat") },
|
|
206
206
|
{ name: "useLtAiConnections", from: resolve("./runtime/composables/use-lt-ai-connections") },
|
|
207
|
+
{ name: "useLtAiSettings", from: resolve("./runtime/composables/use-lt-ai-settings") },
|
|
208
|
+
{ name: "useLtAiStatus", from: resolve("./runtime/composables/use-lt-ai-status") },
|
|
207
209
|
{ name: "useLtAiUsage", from: resolve("./runtime/composables/use-lt-ai-usage") },
|
|
208
210
|
{ name: "useLtAiPrompts", from: resolve("./runtime/composables/use-lt-ai-prompts") },
|
|
209
211
|
{ name: "useLtAiPlaceholders", from: resolve("./runtime/composables/use-lt-ai-placeholders") },
|
|
@@ -8,6 +8,8 @@ export { useLtErrorTranslation } from './use-lt-error-translation.js';
|
|
|
8
8
|
export { useLtAi } from './use-lt-ai.js';
|
|
9
9
|
export { useLtAiChat } from './use-lt-ai-chat.js';
|
|
10
10
|
export { useLtAiConnections } from './use-lt-ai-connections.js';
|
|
11
|
+
export { useLtAiSettings } from './use-lt-ai-settings.js';
|
|
12
|
+
export { useLtAiStatus } from './use-lt-ai-status.js';
|
|
11
13
|
export { useLtAiUsage } from './use-lt-ai-usage.js';
|
|
12
14
|
export { useLtAiAdmin } from './use-lt-ai-admin.js';
|
|
13
15
|
export { useLtAiPrompts } from './use-lt-ai-prompts.js';
|
|
@@ -8,6 +8,8 @@ export { useLtErrorTranslation } from "./use-lt-error-translation.js";
|
|
|
8
8
|
export { useLtAi } from "./use-lt-ai.js";
|
|
9
9
|
export { useLtAiChat } from "./use-lt-ai-chat.js";
|
|
10
10
|
export { useLtAiConnections } from "./use-lt-ai-connections.js";
|
|
11
|
+
export { useLtAiSettings } from "./use-lt-ai-settings.js";
|
|
12
|
+
export { useLtAiStatus } from "./use-lt-ai-status.js";
|
|
11
13
|
export { useLtAiUsage } from "./use-lt-ai-usage.js";
|
|
12
14
|
export { useLtAiAdmin } from "./use-lt-ai-admin.js";
|
|
13
15
|
export { useLtAiPrompts } from "./use-lt-ai-prompts.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User self-service for the current user's own AI settings, today whether MCP
|
|
3
|
+
* asks before a destructive or mutating tool runs (a "bypass permissions"
|
|
4
|
+
* switch). Backed by `GET /ai/settings` and `PUT /ai/settings`
|
|
5
|
+
* (nest-server >= 12.3.0).
|
|
6
|
+
*
|
|
7
|
+
* The answer is always the setting as it takes effect. When the server fixes
|
|
8
|
+
* it (`ai.mcp.confirmation.enforced`), `enforced` is true, the switch should be
|
|
9
|
+
* shown disabled, and `update()` is refused with 403 (`#LTNS_0609`).
|
|
10
|
+
*/
|
|
11
|
+
import type { UseLtAiSettingsReturn } from '../types/ai.js';
|
|
12
|
+
export declare function useLtAiSettings(): UseLtAiSettingsReturn;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { computed, readonly, ref } from "#imports";
|
|
2
|
+
import { ltAiRequest } from "../lib/ai.js";
|
|
3
|
+
export function useLtAiSettings() {
|
|
4
|
+
const settings = ref(null);
|
|
5
|
+
const loading = ref(false);
|
|
6
|
+
const error = ref(null);
|
|
7
|
+
const enforced = computed(() => settings.value?.mcpConfirmationEnforced === true);
|
|
8
|
+
async function load() {
|
|
9
|
+
loading.value = true;
|
|
10
|
+
error.value = null;
|
|
11
|
+
try {
|
|
12
|
+
settings.value = await ltAiRequest("GET", "/settings");
|
|
13
|
+
} catch (err) {
|
|
14
|
+
error.value = err.message;
|
|
15
|
+
} finally {
|
|
16
|
+
loading.value = false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function update(input) {
|
|
20
|
+
loading.value = true;
|
|
21
|
+
error.value = null;
|
|
22
|
+
try {
|
|
23
|
+
settings.value = await ltAiRequest("PUT", "/settings", input);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
error.value = err.message;
|
|
26
|
+
throw err;
|
|
27
|
+
} finally {
|
|
28
|
+
loading.value = false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { enforced, error: readonly(error), load, loading: readonly(loading), settings: readonly(settings), update };
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the API's AI module is on, and whether it serves MCP. Backed by the
|
|
3
|
+
* public `GET /ai/status` of nest-server: 200 `{ mcp }` while the module is on,
|
|
4
|
+
* 404 while it is off (the route does not exist then).
|
|
5
|
+
*
|
|
6
|
+
* The state is shared app-wide (`useState`) and travels from SSR to the client,
|
|
7
|
+
* so layout, route middleware and pages together cause one request per app
|
|
8
|
+
* start. It is sent without credentials: the endpoint is public, and the
|
|
9
|
+
* auth-aware fetch would start its 401 handling for nothing.
|
|
10
|
+
*
|
|
11
|
+
* Fails closed: anything but a well-formed 200 leaves `enabled` false. A 404 is
|
|
12
|
+
* an answer ("AI is off") and sets no `error`; a network error, a timeout, any
|
|
13
|
+
* other status or a malformed body sets `error`.
|
|
14
|
+
*/
|
|
15
|
+
import type { UseLtAiStatusReturn } from '../types/ai.js';
|
|
16
|
+
export declare function useLtAiStatus(): UseLtAiStatusReturn;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { computed, useNuxtApp, useState } from "#imports";
|
|
2
|
+
import { buildLtAiUrl } from "../lib/ai.js";
|
|
3
|
+
const LT_AI_STATUS_KEY = "lt-ai-status";
|
|
4
|
+
const LT_AI_STATUS_TIMEOUT_MS = 5e3;
|
|
5
|
+
const inflight = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
async function fetchLtAiStatus() {
|
|
7
|
+
const failed = (error) => ({ enabled: false, error, loaded: true, mcp: false });
|
|
8
|
+
let response;
|
|
9
|
+
try {
|
|
10
|
+
response = await fetch(buildLtAiUrl("/status"), {
|
|
11
|
+
credentials: "omit",
|
|
12
|
+
headers: { Accept: "application/json" },
|
|
13
|
+
signal: AbortSignal.timeout(LT_AI_STATUS_TIMEOUT_MS)
|
|
14
|
+
});
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return failed(err?.name === "TimeoutError" ? "AI status request timed out" : "AI status request failed");
|
|
17
|
+
}
|
|
18
|
+
if (response.status === 404) {
|
|
19
|
+
return { enabled: false, error: null, loaded: true, mcp: false };
|
|
20
|
+
}
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
return failed(`HTTP ${response.status}`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const body = await response.json();
|
|
26
|
+
if (typeof body?.mcp === "boolean") {
|
|
27
|
+
return { enabled: true, error: null, loaded: true, mcp: body.mcp };
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
return failed("Unexpected AI status response");
|
|
32
|
+
}
|
|
33
|
+
export function useLtAiStatus() {
|
|
34
|
+
const state = useState(LT_AI_STATUS_KEY, () => ({ enabled: false, error: null, loaded: false, mcp: false }));
|
|
35
|
+
const nuxtApp = useNuxtApp();
|
|
36
|
+
function load(options) {
|
|
37
|
+
if (state.value.loaded && !options?.force) {
|
|
38
|
+
return Promise.resolve();
|
|
39
|
+
}
|
|
40
|
+
const running = inflight.get(nuxtApp);
|
|
41
|
+
if (running) {
|
|
42
|
+
return running;
|
|
43
|
+
}
|
|
44
|
+
const request = fetchLtAiStatus().then((status) => {
|
|
45
|
+
state.value = status;
|
|
46
|
+
}).finally(() => inflight.delete(nuxtApp));
|
|
47
|
+
inflight.set(nuxtApp, request);
|
|
48
|
+
return request;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
enabled: computed(() => state.value.enabled),
|
|
52
|
+
error: computed(() => state.value.error),
|
|
53
|
+
load,
|
|
54
|
+
loaded: computed(() => state.value.loaded),
|
|
55
|
+
mcp: computed(() => state.value.mcp)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -323,7 +323,31 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
|
|
|
323
323
|
headers?: {} | {
|
|
324
324
|
[x: string]: string | undefined;
|
|
325
325
|
accept?: ((string & {}) | "application/json" | "text/plain" | "application/octet-stream") | undefined;
|
|
326
|
-
"content-type"
|
|
326
|
+
"content-type"
|
|
327
|
+
/**
|
|
328
|
+
* Register additional Better Auth plugins before auth client initialization.
|
|
329
|
+
*
|
|
330
|
+
* Call this in a Nuxt plugin (client-side) or in app.vue setup before
|
|
331
|
+
* the auth client is used.
|
|
332
|
+
*
|
|
333
|
+
* If the auth client was already created, it will be automatically recreated
|
|
334
|
+
* with the new plugins on next access.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```typescript
|
|
338
|
+
* // plugins/auth-plugins.client.ts
|
|
339
|
+
* import { registerLtAuthPlugins } from '@lenne.tech/nuxt-extensions/lib';
|
|
340
|
+
* import { organizationClient, magicLinkClient } from 'better-auth/client/plugins';
|
|
341
|
+
*
|
|
342
|
+
* export default defineNuxtPlugin(() => {
|
|
343
|
+
* registerLtAuthPlugins([
|
|
344
|
+
* organizationClient(),
|
|
345
|
+
* magicLinkClient(),
|
|
346
|
+
* ]);
|
|
347
|
+
* });
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
?: ((string & {}) | "application/x-www-form-urlencoded" | "application/json" | "text/plain" | "application/octet-stream" | "multipart/form-data") | undefined;
|
|
327
351
|
authorization?: ((string & {}) | `Bearer ${string}` | `Basic ${string}`) | undefined;
|
|
328
352
|
} | undefined;
|
|
329
353
|
body?: any;
|
|
@@ -18,6 +18,10 @@ export const PASSKEY_ERROR_CODES = Object.freeze({
|
|
|
18
18
|
CHALLENGE_NOT_FOUND: { code: "CHALLENGE_NOT_FOUND", message: "Challenge not found" },
|
|
19
19
|
FAILED_TO_UPDATE_PASSKEY: { code: "FAILED_TO_UPDATE_PASSKEY", message: "Failed to update passkey" },
|
|
20
20
|
FAILED_TO_VERIFY_REGISTRATION: { code: "FAILED_TO_VERIFY_REGISTRATION", message: "Failed to verify registration" },
|
|
21
|
+
ORIGIN_NOT_CONFIGURED: {
|
|
22
|
+
code: "ORIGIN_NOT_CONFIGURED",
|
|
23
|
+
message: "Passkey origin is not configured. Set the `origin` option of the passkey plugin or configure `baseURL`."
|
|
24
|
+
},
|
|
21
25
|
PASSKEY_NOT_FOUND: { code: "PASSKEY_NOT_FOUND", message: "Passkey not found" },
|
|
22
26
|
PREVIOUSLY_REGISTERED: { code: "PREVIOUSLY_REGISTERED", message: "Previously registered" },
|
|
23
27
|
REGISTRATION_CANCELLED: { code: "REGISTRATION_CANCELLED", message: "Registration cancelled" },
|
|
@@ -126,6 +126,28 @@ export interface LtAiAvailableConnection {
|
|
|
126
126
|
name?: string;
|
|
127
127
|
selected?: boolean;
|
|
128
128
|
}
|
|
129
|
+
/** The current user's AI settings as they take effect (`GET/PUT /ai/settings`, nest-server >= 12.3.0). */
|
|
130
|
+
export interface LtAiSettings {
|
|
131
|
+
/** Whether MCP asks this user before a destructive tool (and a mutating one, if configured) runs. */
|
|
132
|
+
mcpConfirmation: boolean;
|
|
133
|
+
/** Whether the server fixed `mcpConfirmation`, so the user cannot change it (a change answers 403 `#LTNS_0609`). */
|
|
134
|
+
mcpConfirmationEnforced: boolean;
|
|
135
|
+
}
|
|
136
|
+
/** The user's own choices for `PUT /ai/settings`; a field left out stays unchanged. */
|
|
137
|
+
export interface LtAiSettingsInput {
|
|
138
|
+
mcpConfirmation?: boolean;
|
|
139
|
+
}
|
|
140
|
+
/** Whether the API's AI module is on (`GET /ai/status`), as `useLtAiStatus()` holds it. */
|
|
141
|
+
export interface LtAiStatus {
|
|
142
|
+
/** True only after a well-formed 200: the AI module is on. */
|
|
143
|
+
enabled: boolean;
|
|
144
|
+
/** Why the status could not be read (network, timeout, other status, malformed body); null for an answer, 404 included. */
|
|
145
|
+
error: null | string;
|
|
146
|
+
/** True once an answer or an error is in. */
|
|
147
|
+
loaded: boolean;
|
|
148
|
+
/** Whether the API serves MCP; false whenever `enabled` is false. */
|
|
149
|
+
mcp: boolean;
|
|
150
|
+
}
|
|
129
151
|
/** Usage for a single scope (user or tenant). */
|
|
130
152
|
export interface LtAiUsageScope {
|
|
131
153
|
maxPrompts?: number;
|
|
@@ -429,6 +451,26 @@ export interface UseLtAiConnectionsReturn {
|
|
|
429
451
|
select: (connectionId: string) => Promise<void>;
|
|
430
452
|
selected: ComputedRef<LtAiAvailableConnection | undefined>;
|
|
431
453
|
}
|
|
454
|
+
export interface UseLtAiStatusReturn {
|
|
455
|
+
enabled: ComputedRef<boolean>;
|
|
456
|
+
error: ComputedRef<null | string>;
|
|
457
|
+
/** Reads the status once per app start; `force` asks again. Never throws. */
|
|
458
|
+
load: (options?: {
|
|
459
|
+
force?: boolean;
|
|
460
|
+
}) => Promise<void>;
|
|
461
|
+
loaded: ComputedRef<boolean>;
|
|
462
|
+
mcp: ComputedRef<boolean>;
|
|
463
|
+
}
|
|
464
|
+
export interface UseLtAiSettingsReturn {
|
|
465
|
+
/** True when the server fixed the MCP confirmation: show the switch disabled. */
|
|
466
|
+
enforced: ComputedRef<boolean>;
|
|
467
|
+
error: DeepReadonly<Ref<null | string>>;
|
|
468
|
+
load: () => Promise<void>;
|
|
469
|
+
loading: DeepReadonly<Ref<boolean>>;
|
|
470
|
+
settings: DeepReadonly<Ref<LtAiSettings | null>>;
|
|
471
|
+
/** Stores the user's choice and adopts the answer; throws (and sets `error`) when refused. */
|
|
472
|
+
update: (input: LtAiSettingsInput) => Promise<void>;
|
|
473
|
+
}
|
|
432
474
|
export interface UseLtAiUsageReturn {
|
|
433
475
|
error: DeepReadonly<Ref<null | string>>;
|
|
434
476
|
loading: DeepReadonly<Ref<boolean>>;
|
|
@@ -3,4 +3,4 @@ export type { LtFileInfo, LtUploadItem, LtUploadOptions, LtUploadProgress, LtUpl
|
|
|
3
3
|
export type { LtAiModuleOptions, LtAuthCookieNamesOptions, LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtFormLabelAssociationOptions, LtI18nModuleOptions, LtPreHydrationInputOptions, LtSystemSetupModuleOptions, LtTusModuleOptions, } from './module.js';
|
|
4
4
|
export type { LtErrorTranslationResponse, LtParsedError, UseLtErrorTranslationReturn } from './error.js';
|
|
5
5
|
export type { LtSyncConflict, LtSyncConflictResolution, LtSyncIdFormat, LtSyncItemStatus, LtSyncModuleOptions, LtSyncPushResult, LtSyncRecord, LtSyncResult, UseLtOfflineSyncOptions, UseLtOfflineSyncReturn, } from './sync.js';
|
|
6
|
-
export type { LtAiAction, LtAiAvailableConnection, LtAiBudgetLimit, LtAiBudgetSummary, LtAiConnection, LtAiConnectionInput, LtAiConnectionPreference, LtAiEffectiveSlot, LtAiInteraction, LtAiMessage, LtAiMode, LtAiPlaceholder, LtAiPrompt, LtAiPromptHint, LtAiPromptHintInput, LtAiPromptInput, LtAiPromptRunInput, LtAiResponse, LtAiSlot, LtAiSlotInput, LtAiStreamEvent, LtAiStreamHandlers, LtAiUsage, LtAiUsageInfo, LtAiUsageScope, UseLtAiAdminReturn, UseLtAiChatOptions, UseLtAiChatReturn, UseLtAiConnectionsReturn, UseLtAiPlaceholdersReturn, UseLtAiPromptsReturn, UseLtAiReturn, UseLtAiUsageReturn, } from './ai.js';
|
|
6
|
+
export type { LtAiAction, LtAiAvailableConnection, LtAiBudgetLimit, LtAiBudgetSummary, LtAiConnection, LtAiConnectionInput, LtAiConnectionPreference, LtAiEffectiveSlot, LtAiInteraction, LtAiMessage, LtAiMode, LtAiPlaceholder, LtAiPrompt, LtAiPromptHint, LtAiPromptHintInput, LtAiPromptInput, LtAiPromptRunInput, LtAiResponse, LtAiSettings, LtAiSettingsInput, LtAiSlot, LtAiStatus, LtAiSlotInput, LtAiStreamEvent, LtAiStreamHandlers, LtAiUsage, LtAiUsageInfo, LtAiUsageScope, UseLtAiAdminReturn, UseLtAiChatOptions, UseLtAiChatReturn, UseLtAiConnectionsReturn, UseLtAiPlaceholdersReturn, UseLtAiPromptsReturn, UseLtAiReturn, UseLtAiSettingsReturn, UseLtAiStatusReturn, UseLtAiUsageReturn, } from './ai.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nuxt-extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -79,9 +79,9 @@
|
|
|
79
79
|
"@nuxt/kit": "^4.0.0"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"@better-auth/passkey": ">=1.7.
|
|
82
|
+
"@better-auth/passkey": ">=1.7.16 <1.8.0",
|
|
83
83
|
"@playwright/test": ">=1.0.0",
|
|
84
|
-
"better-auth": ">=1.7.
|
|
84
|
+
"better-auth": ">=1.7.16 <1.8.0",
|
|
85
85
|
"nuxt": "^4.0.0",
|
|
86
86
|
"tus-js-client": ">=4.0.0"
|
|
87
87
|
},
|
|
@@ -100,26 +100,26 @@
|
|
|
100
100
|
}
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@better-auth/passkey": "1.7.
|
|
103
|
+
"@better-auth/passkey": "1.7.16",
|
|
104
104
|
"@nuxt/module-builder": "1.0.3",
|
|
105
|
-
"@nuxt/schema": "4.6.
|
|
105
|
+
"@nuxt/schema": "4.6.4",
|
|
106
106
|
"@nuxt/ui": "4.12.0",
|
|
107
107
|
"@playwright/test": "1.64.1",
|
|
108
108
|
"@types/node": "26.6.6",
|
|
109
|
-
"@vitest/coverage-v8": "5.0
|
|
109
|
+
"@vitest/coverage-v8": "5.1.0",
|
|
110
110
|
"@vue/server-renderer": "3.5.44",
|
|
111
|
-
"better-auth": "1.7.
|
|
111
|
+
"better-auth": "1.7.16",
|
|
112
112
|
"cross-spawn": "7.0.6",
|
|
113
113
|
"fake-indexeddb": "6.2.7",
|
|
114
114
|
"happy-dom": "21.0.4",
|
|
115
|
-
"nuxt": "4.6.
|
|
115
|
+
"nuxt": "4.6.4",
|
|
116
116
|
"oxfmt": "0.75.0",
|
|
117
117
|
"oxlint": "1.90.0",
|
|
118
118
|
"oxlint-tsgolint": "7.0.2005",
|
|
119
119
|
"reka-ui": "2.11.0",
|
|
120
120
|
"tus-js-client": "4.3.1",
|
|
121
121
|
"typescript": "5.9.3",
|
|
122
|
-
"vitest": "5.0
|
|
122
|
+
"vitest": "5.1.0",
|
|
123
123
|
"vue-tsc": "3.3.14"
|
|
124
124
|
},
|
|
125
125
|
"keywords": [
|