@myelophone/nuxt 0.18.2

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 (259) hide show
  1. package/.devcontainer/Dockerfile +7 -0
  2. package/.devcontainer/devcontainer.json +21 -0
  3. package/.editorconfig +43 -0
  4. package/.github/workflows/package.yml +73 -0
  5. package/.nuxtignore +6 -0
  6. package/.releaserc.js +132 -0
  7. package/.yarn/releases/yarn-4.18.0.cjs +1000 -0
  8. package/.yarnrc.yml +8 -0
  9. package/CHANGELOG.md +141 -0
  10. package/LICENSE +77 -0
  11. package/README.md +3004 -0
  12. package/app/app.vue +455 -0
  13. package/app/assets/css/animation.css +81 -0
  14. package/app/assets/css/backgrounds.css +57 -0
  15. package/app/assets/css/base.css +182 -0
  16. package/app/assets/css/index.css +9 -0
  17. package/app/assets/css/links.css +12 -0
  18. package/app/assets/css/multimedia.css +102 -0
  19. package/app/assets/css/preloader.css +68 -0
  20. package/app/assets/css/print.css +40 -0
  21. package/app/assets/css/tailwind.css +20 -0
  22. package/app/assets/css/theme.css +29 -0
  23. package/app/components/consent/GoogleMap.vue +59 -0
  24. package/app/components/consent/Youtube.vue +27 -0
  25. package/app/components/cookie/Banner.vue +70 -0
  26. package/app/components/cookie/ConsentWrapper.vue +71 -0
  27. package/app/components/cookie/PrivacyPolicy.vue +179 -0
  28. package/app/components/cookie/SettingsModal.vue +387 -0
  29. package/app/components/grid/Col.vue +262 -0
  30. package/app/components/grid/Container.vue +236 -0
  31. package/app/components/grid/Row.vue +248 -0
  32. package/app/components/grid/Stack.vue +233 -0
  33. package/app/components/myelophone/Copyright.vue +5 -0
  34. package/app/components/myelophone/Welcome.vue +107 -0
  35. package/app/components/page/FullscreenPreloader.vue +177 -0
  36. package/app/components/page/Preloader.vue +18 -0
  37. package/app/components/page/ReadyLocalized.vue +19 -0
  38. package/app/components/ready/.gitkeep +0 -0
  39. package/app/components/safe/Email.vue +52 -0
  40. package/app/components/safe/ImgWithLoader.vue +180 -0
  41. package/app/components/safe/NuxtImg.vue +25 -0
  42. package/app/components/safe/NuxtPicture.vue +29 -0
  43. package/app/components/section/.gitkeep +0 -0
  44. package/app/components/seo/BrandInfo.vue +76 -0
  45. package/app/components/seo/ContentNoIndex.vue +35 -0
  46. package/app/components/seo/NoIndex.vue +22 -0
  47. package/app/components/seo/OrgInfo.vue +130 -0
  48. package/app/components/seo/ProfileInfo.vue +137 -0
  49. package/app/components/ui/Accordion.vue +163 -0
  50. package/app/components/ui/Alert.vue +179 -0
  51. package/app/components/ui/Avatar.vue +88 -0
  52. package/app/components/ui/Badge.vue +150 -0
  53. package/app/components/ui/Banner.vue +133 -0
  54. package/app/components/ui/Button.vue +175 -0
  55. package/app/components/ui/Card.vue +66 -0
  56. package/app/components/ui/Checkbox.vue +134 -0
  57. package/app/components/ui/Chip.vue +136 -0
  58. package/app/components/ui/CommandPalette.vue +434 -0
  59. package/app/components/ui/Console.vue +583 -0
  60. package/app/components/ui/CursorCreative.vue +148 -0
  61. package/app/components/ui/FeatureAccordion.vue +165 -0
  62. package/app/components/ui/FileUpload.vue +705 -0
  63. package/app/components/ui/GeoDependent.vue +72 -0
  64. package/app/components/ui/Heading.vue +51 -0
  65. package/app/components/ui/Icon.vue +42 -0
  66. package/app/components/ui/Icon8.vue +39 -0
  67. package/app/components/ui/Input.vue +88 -0
  68. package/app/components/ui/LangLink.vue +34 -0
  69. package/app/components/ui/LangVisible.vue +33 -0
  70. package/app/components/ui/LanguageSelect.vue +202 -0
  71. package/app/components/ui/LightBox.vue +155 -0
  72. package/app/components/ui/LocalTime.vue +170 -0
  73. package/app/components/ui/Modal.vue +161 -0
  74. package/app/components/ui/Popup.vue +200 -0
  75. package/app/components/ui/PostContent.server.vue +23 -0
  76. package/app/components/ui/ProtectedEmail.vue +52 -0
  77. package/app/components/ui/RelativeTime.vue +141 -0
  78. package/app/components/ui/ScheduledContent.vue +150 -0
  79. package/app/components/ui/ScrollToTop.client.vue +48 -0
  80. package/app/components/ui/SegmentedControl.vue +148 -0
  81. package/app/components/ui/SiteSearch.vue +1191 -0
  82. package/app/components/ui/Skeleton.vue +91 -0
  83. package/app/components/ui/SmartContrast.vue +74 -0
  84. package/app/components/ui/SnapContainer.vue +222 -0
  85. package/app/components/ui/SnapSection.vue +294 -0
  86. package/app/components/ui/SonnerStyle.vue +7 -0
  87. package/app/components/ui/SplitSection.vue +99 -0
  88. package/app/components/ui/Steps.vue +858 -0
  89. package/app/components/ui/StickyWrapper.vue +46 -0
  90. package/app/components/ui/Table.vue +251 -0
  91. package/app/components/ui/Tabs.vue +451 -0
  92. package/app/components/ui/TextColumn.vue +36 -0
  93. package/app/components/ui/Textarea.vue +251 -0
  94. package/app/components/ui/Toggler.vue +40 -0
  95. package/app/components/ui/Tooltip.vue +524 -0
  96. package/app/components/ui/TruncateText.vue +105 -0
  97. package/app/components/ui/ViewportSpacer.vue +72 -0
  98. package/app/components/view/AnimatedCounter.vue +121 -0
  99. package/app/components/view/AnnouncementBar.vue +104 -0
  100. package/app/components/view/AvatarGroup.vue +104 -0
  101. package/app/components/view/Breadcrumbs.vue +97 -0
  102. package/app/components/view/ContentWithToc.vue +152 -0
  103. package/app/components/view/Copyright.vue +68 -0
  104. package/app/components/view/CountdownTimer.vue +184 -0
  105. package/app/components/view/CreativeCTA.vue +143 -0
  106. package/app/components/view/CurrencySelect.vue +254 -0
  107. package/app/components/view/FeatureBoxGrid.vue +142 -0
  108. package/app/components/view/HorizontalMenu.vue +97 -0
  109. package/app/components/view/ImageCompare.vue +141 -0
  110. package/app/components/view/ImgSlider.vue +333 -0
  111. package/app/components/view/InfiniteMarquee.vue +147 -0
  112. package/app/components/view/InfoBar.vue +86 -0
  113. package/app/components/view/LogoGrid.vue +106 -0
  114. package/app/components/view/LogoSlider.vue +83 -0
  115. package/app/components/view/NotFound.vue +81 -0
  116. package/app/components/view/PartialBackground.vue +313 -0
  117. package/app/components/view/QuoteBig.vue +111 -0
  118. package/app/components/view/ResponsiveMenu.vue +219 -0
  119. package/app/components/view/ReviewsSlider.vue +230 -0
  120. package/app/components/view/SocialBar.vue +78 -0
  121. package/app/components/view/StickyHeadingContent.vue +158 -0
  122. package/app/components/view/TallyForm.vue +295 -0
  123. package/app/components/view/WarningOutdated.vue +113 -0
  124. package/app/composables/useApi.ts +88 -0
  125. package/app/composables/useAppSeo.ts +78 -0
  126. package/app/composables/useBreadcrumbs.ts +106 -0
  127. package/app/composables/useChangeUrl.ts +73 -0
  128. package/app/composables/useCommand.ts +28 -0
  129. package/app/composables/useCookieControl.ts +111 -0
  130. package/app/composables/useCookieModal.ts +9 -0
  131. package/app/composables/useDevice.ts +109 -0
  132. package/app/composables/useGeoFetch.ts +139 -0
  133. package/app/composables/useLoadScript.ts +36 -0
  134. package/app/composables/useModalState.ts +37 -0
  135. package/app/composables/useMultiLang.ts +278 -0
  136. package/app/composables/useNetwork.ts +49 -0
  137. package/app/composables/useNuxtVersion.ts +5 -0
  138. package/app/composables/usePiniaStore.ts +5 -0
  139. package/app/composables/usePopupControl.ts +57 -0
  140. package/app/composables/useQuery.ts +307 -0
  141. package/app/composables/useReadyPage.ts +182 -0
  142. package/app/composables/useSafeList.ts +90 -0
  143. package/app/composables/useScriptLoader.ts +221 -0
  144. package/app/composables/useSiteSearch.ts +32 -0
  145. package/app/composables/useSnapScroll.ts +24 -0
  146. package/app/composables/useStatic.ts +5 -0
  147. package/app/composables/useStorage.ts +252 -0
  148. package/app/composables/useStoreBroadcast.ts +146 -0
  149. package/app/composables/useThemeSync.ts +81 -0
  150. package/app/constants/layout.ts +109 -0
  151. package/app/constants/predefinedCookieScripts.ts +1222 -0
  152. package/app/constants/ui-events.ts +1 -0
  153. package/app/error.vue +169 -0
  154. package/app/layouts/default-blog.vue +5 -0
  155. package/app/layouts/default.vue +3 -0
  156. package/app/locales/en/commands.json +5 -0
  157. package/app/locales/en/cookies.json +182 -0
  158. package/app/locales/en/error.json +4 -0
  159. package/app/locales/en/interface.json +33 -0
  160. package/app/locales/en/myelophone.json +8 -0
  161. package/app/locales/en/outdated.json +5 -0
  162. package/app/locales/en/times.json +18 -0
  163. package/app/locales/pl/commands.json +5 -0
  164. package/app/locales/pl/cookies.json +182 -0
  165. package/app/locales/pl/error.json +4 -0
  166. package/app/locales/pl/interface.json +33 -0
  167. package/app/locales/pl/myelophone.json +8 -0
  168. package/app/locales/pl/outdated.json +5 -0
  169. package/app/locales/pl/times.json +18 -0
  170. package/app/locales/ru/commands.json +5 -0
  171. package/app/locales/ru/cookies.json +182 -0
  172. package/app/locales/ru/error.json +4 -0
  173. package/app/locales/ru/interface.json +33 -0
  174. package/app/locales/ru/myelophone.json +8 -0
  175. package/app/locales/ru/outdated.json +5 -0
  176. package/app/locales/ru/times.json +18 -0
  177. package/app/middleware/blog-layout.ts +28 -0
  178. package/app/middleware/i18n.global.ts +17 -0
  179. package/app/middleware/not-found-status.ts +11 -0
  180. package/app/modules/default-blog/index.ts +39 -0
  181. package/app/modules/default-blog/posts.ts +80 -0
  182. package/app/modules/default-sitemap/index.ts +116 -0
  183. package/app/modules/default-sitemap/runtime/sitemap.get.ts +42 -0
  184. package/app/modules/fullscreen-preloader/module.ts +43 -0
  185. package/app/modules/htaccess/.htaccess +38 -0
  186. package/app/modules/htaccess/index.ts +23 -0
  187. package/app/modules/i18n-shaker.mjs +221 -0
  188. package/app/modules/multi18n/module.ts +71 -0
  189. package/app/modules/myelophone-config/module.ts +241 -0
  190. package/app/pages/[...all].vue +9 -0
  191. package/app/pages/index.vue +3 -0
  192. package/app/pages/post/[...slug].vue +30 -0
  193. package/app/plugins/bfcache-refresh.client.ts +9 -0
  194. package/app/plugins/canonical.ts +30 -0
  195. package/app/plugins/csp-nonce.server.ts +41 -0
  196. package/app/plugins/multi18n.client.ts +21 -0
  197. package/app/plugins/multi18n.server.ts +15 -0
  198. package/app/plugins/nuxt-version.client.ts +4 -0
  199. package/app/plugins/restore.client.ts +21 -0
  200. package/app/plugins/reveal.ts +89 -0
  201. package/app/plugins/url-helper.client.ts +66 -0
  202. package/app/server/api/exchange-rates.get.ts +216 -0
  203. package/app/server/api/healthz.ts +1 -0
  204. package/app/server/api/logout.post.ts +6 -0
  205. package/app/server/middleware/normalize-url.ts +14 -0
  206. package/app/server/middleware/security-headers.ts +147 -0
  207. package/app/server/middleware/url-helper.ts +43 -0
  208. package/app/server/plugins/clean-html-spaces.ts +15 -0
  209. package/app/server/plugins/clean-styles.ts +94 -0
  210. package/app/server/plugins/compression.ts +122 -0
  211. package/app/server/plugins/css-preload.ts +26 -0
  212. package/app/server/plugins/early-hints.ts +31 -0
  213. package/app/server/plugins/force-noindex.ts +43 -0
  214. package/app/server/plugins/html-one-line.ts +17 -0
  215. package/app/server/plugins/inline-style.ts +7 -0
  216. package/app/server/routes/healthz.get.ts +1 -0
  217. package/app/server/tsconfig.json +3 -0
  218. package/app/stores/cart.ts +945 -0
  219. package/app/stores/commands.ts +43 -0
  220. package/app/stores/device.ts +156 -0
  221. package/app/stores/querySync.ts +5 -0
  222. package/app/stores/settings.ts +145 -0
  223. package/app/stores/user.ts +607 -0
  224. package/app/types/cookie.ts +89 -0
  225. package/app/types/myelophone.d.ts +87 -0
  226. package/app/types/site-search.ts +110 -0
  227. package/app/utils/asyncRefresh.ts +4 -0
  228. package/app/utils/blog.server.ts +142 -0
  229. package/app/utils/jsonLd.ts +56 -0
  230. package/app/utils/myelophoneConfig.ts +4 -0
  231. package/app/utils/sortBy.ts +18 -0
  232. package/app/utils/transliterate.ts +75 -0
  233. package/app/utils/ytVideo.ts +37 -0
  234. package/app/workers/site-search.worker.ts +661 -0
  235. package/biome.json +94 -0
  236. package/commitlint.config.js +3 -0
  237. package/nuxt.config.ts +539 -0
  238. package/package.json +101 -0
  239. package/playground/content/.gitkeep +0 -0
  240. package/playground/locales/.gitkeep +0 -0
  241. package/playground/myelophone.dev.ts +45 -0
  242. package/playground/nuxt.config.ts +105 -0
  243. package/playground/package.json +5 -0
  244. package/playground/pages/.gitkeep +0 -0
  245. package/playground/tsconfig.json +3 -0
  246. package/playwright.config.ts +23 -0
  247. package/public/browsers/chrome.svg +77 -0
  248. package/public/browsers/edge.svg +98 -0
  249. package/public/browsers/firefox.svg +300 -0
  250. package/public/favicon.ico +0 -0
  251. package/public/myelophone_eng.png +0 -0
  252. package/public/myelophone_eng_white.png +0 -0
  253. package/spa-loading-template.html +72 -0
  254. package/tests/home.spec.ts +9 -0
  255. package/tests/utils.ts +23 -0
  256. package/tsconfig.json +16 -0
  257. package/vite/plugins/clean-css.ts +190 -0
  258. package/vite/plugins/postcss-add-viewportunits.ts +33 -0
  259. package/vite/plugins/postcss-viewport-fallback.ts +49 -0
