@sourceregistry/sveltekit-oidc 1.3.1 → 1.5.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
@@ -12,17 +12,14 @@ OIDC authentication helpers for SvelteKit with:
12
12
  ## Install
13
13
 
14
14
  ```sh
15
- npm install sveltekit-oidc
15
+ npm install @sourceregistry/sveltekit-oidc
16
16
  ```
17
17
 
18
18
  ## Server Setup
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 '@sourceregistry/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
  }
@@ -121,7 +104,7 @@ export async function load(event) {
121
104
 
122
105
  ```html
123
106
  <script lang="ts">
124
- import { OIDCContext } from 'sveltekit-oidc';
107
+ import { OIDCContext } from '@sourceregistry/sveltekit-oidc';
125
108
  let { data } = $props();
126
109
  </script>
127
110
 
@@ -139,7 +122,7 @@ export async function load(event) {
139
122
  ```html
140
123
  <!-- src/lib/Account.svelte -->
141
124
  <script lang="ts">
142
- import { useOIDC } from 'sveltekit-oidc';
125
+ import { useOIDC } from '@sourceregistry/sveltekit-oidc';
143
126
 
144
127
  const oidc = useOIDC();
145
128
  </script>
@@ -183,19 +166,17 @@ export const oidc = createOIDC({
183
166
  });
184
167
  ```
185
168
 
186
- 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:
187
170
 
188
171
  ```ts
189
172
  // src/app.d.ts
190
- import type { OIDCHandleLocals, OIDCInferClaims } from 'sveltekit-oidc/server';
173
+ import type { OIDCLocals } from '@sourceregistry/sveltekit-oidc/server';
191
174
  import { oidc } from '$lib/server/auth';
192
175
 
193
- export type AppClaims = OIDCInferClaims<typeof oidc>;
194
-
195
176
  declare global {
196
177
  namespace App {
197
178
  interface Locals {
198
- oidc?: OIDCHandleLocals<AppClaims>;
179
+ oidc?: OIDCLocals<typeof oidc>;
199
180
  }
200
181
  }
201
182
  }
@@ -203,14 +184,14 @@ declare global {
203
184
  export {};
204
185
  ```
205
186
 
206
- 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.
207
188
 
208
- `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:
189
+ `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)`, the type flows through automatically:
209
190
 
210
191
  ```html
211
192
  <!-- src/routes/+layout.svelte -->
212
193
  <script lang="ts">
213
- import { OIDCContext } from 'sveltekit-oidc';
194
+ import { OIDCContext } from '@sourceregistry/sveltekit-oidc';
214
195
  let { data, children } = $props();
215
196
  </script>
216
197
 
@@ -222,14 +203,13 @@ Now `event.locals.oidc?.claims?.roles` is `string[]` and `?.tenant` is `string |
222
203
  ```html
223
204
  <!-- src/lib/Account.svelte -->
224
205
  <script lang="ts">
225
- import { useOIDC } from 'sveltekit-oidc';
226
- import type { AppClaims } from '../../app.d.ts';
206
+ import { useOIDC } from '@sourceregistry/sveltekit-oidc';
227
207
 
228
- const oidc = useOIDC<AppClaims>();
208
+ const auth = useOIDC(); // TClaims inferred from App.Locals.oidc
229
209
  </script>
230
210
 
231
- {#if oidc.isAuthenticated}
232
- <p>Roles: {oidc.claims?.roles.join(', ')}</p>
211
+ {#if auth.isAuthenticated}
212
+ <p>Roles: {auth.claims?.roles.join(', ')}</p>
233
213
  {/if}
234
214
  ```
235
215
 
@@ -241,8 +221,9 @@ Backend apps commonly stash application-specific data on the session itself (e.g
241
221
 
242
222
  ```ts
243
223
  // src/lib/server/auth.ts
244
- import { createOIDC, type OIDCSession } from 'sveltekit-oidc/server';
224
+ import { createOIDC, type OIDCSession, type OIDCUserClaims } from '@sourceregistry/sveltekit-oidc/server';
245
225
 
226
+ type AppClaims = OIDCUserClaims & { tenant?: string };
246
227
  type AppSession = OIDCSession<AppClaims> & {
247
228
  tenantId: string;
248
229
  permissions: string[];
@@ -261,20 +242,17 @@ export const oidc = createOIDC({
261
242
  });
262
243
  ```
263
244
 
264
- Extract the inferred type with `OIDCInferSession` and apply it alongside `OIDCInferClaims`:
245
+ `app.d.ts` stays a one-liner `OIDCLocals` picks up both `TClaims` and `TSession` from the instance:
265
246
 
266
247
  ```ts
267
248
  // src/app.d.ts
268
- import type { OIDCHandleLocals, OIDCInferClaims, OIDCInferSession } from 'sveltekit-oidc/server';
249
+ import type { OIDCLocals } from '@sourceregistry/sveltekit-oidc/server';
269
250
  import { oidc } from '$lib/server/auth';
270
251
 
271
- export type AppClaims = OIDCInferClaims<typeof oidc>;
272
- export type AppSession = OIDCInferSession<typeof oidc>;
273
-
274
252
  declare global {
275
253
  namespace App {
276
254
  interface Locals {
277
- oidc?: OIDCHandleLocals<AppClaims, AppSession>;
255
+ oidc?: OIDCLocals<typeof oidc>;
278
256
  }
279
257
  }
280
258
  }
@@ -284,6 +262,16 @@ export {};
284
262
 
285
263
  Now `event.locals.oidc?.session?.tenantId` is `string` and `?.permissions` is `string[]` everywhere — and `oidc.requireAuth(event)` / `handleCallback`'s `onsuccess` resolve to `AppSession` directly.
286
264
 
265
+ Use `OIDCInferClaims` / `OIDCInferSession` when you need the types explicitly in non-Svelte code (utility functions, API helpers, etc.):
266
+
267
+ ```ts
268
+ import type { OIDCInferClaims, OIDCInferSession } from '@sourceregistry/sveltekit-oidc/server';
269
+ import type { oidc } from '$lib/server/auth';
270
+
271
+ type AppClaims = OIDCInferClaims<typeof oidc>;
272
+ type AppSession = OIDCInferSession<typeof oidc>;
273
+ ```
274
+
287
275
  If `transformSession` is omitted, `TSession` defaults to `OIDCSession<TClaims>` — existing setups keep working unchanged.
288
276
 
289
277
  ## Example App
@@ -309,4 +297,4 @@ Set these environment variables to enable it:
309
297
  - Use `transformClaims`, `transformUser`, and `transformSession` to project provider-specific claims into your own session shape.
310
298
  - `check_session_iframe` monitoring only runs when `monitorSession` is enabled, the provider advertises that endpoint, and the session includes `session_state`.
311
299
  - Refresh token handling is automatic when a valid refresh token is present.
312
- - `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`.
300
+ - `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).
@@ -1,4 +1,7 @@
1
- import type { OIDCDiscoveryDocument, OIDCPublicSession, OIDCUserClaims } from '../server/index.js';
1
+ import type { OIDCDiscoveryDocument, OIDCHandleLocals, OIDCPublicSession, OIDCUserClaims } from '../server/index.js';
2
+ type LocalsClaims = App.Locals extends {
3
+ oidc?: OIDCHandleLocals<infer C, any>;
4
+ } ? C : OIDCUserClaims;
2
5
  export type OIDCClientContextValue<TClaims extends OIDCUserClaims = OIDCUserClaims> = {
3
6
  isAuthenticated: boolean;
4
7
  session: OIDCPublicSession<TClaims> | null;
@@ -12,5 +15,6 @@ export type OIDCClientContextValue<TClaims extends OIDCUserClaims = OIDCUserClai
12
15
  revalidate: () => Promise<void>;
13
16
  };
14
17
  export declare function setOIDCContext<TClaims extends OIDCUserClaims = OIDCUserClaims>(value: OIDCClientContextValue<TClaims>): OIDCClientContextValue<TClaims>;
15
- export declare function getOIDCContext<TClaims extends OIDCUserClaims = OIDCUserClaims>(): OIDCClientContextValue<TClaims>;
16
- export declare function useOIDC<TClaims extends OIDCUserClaims = OIDCUserClaims>(): OIDCClientContextValue<TClaims>;
18
+ export declare function getOIDCContext<TClaims extends OIDCUserClaims = LocalsClaims>(): OIDCClientContextValue<TClaims>;
19
+ export declare function useOIDC<TClaims extends OIDCUserClaims = LocalsClaims>(): OIDCClientContextValue<TClaims>;
20
+ export {};
@@ -1,3 +1,3 @@
1
1
  export { default as OIDCContext } from './OIDCContext.svelte';
2
2
  export { getOIDCContext, useOIDC } from './context.js';
3
- export type { OIDCPublicSession, OIDCSessionManagementConfig, OIDCUserClaims } from '../server/index.js';
3
+ export type { OIDCInferClaims, OIDCInferSession, OIDCLocals, OIDCPublicSession, OIDCSessionManagementConfig, OIDCUserClaims } from '../server/index.js';
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sourceregistry/sveltekit-oidc",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "OIDC authentication helpers for SvelteKit applications",
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {