@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.
- package/CLAUDE.md +65 -0
- package/README.md +122 -0
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +15 -1
- package/dist/runtime/components/transition/LtTransitionFade.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionFade.vue.d.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionFadeScale.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionFadeScale.vue.d.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlide.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlide.vue.d.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlideBottom.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlideBottom.vue.d.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlideRevert.d.vue.ts +5 -3
- package/dist/runtime/components/transition/LtTransitionSlideRevert.vue.d.ts +5 -3
- package/dist/runtime/composables/auth/use-lt-auth.js +16 -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-offline-sync.d.ts +21 -0
- package/dist/runtime/composables/use-lt-offline-sync.js +250 -0
- package/dist/runtime/lib/index.d.ts +2 -0
- package/dist/runtime/lib/index.js +2 -0
- package/dist/runtime/lib/sync-hints.d.ts +46 -0
- package/dist/runtime/lib/sync-hints.js +147 -0
- package/dist/runtime/lib/sync-store.d.ts +89 -0
- package/dist/runtime/lib/sync-store.js +149 -0
- package/dist/runtime/lib/sync.d.ts +89 -0
- package/dist/runtime/lib/sync.js +369 -0
- package/dist/runtime/types/ai.d.ts +21 -0
- package/dist/runtime/types/index.d.ts +2 -1
- package/dist/runtime/types/module.d.ts +7 -0
- package/dist/runtime/types/sync.d.ts +127 -0
- package/dist/runtime/types/sync.js +0 -0
- 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.
|
|
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
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.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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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>;
|