@@ -0,0 +1,607 @@
1
+ export type UserId = string | number;
2
+ export type UserAuthStatus =
3
+ 'anonymous' | 'authenticating' | 'authenticated' | 'refreshing' | 'error';
4
+
5
+ export interface UserProfile {
6
+ id: UserId;
7
+ email?: string;
8
+ name?: string;
9
+ avatarUrl?: string;
10
+ roles?: string[];
11
+ permissions?: string[];
12
+ meta?: Record<string, unknown>;
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ export interface UserSession {
17
+ expiresAt?: string | null;
18
+ issuedAt?: string | null;
19
+ provider?: string;
20
+ meta?: Record<string, unknown>;
21
+ }
22
+
23
+ export interface UserState<TProfile extends UserProfile = UserProfile> {
24
+ profile: TProfile | null;
25
+ session: UserSession | null;
26
+ status: UserAuthStatus;
27
+ error: string | null;
28
+ lastAuthenticatedAt: string | null;
29
+ }
30
+
31
+ export interface UserAuthPayload<TProfile extends UserProfile = UserProfile> {
32
+ profile: TProfile;
33
+ session?: UserSession | null;
34
+ }
35
+
36
+ export interface UserSessionExtensionContext<
37
+ TProfile extends UserProfile = UserProfile,
38
+ > {
39
+ profile: TProfile;
40
+ session: UserSession | null;
41
+ state: UserState<TProfile>;
42
+ }
43
+
44
+ export type UserSessionExtender<TProfile extends UserProfile = UserProfile> = (
45
+ ctx: UserSessionExtensionContext<TProfile>,
46
+ ) =>
47
+ | Partial<UserSession>
48
+ | UserSession
49
+ | null
50
+ | void
51
+ | Promise<Partial<UserSession> | UserSession | null | void>;
52
+
53
+ export interface UserStoreConfig {
54
+ storageKey?: string;
55
+ cookieMaxAge?: number;
56
+ persistSession?: boolean;
57
+ sessionExtensionSeconds?: number;
58
+ }
59
+
60
+ export const userBroadcastKeys = [
61
+ 'profile',
62
+ 'session',
63
+ 'status',
64
+ 'error',
65
+ 'lastAuthenticatedAt',
66
+ ] as const;
67
+
68
+ interface PrivacyPreferences {
69
+ functional?: boolean;
70
+ status?: string;
71
+ }
72
+
73
+ interface UserStoreState extends UserState {
74
+ config: Required<UserStoreConfig>;
75
+ }
76
+
77
+ interface UserRuntime {
78
+ stopSubscription: (() => void) | null;
79
+ stopBroadcast: (() => void) | null;
80
+ extendSessionHandler: UserSessionExtender | null;
81
+ extendSessionPromise: Promise<UserSession | null> | null;
82
+ isApplyingState: boolean;
83
+ }
84
+
85
+ const DEFAULT_STORAGE_KEY = 'user';
86
+
87
+ const toJsonSafe = <T>(value: T): T => {
88
+ const seen = new WeakSet<object>();
89
+
90
+ const normalize = (input: unknown): unknown => {
91
+ const raw = toRaw(input);
92
+
93
+ if (raw === null || typeof raw !== 'object') return raw;
94
+ if (raw instanceof Date) return raw.toISOString();
95
+ if (seen.has(raw)) return undefined;
96
+
97
+ seen.add(raw);
98
+
99
+ if (Array.isArray(raw)) {
100
+ return raw.map(normalize).filter((item) => item !== undefined);
101
+ }
102
+
103
+ return Object.fromEntries(
104
+ Object.entries(raw)
105
+ .map(([key, item]) => [key, normalize(item)])
106
+ .filter(([, item]) => item !== undefined),
107
+ );
108
+ };
109
+
110
+ return normalize(value) as T;
111
+ };
112
+
113
+ const normalizeStringArray = (value: unknown): string[] =>
114
+ Array.isArray(value)
115
+ ? [
116
+ ...new Set(
117
+ value
118
+ .map(String)
119
+ .map((item) => item.trim())
120
+ .filter(Boolean),
121
+ ),
122
+ ]
123
+ : [];
124
+
125
+ const normalizeProfile = <TProfile extends UserProfile>(
126
+ profile: Partial<TProfile> | null | undefined,
127
+ ): TProfile | null => {
128
+ if (!profile?.id && profile?.id !== 0) return null;
129
+
130
+ const safe = toJsonSafe(profile) as TProfile;
131
+
132
+ return {
133
+ ...safe,
134
+ id: safe.id,
135
+ email: safe.email ? String(safe.email).trim().toLowerCase() : undefined,
136
+ name: safe.name ? String(safe.name).trim() : undefined,
137
+ avatarUrl: safe.avatarUrl ? String(safe.avatarUrl).trim() : undefined,
138
+ roles: normalizeStringArray(safe.roles),
139
+ permissions: normalizeStringArray(safe.permissions),
140
+ meta: toJsonSafe(safe.meta || {}),
141
+ } as TProfile;
142
+ };
143
+
144
+ const normalizeSession = (
145
+ session: Partial<UserSession> | null | undefined,
146
+ ): UserSession | null => {
147
+ if (!session) return null;
148
+
149
+ const safe = toJsonSafe(session);
150
+
151
+ return {
152
+ ...safe,
153
+ expiresAt: safe.expiresAt ? String(safe.expiresAt) : null,
154
+ issuedAt: safe.issuedAt ? String(safe.issuedAt) : null,
155
+ provider: safe.provider ? String(safe.provider) : undefined,
156
+ meta: toJsonSafe(safe.meta || {}),
157
+ };
158
+ };
159
+
160
+ const normalizeStatus = (
161
+ status: unknown,
162
+ hasProfile: boolean,
163
+ ): UserAuthStatus => {
164
+ if (
165
+ status === 'anonymous' ||
166
+ status === 'authenticating' ||
167
+ status === 'authenticated' ||
168
+ status === 'refreshing' ||
169
+ status === 'error'
170
+ ) {
171
+ return status;
172
+ }
173
+
174
+ return hasProfile ? 'authenticated' : 'anonymous';
175
+ };
176
+
177
+ const normalizeUserState = <TProfile extends UserProfile>(
178
+ state: Partial<UserState<TProfile>> | null | undefined,
179
+ ): UserState<TProfile> => {
180
+ const profile = normalizeProfile<TProfile>(state?.profile);
181
+ const session = normalizeSession(state?.session);
182
+
183
+ return {
184
+ profile,
185
+ session,
186
+ status: normalizeStatus(state?.status, Boolean(profile)),
187
+ error: state?.error ? String(state.error) : null,
188
+ lastAuthenticatedAt: state?.lastAuthenticatedAt
189
+ ? String(state.lastAuthenticatedAt)
190
+ : null,
191
+ };
192
+ };
193
+
194
+ const parseCookieObject = <T>(value: unknown): T | null => {
195
+ if (!value) return null;
196
+ if (typeof value === 'object') return value as T;
197
+
198
+ try {
199
+ return JSON.parse(decodeURIComponent(String(value))) as T;
200
+ } catch {
201
+ return null;
202
+ }
203
+ };
204
+
205
+ const hasFunctionalCookieConsent = () => {
206
+ const preferences = useCookie<string | PrivacyPreferences | null>(
207
+ 'privacy-preferences',
208
+ );
209
+ const parsed = parseCookieObject<PrivacyPreferences>(preferences.value);
210
+
211
+ return parsed?.status === 'accepted' && parsed.functional === true;
212
+ };
213
+
214
+ const shouldPersistUserState = (state: UserState) =>
215
+ (state.status === 'authenticated' || state.status === 'refreshing') &&
216
+ Boolean(state.profile);
217
+
218
+ const isExpiredSession = (session: UserSession | null) => {
219
+ if (!session?.expiresAt) return false;
220
+
221
+ const expiresAt = new Date(session.expiresAt).getTime();
222
+ return Number.isFinite(expiresAt) && expiresAt <= Date.now();
223
+ };
224
+
225
+ const readStoredUserState = () => {
226
+ if (!hasFunctionalCookieConsent()) return null;
227
+
228
+ const cookie = useCookie<UserState | string | null>(DEFAULT_STORAGE_KEY);
229
+ return parseCookieObject<UserState>(cookie.value);
230
+ };
231
+
232
+ const userRuntime = new WeakMap<object, UserRuntime>();
233
+
234
+ const getUserRuntime = (store: object): UserRuntime => {
235
+ const existing = userRuntime.get(store);
236
+ if (existing) return existing;
237
+
238
+ const runtime: UserRuntime = {
239
+ stopSubscription: null,
240
+ stopBroadcast: null,
241
+ extendSessionHandler: null,
242
+ extendSessionPromise: null,
243
+ isApplyingState: false,
244
+ };
245
+ userRuntime.set(store, runtime);
246
+
247
+ return runtime;
248
+ };
249
+
250
+ const serializeError = (error: unknown) =>
251
+ error instanceof Error ? error.message : String(error || 'Unknown error');
252
+
253
+ export const useUserStore = defineStore('user', {
254
+ state: (): UserStoreState => {
255
+ const initial = normalizeUserState(readStoredUserState());
256
+
257
+ return {
258
+ ...initial,
259
+ config: {
260
+ storageKey: DEFAULT_STORAGE_KEY,
261
+ cookieMaxAge: 30 * 86400,
262
+ persistSession: true,
263
+ sessionExtensionSeconds: 3600,
264
+ },
265
+ };
266
+ },
267
+
268
+ getters: {
269
+ isAuthenticated: (state): boolean =>
270
+ (state.status === 'authenticated' ||
271
+ state.status === 'refreshing') &&
272
+ Boolean(state.profile) &&
273
+ !isExpiredSession(state.session),
274
+
275
+ isAnonymous: (state): boolean => state.status === 'anonymous',
276
+
277
+ isPending: (state): boolean =>
278
+ state.status === 'authenticating' || state.status === 'refreshing',
279
+
280
+ isSessionExpired: (state): boolean => isExpiredSession(state.session),
281
+
282
+ userId: (state): UserId | null => state.profile?.id ?? null,
283
+
284
+ user: (state): UserProfile | null => state.profile,
285
+
286
+ roles: (state): string[] => state.profile?.roles || [],
287
+
288
+ permissions: (state): string[] => state.profile?.permissions || [],
289
+ },
290
+
291
+ actions: {
292
+ stateSnapshot(): UserState {
293
+ return normalizeUserState({
294
+ profile: this.profile,
295
+ session: this.session,
296
+ status: this.status,
297
+ error: this.error,
298
+ lastAuthenticatedAt: this.lastAuthenticatedAt,
299
+ });
300
+ },
301
+
302
+ initUser() {
303
+ const runtime = getUserRuntime(this);
304
+ if (runtime.stopSubscription) return runtime.stopSubscription;
305
+
306
+ this.hydrateFromClientStorage();
307
+ this.persist();
308
+
309
+ runtime.stopBroadcast = useStoreBroadcast(this as any, {
310
+ keys: userBroadcastKeys,
311
+ });
312
+
313
+ const stopWatch = watch(
314
+ () =>
315
+ [
316
+ this.profile,
317
+ this.session,
318
+ this.status,
319
+ this.error,
320
+ this.lastAuthenticatedAt,
321
+ ] as const,
322
+ () => {
323
+ if (runtime.isApplyingState) return;
324
+ this.persist();
325
+ },
326
+ { deep: true, flush: 'post' },
327
+ );
328
+
329
+ runtime.stopSubscription = () => {
330
+ stopWatch();
331
+ runtime.stopBroadcast?.();
332
+ runtime.stopBroadcast = null;
333
+ runtime.stopSubscription = null;
334
+ };
335
+
336
+ return runtime.stopSubscription;
337
+ },
338
+
339
+ configureUser(options: UserStoreConfig) {
340
+ this.config = {
341
+ ...this.config,
342
+ ...options,
343
+ storageKey: options.storageKey || this.config.storageKey,
344
+ cookieMaxAge: options.cookieMaxAge ?? this.config.cookieMaxAge,
345
+ persistSession:
346
+ options.persistSession ?? this.config.persistSession,
347
+ sessionExtensionSeconds: Math.max(
348
+ 1,
349
+ options.sessionExtensionSeconds ??
350
+ this.config.sessionExtensionSeconds,
351
+ ),
352
+ };
353
+ this.persist();
354
+ },
355
+
356
+ persist() {
357
+ const state = normalizeUserState({
358
+ ...this.stateSnapshot(),
359
+ session: this.config.persistSession ? this.session : null,
360
+ });
361
+ const userCookie = useCookie<UserState | null>(
362
+ this.config.storageKey,
363
+ {
364
+ path: '/',
365
+ maxAge: this.config.cookieMaxAge,
366
+ sameSite: 'lax',
367
+ },
368
+ );
369
+
370
+ if (!shouldPersistUserState(state)) {
371
+ userCookie.value = null;
372
+
373
+ if (import.meta.client) {
374
+ localStorage.removeItem(this.config.storageKey);
375
+ }
376
+ return;
377
+ }
378
+
379
+ if (hasFunctionalCookieConsent()) {
380
+ userCookie.value = state;
381
+ return;
382
+ }
383
+
384
+ userCookie.value = null;
385
+
386
+ if (import.meta.client) {
387
+ localStorage.setItem(
388
+ this.config.storageKey,
389
+ JSON.stringify(state),
390
+ );
391
+ }
392
+ },
393
+
394
+ hydrateFromClientStorage() {
395
+ if (!import.meta.client || hasFunctionalCookieConsent()) return;
396
+
397
+ try {
398
+ const raw = localStorage.getItem(this.config.storageKey);
399
+ if (!raw) return;
400
+ this.applyState(JSON.parse(raw) as UserState);
401
+ } catch {
402
+ localStorage.removeItem(this.config.storageKey);
403
+ }
404
+ },
405
+
406
+ applyState(state: Partial<UserState> | null | undefined) {
407
+ const runtime = getUserRuntime(this);
408
+ const next = normalizeUserState(state);
409
+
410
+ runtime.isApplyingState = true;
411
+ try {
412
+ this.profile = next.profile;
413
+ this.session = next.session;
414
+ this.status = next.status;
415
+ this.error = next.error;
416
+ this.lastAuthenticatedAt = next.lastAuthenticatedAt;
417
+ } finally {
418
+ runtime.isApplyingState = false;
419
+ }
420
+
421
+ this.persist();
422
+ },
423
+
424
+ setAuthenticating() {
425
+ this.status = 'authenticating';
426
+ this.error = null;
427
+ },
428
+
429
+ setRefreshing() {
430
+ this.status = 'refreshing';
431
+ this.error = null;
432
+ },
433
+
434
+ setAuthenticated<TProfile extends UserProfile>(
435
+ payload: UserAuthPayload<TProfile>,
436
+ ) {
437
+ const profile = normalizeProfile(payload.profile);
438
+ if (!profile) {
439
+ throw new Error(
440
+ 'Authenticated user profile must include an id.',
441
+ );
442
+ }
443
+
444
+ this.profile = profile;
445
+ this.session = normalizeSession(payload.session);
446
+ this.status = 'authenticated';
447
+ this.error = null;
448
+ this.lastAuthenticatedAt = new Date().toISOString();
449
+ this.persist();
450
+ },
451
+
452
+ setProfile<TProfile extends UserProfile>(profile: Partial<TProfile>) {
453
+ const nextProfile = normalizeProfile({
454
+ ...(this.profile || {}),
455
+ ...profile,
456
+ } as TProfile);
457
+ if (!nextProfile) return;
458
+
459
+ this.profile = nextProfile;
460
+ if (this.status === 'anonymous') this.status = 'authenticated';
461
+ this.persist();
462
+ },
463
+
464
+ patchProfile(value: Record<string, unknown>) {
465
+ if (!this.profile) return;
466
+
467
+ this.profile = normalizeProfile({
468
+ ...this.profile,
469
+ ...toJsonSafe(value),
470
+ }) as UserProfile;
471
+ this.persist();
472
+ },
473
+
474
+ setSession(session: Partial<UserSession> | null) {
475
+ this.session = normalizeSession(session);
476
+ this.persist();
477
+ },
478
+
479
+ setSessionExtender(fn: UserSessionExtender | null) {
480
+ const runtime = getUserRuntime(this);
481
+ runtime.extendSessionHandler = fn;
482
+
483
+ return () => {
484
+ if (runtime.extendSessionHandler === fn) {
485
+ runtime.extendSessionHandler = null;
486
+ }
487
+ };
488
+ },
489
+
490
+ async extendSession() {
491
+ const runtime = getUserRuntime(this);
492
+ if (runtime.extendSessionPromise)
493
+ return runtime.extendSessionPromise;
494
+ if (!this.profile) return null;
495
+
496
+ if (!runtime.extendSessionHandler) {
497
+ if (this.isSessionExpired) {
498
+ this.logout();
499
+ return null;
500
+ }
501
+
502
+ const issuedAt = new Date();
503
+ this.session = normalizeSession({
504
+ ...this.session,
505
+ issuedAt: issuedAt.toISOString(),
506
+ expiresAt: new Date(
507
+ issuedAt.getTime() +
508
+ this.config.sessionExtensionSeconds * 1000,
509
+ ).toISOString(),
510
+ });
511
+ this.status = 'authenticated';
512
+ this.error = null;
513
+ this.persist();
514
+ return this.session;
515
+ }
516
+
517
+ const previousStatus = this.status;
518
+ this.status = 'refreshing';
519
+ this.error = null;
520
+
521
+ runtime.extendSessionPromise = Promise.resolve()
522
+ .then(() =>
523
+ runtime.extendSessionHandler?.({
524
+ profile: this.profile as UserProfile,
525
+ session: this.session,
526
+ state: this.stateSnapshot(),
527
+ }),
528
+ )
529
+ .then((session) => {
530
+ if (session !== undefined) {
531
+ this.session = normalizeSession(session);
532
+ }
533
+ this.status = this.profile ? 'authenticated' : 'anonymous';
534
+ this.persist();
535
+ return this.session;
536
+ })
537
+ .catch((error) => {
538
+ this.status = previousStatus;
539
+ this.error = serializeError(error);
540
+ this.persist();
541
+ throw error;
542
+ })
543
+ .finally(() => {
544
+ runtime.extendSessionPromise = null;
545
+ });
546
+
547
+ return runtime.extendSessionPromise;
548
+ },
549
+
550
+ hasRole(role: string) {
551
+ return this.roles.includes(role);
552
+ },
553
+
554
+ hasAnyRole(roles: string[]) {
555
+ return roles.some((role) => this.hasRole(role));
556
+ },
557
+
558
+ hasPermission(permission: string) {
559
+ return this.permissions.includes(permission);
560
+ },
561
+
562
+ hasAnyPermission(permissions: string[]) {
563
+ return permissions.some((permission) =>
564
+ this.hasPermission(permission),
565
+ );
566
+ },
567
+
568
+ setError(error: unknown) {
569
+ this.status = 'error';
570
+ this.error = serializeError(error);
571
+ this.persist();
572
+ },
573
+
574
+ clearError() {
575
+ this.error = null;
576
+ if (this.profile) {
577
+ this.status = 'authenticated';
578
+ } else {
579
+ this.status = 'anonymous';
580
+ }
581
+ this.persist();
582
+ },
583
+
584
+ logout() {
585
+ this.profile = null;
586
+ this.session = null;
587
+ this.status = 'anonymous';
588
+ this.error = null;
589
+ this.lastAuthenticatedAt = null;
590
+ this.resetStorage();
591
+ },
592
+
593
+ resetStorage() {
594
+ const userCookie = useCookie<UserState | null>(
595
+ this.config.storageKey,
596
+ {
597
+ path: '/',
598
+ },
599
+ );
600
+ userCookie.value = null;
601
+
602
+ if (import.meta.client) {
603
+ localStorage.removeItem(this.config.storageKey);
604
+ }
605
+ },
606
+ },
607
+ });
@@ -0,0 +1,89 @@
1
+ export const COOKIE_CATEGORIES = [
2
+ 'necessary',
3
+ 'analytics',
4
+ 'marketing',
5
+ 'functional',
6
+ ] as const;
7
+
8
+ export type CookieCategory = (typeof COOKIE_CATEGORIES)[number];
9
+
10
+ export type CookieSelection = {
11
+ [K in CookieCategory]: boolean;
12
+ };
13
+
14
+ export type LocalizedCookieText = string | Record<string, string>;
15
+
16
+ export type CookieScriptLegalBasis =
17
+ | 'consent'
18
+ | 'cookieless'
19
+ | 'legitimateInterest'
20
+ | 'contract'
21
+ | 'legalObligation'
22
+ | 'essential';
23
+
24
+ export interface CookiePreferences extends CookieSelection {
25
+ timestamp: number;
26
+ status: 'accepted' | 'declined' | 'pending';
27
+ }
28
+
29
+ export interface CookieScriptDef {
30
+ id: string;
31
+ name: LocalizedCookieText;
32
+ nameKey?: string;
33
+ description?: LocalizedCookieText;
34
+ descriptionKey?: string;
35
+ provider?: string;
36
+ legalBasis?: CookieScriptLegalBasis;
37
+ legal?: CookieScriptLegalInfo;
38
+ loadKey?: string;
39
+ src?: string;
40
+ code?: string;
41
+ initializers?: CookieScriptInitializer[];
42
+ beforeLoad?: string;
43
+ onConsentChange?: string;
44
+ categorySettings?: Partial<
45
+ Record<CookieCategory, CookieScriptCategorySettings>
46
+ >;
47
+ options?: any;
48
+ }
49
+
50
+ export interface CookieScriptInitializer {
51
+ key: string;
52
+ code: string;
53
+ }
54
+
55
+ export interface CookieScriptLegalInfo {
56
+ purpose?: LocalizedCookieText;
57
+ purposeKey?: string;
58
+ dataCollected?: LocalizedCookieText[];
59
+ dataCollectedKeys?: string[];
60
+ cookiesUsed?: boolean;
61
+ cookiesDescription?: LocalizedCookieText;
62
+ cookiesDescriptionKey?: string;
63
+ retention?: LocalizedCookieText;
64
+ retentionKey?: string;
65
+ privacyPolicyUrl?: string;
66
+ officialDocsUrl?: string;
67
+ internationalTransfer?: boolean;
68
+ notes?: LocalizedCookieText;
69
+ notesKey?: string;
70
+ }
71
+
72
+ export interface CookieScriptCategorySettings {
73
+ description?: LocalizedCookieText;
74
+ descriptionKey?: string;
75
+ initializers?: CookieScriptInitializer[];
76
+ beforeLoad?: string;
77
+ onConsentChange?: string;
78
+ options?: any;
79
+ }
80
+
81
+ export type CookieScriptsConfig = Partial<
82
+ Record<CookieCategory, CookieScriptDef[]>
83
+ > & {
84
+ notices?: CookieScriptDef[];
85
+ };
86
+
87
+ export type CookieControlConfig = {
88
+ enabled: boolean;
89
+ };