@lenne.tech/nuxt-extensions 1.20.1 → 1.22.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.
Files changed (36) hide show
  1. package/CLAUDE.md +65 -0
  2. package/README.md +122 -0
  3. package/dist/module.d.mts +1 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +15 -1
  6. package/dist/runtime/components/transition/LtTransitionFade.d.vue.ts +5 -3
  7. package/dist/runtime/components/transition/LtTransitionFade.vue.d.ts +5 -3
  8. package/dist/runtime/components/transition/LtTransitionFadeScale.d.vue.ts +5 -3
  9. package/dist/runtime/components/transition/LtTransitionFadeScale.vue.d.ts +5 -3
  10. package/dist/runtime/components/transition/LtTransitionSlide.d.vue.ts +5 -3
  11. package/dist/runtime/components/transition/LtTransitionSlide.vue.d.ts +5 -3
  12. package/dist/runtime/components/transition/LtTransitionSlideBottom.d.vue.ts +5 -3
  13. package/dist/runtime/components/transition/LtTransitionSlideBottom.vue.d.ts +5 -3
  14. package/dist/runtime/components/transition/LtTransitionSlideRevert.d.vue.ts +5 -3
  15. package/dist/runtime/components/transition/LtTransitionSlideRevert.vue.d.ts +5 -3
  16. package/dist/runtime/composables/auth/use-lt-auth.js +16 -1
  17. package/dist/runtime/composables/index.d.ts +2 -0
  18. package/dist/runtime/composables/index.js +2 -0
  19. package/dist/runtime/composables/use-lt-ai-settings.d.ts +12 -0
  20. package/dist/runtime/composables/use-lt-ai-settings.js +32 -0
  21. package/dist/runtime/composables/use-lt-offline-sync.d.ts +21 -0
  22. package/dist/runtime/composables/use-lt-offline-sync.js +250 -0
  23. package/dist/runtime/lib/index.d.ts +2 -0
  24. package/dist/runtime/lib/index.js +2 -0
  25. package/dist/runtime/lib/sync-hints.d.ts +46 -0
  26. package/dist/runtime/lib/sync-hints.js +147 -0
  27. package/dist/runtime/lib/sync-store.d.ts +89 -0
  28. package/dist/runtime/lib/sync-store.js +149 -0
  29. package/dist/runtime/lib/sync.d.ts +89 -0
  30. package/dist/runtime/lib/sync.js +369 -0
  31. package/dist/runtime/types/ai.d.ts +21 -0
  32. package/dist/runtime/types/index.d.ts +2 -1
  33. package/dist/runtime/types/module.d.ts +7 -0
  34. package/dist/runtime/types/sync.d.ts +127 -0
  35. package/dist/runtime/types/sync.js +0 -0
  36. package/package.json +9 -8
package/CLAUDE.md CHANGED
@@ -40,11 +40,18 @@ 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) |
43
44
  | `useLtAiUsage()` | Full token/prompt usage breakdown (`GET /ai/usage`) |
44
45
  | `useLtAiPrompts()` | User-facing CRUD for re-usable prompt snippets ("Vorlagen", `scope: 'user'` / `'tenant'`) |
45
46
  | `useLtAiPlaceholders()` | Loads the backend's `{{placeholder}}` registry for editor sidebars |
46
47
  | `useLtAiAdmin()` | Admin CRUD: connections, preferences, budget limits, slots, prompt hints, interactions — server-gated |
47
48
 
49
+ ### Offline sync (nest-server sync module, opt-in)
50
+
51
+ | Composable | Purpose |
52
+ |-----------|---------|
53
+ | `useLtOfflineSync(model, options?)` | Offline read/write of one model in IndexedDB; push/pull against `/sync`, hints, conflicts, tabs, owner check |
54
+
48
55
  ## Module Configuration
49
56
 
50
57
  Configure in `nuxt.config.ts` (top-level key is `ltExtensions`):
