@sourceregistry/sveltekit-oidc 1.3.0 → 1.4.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
@@ -19,10 +19,7 @@ npm install sveltekit-oidc
19
19
 
20
20
  ```ts
21
21
  // src/lib/server/auth.ts
22
- import {
23
- createInMemoryBackChannelLogoutStore,
24
- createOIDC
25
- } from 'sveltekit-oidc/server';
22
+ import { createOIDC } from 'sveltekit-oidc/server';
26
23
 
27
24
  export const oidc = createOIDC({
28
25
  issuer: 'https://your-idp.example.com',
@@ -32,22 +29,8 @@ export const oidc = createOIDC({
32
29
  clientAuthMethod: 'client_secret_basic',
33
30
  loginPath: '/auth/login',
34
31
  redirectPath: '/auth/callback',
35
- scope: ['openid', 'profile', 'email', 'offline_access'],
36
- clockSkewSeconds: 30,
37
- fetchUserInfo: true,
38
- backChannelLogoutStore: createInMemoryBackChannelLogoutStore(),
39
- transformSession(session) {
40
- return {
41
- ...session,
42
- groups: session.groups,
43
- user: session.user
44
- ? {
45
- ...session.user,
46
- groups: session.groups
47
- }
48
- : session.user
49
- };
50
- },
32
+ scope: 'openid profile email offline_access',
33
+ backChannelLogoutStore: 'memory',
51
34
  cookieOptions: {
52
35
  secure: process.env.NODE_ENV === 'production'
53
36
  }
@@ -129,6 +112,7 @@ export async function load(event) {
129
112
  session={data.session}
130
113
  config={data.sessionManagement}
131
114
  logoutPath="/auth/logout"
115
+ monitorSession={false}
132
116
  redirectIfUnauthenticated={false}
133
117
  >
134
118
  <Account />
@@ -160,6 +144,10 @@ export async function load(event) {
160
144
  - periodic `invalidateAll()` revalidation so revoked server sessions are detected
161
145
  - a client context for nested auth-aware components through `useOIDC()` / `getOIDCContext()`
162
146
 
147
+ Set `monitorSession={false}` when you want to keep the client context but leave remote session
148
+ revocation checks to your server-side guard or another mechanism. This disables
149
+ `check_session_iframe` polling without changing the provider metadata exposed through the context.
150
+
163
151
  ## Typed Custom Claims
164
152
 
165
153
  `createOIDC` infers a `TClaims` type from whatever `transformClaims` / `transformUser` / `transformSession` return, and threads it through the session, `event.locals.oidc`, `OIDCPublicSession`, and the client context — no casts needed.
@@ -178,19 +166,17 @@ export const oidc = createOIDC({
178
166
  });
179
167
  ```
180
168
 
181
- Extract the inferred type with `OIDCInferClaims` and apply it to `App.Locals` so `event.locals.oidc` is typed everywhere:
169
+ Wire `App.Locals` so `event.locals.oidc` is fully typed everywhere — `OIDCLocals` infers everything directly from the instance:
182
170
 
183
171
  ```ts
184
172
  // src/app.d.ts
185
- import type { OIDCHandleLocals, OIDCInferClaims } from 'sveltekit-oidc/server';
173
+ import type { OIDCLocals } from 'sveltekit-oidc/server';
186
174
  import { oidc } from '$lib/server/auth';
187
175
 
188
- export type AppClaims = OIDCInferClaims<typeof oidc>;
189
-
190
176
  declare global {
191
177
  namespace App {
192
178
  interface Locals {
193
- oidc?: OIDCHandleLocals<AppClaims>;
179
+ oidc?: OIDCLocals<typeof oidc>;
194
180
  }
195
181
  }
196
182
  }
@@ -198,7 +184,16 @@ declare global {
198
184
  export {};
199
185
  ```
200
186
 
201
- Now `event.locals.oidc?.claims?.roles` is `string[]` and `?.tenant` is `string | undefined` in every hook, load function, and action.
187
+ Now `event.locals.oidc?.claims?.roles` is `string[]` and `?.tenant` is `string | undefined` in every hook, load function, and action — no separate type aliases needed.
188
+
189
+ When you need the claim type explicitly (e.g. in a Svelte component), use `OIDCInferClaims`:
190
+
191
+ ```ts
192
+ import type { OIDCInferClaims } from 'sveltekit-oidc/server';
193
+ import { oidc } from '$lib/server/auth';
194
+
195
+ type AppClaims = OIDCInferClaims<typeof oidc>;
196
+ ```
202
197
 
