@lenne.tech/nuxt-extensions 1.11.1 → 1.12.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 CHANGED
@@ -76,6 +76,10 @@ export default defineNuxtConfig({
76
76
  enabled: true,
77
77
  basePath: '/ai', // Must match the nest-server AI controller
78
78
  },
79
+ preHydrationInput: {
80
+ enabled: true, // keep text typed before hydration instead of losing it
81
+ maxRestoreMs: 1500, // how long to keep repairing deferred subtrees
82
+ },
79
83
  },
80
84
  });
81
85
  ```
@@ -171,6 +175,35 @@ export default defineNuxtRouteMiddleware((to) => {
171
175
  });
172
176
  ```
173
177
 
178
+ ## Pre-Hydration Input Preservation
179
+
180
+ **The defect:** until Vue hydrates a server-rendered `<input>`, it carries no framework
181
+ listener. Text typed in that window goes into the DOM node, the `input` event reaches nothing,
182
+ and `v-model`'s mounted hook then overwrites it. The entry is **erased, not delayed**. It is
183
+ load-dependent, so it hides in development.
184
+
185
+ **Vue already fixes most of it.** Since 3.5.41 (vuejs/core#14411) Vue adopts the typed value
186
+ into the model on hydration — but only for `type="text"` and `textarea`. `email`, `password`,
187
+ `tel`, `url`, `search` and `number` are still erased. Upstream tracking: vuejs/core#15210.
188
+
189
+ **What this module adds:** `runtime/plugins/pre-hydration-input.client.ts` snapshots edited
190
+ fields at `app:beforeMount` and restores them at `app:mounted`, dispatching a synthetic
191
+ `input` event so `v-model` adopts the value.
192
+
193
+ **What this means when you debug a consuming project:**
194
+
195
+ - **Do not add a `readonly`-until-mounted guard to inputs.** It was tried and removed: it makes
196
+ the field look usable while silently refusing, suppresses browser autofill (`readonly` is the
197
+ documented technique for exactly that), makes screen readers announce "read only" with no
198
+ announcement when it flips back, and stops mobile browsers raising the on-screen keyboard.
199
+ - A field is treated as edited when `value !== defaultValue`, so server-pre-filled values are
200
+ never restored over a legitimate change made during hydration.
201
+ - Each field is restored at most once; a later programmatic clear is not undone.
202
+ - This is a **stopgap**. When Vue widens its gate, delete the plugin —
203
+ `test/pre-hydration-input.test.ts` pins the current behaviour and will fail on the types Vue
204
+ takes over.
205
+ - Opt out with `ltExtensions.preHydrationInput.enabled: false`.
206
+
174
207
  ## Development Rules
175
208
 
176
209
  1. **ALWAYS read source code** in `dist/runtime/` to understand available composables and components
package/README.md CHANGED
@@ -41,6 +41,7 @@ npm install tus-js-client
41
41
  - **Transition Components** - Ready-to-use Vue transition wrappers
42
42
  - **i18n Support** - English and German translations (works without i18n too)
43
43
  - **Auto-imports** - All composables and components are auto-imported
44
+ - **Pre-hydration input preservation** - text typed before hydration is kept, not erased
44
45
 
45
46
  ## Environment Variables
46
47
 
@@ -156,6 +157,70 @@ export default defineNuxtConfig({
156
157
  });
157
158
  ```
158
159
 
160
+ ## Pre-Hydration Input Preservation
161
+
162
+ Text typed before a page has hydrated is kept instead of being silently thrown away.
163
+
164
+ ### The problem
165
+
166
+ Until Vue hydrates a server-rendered `<input>`, the element carries **no framework listener**.
167
+ Text typed in that window is written to the DOM node, the `input` event reaches nothing, and
168
+ `v-model`'s mounted hook then writes the model value back over it. The entry is **erased, not
169
+ delayed** — no error, no toast, nothing persisted.
170
+
171
+ It is load-dependent, so it hides in development and shows up on a cold cache, a slow device
172
+ or a throttled CPU — most often on the login screen, where people type on sight.
173
+
174
+ ### What Vue already does, and where it stops
175
+
176
+ Vue fixed this in **3.5.41** ([vuejs/core#14411](https://github.com/vuejs/core/pull/14411)):
177
+ on hydration it compares the field's live value against what the server rendered and, when
178
+ they differ, adopts the typed value into the model instead of overwriting the node.
179
+
180
+ That adoption is gated on `type="text"` and `textarea`. **Every other type still loses the
181
+ entry** — and those are exactly the fields a sign-in form uses:
182
+
183
+ | Input type | Typed before hydration |
184
+ |------------|------------------------|
185
+ | `text`, `textarea` | kept by Vue itself |
186
+ | `email`, `password`, `tel`, `url`, `search`, `number` | **erased** |
187
+
188
+ Widening the gate is tracked upstream as
189
+ [vuejs/core#15210](https://github.com/vuejs/core/issues/15210) — open, labelled
190
+ `p2-edge-case`, no milestone.
191
+
192
+ ### What this module adds
193
+
194
+ A small client plugin that closes the remaining gap. Just before hydration it reads what is
195
+ in each field; just after, it writes back anything that was overwritten and dispatches a
196
+ synthetic `input` event so `v-model` adopts it.
197
+
198
+ A field counts as edited when its value differs from `defaultValue` — the same test Vue uses,
199
+ so values the server pre-filled are never mistaken for user input. A browser autofill that
200
+ lands before hydration is recovered the same way.
201
+
202
+ Fields stay ordinary editable fields throughout. Nothing is made `readonly`, so autofill,
203
+ screen-reader semantics and the mobile on-screen keyboard are untouched, and the user never
204
+ faces a field that looks usable and silently refuses.
205
+
206
+ **This is a stopgap.** When Vue covers the remaining types, delete it —
207
+ `test/pre-hydration-input.test.ts` pins the current gate and will fail on the types Vue takes
208
+ over, which is the signal.
209
+
210
+ ### Opting out
211
+
212
+ ```ts
213
+ export default defineNuxtConfig({
214
+ ltExtensions: {
215
+ preHydrationInput: {
216
+ enabled: false,
217
+ // or keep it on and only change how long deferred subtrees are waited for:
218
+ // maxRestoreMs: 3000,
219
+ },
220
+ },
221
+ });
222
+ ```
223
+
159
224
  ## Usage
160
225
 
161
226
  ### Authentication
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.11.1";
6
+ declare const version = "1.12.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,9 +1,9 @@
1
1
  {
2
2
  "configKey": "ltExtensions",
3
3
  "name": "@lenne.tech/nuxt-extensions",
4
- "version": "1.11.1",
4
+ "version": "1.12.0",
5
5
  "builder": {
6
- "@nuxt/module-builder": "1.0.2",
6
+ "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
8
8
  }
9
9
  }
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineNuxtModule, createResolver, tryResolveModule, addImports, addComponent, addPlugin, addRouteMiddleware } from '@nuxt/kit';
2
2
 
3
3
  const name = "@lenne.tech/nuxt-extensions";
4
- const version = "1.11.1";
4
+ const version = "1.12.0";
5
5
  const configKey = "ltExtensions";
6
6
  const DEFAULT_LT_AUTH_STATE_COOKIE = "lt-auth-state";
7
7
  const DEFAULT_LT_JWT_TOKEN_COOKIE = "lt-jwt-token";
@@ -39,6 +39,9 @@ const defaultOptions = {
39
39
  i18n: {
40
40
  autoMerge: true
41
41
  },
42
+ preHydrationInput: {
43
+ enabled: true
44
+ },
42
45
  tus: {
43
46
  defaultChunkSize: 5 * 1024 * 1024,
44
47
  // 5MB
@@ -64,6 +67,7 @@ const module$1 = defineNuxtModule({
64
67
  },
65
68
  errorTranslation: { ...defaultOptions.errorTranslation, ...options.errorTranslation },
66
69
  i18n: { ...defaultOptions.i18n, ...options.i18n },
70
+ preHydrationInput: { ...defaultOptions.preHydrationInput, ...options.preHydrationInput },
67
71
  tus: { ...defaultOptions.tus, ...options.tus }
68
72
  };
69
73
  const passkeyAvailable = !!await tryResolveModule("@better-auth/passkey/client", new URL(import.meta.url));
@@ -120,6 +124,10 @@ const module$1 = defineNuxtModule({
120
124
  enabled: resolvedOptions.errorTranslation?.enabled ?? true,
121
125
  defaultLocale: resolvedOptions.errorTranslation?.defaultLocale || "de"
122
126
  },
127
+ preHydrationInput: {
128
+ enabled: resolvedOptions.preHydrationInput?.enabled ?? true,
129
+ maxRestoreMs: resolvedOptions.preHydrationInput?.maxRestoreMs ?? 1500
130
+ },
123
131
  tus: {
124
132
  defaultChunkSize: resolvedOptions.tus?.defaultChunkSize || 5 * 1024 * 1024,
125
133
  defaultEndpoint: resolvedOptions.tus?.defaultEndpoint || "/files/upload"
@@ -208,6 +216,9 @@ const module$1 = defineNuxtModule({
208
216
  });
209
217
  });
210
218
  }
219
+ if (resolvedOptions.preHydrationInput.enabled !== false) {
220
+ addPlugin(resolve("./runtime/plugins/pre-hydration-input.client"));
221
+ }
211
222
  nuxt.options.build.transpile.push(resolve("./runtime"));
212
223
  nuxt.options.build.transpile.push("tus-js-client");
213
224
  console.log(`[${name}] Module loaded with config:`, {
@@ -110,7 +110,7 @@ export function useLtAuth() {
110
110
  if (!isJwtMode.value || !jwtToken.value) return false;
111
111
  return switchToJwtMode();
112
112
  }
113
- const PATHS_REQUIRING_COOKIES = ["/passkey/", "/two-factor/", "/2fa/"];
113
+ const PATHS_REQUIRING_COOKIES = ["/passkey/", "/two-factor/", "/2fa/", "/get-session"];
114
114
  function urlRequiresCookies(url) {
115
115
  return PATHS_REQUIRING_COOKIES.some((path) => url.includes(path));
116
116
  }
@@ -284,7 +284,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
284
284
  requestPasswordReset: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
285
285
  email: string;
286
286
  redirectTo?: string | undefined;
287
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
287
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
288
288
  email: string;
289
289
  redirectTo?: string | undefined;
290
290
  } & {
@@ -488,8 +488,9 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
488
488
  scopes?: string[] | undefined;
489
489
  requestSignUp?: boolean | undefined;
490
490
  loginHint?: string | undefined;
491
+ additionalParams?: Record<string, string> | undefined;
491
492
  additionalData?: Record<string, any> | undefined;
492
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
493
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<Omit<{
493
494
  provider: (string & {}) | "linear" | "huggingface" | "github" | "apple" | "atlassian" | "cognito" | "discord" | "facebook" | "figma" | "microsoft" | "google" | "slack" | "spotify" | "twitch" | "twitter" | "dropbox" | "kick" | "linkedin" | "gitlab" | "tiktok" | "reddit" | "roblox" | "salesforce" | "vk" | "zoom" | "notion" | "kakao" | "naver" | "line" | "paybin" | "paypal" | "polar" | "railway" | "vercel" | "wechat";
494
495
  callbackURL?: string | undefined;
495
496
  newUserCallbackURL?: string | undefined;
@@ -512,9 +513,13 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
512
513
  scopes?: string[] | undefined;
513
514
  requestSignUp?: boolean | undefined;
514
515
  loginHint?: string | undefined;
516
+ additionalParams?: Record<string, string> | undefined;
515
517
  additionalData?: Record<string, any> | undefined;
516
518
  } & {
517
519
  fetchOptions?: FetchOptions | undefined;
520
+ }, "provider"> & {
521
+ provider: string | (string & {});
522
+ fetchOptions?: FetchOptions | undefined;
518
523
  }>, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
519
524
  redirect: boolean;
520
525
  url: string;
@@ -546,11 +551,20 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
546
551
  message?: string | undefined;
547
552
  }, FetchOptions["throw"] extends true ? true : false>>;
548
553
  };
549
- signOut: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<{
550
- query?: Record<string, any> | undefined;
554
+ signOut: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
555
+ callbackURL?: string | undefined;
556
+ disableRedirect?: boolean | undefined;
557
+ state?: string | undefined;
558
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<{
559
+ callbackURL?: string | undefined;
560
+ disableRedirect?: boolean | undefined;
561
+ state?: string | undefined;
562
+ } & {
551
563
  fetchOptions?: FetchOptions | undefined;
552
564
  }> | undefined, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
553
565
  success: boolean;
566
+ url: string | undefined;
567
+ redirect: boolean | undefined;
554
568
  }, {
555
569
  code?: string | undefined;
556
570
  message?: string | undefined;
@@ -664,7 +678,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
664
678
  verifyEmail: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<{
665
679
  token: string;
666
680
  callbackURL?: string | undefined;
667
- }> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
681
+ }> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
668
682
  query: {
669
683
  token: string;
670
684
  callbackURL?: string | undefined;
@@ -679,7 +693,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
679
693
  sendVerificationEmail: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
680
694
  email: string;
681
695
  callbackURL?: string | undefined;
682
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
696
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
683
697
  email: string;
684
698
  callbackURL?: string | undefined;
685
699
  } & {
@@ -693,7 +707,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
693
707
  changeEmail: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
694
708
  newEmail: string;
695
709
  callbackURL?: string | undefined;
696
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
710
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
697
711
  newEmail: string;
698
712
  callbackURL?: string | undefined;
699
713
  } & {
@@ -704,7 +718,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
704
718
  code?: string | undefined;
705
719
  message?: string | undefined;
706
720
  }, FetchOptions["throw"] extends true ? true : false>>;
707
- updateSession: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<Partial<{}>> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<import("better-auth/client").InferSessionUpdateCtx<{
721
+ updateSession: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<Partial<{}>> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<import("better-auth/client").InferSessionUpdateCtx<{
708
722
  basePath: string;
709
723
  baseURL: any;
710
724
  fetchOptions: {
@@ -729,7 +743,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
729
743
  updateUser: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<Partial<{}> & {
730
744
  name?: string | undefined;
731
745
  image?: string | undefined | null;
732
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<import("better-auth/client").InferUserUpdateCtx<{
746
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<import("better-auth/client").InferUserUpdateCtx<{
733
747
  basePath: string;
734
748
  baseURL: any;
735
749
  fetchOptions: {
@@ -746,7 +760,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
746
760
  callbackURL?: string | undefined;
747
761
  password?: string | undefined;
748
762
  token?: string | undefined;
749
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<{
763
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<{
750
764
  callbackURL?: string | undefined;
751
765
  password?: string | undefined;
752
766
  token?: string | undefined;
@@ -762,7 +776,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
762
776
  callback: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<{
763
777
  token: string;
764
778
  callbackURL?: string | undefined;
765
- }> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
779
+ }> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
766
780
  query: {
767
781
  token: string;
768
782
  callbackURL?: string | undefined;
@@ -776,7 +790,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
776
790
  message?: string | undefined;
777
791
  }, FetchOptions["throw"] extends true ? true : false>>;
778
792
  };
779
- listSessions: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<{
793
+ listSessions: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<{
780
794
  query?: Record<string, any> | undefined;
781
795
  fetchOptions?: FetchOptions | undefined;
782
796
  }> | undefined, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<import("better-auth").Prettify<{
@@ -794,7 +808,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
794
808
  }, FetchOptions["throw"] extends true ? true : false>>;
795
809
  revokeSession: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
796
810
  token: string;
797
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
811
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
798
812
  token: string;
799
813
  } & {
800
814
  fetchOptions?: FetchOptions | undefined;
@@ -804,7 +818,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
804
818
  code?: string | undefined;
805
819
  message?: string | undefined;
806
820
  }, FetchOptions["throw"] extends true ? true : false>>;
807
- revokeSessions: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<{
821
+ revokeSessions: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<{
808
822
  query?: Record<string, any> | undefined;
809
823
  fetchOptions?: FetchOptions | undefined;
810
824
  }> | undefined, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
@@ -813,7 +827,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
813
827
  code?: string | undefined;
814
828
  message?: string | undefined;
815
829
  }, FetchOptions["throw"] extends true ? true : false>>;
816
- revokeOtherSessions: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<{
830
+ revokeOtherSessions: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<{
817
831
  query?: Record<string, any> | undefined;
818
832
  fetchOptions?: FetchOptions | undefined;
819
833
  }> | undefined, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
@@ -830,14 +844,15 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
830
844
  nonce?: string | undefined;
831
845
  accessToken?: string | undefined;
832
846
  refreshToken?: string | undefined;
833
- scopes?: string[] | undefined;
834
847
  } | undefined;
835
848
  requestSignUp?: boolean | undefined;
836
849
  scopes?: string[] | undefined;
837
850
  errorCallbackURL?: string | undefined;
838
851
  disableRedirect?: boolean | undefined;
852
+ loginHint?: string | undefined;
853
+ additionalParams?: Record<string, string> | undefined;
839
854
  additionalData?: Record<string, any> | undefined;
840
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
855
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
841
856
  provider: unknown;
842
857
  callbackURL?: string | undefined;
843
858
  idToken?: {
@@ -845,12 +860,13 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
845
860
  nonce?: string | undefined;
846
861
  accessToken?: string | undefined;
847
862
  refreshToken?: string | undefined;
848
- scopes?: string[] | undefined;
849
863
  } | undefined;
850
864
  requestSignUp?: boolean | undefined;
851
865
  scopes?: string[] | undefined;
852
866
  errorCallbackURL?: string | undefined;
853
867
  disableRedirect?: boolean | undefined;
868
+ loginHint?: string | undefined;
869
+ additionalParams?: Record<string, string> | undefined;
854
870
  additionalData?: Record<string, any> | undefined;
855
871
  } & {
856
872
  fetchOptions?: FetchOptions | undefined;
@@ -861,7 +877,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
861
877
  code?: string | undefined;
862
878
  message?: string | undefined;
863
879
  }, FetchOptions["throw"] extends true ? true : false>>;
864
- listAccounts: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<{
880
+ listAccounts: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<{
865
881
  query?: Record<string, any> | undefined;
866
882
  fetchOptions?: FetchOptions | undefined;
867
883
  }> | undefined, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
@@ -871,17 +887,16 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
871
887
  updatedAt: Date;
872
888
  userId: string;
873
889
  providerId: string;
890
+ issuer: string;
874
891
  accountId: string;
875
892
  }[], {
876
893
  code?: string | undefined;
877
894
  message?: string | undefined;
878
895
  }, FetchOptions["throw"] extends true ? true : false>>;
879
896
  unlinkAccount: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
880
- providerId: string;
881
- accountId?: string | undefined;
882
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
883
- providerId: string;
884
- accountId?: string | undefined;
897
+ accountId: string;
898
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
899
+ accountId: string;
885
900
  } & {
886
901
  fetchOptions?: FetchOptions | undefined;
887
902
  }>, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
@@ -891,12 +906,18 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
891
906
  message?: string | undefined;
892
907
  }, FetchOptions["throw"] extends true ? true : false>>;
893
908
  refreshToken: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
894
- providerId: string;
895
- accountId?: string | undefined;
909
+ accountId: string;
896
910
  userId?: string | undefined;
897
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
898
- providerId: string;
899
- accountId?: string | undefined;
911
+ } | {
912
+ useAccountCookie: true;
913
+ userId?: string | undefined;
914
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
915
+ accountId: string;
916
+ userId?: string | undefined;
917
+ } & {
918
+ fetchOptions?: FetchOptions | undefined;
919
+ }> | import("better-auth").Prettify<{
920
+ useAccountCookie: true;
900
921
  userId?: string | undefined;
901
922
  } & {
902
923
  fetchOptions?: FetchOptions | undefined;
@@ -914,12 +935,18 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
914
935
  message?: string | undefined;
915
936
  }, FetchOptions["throw"] extends true ? true : false>>;
916
937
  getAccessToken: <FetchOptions extends import("better-auth").ClientFetchOption<Partial<{
917
- providerId: string;
918
- accountId?: string | undefined;
938
+ accountId: string;
919
939
  userId?: string | undefined;
920
- }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, any> | undefined>>(data_0: import("better-auth").Prettify<{
921
- providerId: string;
922
- accountId?: string | undefined;
940
+ } | {
941
+ useAccountCookie: true;
942
+ userId?: string | undefined;
943
+ }> & Record<string, any>, Partial<Record<string, any>> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
944
+ accountId: string;
945
+ userId?: string | undefined;
946
+ } & {
947
+ fetchOptions?: FetchOptions | undefined;
948
+ }> | import("better-auth").Prettify<{
949
+ useAccountCookie: true;
923
950
  userId?: string | undefined;
924
951
  } & {
925
952
  fetchOptions?: FetchOptions | undefined;
@@ -933,19 +960,29 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
933
960
  message?: string | undefined;
934
961
  }, FetchOptions["throw"] extends true ? true : false>>;
935
962
  accountInfo: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<{
936
- accountId?: string | undefined;
937
- providerId?: string | undefined;
963
+ accountId: string;
938
964
  userId?: string | undefined;
939
- }> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<{
940
- query?: {
941
- accountId?: string | undefined;
942
- providerId?: string | undefined;
965
+ } | {
966
+ useAccountCookie: true;
967
+ userId?: string | undefined;
968
+ }> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0: import("better-auth").Prettify<{
969
+ query: {
970
+ accountId: string;
943
971
  userId?: string | undefined;
944
- } | undefined;
972
+ } | {
973
+ useAccountCookie: true;
974
+ userId?: string | undefined;
975
+ };
945
976
  fetchOptions?: FetchOptions | undefined;
946
- }> | undefined, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
977
+ }>, data_1?: FetchOptions | undefined) => Promise<import("better-auth/client").BetterFetchResponse<{
978
+ account: {
979
+ id: string;
980
+ providerId: string;
981
+ issuer: string;
982
+ accountId: string;
983
+ };
947
984
  user: import("better-auth").OAuth2UserInfo;
948
- data: Record<string, any>;
985
+ data: object;
949
986
  }, {
950
987
  code?: string | undefined;
951
988
  message?: string | undefined;
@@ -953,7 +990,7 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
953
990
  getSession: <FetchOptions extends import("better-auth").ClientFetchOption<never, Partial<{
954
991
  disableCookieCache?: unknown;
955
992
  disableRefresh?: unknown;
956
- }> & Record<string, any>, Record<string, any> | undefined>>(data_0?: import("better-auth").Prettify<{
993
+ }> & Record<string, any>, Record<string, string | undefined> | undefined>>(data_0?: import("better-auth").Prettify<{
957
994
  query?: {
958
995
  disableCookieCache?: unknown;
959
996
  disableRefresh?: unknown;
@@ -983,6 +1020,27 @@ export declare function createLtAuthClient(config?: LtAuthClientConfig): {
983
1020
  code?: string | undefined;
984
1021
  message?: string | undefined;
985
1022
  }, FetchOptions["throw"] extends true ? true : false>>;
1023
+ hydrateSession: (session: {
1024
+ user: import("better-auth").StripEmptyObjects<{
1025
+ id: string;
1026
+ createdAt: Date;
1027
+ updatedAt: Date;
1028
+ email: string;
1029
+ emailVerified: boolean;
1030
+ name: string;
1031
+ image?: string | null | undefined;
1032
+ }>;
1033
+ session: import("better-auth").StripEmptyObjects<{
1034
+ id: string;
1035
+ createdAt: Date;
1036
+ updatedAt: Date;
1037
+ userId: string;
1038
+ expiresAt: Date;
1039
+ token: string;
1040
+ ipAddress?: string | null | undefined;
1041
+ userAgent?: string | null | undefined;
1042
+ }>;
1043
+ } | null) => void;
986
1044
  $ERROR_CODES: {
987
1045
  USER_NOT_FOUND: {
988
1046
  readonly code: "USER_NOT_FOUND";
@@ -39,7 +39,8 @@ export function getOrCreateLtAuthClient(config) {
39
39
  }
40
40
  export function createLtAuthClient(config = {}) {
41
41
  const useProxy = isLocalDevApiProxy();
42
- const defaultBaseURL = useProxy ? "" : import.meta.env?.VITE_API_URL || process.env.API_URL || "";
42
+ const envApiUrl = typeof import.meta.env?.VITE_API_URL === "string" ? import.meta.env.VITE_API_URL : void 0;
43
+ const defaultBaseURL = useProxy ? "" : envApiUrl || process.env.API_URL || "";
43
44
  const defaultBasePath = useProxy ? "/api/iam" : "/iam";
44
45
  const {
45
46
  baseURL = defaultBaseURL,
@@ -29,7 +29,17 @@ export default (nuxtApp) => {
29
29
  "/forgot-password",
30
30
  "/reset-password",
31
31
  "/verify-email",
32
- "/session",
32
+ // Better Auth's session routes, spelled out. A generic '/session' entry
33
+ // used to stand here and never matched anything: the routes are
34
+ // `get-session`, `list-sessions`, `revoke-session`, … — in every one of
35
+ // them the character before `session` is `-`, not `/`, so `url.includes`
36
+ // returned false. `/get-session` matters most: it is the URL the session
37
+ // probe below calls, and exempting it is what keeps a 401 from the probe
38
+ // out of this handler.
39
+ "/get-session",
40
+ "/list-sessions",
41
+ "/revoke-session",
42
+ "/revoke-other-sessions",
33
43
  "/token",
34
44
  // Passkey endpoints - handled by authFetch with JWT fallback
35
45
  "/passkey/",
@@ -43,19 +53,38 @@ export default (nuxtApp) => {
43
53
  ];
44
54
  return authEndpoints.some((endpoint) => url.includes(endpoint));
45
55
  }
56
+ const ALIVE_VERDICT_TTL_MS = 1e3;
57
+ let aliveVerdictValidUntil = 0;
46
58
  async function isSessionStillAlive() {
47
59
  try {
48
- const { fetchWithAuth } = getAuth();
60
+ const { fetchWithAuth, isJwtMode, switchToJwtMode } = getAuth();
49
61
  const response = await fetchWithAuth(`${getLtApiBase()}/get-session`, { method: "GET" });
50
- if (!response.ok) {
62
+ if (response.status === 401 || response.status === 403) {
51
63
  return false;
52
64
  }
65
+ if (!response.ok) {
66
+ return null;
67
+ }
53
68
  const data = await response.json().catch(() => null);
54
- return Boolean(data && (data.user || data.session));
69
+ const alive = Boolean(data && (data.user || data.session));
70
+ if (alive && isJwtMode.value) {
71
+ await switchToJwtMode().catch(() => false);
72
+ }
73
+ return alive;
55
74
  } catch {
56
75
  return null;
57
76
  }
58
77
  }
78
+ async function probeSessionAlive() {
79
+ if (Date.now() < aliveVerdictValidUntil) {
80
+ return true;
81
+ }
82
+ const verdict = await isSessionStillAlive();
83
+ if (verdict === true) {
84
+ aliveVerdictValidUntil = Date.now() + ALIVE_VERDICT_TTL_MS;
85
+ }
86
+ return verdict;
87
+ }
59
88
  async function handleUnauthorized(requestUrl) {
60
89
  if (isHandling401) {
61
90
  return;
@@ -66,28 +95,35 @@ export default (nuxtApp) => {
66
95
  if (isPublicAuthRoute()) {
67
96
  return;
68
97
  }
98
+ const { clearUser, isAuthenticated } = getAuth();
99
+ if (!isAuthenticated.value) {
100
+ return;
101
+ }
69
102
  isHandling401 = true;
103
+ let loggingOut = false;
70
104
  try {
71
- const { clearUser, isAuthenticated } = getAuth();
72
- if (isAuthenticated.value) {
73
- const sessionAlive = await isSessionStillAlive();
74
- if (sessionAlive !== false) {
75
- console.debug(
76
- sessionAlive ? `[LtAuth Interceptor] 401 from ${requestUrl ?? "unknown URL"} but session is still valid \u2014 treating it as a permission error, not logging out` : "[LtAuth Interceptor] 401 received but session state could not be verified \u2014 not logging out"
77
- );
78
- return;
79
- }
80
- console.debug("[LtAuth Interceptor] Session expired, logging out...");
81
- clearUser();
82
- const router = nuxtApp.$router;
83
- const currentPath = router?.currentRoute?.value?.fullPath;
84
- const redirectQuery = currentPath && currentPath !== loginPath ? `?redirect=${encodeURIComponent(currentPath)}` : "";
85
- window.location.href = loginPath + redirectQuery;
105
+ const sessionAlive = await probeSessionAlive();
106
+ if (sessionAlive !== false) {
107
+ console.debug(
108
+ sessionAlive ? `[LtAuth Interceptor] 401 from ${requestUrl ?? "unknown URL"} but session is still valid \u2014 treating it as a permission error, not logging out` : "[LtAuth Interceptor] 401 received but session state could not be verified \u2014 not logging out"
109
+ );
110
+ return;
86
111
  }
112
+ console.debug("[LtAuth Interceptor] Session expired, logging out...");
113
+ loggingOut = true;
114
+ clearUser();
115
+ const router = nuxtApp.$router;
116
+ const currentPath = router?.currentRoute?.value?.fullPath;
117
+ const redirectQuery = currentPath && currentPath !== loginPath ? `?redirect=${encodeURIComponent(currentPath)}` : "";
118
+ window.location.href = loginPath + redirectQuery;
87
119
  } finally {
88
- setTimeout(() => {
120
+ if (loggingOut) {
121
+ setTimeout(() => {
122
+ isHandling401 = false;
123
+ }, 1e3);
124
+ } else {
89
125
  isHandling401 = false;
90
- }, 1e3);
126
+ }
91
127
  }
92
128
  }
93
129
  const wrapMarker = "__ltAuthFetchWrapped";
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Pre-hydration input preservation — the part Vue does not do yet.
3
+ *
4
+ * ## What Vue already handles
5
+ *
6
+ * Until Vue hydrates a server-rendered `<input>`, the element carries no framework listener.
7
+ * What the user types is written to the DOM node, the `input` event reaches no model, and
8
+ * `v-model`'s mounted hook then writes the model value back over it — the entry is ERASED,
9
+ * not delayed.
10
+ *
11
+ * Vue fixed this in `vModelText` (vuejs/core#14411, shipped in 3.5.41): `created` records
12
+ * `el.defaultValue`, and `mounted` compares the live value against it. When they differ, the
13
+ * DOM value is pushed INTO the model instead of the node being overwritten.
14
+ *
15
+ * That adoption is gated on `el.type === 'text' || el.type === 'textarea'`. **Every other
16
+ * input type still falls through to the erasure** — `email`, `password`, `tel`, `url`,
17
+ * `search`, `number`. Verified against Vue 3.5.41 in `test/pre-hydration-input.test.ts`.
18
+ *
19
+ * Widening the gate is tracked upstream as vuejs/core#15210 (open, `p2-edge-case`, no
20
+ * milestone, PR #15211 pending). **This plugin is a stopgap for exactly that gap and should
21
+ * be deleted once the upstream fix lands** — the test above will start failing on the types
22
+ * Vue has taken over, which is the signal to remove it.
23
+ *
24
+ * ## Why this matters more than the gate suggests
25
+ *
26
+ * The uncovered types are precisely the ones a sign-in form uses. That is also where people
27
+ * type on sight, and where losing the entry is most expensive: a password silently truncated
28
+ * to its last few characters produces "invalid credentials", which sends the user down the
29
+ * wrong diagnostic path entirely.
30
+ *
31
+ * ## How it works
32
+ *
33
+ * At `app:beforeMount` the client bundle has loaded but hydration has not run, so every
34
+ * server-rendered field still holds whatever the user typed. A field is treated as edited
35
+ * when its live value differs from `defaultValue` — the same test Vue itself uses, so the
36
+ * server's own pre-filled values are never mistaken for user input.
37
+ *
38
+ * At `app:mounted` any field whose value was overwritten gets it back, followed by a
39
+ * synthetic `input` event so the framework's own listener adopts it. Setting `.value` alone
40
+ * would not do: the model would still hold the old value and the next render would wipe the
41
+ * node again.
42
+ *
43
+ * A browser autofill that landed before hydration is recovered the same way.
44
+ *
45
+ * ## Why not make the field `readonly` until mounted
46
+ *
47
+ * Declining the keystroke was the first approach tried and it is worse. The user is left with
48
+ * a field that looks usable and silently refuses, which reads as a broken page. It also costs
49
+ * real things: `readonly` is the documented technique for SUPPRESSING browser autofill,
50
+ * screen readers announce "read only" and never announce the silent flip back, and mobile
51
+ * browsers do not raise the on-screen keyboard for it. Preserving the entry avoids all of
52
+ * that, because the field is never anything other than a normal, editable field.
53
+ */
54
+ type EditableField = HTMLInputElement | HTMLTextAreaElement;
55
+ /**
56
+ * Collect fields the user edited before hydration.
57
+ *
58
+ * `value !== defaultValue` is the discriminator, matching Vue's own: `defaultValue` is what
59
+ * the server rendered, so a difference can only have come from the user (or from an autofill,
60
+ * which is just as worth keeping). Server-supplied values therefore never enter the snapshot
61
+ * and can never be restored over a legitimate change made during hydration.
62
+ */
63
+ export declare function collectEditedFields(root: ParentNode): Map<EditableField, string>;
64
+ /**
65
+ * Write a preserved value back onto a field and tell the framework about it.
66
+ *
67
+ * @returns `true` when a value was actually restored.
68
+ */
69
+ export declare function restoreField(field: EditableField, value: string): boolean;
70
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
71
+ export default _default;
@@ -0,0 +1,57 @@
1
+ import { defineNuxtPlugin, useRuntimeConfig } from "#imports";
2
+ export function collectEditedFields(root) {
3
+ const edited = /* @__PURE__ */ new Map();
4
+ for (const node of root.querySelectorAll("input, textarea")) {
5
+ const field = node;
6
+ if (field.value !== field.defaultValue) {
7
+ edited.set(field, field.value);
8
+ }
9
+ }
10
+ return edited;
11
+ }
12
+ export function restoreField(field, value) {
13
+ if (!field.isConnected || field.value === value) {
14
+ return false;
15
+ }
16
+ field.value = value;
17
+ field.dispatchEvent(new Event("input", { bubbles: true }));
18
+ return true;
19
+ }
20
+ export default defineNuxtPlugin({
21
+ // After the app's own plugins: the restore has to land once hydration has done its writes.
22
+ enforce: "post",
23
+ name: "lt-pre-hydration-input",
24
+ setup(nuxtApp) {
25
+ const options = useRuntimeConfig().public.ltExtensions?.preHydrationInput;
26
+ if (options?.enabled === false) {
27
+ return;
28
+ }
29
+ const maxRestoreMs = options?.maxRestoreMs ?? 1500;
30
+ let edited = /* @__PURE__ */ new Map();
31
+ nuxtApp.hook("app:beforeMount", () => {
32
+ edited = collectEditedFields(document);
33
+ });
34
+ nuxtApp.hook("app:mounted", () => {
35
+ if (edited.size === 0) {
36
+ return;
37
+ }
38
+ const sweep = () => {
39
+ for (const [field, value] of edited) {
40
+ if (restoreField(field, value)) {
41
+ edited.delete(field);
42
+ }
43
+ }
44
+ };
45
+ sweep();
46
+ if (maxRestoreMs <= 0) {
47
+ edited = /* @__PURE__ */ new Map();
48
+ return;
49
+ }
50
+ const interval = setInterval(sweep, 250);
51
+ setTimeout(() => {
52
+ clearInterval(interval);
53
+ edited = /* @__PURE__ */ new Map();
54
+ }, maxRestoreMs);
55
+ });
56
+ }
57
+ });
@@ -1,5 +1,5 @@
1
1
  export type { LtAuthClientConfig, LtAuthMode, LtAuthResponse, LtAuthState, LtPasskeyAuthResult, LtPasskeyRegisterResult, LtUser, UseLtAuthReturn } from './auth.js';
2
2
  export type { LtFileInfo, LtUploadItem, LtUploadOptions, LtUploadProgress, LtUploadStatus, UseLtFileReturn, UseLtTusUploadReturn } from './upload.js';
3
- export type { LtAiModuleOptions, LtAuthCookieNamesOptions, LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtI18nModuleOptions, LtSystemSetupModuleOptions, LtTusModuleOptions, } from './module.js';
3
+ export type { LtAiModuleOptions, LtAuthCookieNamesOptions, LtAuthModuleOptions, LtErrorTranslationModuleOptions, LtExtensionsModuleOptions, LtExtensionsPublicRuntimeConfig, LtI18nModuleOptions, LtPreHydrationInputOptions, LtSystemSetupModuleOptions, LtTusModuleOptions, } from './module.js';
4
4
  export type { LtErrorTranslationResponse, LtParsedError, UseLtErrorTranslationReturn } from './error.js';
5
5
  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';
@@ -129,9 +129,61 @@ export interface LtExtensionsModuleOptions {
129
129
  errorTranslation?: LtErrorTranslationModuleOptions;
130
130
  /** i18n configuration */
131
131
  i18n?: LtI18nModuleOptions;
132
+ /** Pre-hydration input preservation configuration */
133
+ preHydrationInput?: LtPreHydrationInputOptions;
132
134
  /** TUS upload module configuration */
133
135
  tus?: LtTusModuleOptions;
134
136
  }
137
+ /**
138
+ * Pre-hydration input preservation.
139
+ *
140
+ * Until Vue hydrates a server-rendered `<input>`, the element carries no framework listener:
141
+ * text typed into it reaches no model and is then overwritten by the mounting value — ERASED,
142
+ * not delayed.
143
+ *
144
+ * Vue fixed this in 3.5.41 (vuejs/core#14411), but only for `type="text"` and `textarea`.
145
+ * Every other type still loses the entry: `email`, `password`, `tel`, `url`, `search`,
146
+ * `number` — precisely the fields a sign-in form uses, and where people are most likely to
147
+ * type on sight. Widening it is tracked upstream as vuejs/core#15210.
148
+ *
149
+ * This closes that remaining gap: what the user typed is read out of the DOM just before
150
+ * hydration and written back afterwards, with a synthetic `input` event so `v-model` adopts
151
+ * it. Fields stay ordinary editable fields throughout — nothing is made `readonly`, so
152
+ * autofill, screen-reader semantics and the mobile keyboard are untouched. A browser autofill
153
+ * that lands before hydration is recovered the same way.
154
+ *
155
+ * Applies to every `<input>` and `<textarea>` on the page, regardless of which UI library
156
+ * rendered it. It is a stopgap and is meant to be removed once Vue covers the remaining
157
+ * types.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * export default defineNuxtConfig({
162
+ * ltExtensions: {
163
+ * preHydrationInput: { enabled: false },
164
+ * },
165
+ * });
166
+ * ```
167
+ */
168
+ export interface LtPreHydrationInputOptions {
169
+ /**
170
+ * Enable preservation. Defaults to `true`.
171
+ *
172
+ * Turn it off only with a reason: the alternative is that a user who types immediately
173
+ * after a page appears loses the entry silently.
174
+ */
175
+ enabled?: boolean;
176
+ /**
177
+ * How long after mount to keep restoring, in milliseconds. Defaults to `1500`; `0` restores
178
+ * once and stops immediately.
179
+ *
180
+ * Most fields are repaired in the first pass. The window exists for subtrees whose mount is
181
+ * deferred — an unresolved `<Suspense>` boundary writes its model value later than
182
+ * `app:mounted`. When it elapses the snapshot is dropped, so preserved values, passwords
183
+ * included, are not kept alive for the session.
184
+ */
185
+ maxRestoreMs?: number;
186
+ }
135
187
  /**
136
188
  * Public runtime config added by this module
137
189
  */
@@ -167,6 +219,10 @@ export interface LtExtensionsPublicRuntimeConfig {
167
219
  enabled: boolean;
168
220
  defaultLocale: string;
169
221
  };
222
+ preHydrationInput: {
223
+ enabled: boolean;
224
+ maxRestoreMs: number;
225
+ };
170
226
  tus: {
171
227
  defaultChunkSize: number;
172
228
  defaultEndpoint: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nuxt-extensions",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Reusable Nuxt 4 composables, components, and Better-Auth integration for lenne.tech projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -99,21 +99,22 @@
99
99
  }
100
100
  },