@@ -76,6 +83,10 @@ export default defineNuxtConfig({
76
83
  enabled: true,
77
84
  basePath: '/ai', // Must match the nest-server AI controller
78
85
  },
86
+ sync: {
87
+ enabled: false, // opt-in: useLtOfflineSync() + sign-out cleanup
88
+ basePath: '/sync', // Must match the nest-server sync module
89
+ },
79
90
  formLabelAssociation: {
80
91
  enabled: true, // repair <label for> broken by an SSR/client useId() divergence
81
92
  maxRepairMs: 1500, // how long to keep repairing deferred subtrees
@@ -112,6 +123,60 @@ The API base URL is resolved at runtime from `NUXT_PUBLIC_API_URL` (client + SSR
112
123
  - `useLtAiAdmin()` is exposed as a regular composable; admin gating is enforced server-side (`@Restricted(ADMIN)`). Render admin UI behind a frontend route guard for UX, but trust the backend for authorization.
113
124
  - The chat composable's `messages` ref is a shallow `Readonly<Ref<LtAiMessage[]>>` so child components can bind individual messages while preserving streaming reactivity.
114
125
 
126
+ ## Offline Sync
127
+
128
+ `useLtOfflineSync()` is the client of the nest-server sync module (>= 12.2.0); the protocol is
129
+ `docs/offline-sync-protocol.md` in nest-server. Both ends are opt-in: `ltExtensions.sync.enabled`
130
+ here, a sync model (`@PersistedModel({ sync })` + `@SyncService`) on the API.
131
+
132
+ **The local data belongs to the signed-in user, and only explicit sign-out deletes it:**
133
+
134
+ - `useLtAuth().signOut()` sends what is unsent (at most 5 s), signs out, then deletes the whole
135
+ IndexedDB database `lt-offline-sync`. **A custom sign-out that bypasses it must call
136
+ `flushLtSyncBeforeSignOut(userId)` and `deleteLtSyncDatabase()` (from `/lib`) itself** — otherwise
137
+ the user's records stay on the device.
138
+ - **The flush sends only the signing-out user's own changes.** After an expired session the outbox
139
+ of the PREVIOUS user is still there; pushed under the current session it would land in the
140
+ current user's account. `flushLtSyncBeforeSignOut` compares the stored owner with `userId`.
141
+ - **An expired session deletes nothing.** The 401 interceptor calls `clearUser()`, not `signOut()`,
142
+ on purpose: the protocol promises that after signing in again the same changes go out without
143
+ duplicates. Never hook the sync cleanup into `clearUser()` — that would delete unsent work on
144
+ every expired session.
145
+ - Another user wipes the data before anything is shown or pulled (`claimLtSyncOwner`, one
146
+ transaction); so does data WITHOUT an owner. Nobody signed in sees nothing.
147
+ - **Every write of a sync run is fenced by the owner it started for** (`runLtSyncTx(…, { owner })`).
148
+ An answer arriving after sign-out deleted the database, or after another account claimed it,
149
+ would otherwise reopen the database and write the previous user's records for the next one.
150
+ - Local data is kept per model AND tenant (`ltSyncScope`): the server stamps a create into the
151
+ tenant of its `X-Tenant-Id`, so one tenant's unsent change must never go out under another.
152
+
153
+ **Rules that keep the engine correct (`src/runtime/lib/sync.ts`):**
154
+
155
+ - A push carries at most ONE change per record, and the next one goes out only once the one before
156
+ it is `applied` or `merged`. Its `baseVersion` assumes exactly that; sent while the first is open
157
+ (`retry`) or after it was refused (`rejected`, `not_found`, `conflict`), it can meet another
158
+ writer's version with the same number and be applied on top of it. So it waits behind a `retry`
159
+ and is dropped after a refusal (protocol §11 since nest-server 12.3.0, which also guards this
160
+ server-side within one request).
161
+ - An outbox item a push has carried (`attempted`) is never edited again; a later edit becomes a new
162
+ item. Read-and-mark happens in one IndexedDB transaction.
163
+ - `onConflict` runs outside any IndexedDB transaction (an awaited hook would let the transaction
164
+ auto-commit). Nothing inside `runLtSyncTx()` may await anything but that transaction's requests.
165
+ - Hints are read with `fetch`, not `EventSource` (no `Authorization` header in JWT mode). Every
166
+ reconnect pulls once: the server ends the stream on purpose and never replays the hint it missed.
167
+ - ONE hint stream per tab and tenant (`subscribeLtSyncHints`), for every model in use. A stream per
168
+ composable call holds a connection each; over HTTP/1.1 six of them stall every API request.
169
+ - A refused change (`rejected`) to a record the server has drops the cursor, so the pull right after
170
+ restores the server's state; otherwise the device would keep showing the refused value.
171
+
172
+ **Tests:** `test/sync-engine.test.ts` runs the protocol's five client scenarios (offline
173
+ create/change/delete, conflict, two tabs, expired session, cleared storage) against
174
+ `test/fixtures/sync-server.ts` with `fake-indexeddb`. The browser E2E against a real nest-server
175
+ lives in the nuxt-base-starter (DEV-3919).
176
+
177
+ A service worker must never cache `/sync` or `/iam` responses (nest-server
178
+ `src/core/modules/sync/SECURITY.md`).
179
+
115
180
  ## Testing Helpers
116
181
 
117
182
  Import from `@lenne.tech/nuxt-extensions/testing`:
package/README.md CHANGED
@@ -79,6 +79,7 @@ overrides:
79
79
  - **Better-Auth Integration** - Login, 2FA, Passkey/WebAuthn support
80
80
  - **Cookie/JWT Dual-Mode** - Automatic fallback from cookies to JWT
81
81
  - **TUS File Upload** - Resumable uploads with pause/resume
82
+ - **Offline Sync** - Read and write offline, reconcile with the nest-server sync module (opt-in)
82
83
  - **Transition Components** - Ready-to-use Vue transition wrappers
83
84
  - **i18n Support** - English and German translations (works without i18n too)
84
85
  - **Auto-imports** - All composables and components are auto-imported
@@ -196,6 +197,12 @@ export default defineNuxtConfig({
196
197
  enabled: true, // Enable AI composables / auto-imports
197
198
  basePath: '/ai', // Must match the nest-server AI controller
198
199
  },
200
+
201
+ // Offline sync (opt-in, see "Offline Sync" below)
202
+ sync: {
203
+ enabled: false, // Enable useLtOfflineSync() and the sign-out cleanup
204
+ basePath: '/sync', // Must match the nest-server sync module
205
+ },
199
206
  },
200
207
  });