203
198
  `OIDCContext` is a generic component, but Svelte doesn't support passing explicit type arguments in markup — `TClaims` is inferred from the `session` prop instead. Since `data.session` comes from `oidc.getPublicSession(event)`, it's already typed `OIDCPublicSession<AppClaims> | null`, so the inference flows through automatically:
204
199
 
@@ -256,20 +251,17 @@ export const oidc = createOIDC({
256
251
  });
257
252
  ```
258
253
 
259
- Extract the inferred type with `OIDCInferSession` and apply it alongside `OIDCInferClaims`:
254
+ `app.d.ts` stays a one-liner `OIDCLocals` picks up both `TClaims` and `TSession` from the instance:
260
255
 
261
256
  ```ts
262
257
  // src/app.d.ts
263
- import type { OIDCHandleLocals, OIDCInferClaims, OIDCInferSession } from 'sveltekit-oidc/server';
258
+ import type { OIDCLocals } from 'sveltekit-oidc/server';
264
259
  import { oidc } from '$lib/server/auth';
265
260
 
266
- export type AppClaims = OIDCInferClaims<typeof oidc>;
267
- export type AppSession = OIDCInferSession<typeof oidc>;
268
-
269
261
  declare global {
270
262
  namespace App {
271
263
  interface Locals {
272
- oidc?: OIDCHandleLocals<AppClaims, AppSession>;
264
+ oidc?: OIDCLocals<typeof oidc>;
273
265
  }
274
266
  }
275
267
  }
@@ -279,6 +271,16 @@ export {};
279
271
 
280
272
  Now `event.locals.oidc?.session?.tenantId` is `string` and `?.permissions` is `string[]` everywhere — and `oidc.requireAuth(event)` / `handleCallback`'s `onsuccess` resolve to `AppSession` directly.
281
273
 
274
+ Use `OIDCInferClaims` / `OIDCInferSession` when you need the types explicitly:
275
+
276
+ ```ts
277
+ import type { OIDCInferClaims, OIDCInferSession } from 'sveltekit-oidc/server';
278
+ import { oidc } from '$lib/server/auth';
279
+
280
+ type AppClaims = OIDCInferClaims<typeof oidc>;
281
+ type AppSession = OIDCInferSession<typeof oidc>;
282
+ ```
283
+
282
284
  If `transformSession` is omitted, `TSession` defaults to `OIDCSession<TClaims>` — existing setups keep working unchanged.
283
285
 
284
286
  ## Example App
@@ -302,6 +304,6 @@ Set these environment variables to enable it:
302
304
  - The library validates `id_token` and `logout_token` values through `@sourceregistry/node-jwt` and provider JWKS metadata.
303
305
  - `groups` are normalized onto the session from `groups` and `roles` claims when present.
304
306
  - Use `transformClaims`, `transformUser`, and `transformSession` to project provider-specific claims into your own session shape.
305
- - `check_session_iframe` monitoring only runs when the provider advertises that endpoint and the session includes `session_state`.
307
+ - `check_session_iframe` monitoring only runs when `monitorSession` is enabled, the provider advertises that endpoint, and the session includes `session_state`.
306
308
  - Refresh token handling is automatic when a valid refresh token is present.