101
101
  "devDependencies": {
102
- "@better-auth/passkey": "1.6.23",
103
- "@nuxt/module-builder": "1.0.2",
104
- "@nuxt/schema": "4.4.8",
105
- "@playwright/test": "1.61.1",
106
- "@types/node": "26.1.1",
107
- "@vitest/coverage-v8": "4.1.10",
108
- "better-auth": "1.6.23",
109
- "happy-dom": "20.10.6",
110
- "nuxt": "4.4.8",
111
- "oxfmt": "0.59.0",
112
- "oxlint": "1.74.0",
102
+ "@better-auth/passkey": "1.7.1",
103
+ "@nuxt/module-builder": "1.0.3",
104
+ "@nuxt/schema": "4.5.2",
105
+ "@playwright/test": "1.62.1",
106
+ "@types/node": "26.2.0",
107
+ "@vitest/coverage-v8": "4.1.11",
108
+ "@vue/server-renderer": "3.5.41",
109
+ "better-auth": "1.7.1",
110
+ "happy-dom": "20.11.6",
111
+ "nuxt": "4.5.2",
112
+ "oxfmt": "0.64.0",
113
+ "oxlint": "1.79.0",
113
114
  "tus-js-client": "4.3.1",
114
115
  "typescript": "5.9.3",
115
- "vitest": "4.1.10",
116
- "vue-tsc": "3.3.7"
116
+ "vitest": "4.1.11",
117
+ "vue-tsc": "3.3.11"
117
118
  },
118
119
  "keywords": [
119
120
  "nuxt",