201
208
  ```
@@ -702,6 +709,45 @@ await load();
702
709
  is private to the owner; `scope: 'tenant'` is shared across the owner's tenant. The list
703
710
  returned by `load()` is already server-filtered to what the user is allowed to see.
704
711
 
712
+ ### MCP confirmation switch
713
+
714
+ Over MCP (Claude Desktop, Claude Code and other clients), nest-server 12.3.0 or later asks the
715
+ user before a destructive tool runs, and before a mutating one if the server is configured that
716
+ way. Each user may switch the question off for themselves, like a "bypass permissions" mode.
717
+ `useLtAiSettings()` reads and changes that switch (`GET/PUT /ai/settings`):
718
+
719
+ ```vue
720
+ <script setup lang="ts">
721
+ const { enforced, load, settings, update } = useLtAiSettings();
722
+ await load();
723
+
724
+ async function toggle(value: boolean) {
725
+ try {
726
+ await update({ mcpConfirmation: value });
727
+ } catch {
728
+ // `error` holds the message; `settings` keeps the previous value, so the switch snaps back
729
+ }
730
+ }
731
+ </script>
732
+
733
+ <template>
734
+ <USwitch
735
+ :model-value="settings?.mcpConfirmation"
736
+ :disabled="enforced || !settings"
737
+ label="Ask before destructive AI actions over MCP"
738
+ @update:model-value="toggle"
739
+ />
740
+ </template>
741
+ ```
742
+
743
+ - `settings` is always the value as it takes effect, never just the stored choice. `update()`
744
+ adopts the server's answer instead of the input.
745
+ - `enforced` is true when the server fixed the value (`ai.mcp.confirmation.enforced`). Show the
746
+ switch disabled. A change is refused with 403 `#LTNS_0609`, which `update()` throws.
747
+ - Switching it off lets an MCP client run destructive tools in this user's name without asking.
748
+ Say so next to the switch. The tool's own `authorize()` check and the admin's `deny` / `ask`
749
+ rules still apply, and other users and the in-app chat are not affected.
750
+
705
751
  ### Lower-level + supporting composables