307
- - `event.locals.oidc` is attached by the hook and typed via `OIDCHandleLocals<TClaims>`; see [Typed Custom Claims](#typed-custom-claims) for wiring your own claim shapes through `app.d.ts`.
309
+ - `event.locals.oidc` is attached by the hook; wire it in `app.d.ts` with `OIDCLocals<typeof oidc>` — see [Typed Custom Claims](#typed-custom-claims).
@@ -19,6 +19,7 @@
19
19
  loginPath,
20
20
  logoutPath = '/auth/logout',
21
21
  checkSessionIntervalMs = 5000,
22
+ monitorSession = true,
22
23
  revalidateIntervalMs = 30000,
23
24
  redirectOnExpired = 'login',
24
25
  redirectOnRevoked = 'login',
@@ -30,6 +31,7 @@
30
31
  loginPath?: string;
31
32
  logoutPath?: string;
32
33
  checkSessionIntervalMs?: number;
34
+ monitorSession?: boolean;
33
35
  revalidateIntervalMs?: number;
34
36
  redirectOnExpired?: RedirectMode;
35
37
  redirectOnRevoked?: RedirectMode;
@@ -47,7 +49,7 @@
47
49
  config.metadata.check_session_iframe ?? config.checkSessionIframe ?? undefined
48
50
  );
49
51
  const canMonitorIframe = $derived(
50
- Boolean(session?.isAuthenticated && session?.sessionState && iframeUrl)
52
+ Boolean(monitorSession && session?.isAuthenticated && session?.sessionState && iframeUrl)
51
53
  );
52
54
 
53
55
  const context = setOIDCContext<TClaims>({
@@ -7,6 +7,7 @@ declare function $$render<TClaims extends OIDCUserClaims = OIDCUserClaims>(): {
7
7
  loginPath?: string;
8
8
  logoutPath?: string;
9
9
  checkSessionIntervalMs?: number;
10
+ monitorSession?: boolean;
10
11
  revalidateIntervalMs?: number;
11
12
  redirectOnExpired?: "none" | "login" | "logout" | "reload";
12
13
  redirectOnRevoked?: "none" | "login" | "logout" | "reload";
@@ -6,7 +6,34 @@ import { asAuthorizationHeader, createClientSecretJwtAssertion, createPrivateKey
6
6
  import { normalizeTokens, shouldRefresh } from './session.js';
7
7
  import { absoluteUrl, base64UrlEncode, buildCookieOptions, collectGroups, createPKCEPair, normalizeIssuer, normalizeScope, parseProviderError, toPublicSession } from './utils.js';
8
8
  export { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
9
+ import { createInMemoryBackChannelLogoutStore, createInMemorySessionStore } from './store.js';
10
+ function buildLogger(logger) {
11
+ const noop = () => { };
12
+ if (logger === false)
13
+ return { debug: noop, info: noop, warn: noop, error: noop };
14
+ if (logger)
15
+ return {
16
+ debug: (...a) => logger.debug?.(...a),
17
+ info: (...a) => logger.info?.(...a),
18
+ warn: (...a) => logger.warn?.(...a),
19
+ error: (...a) => logger.error?.(...a),
20
+ };
21
+ const p = '[sveltekit-oidc]';
22
+ return {
23
+ debug: (...a) => console.debug(p, ...a),
24
+ info: (...a) => console.info(p, ...a),
25
+ warn: (...a) => console.warn(p, ...a),
26
+ error: (...a) => console.error(p, ...a),
27
+ };
28
+ }
9
29
  export function createOIDC(options) {
30
+ const log = buildLogger(options.logger);
31
+ const sessionStore = options.sessionStore === 'memory'
32
+ ? createInMemorySessionStore()
33
+ : options.sessionStore;
34
+ const backChannelLogoutStore = options.backChannelLogoutStore === 'memory'
35
+ ? createInMemoryBackChannelLogoutStore()
36
+ : options.backChannelLogoutStore;
10
37
  const cookieOptions = buildCookieOptions(options.cookieOptions);
11
38
  const clockSkewSeconds = options.clockSkewSeconds ?? 30;
12
39
  const refreshToleranceSeconds = options.refreshToleranceSeconds ?? 30;
@@ -20,7 +47,7 @@ export function createOIDC(options) {
20
47
  let metadataPromise;
21
48
  let jwksPromise;
22
49
  async function readPersistedSession(cookies) {
23
- if (!options.sessionStore) {
50
+ if (!sessionStore) {
24
51
  const session = cookieStore.readSession(cookies);
25
52
  return session ? { session } : null;
26
53
  }
@@ -28,7 +55,7 @@ export function createOIDC(options) {
28
55
  if (!reference?.id) {
29
56
  return null;
30
57
  }
31
- const session = await options.sessionStore.get(reference.id);
58
+ const session = await sessionStore.get(reference.id);
32
59
  if (!session) {
33
60
  cookieStore.clearSessionReference(cookies);
34
61
  return null;
@@ -39,22 +66,22 @@ export function createOIDC(options) {
39
66
  };
40
67
  }
41
68
  async function writePersistedSession(cookies, session, sessionId) {
42
- if (!options.sessionStore) {
69
+ if (!sessionStore) {
43
70
  cookieStore.writeSession(cookies, session);
44
71
  return;
45
72
  }
46
73
  const id = sessionId ?? base64UrlEncode(randomBytes(24));
47
- await options.sessionStore.set(id, session);
74
+ await sessionStore.set(id, session);
48
75
  cookieStore.writeSessionReference(cookies, { id });
49
76
  }
50
77
  async function clearPersistedSession(cookies, sessionId) {
51
- if (!options.sessionStore) {
78
+ if (!sessionStore) {
52
79
  cookieStore.clearSession(cookies);
53
80
  return;
54
81
  }
55
82
  const id = sessionId ?? cookieStore.readSessionReference(cookies)?.id;
56
83
  if (id) {
57
- await options.sessionStore.delete(id);
84
+ await sessionStore.delete(id);
58
85
  }
59
86
  cookieStore.clearSessionReference(cookies);
60
87
  }
@@ -237,10 +264,10 @@ export function createOIDC(options) {
237
264
  });
238
265
  }
239
266
  async function isRevoked(session) {
240
- if (!session || !options.backChannelLogoutStore) {
267
+ if (!session || !backChannelLogoutStore) {
241
268
  return false;
242
269
  }
243
- return options.backChannelLogoutStore.isRevoked(session);
270
+ return backChannelLogoutStore.isRevoked(session);
244
271
  }
245
272
  async function maybeRefreshSession(event, persisted) {
246
273
  const session = persisted?.session ?? null;
@@ -285,7 +312,8 @@ export function createOIDC(options) {
285
312
  await writePersistedSession(event.cookies, finalSession, persisted?.id);
286
313
  return finalSession;
287
314
  }
288
- catch {
315
+ catch (err) {
316
+ log.error('Token refresh failed — clearing session', err);
289
317
  await clearPersistedSession(event.cookies, persisted?.id);
290
318
  return null;
291
319
  }
@@ -337,7 +365,8 @@ export function createOIDC(options) {
337
365
  const code = event.url.searchParams.get('code');
338
366
  if (!stateCookie || !state || !code || stateCookie.state !== state) {
339
367
  cookieStore.clearState(event.cookies);
340
- throw error(400, { message: 'Invalid or expired OIDC callback state' });
368
+ log.warn('Invalid or expired callback state — restarting login flow');
369
+ return signIn(event);
341
370
  }
342
371
  cookieStore.clearState(event.cookies);
343
372
  const tokenResponse = await exchangeCode({
@@ -415,7 +444,7 @@ export function createOIDC(options) {
415
444
  if (!metadata.backchannel_logout_supported) {
416
445
  throw error(400, { message: 'Provider does not advertise back-channel logout support' });
417
446
  }
418
- if (!options.backChannelLogoutStore) {
447
+ if (!backChannelLogoutStore) {
419
448
  throw error(500, {
420
449
  message: 'backChannelLogoutStore is required for back-channel logout with cookie sessions'
421
450
  });
@@ -426,7 +455,7 @@ export function createOIDC(options) {
426
455
  throw error(400, { message: 'logout_token is required' });
427
456
  }
428
457
  const claims = await validateBackChannelLogoutToken(logoutToken);
429
- await options.backChannelLogoutStore.revoke({
458
+ await backChannelLogoutStore.revoke({
430
459
  issuer: claims.iss,
431
460
  clientId: options.clientId,
432
461
  sid: claims.sid,
@@ -133,6 +133,12 @@ export type OIDCSessionManagementConfig = {
133
133
  backChannelLogoutSupported: boolean;
134
134
  backChannelLogoutSessionSupported: boolean;
135
135
  };
136
+ export type OIDCLogger = {
137
+ debug?: (...args: unknown[]) => void;
138
+ info?: (...args: unknown[]) => void;
139
+ warn?: (...args: unknown[]) => void;
140
+ error?: (...args: unknown[]) => void;
141
+ };
136
142
  export type OIDCOptions<TClaims extends OIDCUserClaims = OIDCUserClaims, TSession extends OIDCSession<TClaims> = OIDCSession<TClaims>> = {
137
143
  issuer?: string;
138
144
  discoveryUrl?: string;
@@ -144,7 +150,7 @@ export type OIDCOptions<TClaims extends OIDCUserClaims = OIDCUserClaims, TSessio
144
150
  loginPath?: string;
145
151
  redirectPath?: string;
146
152
  postLogoutRedirectUri?: string;
147
- scope?: string[];
153
+ scope?: string | string[];
148
154
  audience?: string;
149
155
  fetchUserInfo?: boolean;
150
156
  sessionCookieName?: string;
@@ -155,8 +161,8 @@ export type OIDCOptions<TClaims extends OIDCUserClaims = OIDCUserClaims, TSessio
155
161
  refreshToleranceSeconds?: number;
156
162
  defaultLoginRedirect?: string;
157
163
  defaultLogoutRedirect?: string;
158
- sessionStore?: OIDCSessionStore<TClaims, TSession>;
159
- backChannelLogoutStore?: OIDCBackChannelLogoutStore<TClaims, TSession>;
164
+ sessionStore?: OIDCSessionStore<TClaims, TSession> | 'memory';
165
+ backChannelLogoutStore?: OIDCBackChannelLogoutStore<TClaims, TSession> | 'memory';
160
166
  transformClaims?: (claims: OIDCUserClaims) => MaybePromise<TClaims>;
161
167
  transformUser?: (user: OIDCUserClaims | undefined, context: {
162
168
  claims?: TClaims;
@@ -169,11 +175,12 @@ export type OIDCOptions<TClaims extends OIDCUserClaims = OIDCUserClaims, TSessio
169
175
  isRefresh: boolean;
170
176
  }) => MaybePromise<TSession>;
171
177
  endpoints?: Partial<OIDCDiscoveryDocument>;
178
+ logger?: OIDCLogger | false;
172
179
  };
173
180
  export type OIDCLoginOptions = {
174
181
  returnTo?: string;
175
182
  prompt?: 'login' | 'consent' | 'none' | 'select_account';
176
- scope?: string[];
183
+ scope?: string | string[];
177
184
  extraParams?: Record<string, string>;
178
185
  };
179
186
  export type OIDCLogoutOptions = {
@@ -256,3 +263,4 @@ export type OIDCInstance<TClaims extends OIDCUserClaims = OIDCUserClaims, TSessi
256
263
  };
257
264
  export type OIDCInferClaims<T> = T extends OIDCInstance<infer TClaims> ? TClaims : OIDCUserClaims;
258
265
  export type OIDCInferSession<T> = T extends OIDCInstance<infer _TClaims, infer TSession> ? TSession : OIDCSession<OIDCUserClaims>;
266
+ export type OIDCLocals<T extends OIDCInstance<any, any>> = OIDCHandleLocals<OIDCInferClaims<T>, OIDCInferSession<T>>;
@@ -6,7 +6,7 @@ export declare function createPKCEPair(length?: number): {
6
6
  challenge: string;
7
7
  };
8
8
  export declare function normalizeIssuer(issuer: string): string;
9
- export declare function normalizeScope(scope?: string[]): string[];
9
+ export declare function normalizeScope(scope?: string | string[]): string[];
10
10
  export declare function normalizeStringArray(value: unknown): string[];
11
11
  export declare function collectGroups<TClaims extends OIDCUserClaims = OIDCUserClaims>(...sources: Array<TClaims | undefined>): string[];
12
12
  export declare function createSignedValue(value: string, secret: string): string;
@@ -13,7 +13,10 @@ export function normalizeIssuer(issuer) {
13
13
  return issuer.endsWith('/') ? issuer.slice(0, -1) : issuer;
14
14
  }
15
15
  export function normalizeScope(scope) {
16
- return scope?.length ? [...new Set(scope)] : ['openid', 'profile', 'email'];
16
+ if (!scope)
17
+ return ['openid', 'profile', 'email'];
18
+ const arr = typeof scope === 'string' ? scope.trim().split(/\s+/) : scope;
19
+ return arr.length ? [...new Set(arr)] : ['openid', 'profile', 'email'];
17
20
  }
18
21
  export function normalizeStringArray(value) {
19
22
  if (!value)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sourceregistry/sveltekit-oidc",
3
- "version": "1.3.0",
4
- "description": "OIDC authentication helpers for SvelteKit applications.",
3
+ "version": "1.4.0",
4
+ "description": "OIDC authentication helpers for SvelteKit applications",
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {
7
7
  "dev": "vite dev",