@sourceregistry/sveltekit-oidc 2.0.1 → 2.1.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/README.md CHANGED
@@ -56,7 +56,8 @@ export const oidc = createOIDC<Identity, RequestData>({
56
56
  }),
57
57
 
58
58
  beforeSessionPersist: async ({session, reason}) => {
59
- await synchronizeUser(session.identity, reason);
59
+ const identity = await synchronizeUser(session.identity, reason);
60
+ return {...session, identity};
60
61
  },
61
62
 
62
63
  loadRequestData: async ({session, event}) => ({
@@ -75,23 +76,30 @@ export const oidc = createOIDC<Identity, RequestData>({
75
76
 
76
77
  The extension points have deliberately literal names:
77
78
 
78
- | Extension point | When it runs | Persisted |
79
- | ---------------------- | --------------------------------------------------------------- | ----------------------- |
80
- | `resolveIdentity` | After provider data is validated, on login and refresh | Its result is persisted |
81
- | `beforeSessionPersist` | Immediately before a login or refreshed session is written | Side effects only |
82
- | `loadRequestData` | Once while `handle` builds an authenticated request context | Never |
83
- | `createPublicSession` | When `getPublicSession` or `toPublicSession` projects a session | Never |
79
+ | Extension point | When it runs | Persisted |
80
+ | ----------------------- | ----------------------------------------------------------------- | ------------------------------------------------ |
81
+ | `resolveIdentity` | After provider data is validated, on login and refresh | Its result is persisted |
82
+ | `beforeSessionPersist` | Immediately before a login or refreshed session is written | Returned session replaces it; `void` keeps it |
83
+ | `loadRequestData` | Once while `handle` builds an authenticated request context | Never |
84
+ | `createPublicSession` | When `getPublicSession` or `toPublicSession` projects a session | Never |
84
85
 
85
- Both login and refresh are explicit in the callback context:
86
+ Both login and refresh are explicit in the callback context. Returning a session from
87
+ `beforeSessionPersist` is what makes it the right place to provision or enrich application data —
88
+ e.g. upserting a user row — before the very first session for that user is persisted:
86
89
 
87
90
  ```ts
88
91
  beforeSessionPersist: async ({session, reason}) => {
89
- if (reason === 'login') {
90
- await recordLogin(session.identity);
91
- }
92
+ if (reason !== 'login') return;
93
+ const user = await upsertUser(session.identity);
94
+ return {...session, identity: {...session.identity, ...user}};
92
95
  };
93
96
  ```
94
97
 
98
+ `resolveIdentity` runs first and may only be able to *read* application data (the user may not
99
+ exist yet on a first login). `beforeSessionPersist` runs next, right before the write, so a session
100
+ mutated or replaced there is the one every subsequent read of that session — including the result
101
+ returned from `handleCallback`/`callbackHandler`'s `onsuccess` — actually sees.
102
+
95
103
  ## SvelteKit hook
96
104
 
97
105
  ```ts
@@ -159,6 +167,49 @@ The underlying operations are also available directly when a route needs custom
159
167
  - `handleCallback(event)`
160
168
  - `logout(event, options)`
161
169
  - `handleBackChannelLogout(event)`
170
+
171
+ ### Request flow
172
+
173
+ ```mermaid
174
+ sequenceDiagram
175
+ participant Browser
176
+ participant login as loginHandler
177
+ participant callback as callbackHandler
178
+ participant logout as logoutHandler
179
+ participant bcl as backChannelLogoutHandler
180
+ participant OP as OpenID Provider
181
+
182
+ Browser->>login: GET /auth/login
183
+ login->>login: create PKCE pair, state, nonce
184
+ login-->>Browser: 302 redirect to OP authorize endpoint
185
+ Browser->>OP: authenticate
186
+ OP-->>Browser: 302 redirect with code & state
187
+
188
+ Browser->>callback: GET /auth/callback?code&state
189
+ callback->>OP: POST token endpoint (exchange code)
190
+ OP-->>callback: id_token, access_token, refresh_token
191
+ callback->>OP: verify id_token against JWKS
192
+ callback->>OP: GET userinfo endpoint (optional)
193
+ callback->>callback: resolveIdentity(idTokenClaims, userInfo)
194
+ callback->>callback: beforeSessionPersist(session, reason:'login')
195
+ Note over callback: a returned session here replaces<br/>what gets persisted and returned
196
+ callback->>callback: write session (cookie or sessionStore)
197
+ callback-->>Browser: onsuccess(event, result) or 302 redirect
198
+
199
+ Browser->>logout: POST /auth/logout
200
+ logout->>logout: clear persisted session
201
+ logout-->>Browser: 302 redirect to OP end_session endpoint or local page
202
+
203
+ OP->>bcl: POST /auth/backchannel-logout (logout_token)
204
+ bcl->>OP: verify logout_token against JWKS
205
+ bcl->>bcl: backChannelLogoutStore.revoke(sid/sub)
206
+ bcl-->>OP: 200 OK
207
+ Note over bcl: next getSession()/requireAuth() call<br/>for that sid/sub treats the session as revoked
208
+ ```
209
+
210
+ `handle` (the SvelteKit hook) wraps every request outside of these four routes: it calls
211
+ `getSession`, which transparently refreshes an expiring session — running `resolveIdentity` and
212
+ `beforeSessionPersist` again with `reason: 'refresh'` — before exposing `event.locals.oidc`.
162
213
  - `getSession(event)`
163
214
  - `requireAuth(event)`
164
215
  - `clearSession(cookies)`
@@ -211,6 +262,12 @@ returns. `getPublicSession(event)` is available when the hook has not already lo
211
262
  `OIDCContext` supports local expiry handling, targeted SvelteKit revalidation,
212
263
  `check_session_iframe` monitoring, and local or provider logout.
213
264
 
265
+ When the OP iframe reports `changed`, the component first performs the Session Management 1.0
266
+ `prompt=none` authorization check in a hidden iframe. The login handler supplies the current ID token
267
+ as `id_token_hint`; a matching End-User refreshes the local session, while an OP error or a different
268
+ End-User clears it. Applications using the standard `loginHandler()` and `callbackHandler()` routes do
269
+ not need an additional endpoint.
270
+
214
271
  ## Session stores
215
272
 
216
273
  Without `sessionStore`, the encrypted session is stored in the cookie. For server-side sessions:
@@ -7,11 +7,11 @@
7
7
 
8
8
  <script lang="ts" generics="TIdentity extends OIDCUserClaims = OIDCUserClaims">
9
9
  import {beforeNavigate, invalidate, invalidateAll} from '$app/navigation';
10
- import {tick} from 'svelte';
10
+ import {onDestroy, tick} from 'svelte';
11
11
  import type {Snippet} from 'svelte';
12
12
 
13
13
  import {setOIDCContext} from './context.js';
14
- import {classifySessionMonitorMessage} from './session-monitor.js';
14
+ import {classifySessionMonitorEvent} from './session-monitor.js';
15
15
 
16
16
  type RedirectMode = 'login' | 'logout' | 'reload' | 'none';
17
17
 
@@ -51,7 +51,10 @@
51
51
  children?: Snippet;
52
52
  } = $props();
53
53
 
54
- let iframe = $state<HTMLIFrameElement | undefined>(undefined);
54
+ let sessionIframe = $state<HTMLIFrameElement | undefined>(undefined);
55
+ let silentReauthenticationIframe = $state<HTMLIFrameElement | undefined>(undefined);
56
+ let silentReauthenticationUrl = $state<string | undefined>(undefined);
57
+ let silentReauthenticationTimeout: number | undefined;
55
58
  let status = $state<'authenticated' | 'unauthenticated' | 'expired' | 'revoked'>('unauthenticated');
56
59
  let revalidating = $state(false);
57
60
  let handledUnauthenticated = $state(false);
@@ -113,8 +116,14 @@
113
116
  }
114
117
  });
115
118
 
116
- function buildLoginUrl(returnTo = `${window.location.pathname}${window.location.search}`) {
117
- return `${resolvedLoginPath}?returnTo=${encodeURIComponent(returnTo)}`;
119
+ function buildLoginUrl(
120
+ returnTo = `${window.location.pathname}${window.location.search}`,
121
+ prompt?: 'none'
122
+ ) {
123
+ const url = new URL(resolvedLoginPath, window.location.href);
124
+ url.searchParams.set('returnTo', returnTo);
125
+ if (prompt) url.searchParams.set('prompt', prompt);
126
+ return `${url.pathname}${url.search}${url.hash}`;
118
127
  }
119
128
 
120
129
  async function logout(clearSessionOnly = false) {
@@ -204,6 +213,58 @@
204
213
  login();
205
214
  }
206
215
 
216
+ async function finishSilentReauthentication(result: 'authenticated' | 'logged_out') {
217
+ if (silentReauthenticationTimeout) {
218
+ window.clearTimeout(silentReauthenticationTimeout);
219
+ silentReauthenticationTimeout = undefined;
220
+ }
221
+ silentReauthenticationUrl = undefined;
222
+ await revalidate();
223
+ await tick();
224
+
225
+ if (result === 'authenticated' && session?.isAuthenticated) {
226
+ status = 'authenticated';
227
+ debug('silent_reauthentication_completed', {sub: session.sub});
228
+ return;
229
+ }
230
+
231
+ status = 'revoked';
232
+ debug('silent_reauthentication_logged_out');
233
+ await handleRedirect(redirectOnRevoked);
234
+ }
235
+
236
+ function startSilentReauthentication() {
237
+ if (silentReauthenticationUrl) return;
238
+
239
+ debug('silent_reauthentication_started');
240
+ silentReauthenticationUrl = buildLoginUrl(
241
+ `${window.location.pathname}${window.location.search}`,
242
+ 'none'
243
+ );
244
+ silentReauthenticationTimeout = window.setTimeout(() => {
245
+ debug('silent_reauthentication_timed_out');
246
+ void logout(true).then(() => finishSilentReauthentication('logged_out'));
247
+ }, 30_000);
248
+ }
249
+
250
+ function handleSilentReauthenticationLoad() {
251
+ if (!silentReauthenticationUrl || !silentReauthenticationIframe?.contentWindow) return;
252
+
253
+ try {
254
+ const documentElement = silentReauthenticationIframe.contentWindow.document.documentElement;
255
+ const result = documentElement.dataset.oidcSilentReauth;
256
+ if (result === 'authenticated' || result === 'logged_out') {
257
+ void finishSilentReauthentication(result);
258
+ }
259
+ } catch {
260
+ // The authorization request is currently displaying the cross-origin OP document.
261
+ }
262
+ }
263
+
264
+ onDestroy(() => {
265
+ if (silentReauthenticationTimeout) window.clearTimeout(silentReauthenticationTimeout);
266
+ });
267
+
207
268
  $effect(() => {
208
269
  const isAuthenticated = Boolean(session?.isAuthenticated);
209
270
  // Suppress authenticated→unauthenticated transition while a revalidation is
@@ -285,25 +346,21 @@
285
346
  });
286
347
 
287
348
  $effect(() => {
288
- if (!canMonitorIframe || !iframeUrl || !iframe) {
349
+ if (!canMonitorIframe || !iframeUrl || !sessionIframe) {
289
350
  return;
290
351
  }
291
352
 
292
353
  const targetOrigin = new URL(iframeUrl).origin;
293
354
  const poll = window.setInterval(() => {
294
- if (!iframe?.contentWindow || !session?.sessionState) {
355
+ if (!sessionIframe?.contentWindow || !session?.sessionState) {
295
356
  return;
296
357
  }
297
358
 
298
- iframe.contentWindow.postMessage(`${config.clientId} ${session.sessionState}`, targetOrigin);
359
+ sessionIframe.contentWindow.postMessage(`${config.clientId} ${session.sessionState}`, targetOrigin);
299
360
  }, checkSessionIntervalMs);
300
361
 
301
362
  const onMessage = (event: MessageEvent) => {
302
- if (event.origin !== targetOrigin) {
303
- return;
304
- }
305
-
306
- const result = classifySessionMonitorMessage(event.data);
363
+ const result = classifySessionMonitorEvent(event, targetOrigin, sessionIframe?.contentWindow);
307
364
  if (result === 'error') {
308
365
  // `error` means the OP could not determine session state (for example,
309
366
  // transient storage or network denial). It is not proof of revocation.
@@ -312,8 +369,7 @@
312
369
  }
313
370
  if (result === 'changed') {
314
371
  debug('iframe_session_event', {result, origin: event.origin});
315
- status = 'revoked';
316
- void logout(true).then(() => handleRedirect(redirectOnRevoked));
372
+ startSilentReauthentication();
317
373
  }
318
374
  };
319
375
 
@@ -328,7 +384,8 @@
328
384
 
329
385
  {#if canMonitorIframe && iframeUrl}
330
386
  <iframe
331
- bind:this={iframe}
387
+ id="oidc-session-monitor"
388
+ bind:this={sessionIframe}
332
389
  title="OIDC session monitor"
333
390
  src={iframeUrl}
334
391
  hidden
@@ -336,4 +393,15 @@
336
393
  ></iframe>
337
394
  {/if}
338
395
 
396
+ {#if silentReauthenticationUrl}
397
+ <iframe
398
+ bind:this={silentReauthenticationIframe}
399
+ title="OIDC silent re-authentication"
400
+ src={silentReauthenticationUrl}
401
+ onload={handleSilentReauthenticationLoad}
402
+ hidden
403
+ aria-hidden="true"
404
+ ></iframe>
405
+ {/if}
406
+
339
407
  {@render children?.()}
@@ -1,2 +1,3 @@
1
1
  export type OIDCSessionMonitorMessage = 'changed' | 'unchanged' | 'error' | 'ignored';
2
2
  export declare function classifySessionMonitorMessage(data: unknown): OIDCSessionMonitorMessage;
3
+ export declare function classifySessionMonitorEvent(event: Pick<MessageEvent, 'data' | 'origin' | 'source'>, expectedOrigin: string, expectedSource: unknown): OIDCSessionMonitorMessage;
@@ -1,3 +1,9 @@
1
1
  export function classifySessionMonitorMessage(data) {
2
2
  return data === 'changed' || data === 'unchanged' || data === 'error' ? data : 'ignored';
3
3
  }
4
+ export function classifySessionMonitorEvent(event, expectedOrigin, expectedSource) {
5
+ if (event.origin !== expectedOrigin || !expectedSource || event.source !== expectedSource) {
6
+ return 'ignored';
7
+ }
8
+ return classifySessionMonitorMessage(event.data);
9
+ }
@@ -8,6 +8,21 @@ import { absoluteUrl, base64UrlEncode, buildCookieOptions, collectGroups, create
8
8
  import { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
9
9
  export { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
10
10
  const OIDC_SESSION_REVALIDATION_DEPENDENCY = 'oidc:session';
11
+ const LOGIN_PROMPTS = new Set([
12
+ 'login',
13
+ 'consent',
14
+ 'none',
15
+ 'select_account'
16
+ ]);
17
+ function silentReauthenticationResponse(status) {
18
+ return new Response(`<!doctype html><html lang="en" data-oidc-silent-reauth="${status}"><head><meta charset="utf-8"><title>OIDC session check</title></head><body></body></html>`, {
19
+ headers: {
20
+ 'Content-Type': 'text/html; charset=utf-8',
21
+ 'Cache-Control': 'no-store',
22
+ 'Content-Security-Policy': "default-src 'none'; frame-ancestors 'self'"
23
+ }
24
+ });
25
+ }
11
26
  function buildLogger(logger) {
12
27
  const noop = () => { };
13
28
  if (logger === false)
@@ -366,19 +381,19 @@ export function createOIDC(options) {
366
381
  tokens: normalizeTokens(tokenResponse, defaultScope, session.tokens),
367
382
  refreshedAt: Math.floor(Date.now() / 1000)
368
383
  };
369
- await options.beforeSessionPersist?.({
384
+ const persistedSession = (await options.beforeSessionPersist?.({
370
385
  session: nextSession,
371
386
  reason: 'refresh',
372
387
  event,
373
388
  tokenResponse
374
- });
375
- await writePersistedSession(cookies, nextSession, persisted?.id);
389
+ })) ?? nextSession;
390
+ await writePersistedSession(cookies, persistedSession, persisted?.id);
376
391
  log.debug('OIDC session tokens refreshed', {
377
- expiresAt: nextSession.tokens.expiresAt,
378
- refreshExpiresAt: nextSession.tokens.refreshExpiresAt,
379
- hasRefreshToken: Boolean(nextSession.tokens.refreshToken)
392
+ expiresAt: persistedSession.tokens.expiresAt,
393
+ refreshExpiresAt: persistedSession.tokens.refreshExpiresAt,
394
+ hasRefreshToken: Boolean(persistedSession.tokens.refreshToken)
380
395
  });
381
- return nextSession;
396
+ return persistedSession;
382
397
  }
383
398
  catch (err) {
384
399
  log.error('Token refresh failed — clearing session', err);
@@ -395,11 +410,14 @@ export function createOIDC(options) {
395
410
  const state = base64UrlEncode(randomBytes(24));
396
411
  const nonce = base64UrlEncode(randomBytes(24));
397
412
  const returnTo = internalRedirectPath(event, loginOptions.returnTo ?? options.defaultLoginRedirect, '/');
413
+ const existingSession = loginOptions.prompt === 'none' ? await readPersistedSession(event.cookies) : null;
398
414
  cookieStore.writeState(event.cookies, {
399
415
  state,
400
416
  nonce,
401
417
  codeVerifier: pkce.verifier,
402
418
  returnTo,
419
+ prompt: loginOptions.prompt,
420
+ originalSub: existingSession?.session.sub,
403
421
  createdAt: Math.floor(Date.now() / 1000)
404
422
  });
405
423
  const redirectUri = absoluteUrl(event, redirectPath);
@@ -418,6 +436,9 @@ export function createOIDC(options) {
418
436
  if (loginOptions.prompt) {
419
437
  authorizationUrl.searchParams.set('prompt', loginOptions.prompt);
420
438
  }
439
+ if (loginOptions.prompt === 'none' && existingSession?.session.tokens.idToken) {
440
+ authorizationUrl.searchParams.set('id_token_hint', existingSession.session.tokens.idToken);
441
+ }
421
442
  for (const [key, value] of Object.entries(loginOptions.extraParams ?? {})) {
422
443
  authorizationUrl.searchParams.set(key, value);
423
444
  }
@@ -448,6 +469,9 @@ export function createOIDC(options) {
448
469
  });
449
470
  }
450
471
  const idTokenClaims = await validateIdToken(tokenResponse.id_token, stateCookie.nonce);
472
+ if (stateCookie.prompt === 'none' && stateCookie.originalSub && idTokenClaims.sub !== stateCookie.originalSub) {
473
+ throw error(401, { message: 'Silent re-authentication returned a different End-User' });
474
+ }
451
475
  const userInfo = options.fetchUserInfo === false
452
476
  ? undefined
453
477
  : await fetchUserInfo(tokenResponse.access_token).catch(() => undefined);
@@ -470,15 +494,16 @@ export function createOIDC(options) {
470
494
  createdAt: now,
471
495
  refreshedAt: now
472
496
  };
473
- await options.beforeSessionPersist?.({
497
+ const persistedSession = (await options.beforeSessionPersist?.({
474
498
  session,
475
499
  reason: 'login',
476
500
  event: event,
477
501
  tokenResponse
478
- });
479
- await writePersistedSession(event.cookies, session);
502
+ })) ?? session;
503
+ const existingSession = stateCookie.prompt === 'none' ? await readPersistedSession(event.cookies) : null;
504
+ await writePersistedSession(event.cookies, persistedSession, existingSession?.id);
480
505
  return {
481
- session,
506
+ session: persistedSession,
482
507
  returnTo: stateCookie.returnTo
483
508
  };
484
509
  }
@@ -566,19 +591,36 @@ export function createOIDC(options) {
566
591
  function loginHandler(defaults = {}) {
567
592
  return async (event) => {
568
593
  const returnTo = event.url.searchParams.get('returnTo') ?? defaults.returnTo;
569
- return signIn(event, { ...defaults, returnTo });
594
+ const requestedPrompt = event.url.searchParams.get('prompt');
595
+ const prompt = requestedPrompt && LOGIN_PROMPTS.has(requestedPrompt)
596
+ ? requestedPrompt
597
+ : defaults.prompt;
598
+ return signIn(event, { ...defaults, returnTo, prompt });
570
599
  };
571
600
  }
572
601
  function callbackHandler(handlerOptions = {}) {
573
602
  return async (event) => {
603
+ const stateCookie = cookieStore.readState(event.cookies);
604
+ const callbackState = event.url.searchParams.get('state');
605
+ const isSilentReauthentication = Boolean(stateCookie?.prompt === 'none' && callbackState && callbackState === stateCookie.state);
574
606
  try {
575
607
  const result = await handleCallback(event);
608
+ if (isSilentReauthentication) {
609
+ return silentReauthenticationResponse('authenticated');
610
+ }
576
611
  const response = await handlerOptions.onsuccess?.(event, result);
577
612
  if (response)
578
613
  return response;
579
614
  throw redirect(302, internalRedirectPath(event, handlerOptions.redirectTo ?? result.returnTo, '/'));
580
615
  }
581
616
  catch (err) {
617
+ if (isSilentReauthentication) {
618
+ cookieStore.clearState(event.cookies);
619
+ const persisted = await readPersistedSession(event.cookies);
620
+ await clearPersistedSession(event.cookies, persisted?.id);
621
+ log.debug('Silent OIDC re-authentication failed — clearing local session');
622
+ return silentReauthenticationResponse('logged_out');
623
+ }
582
624
  const response = await handlerOptions.onfailure?.(event, err);
583
625
  if (response) {
584
626
  return response;
@@ -181,13 +181,19 @@ export type OIDCOptions<TIdentity extends OIDCUserClaims = OIDCUserClaims, TRequ
181
181
  userInfo?: OIDCUserClaims;
182
182
  reason: OIDCSessionReason;
183
183
  }) => MaybePromise<TIdentity>;
184
- /** Runs immediately before a login or refreshed session is persisted. */
184
+ /**
185
+ * Runs immediately before a login or refreshed session is persisted.
186
+ * Returning a session replaces the one that gets persisted and handed back to the
187
+ * caller (`handleCallback`'s result, `getSession`'s result); returning `void` keeps it
188
+ * unchanged. Use this to enrich or provision application data — e.g. upsert a user row —
189
+ * before the session is written, rather than after via a route's own callback hook.
190
+ */
185
191
  beforeSessionPersist?: (context: {
186
192
  session: OIDCSession<TIdentity>;
187
193
  reason: OIDCSessionReason;
188
194
  event?: MinimalRequestEvent;
189
195
  tokenResponse: OIDCTokenResponse;
190
- }) => MaybePromise<void>;
196
+ }) => MaybePromise<OIDCSession<TIdentity> | void>;
191
197
  /**
192
198
  * Loads application-owned data once for each authenticated request handled by
193
199
  * `handle`. The result is exposed as `event.locals.oidc.data` and is never
@@ -244,6 +250,8 @@ export type OIDCStateCookie = {
244
250
  nonce: string;
245
251
  codeVerifier: string;
246
252
  returnTo: string;
253
+ prompt?: OIDCLoginOptions['prompt'];
254
+ originalSub?: string;
247
255
  createdAt: number;
248
256
  };
249
257
  export type MinimalRequestEvent = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sourceregistry/sveltekit-oidc",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "OIDC authentication helpers for SvelteKit applications",
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {
@@ -62,18 +62,18 @@
62
62
  "@sveltejs/kit": "^2.63.1",
63
63
  "@sveltejs/package": "^2.5.8",
64
64
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
65
- "@types/node": "^25.9.2",
65
+ "@types/node": "^26.2.0",
66
66
  "@semantic-release/changelog": "^6.0.3",
67
67
  "@semantic-release/git": "^10.0.1",
68
- "publint": "^0.3.21",
68
+ "publint": "^0.3.23",
69
69
  "svelte": "^5.56.3",
70
- "svelte-check": "^4.6.0",
70
+ "svelte-check": "^4.7.5",
71
71
  "typescript": "^6.0.3",
72
- "vite": "^8.0.16",
72
+ "vite": "^8.2.1",
73
73
  "@vitest/coverage-v8": "^4.1.8",
74
- "vitest": "^4.1.8",
74
+ "vitest": "^4.1.10",
75
75
  "@sourceregistry/semantic-release-jsr": "^1.1.1",
76
- "typedoc": "^0.28.19"
76
+ "typedoc": "^0.28.20"
77
77
  },
78
78
  "keywords": [
79
79
  "sveltekit",