706
752
 
707
753
  | Composable | Purpose |
@@ -709,6 +755,7 @@ returned by `load()` is already server-filtered to what the user is allowed to s
709
755
  | `useLtAi()` | One-shot `prompt(input)` and streaming `promptStream(input, handlers)` (POST `/ai/stream`) |
710
756
  | `useLtAiChat()` | Multi-turn chat state, streaming, budget, confirmation, `stop()`/`clear()`, optional `maxMessages` cap, auto-stop on component unmount |
711
757
  | `useLtAiConnections()` | User self-service: available connections + `select()` (`selected`/`locked`) |
758
+ | `useLtAiSettings()` | The user's own AI settings: the MCP confirmation switch + `update()` (`enforced`) |
712
759
  | `useLtAiUsage()` | Full token/prompt usage breakdown (`GET /ai/usage`) |
713
760
  | `useLtAiPrompts()` | User-facing CRUD for re-usable prompt snippets ("Vorlagen", `scope: 'user'` / `'tenant'`) |
714
761
  | `useLtAiPlaceholders()` | Loads `{{placeholder}}` registry from the backend so slot / prompt editors render a dynamic helper sidebar |
@@ -722,6 +769,79 @@ All AI DTOs are exported as `LtAi*` types. The streaming endpoint is consumed vi
722
769
  > is enforced server-side (`@Restricted(ADMIN)`). Render admin UI behind a frontend
723
770
  > route guard for UX, but trust the backend for authorization.
724
771
 
772
+ ## Offline Sync
773
+
774
+ Read and write while disconnected, reconcile when back. `useLtOfflineSync()` is the client of the
775
+ `@lenne.tech/nest-server` **sync module** (12.2.0 or later); the protocol is
776
+ `docs/offline-sync-protocol.md` in nest-server. It is opt-in, both here and on the server:
777
+
778
+ ```typescript
779
+ // nuxt.config.ts
780
+ ltExtensions: {
781
+ sync: { enabled: true, basePath: '/sync' }, // basePath must match the nest-server sync module
782
+ }
783
+ ```
784
+
785
+ The API needs a sync model too: `@PersistedModel({ sync })` on the model and `@SyncService` on its
786
+ CrudService (nest-server `src/core/modules/sync/README.md`).
787
+
788
+ ```vue
789
+ <script setup lang="ts">
790
+ const { create, needsLogin, offline, pendingCount, records, remove } = useLtOfflineSync<{ title: string }>('note');
791
+
792
+ // Works offline: the change shows at once and waits in the outbox until the next sync
793
+ const add = (title: string) => create({ title });
794
+ </script>
795
+
796
+ <template>
797
+ <p v-if="offline">Offline — {{ pendingCount }} changes waiting</p>
798
+ <p v-if="needsLogin">Please sign in again; nothing is lost</p>
799
+ <ul>
800
+ <li v-for="note in records" :key="note.id">
801
+ {{ note.data?.title }} <button @click="remove(note.id)">Delete</button>
802
+ </li>
803
+ </ul>
804
+ </template>
805
+ ```
806
+
807
+ **What it does:**
808
+
809
+ - Keeps records, cursors and unsent changes in IndexedDB. Each change carries a `mutationId` and
810
+ the `baseVersion` it is based on, so a repeated send never duplicates and a stale one conflicts.
811
+ - Syncs at start, on `online`, on focus, shortly after a local change, and after a server hint
812
+ (`GET <basePath>/hints`, read with `fetch` so it signs in with a cookie and a JWT alike). Every
813
+ reconnect of the hint stream pulls once: the server ends the stream on purpose and does not
814
+ replay what it missed. All instances of a tab share one stream per tenant, and a hidden tab
815
+ keeps none open.
816
+ - Handles every item status and HTTP status of the protocol: `401` keeps everything (sign in again,
817
+ the same changes go out without duplicates), `410` drops the local records and pulls everything
818
+ again while keeping unsent changes, `403` on a pull drops the model's records, `429`/`503` and
819
+ network errors back off. A refused change is dropped and the record returns to the server's state.
820
+ - Tabs share the data: one syncs at a time (Web Locks), the others show the result
821
+ (BroadcastChannel).
822
+
823
+ **Options:** `idFormat` (`'objectId'` default, `'uuidv7'` for models declared with it),
824
+ `onConflict` (default: the server wins; return `{ data }` to send your answer again on top of the
825
+ server's version), `tenantId` (`X-Tenant-Id`; records and unsent changes are kept per tenant, so a
826
+ change made under one tenant never goes out under another), `pageLimit`, `hints: false`,
827
+ `auto: false` (only `sync()` syncs).
828
+
829
+ **Whose data it is.** The local data belongs to the signed-in user:
830
+
831
+ | Event | Local data |
832
+ |-------|-----------|
833
+ | `useLtAuth().signOut()` | The user's own unsent changes are sent (at most 5 s), then the whole database is deleted |
834
+ | Another user signs in | Deleted before anything is shown or pulled |
835
+ | Session expired, same user signs in again | Kept; the outbox goes out without duplicates |
836
+ | Nobody signed in | Nothing shown, changes refused |
837
+
838
+ A sync answer that arrives after the sign-out or the account switch is discarded, not stored. A
839
+ sign-out flow that bypasses `useLtAuth().signOut()` must do the same itself:
840
+ `flushLtSyncBeforeSignOut(userId)` and `deleteLtSyncDatabase()` from
841
+ `@lenne.tech/nuxt-extensions/lib`. The flush sends only when the local data belongs to `userId`.
842
+ `pendingCount` lets the app warn before signing out. A service worker must never cache responses
843
+ of `/sync` or `/iam` (nest-server `src/core/modules/sync/SECURITY.md`).
844
+
725
845
  ## i18n Support
726
846
 
727
847
  The package works **with or without** `@nuxtjs/i18n`:
@@ -749,10 +869,12 @@ The package works **with or without** `@nuxtjs/i18n`:
749
869
  | `useLtAi()` | One-shot `prompt()` + streaming `promptStream()` for the nest-server AI module |
750
870
  | `useLtAiChat()` | Multi-turn chat state, streaming, budget summary, confirmation gate, `maxMessages` cap |
751
871
  | `useLtAiConnections()` | User self-service connection list + `select()` |
872
+ | `useLtAiSettings()` | The user's own AI settings (MCP confirmation switch, nest-server >= 12.3.0) |
752
873
  | `useLtAiUsage()` | Token/prompt usage breakdown per user / tenant |
753
874
  | `useLtAiPrompts()` | User-facing prompt snippet CRUD (`'user'` / `'tenant'` scope) |
754
875
  | `useLtAiPlaceholders()` | Loads the backend's `{{placeholder}}` registry |
755
876
  | `useLtAiAdmin()` | Admin CRUD for connections, preferences, budget limits, slots, prompt hints, interactions |
877
+ | `useLtOfflineSync()` | Offline read/write of one model, synced with the nest-server sync module (opt-in) |
756
878
 
757
879
  ### Components
758
880
 
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.20.1";
6
+ declare const version = "1.22.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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "configKey": "ltExtensions",
3
3
  "name": "@lenne.tech/nuxt-extensions",
4
- "version": "1.20.1",
4
+ "version": "1.22.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
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.20.1";
5
+ const version = "1.22.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";
@@ -46,6 +46,11 @@ const defaultOptions = {
46
46
  preHydrationInput: {
47
47
  enabled: true
48
48
  },
49
+ // Opt-in: an app without offline sync gets no auto-import and no sign-out cleanup
50
+ sync: {
51
+ basePath: "/sync",
52
+ enabled: false
53
+ },
49
54
  tus: {
50
55
  defaultChunkSize: 5 * 1024 * 1024,
51
56
  // 5MB
@@ -74,6 +79,7 @@ const module$1 = defineNuxtModule({
74
79
  formLabelAssociation: { ...defaultOptions.formLabelAssociation, ...options.formLabelAssociation },
75
80
  i18n: { ...defaultOptions.i18n, ...options.i18n },
76
81
  preHydrationInput: { ...defaultOptions.preHydrationInput, ...options.preHydrationInput },
82
+ sync: { ...defaultOptions.sync, ...options.sync },
77
83
  tus: { ...defaultOptions.tus, ...options.tus }
78
84
  };
79
85
  const passkeyAvailable = !!await tryResolveModule("@better-auth/passkey/client", [new URL(import.meta.url), pathToFileURL(`${nuxt.options.rootDir}/`)]);
@@ -146,6 +152,10 @@ const module$1 = defineNuxtModule({
146
152
  enabled: resolvedOptions.preHydrationInput?.enabled ?? true,
147
153
  maxRestoreMs: resolvedOptions.preHydrationInput?.maxRestoreMs ?? 1500
148
154
  },
155
+ sync: {
156
+ basePath: resolvedOptions.sync?.basePath || "/sync",
157
+ enabled: resolvedOptions.sync?.enabled ?? false
158
+ },
149
159
  tus: {
150
160
  defaultChunkSize: resolvedOptions.tus?.defaultChunkSize || 5 * 1024 * 1024,
151
161
  defaultEndpoint: resolvedOptions.tus?.defaultEndpoint || "/files/upload",
@@ -194,6 +204,7 @@ const module$1 = defineNuxtModule({
194
204
  { name: "useLtAi", from: resolve("./runtime/composables/use-lt-ai") },
195
205
  { name: "useLtAiChat", from: resolve("./runtime/composables/use-lt-ai-chat") },
196
206
  { name: "useLtAiConnections", from: resolve("./runtime/composables/use-lt-ai-connections") },
207
+ { name: "useLtAiSettings", from: resolve("./runtime/composables/use-lt-ai-settings") },
197
208
  { name: "useLtAiUsage", from: resolve("./runtime/composables/use-lt-ai-usage") },
198
209
  { name: "useLtAiPrompts", from: resolve("./runtime/composables/use-lt-ai-prompts") },
199
210
  { name: "useLtAiPlaceholders", from: resolve("./runtime/composables/use-lt-ai-placeholders") },
@@ -203,6 +214,9 @@ const module$1 = defineNuxtModule({
203
214
  { name: "parseLtAiSseStream", from: resolve("./runtime/lib/ai") }
204
215
  ]);
205
216
  }
217
+ if (resolvedOptions.sync?.enabled) {
218
+ addImports([{ name: "useLtOfflineSync", from: resolve("./runtime/composables/use-lt-offline-sync") }]);
219
+ }
206
220
  const transitionComponents = ["LtTransitionFade", "LtTransitionSlide", "LtTransitionSlideBottom", "LtTransitionSlideRevert", "LtTransitionFadeScale"];
207
221
  for (const componentName of transitionComponents) {
208
222
  addComponent({
@@ -21,10 +21,12 @@ type __VLS_Props = {
21
21
  /** Duration in ms for the enter animation (default: 100) */
22
22
  startDuration?: `${number}` | number;
23
23
  };
24
- declare var __VLS_7: {};
25
- type __VLS_Slots = {} & {
26
- default?: (props: typeof __VLS_7) => any;
24
+ declare const __VLS_template: () => {
25
+ __VLS_Slots: {
26
+ default?: (props: {}) => any;
27
+ };
27
28
  };
29
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
28
30
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
29
31
  leaveDuration: number | `${number}`;
30
32
  startDuration: number | `${number}`;
@@ -21,10 +21,12 @@ type __VLS_Props = {
21
21
  /** Duration in ms for the enter animation (default: 100) */
22
22
  startDuration?: `${number}` | number;
23
23
  };
24
- declare var __VLS_7: {};
25
- type __VLS_Slots = {} & {
26
- default?: (props: typeof __VLS_7) => any;
24
+ declare const __VLS_template: () => {
25
+ __VLS_Slots: {
26
+ default?: (props: {}) => any;
27
+ };
27
28
  };
29
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
28
30
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
29
31
  leaveDuration: number | `${number}`;
30
32
  startDuration: number | `${number}`;
@@ -24,10 +24,12 @@ type __VLS_Props = {
24
24
  /** Duration in ms for the enter animation (default: 100) */
25
25
  startDuration?: `${number}` | number;
26
26
  };
27
- declare var __VLS_7: {};
28
- type __VLS_Slots = {} & {
29
- default?: (props: typeof __VLS_7) => any;
27
+ declare const __VLS_template: () => {
28
+ __VLS_Slots: {
29
+ default?: (props: {}) => any;
30
+ };
30
31
  };
32
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
31
33
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
32
34
  leaveDuration: number | `${number}`;
33
35
  startDuration: number | `${number}`;
@@ -24,10 +24,12 @@ type __VLS_Props = {
24
24
  /** Duration in ms for the enter animation (default: 100) */
25
25
  startDuration?: `${number}` | number;
26
26
  };
27
- declare var __VLS_7: {};
28
- type __VLS_Slots = {} & {
29
- default?: (props: typeof __VLS_7) => any;
27
+ declare const __VLS_template: () => {
28
+ __VLS_Slots: {
29
+ default?: (props: {}) => any;
30
+ };
30
31
  };
32
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
31
33
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
32
34
  leaveDuration: number | `${number}`;
33
35
  startDuration: number | `${number}`;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,7 +1,9 @@
1
- declare var __VLS_7: {};
2
- type __VLS_Slots = {} & {
3
- default?: (props: typeof __VLS_7) => any;
1
+ declare const __VLS_template: () => {
2
+ __VLS_Slots: {
3
+ default?: (props: {}) => any;
4
+ };
4
5
  };
6
+ type __VLS_Slots = ReturnType<typeof __VLS_template>['__VLS_Slots'];
5
7
  declare const __VLS_base: import("@vue/runtime-core").DefineComponent<{}, {}, {}, {}, {}, import("@vue/runtime-core").ComponentOptionsMixin, import("@vue/runtime-core").ComponentOptionsMixin, {}, string, import("@vue/runtime-core").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("@vue/runtime-core").ComponentProvideOptions, true, {}, any>;
6
8
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
7
9
  declare const _default: typeof __VLS_export;
@@ -1,8 +1,17 @@
1
- import { useNuxtApp, useCookie, useState, useRequestHeaders, ref, computed, watch } from "#imports";
1
+ import { useNuxtApp, useCookie, useState, useRequestHeaders, useRuntimeConfig, ref, computed, watch } from "#imports";
2
2
  import { ltArrayBufferToBase64Url, ltBase64UrlToUint8Array } from "../../utils/crypto.js";
3
3
  import { clearLtAuthCookies, getLtApiBase, getLtAuthCookieNames, resolveLtAuthState } from "../../lib/auth-state.js";
4
4
  import { useLtAuthClient } from "../use-lt-auth-client.js";
5
5
  const VALIDATE_SESSION_TIMEOUT_MS = 1e4;
6
+ const LT_SYNC_SIGN_OUT_FLUSH_MS = 5e3;
7
+ function isLtSyncEnabled() {
8
+ if (!import.meta.client) return false;
9
+ try {
10
+ return !!useRuntimeConfig().public.ltExtensions?.sync?.enabled;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
6
15
  let staleJwtRefresh = null;
7
16
  function useTranslation() {
8
17
  const nuxtApp = useNuxtApp();
@@ -291,8 +300,14 @@ export function useLtAuth() {
291
300
  const signOut = async (options) => {
292
301
  isLoading.value = true;
293
302
  try {
303
+ const sync = isLtSyncEnabled() ? await import("../../lib/sync.js") : null;
304
+ await sync?.flushLtSyncBeforeSignOut(user.value?.id ? String(user.value.id) : void 0, LT_SYNC_SIGN_OUT_FLUSH_MS);
294
305
  const result = await authClient.signOut(options);
295
306
  clearUser();
307
+ if (sync) {
308
+ const { deleteLtSyncDatabase } = await import("../../lib/sync-store.js");
309
+ await deleteLtSyncDatabase().catch((error) => console.error("[LtAuth] Could not delete the local offline-sync data on sign-out:", error));
310
+ }
296
311
  return result;
297
312
  } finally {
298
313
  isLoading.value = false;
@@ -8,7 +8,9 @@ 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';
11
12
  export { useLtAiUsage } from './use-lt-ai-usage.js';
12
13
  export { useLtAiAdmin } from './use-lt-ai-admin.js';
13
14
  export { useLtAiPrompts } from './use-lt-ai-prompts.js';
14
15
  export { useLtAiPlaceholders } from './use-lt-ai-placeholders.js';
16
+ export { useLtOfflineSync } from './use-lt-offline-sync.js';
@@ -8,7 +8,9 @@ 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";
11
12
  export { useLtAiUsage } from "./use-lt-ai-usage.js";
12
13
  export { useLtAiAdmin } from "./use-lt-ai-admin.js";
13
14
  export { useLtAiPrompts } from "./use-lt-ai-prompts.js";
14
15
  export { useLtAiPlaceholders } from "./use-lt-ai-placeholders.js";
16
+ export { useLtOfflineSync } from "./use-lt-offline-sync.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,21 @@
1
+ /**
2
+ * Offline sync of one model with the nest-server sync module (>= 12.2.0).
3
+ *
4
+ * Records live in IndexedDB; changes made offline wait in an outbox and go out with the next sync.
5
+ * The composable syncs at start, on `online`, on focus and after a server hint, and pulls once on
6
+ * every reconnect of the hint stream (protocol §9). Tabs coordinate: one syncs at a time (Web Locks),
7
+ * the others pick up the result (BroadcastChannel); without either API it stays correct, only busier.
8
+ *
9
+ * The local data belongs to the signed-in user. A different user wipes it before anything is shown
10
+ * or pulled; `useLtAuth().signOut()` sends what is unsent and then deletes it. An expired session
11
+ * deletes nothing: after signing in again the outbox goes out without duplicates (protocol §4).
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const notes = useLtOfflineSync<{ title: string }>('note');
16
+ * await notes.create({ title: 'Einkaufen' });
17
+ * // notes.records.value, notes.pendingCount.value, notes.offline.value, notes.needsLogin.value
18
+ * ```
19
+ */
20
+ import type { UseLtOfflineSyncOptions, UseLtOfflineSyncReturn } from '../types/sync.js';
21
+ export declare function useLtOfflineSync<T extends Record<string, unknown> = Record<string, unknown>>(model: string, options?: UseLtOfflineSyncOptions<T>): UseLtOfflineSyncReturn<T>;