@strivacity/sdk-svelte 4.0.0-beta.1 → 4.0.0-beta.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 (56) hide show
  1. package/README.md +160 -104
  2. package/package.json +5 -4
  3. package/src/client/components.svelte.ts +6 -5
  4. package/src/client/hooks.svelte.ts +17 -147
  5. package/src/client/types.ts +16 -102
  6. package/src/server/sdk.ts +52 -28
  7. package/testing/tests/components.spec.ts +1 -1
  8. package/testing/tests/index.spec.ts +10 -0
  9. package/testing/tests/server/sdk.spec.ts +59 -15
  10. package/dist/assets/rolldown-runtime.cjs +0 -1
  11. package/dist/client/components.svelte.cjs +0 -2
  12. package/dist/client/components.svelte.cjs.map +0 -1
  13. package/dist/client/components.svelte.d.ts +0 -11
  14. package/dist/client/components.svelte.mjs +0 -2
  15. package/dist/client/components.svelte.mjs.map +0 -1
  16. package/dist/client/hooks.svelte.cjs +0 -2
  17. package/dist/client/hooks.svelte.cjs.map +0 -1
  18. package/dist/client/hooks.svelte.d.ts +0 -27
  19. package/dist/client/hooks.svelte.mjs +0 -2
  20. package/dist/client/hooks.svelte.mjs.map +0 -1
  21. package/dist/client/index.d.ts +0 -8
  22. package/dist/client/index.mjs +0 -1
  23. package/dist/client/storages.cjs +0 -1
  24. package/dist/client/storages.d.ts +0 -1
  25. package/dist/client/storages.mjs +0 -1
  26. package/dist/client/types.cjs +0 -1
  27. package/dist/client/types.d.ts +0 -238
  28. package/dist/client/types.mjs +0 -1
  29. package/dist/client/utils.cjs +0 -1
  30. package/dist/client/utils.d.ts +0 -1
  31. package/dist/client/utils.mjs +0 -1
  32. package/dist/errors.cjs +0 -1
  33. package/dist/errors.d.ts +0 -1
  34. package/dist/errors.mjs +0 -1
  35. package/dist/server/index.cjs +0 -1
  36. package/dist/server/index.d.ts +0 -6
  37. package/dist/server/index.mjs +0 -1
  38. package/dist/server/sdk.cjs +0 -2
  39. package/dist/server/sdk.cjs.map +0 -1
  40. package/dist/server/sdk.d.ts +0 -8
  41. package/dist/server/sdk.mjs +0 -2
  42. package/dist/server/sdk.mjs.map +0 -1
  43. package/dist/server/storages.cjs +0 -2
  44. package/dist/server/storages.cjs.map +0 -1
  45. package/dist/server/storages.d.ts +0 -10
  46. package/dist/server/storages.mjs +0 -2
  47. package/dist/server/storages.mjs.map +0 -1
  48. package/dist/server/types.cjs +0 -1
  49. package/dist/server/types.d.ts +0 -35
  50. package/dist/server/types.mjs +0 -1
  51. package/dist/server/utils.cjs +0 -1
  52. package/dist/server/utils.d.ts +0 -1
  53. package/dist/server/utils.mjs +0 -1
  54. package/dist/types.cjs +0 -1
  55. package/dist/types.d.ts +0 -2
  56. package/dist/types.mjs +0 -1
package/README.md CHANGED
@@ -103,7 +103,7 @@ export const sdkOptions: SDKInitConfig = {
103
103
  redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
104
104
  scopes: ['openid', 'profile', 'email'], // requested user permissions/data
105
105
 
106
- // Omit this line entirely for client-managed sessions
106
+ // Omit these lines entirely for client-managed sessions
107
107
  serverSessionUri: '/auth/login',
108
108
  };
109
109
  ```
@@ -115,15 +115,18 @@ Only needed for server-managed sessions - skip this step (and step 3's hook) if
115
115
  ```ts
116
116
  // src/lib/server/strivacity.ts
117
117
  import { createServerSDK } from '@strivacity/sdk-svelte/server';
118
+ import { env } from '$env/dynamic/private';
118
119
  import { sdkOptions } from '../options';
119
120
 
120
121
  export const sdk = createServerSDK({
121
122
  ...sdkOptions,
122
- secret: process.env.SECRET, // http-only cookie encryption key (random 32+ characters)
123
+ secret: env.SECRET, // http-only cookie encryption key (random 32+ characters)
123
124
  postLoginRedirectUri: '/profile',
124
125
  });
125
126
  ```
126
127
 
128
+ > Read the secret from a server-only module such as `$env/dynamic/private` (or `$env/static/private`) - never from a `PUBLIC_`/`VITE_`-prefixed variable, which can end up in client bundles. The [example app](../../apps/sveltekit/src/lib/server/strivacity.ts) reads `import.meta.env.VITE_SECRET` for demo convenience only.
129
+
127
130
  ### 3. Wire up the server hook
128
131
 
129
132
  A single `handle` hook wires up every auth route automatically - it inspects the request path and either serves it directly or calls `resolve(event)` to fall through to the rest of your app:
@@ -142,16 +145,16 @@ export const handle: Handle = sdk.handle;
142
145
  | `GET` | `/auth/register` | Starts the registration flow and redirects to the IDP | `302` redirect to IDP |
143
146
  | `GET` | `/auth/callback` | Completes authentication (handles the IDP callback) | `302` redirect or popup close script |
144
147
  | `GET` | `/auth/refresh` | Refreshes the access token | `204 No Content` or `302` redirect |
145
- | `GET` | `/auth/revoke` | Revokes tokens and clears the session | `204 No Content` |
146
- | `GET` | `/auth/logout` | Ends the session and redirects to the IDP logout page | `302` redirect to IDP logout |
147
- | `GET` | `/auth/entry` | Handles external flow entry (e.g., password reset link) - embedded/native modes only | JSON with session data |
148
- | `POST` | `/auth/backchannel-logout` | Processes back-channel logout requests from the IDP | `204 No Content` |
148
+ | `GET` | `/auth/revoke` | Revokes tokens and clears the session | `302` redirect to `postLogoutRedirectUri` |
149
+ | `GET` | `/auth/logout` | Ends the session and redirects to the IDP logout page; accepts a same-origin `?returnTo=` (default: `postLogoutRedirectUri`) | `302` to IDP logout (straight to `returnTo` when the session has no ID token) |
150
+ | `GET` | `/auth/entry` | Handles external flow entry (e.g., password reset link) - embedded/native modes only | `302` redirect to `loginUri` |
151
+ | `POST` | `/auth/backchannel-logout` | Processes back-channel logout requests from the IDP | `200 OK` (`400` invalid token, `501` storage without `deleteByLogoutToken`, `500` delete failed) |
149
152
 
150
153
  > The `/auth` prefix and route names come from `authUrlPrefix` - see [Server configuration reference](#server-configuration-reference). Everything else falls through to `resolve(event)`, so `handle` is safe to compose with your own hooks via SvelteKit's [`sequence()`](https://svelte.dev/docs/kit/@sveltejs-kit-hooks#sequence).
151
154
 
152
155
  ### 4. Wrap your app with the client provider
153
156
 
154
- `createStyAuthProvider` is a plain function, not a wrapping component - call it once at the top of your root layout's `<script>` (Svelte's context APIs only work during a component's own initialization, so this can't be done from `onMount` or an event handler):
157
+ `createStyAuthProvider` is a plain function, not a wrapping component - call it once at the top of your root layout's `<script>` (Svelte's context APIs only work during a component's own initialization, so this can't be done from `onMount` or an event handler). The client SDK only exists in the browser: during server-side rendering `ctx.sdk` is `undefined` and `ctx.loading` stays `true`, so read `ctx.sdk` and call the SDK methods from `onMount`, `$effect` or event handlers, never at the top level of a `<script>`:
155
158
 
156
159
  ```svelte
157
160
  <!-- src/routes/+layout.svelte -->
@@ -173,11 +176,12 @@ For server-managed sessions, resolve the session once in a `+layout.server.ts` `
173
176
  ```ts
174
177
  // src/routes/+layout.server.ts
175
178
  import type { LayoutServerLoad } from './$types';
179
+ import { toClientSession } from '@strivacity/sdk-svelte/server';
176
180
  import { sdk } from '$lib/server/strivacity';
177
181
 
178
182
  export const load: LayoutServerLoad = async (event) => {
179
183
  return {
180
- session: await sdk.getSession(event),
184
+ session: toClientSession(await sdk.getSession(event)),
181
185
  };
182
186
  };
183
187
  ```
@@ -200,13 +204,15 @@ export const load: LayoutServerLoad = async (event) => {
200
204
  {@render children()}
201
205
  ```
202
206
 
207
+ > **Note:** The session passed to the provider is serialized into the rendered HTML, so `toClientSession()` (exported from both `@strivacity/sdk-svelte/server` and `@strivacity/sdk-svelte/client`) removes the long-lived credentials before handing it over: the refresh token (the Server SDK refreshes the session itself, see [Accessing the session server-side](#accessing-the-session-server-side)) and the ID token (only the server-side `/auth/logout` route needs it).
208
+
203
209
  ---
204
210
 
205
211
  ## Client SDK
206
212
 
207
213
  ### Authentication modes
208
214
 
209
- The `mode` set in `src/lib/options.ts` (see [Quick start](#quick-start)) controls which of the four flows below is active - the `createStyAuthProvider` bootstrap shown in the [core SDK docs](../sdk-core/README.md#choosing-a-mode) is already handled by `src/routes/+layout.svelte`, so the examples below start directly from the page level.
215
+ The `mode` set in `src/lib/options.ts` (see [Quick start](#quick-start)) controls which of the four flows below is active. The examples below start directly from the page level, because the `createStyAuthProvider` bootstrap from [step 4 of the Quick start](#4-wrap-your-app-with-the-client-provider) already runs in `src/routes/+layout.svelte`.
210
216
 
211
217
  #### redirect mode
212
218
 
@@ -367,14 +373,14 @@ Call this to clear the session and redirect to the Strivacity end-session endpoi
367
373
  ```svelte
368
374
  <!-- src/routes/logout/+page.svelte -->
369
375
  <script lang="ts">
370
- import { onMount } from 'svelte';
371
376
  import { useStrivacity } from '@strivacity/sdk-svelte/client';
372
377
 
373
378
  const ctx = useStrivacity();
374
379
 
375
- onMount(async () => {
376
- if (ctx.isAuthenticated) {
377
- await ctx.logout();
380
+ // Wait until the SDK has restored the session: on a direct page load `isAuthenticated` is still `false` while `loading` is `true`
381
+ $effect(() => {
382
+ if (!ctx.loading && ctx.isAuthenticated) {
383
+ void ctx.logout();
378
384
  }
379
385
  });
380
386
  </script>
@@ -386,7 +392,7 @@ Call this to clear the session and redirect to the Strivacity end-session endpoi
386
392
 
387
393
  **Server-managed sessions**:
388
394
 
389
- With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
395
+ With `serverSessionUri` configured, the `logout()` call above doesn't clear the server session cookie, so redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint, which then sends the user back to `postLogoutRedirectUri`. To land somewhere else, add a same-origin `?returnTo=` (e.g. `/auth/logout?returnTo=/goodbye`); a missing, invalid, or cross-origin value falls back to `postLogoutRedirectUri`. Whichever URL is used must be registered as an allowed post-logout redirect URI:
390
396
 
391
397
  ```svelte
392
398
  <!-- src/routes/logout/+page.svelte -->
@@ -438,7 +444,7 @@ Call these methods to manage the session and access token client-side.
438
444
 
439
445
  **Server-managed sessions**:
440
446
 
441
- With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the auth routes, then let the hook redirect back:
447
+ With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the auth routes, then let the hook redirect back. `ctx.sdk.refresh({ returnTo: '/profile' })` performs the same refresh navigation for you:
442
448
 
443
449
  ```svelte
444
450
  <script lang="ts">
@@ -525,11 +531,31 @@ Popup mode always needs client-side JavaScript to open the window, so there's no
525
531
 
526
532
  **Client-managed sessions**:
527
533
 
528
- The popup resolves automatically - no callback page is needed. Token exchange happens inside the popup and the result is posted back to the opener window.
534
+ The pop-up still navigates to your `redirectUri`, so you still need a callback page there - without it nothing posts the result back to the opener. Call `handleCallback()` on that page; in popup mode it never resolves (the opener closes the pop-up once it receives the result), so don't rely on code after it running:
535
+
536
+ ```svelte
537
+ <!-- src/routes/callback/+page.svelte -->
538
+ <script lang="ts">
539
+ import { onMount } from 'svelte';
540
+ import { useStrivacity } from '@strivacity/sdk-svelte/client';
541
+
542
+ const ctx = useStrivacity();
543
+
544
+ onMount(() => {
545
+ void ctx.handleCallback();
546
+ });
547
+ </script>
548
+
549
+ <section>
550
+ <h1>Logging in...</h1>
551
+ </section>
552
+ ```
553
+
554
+ `login()` resolves in the opener once the result has been received and, for client-managed sessions, the tokens have been exchanged.
529
555
 
530
556
  **Server-managed sessions**:
531
557
 
532
- Same as client managed - the popup's internal callback request is also transparently proxied through `/auth/callback`, and the result is posted back to the opener window exactly the same way.
558
+ The callback page must not call `handleCallback()`: forward the query string to `/auth/callback` instead, as in the server-managed redirect callback. The Server SDK exchanges the code, sets the session cookie and responds with a page that posts the result to the opener and closes the pop-up.
533
559
 
534
560
  ##### Registration
535
561
 
@@ -592,14 +618,14 @@ Call this to clear the session and redirect to the Strivacity end-session endpoi
592
618
  ```svelte
593
619
  <!-- src/routes/logout/+page.svelte -->
594
620
  <script lang="ts">
595
- import { onMount } from 'svelte';
596
621
  import { useStrivacity } from '@strivacity/sdk-svelte/client';
597
622
 
598
623
  const ctx = useStrivacity();
599
624
 
600
- onMount(async () => {
601
- if (ctx.isAuthenticated) {
602
- await ctx.logout();
625
+ // Wait until the SDK has restored the session: on a direct page load `isAuthenticated` is still `false` while `loading` is `true`
626
+ $effect(() => {
627
+ if (!ctx.loading && ctx.isAuthenticated) {
628
+ void ctx.logout();
603
629
  }
604
630
  });
605
631
  </script>
@@ -611,7 +637,7 @@ Call this to clear the session and redirect to the Strivacity end-session endpoi
611
637
 
612
638
  **Server-managed sessions**:
613
639
 
614
- With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
640
+ With `serverSessionUri` configured, the `logout()` call above doesn't clear the server session cookie, so redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint, which then sends the user back to `postLogoutRedirectUri`. To land somewhere else, add a same-origin `?returnTo=` (e.g. `/auth/logout?returnTo=/goodbye`); a missing, invalid, or cross-origin value falls back to `postLogoutRedirectUri`. Whichever URL is used must be registered as an allowed post-logout redirect URI:
615
641
 
616
642
  ```svelte
617
643
  <!-- src/routes/logout/+page.svelte -->
@@ -662,7 +688,7 @@ Call these methods to manage the session and access token client-side.
662
688
 
663
689
  **Server-managed sessions**:
664
690
 
665
- With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the auth routes, then let the hook redirect back:
691
+ With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the auth routes, then let the hook redirect back. `ctx.sdk.refresh({ returnTo: '/profile' })` performs the same refresh navigation for you:
666
692
 
667
693
  ```svelte
668
694
  <script lang="ts">
@@ -701,13 +727,12 @@ The login UI renders inside your own page using Strivacity web components (`<sty
701
727
  import { goto } from '$app/navigation';
702
728
  import { resolve } from '$app/paths';
703
729
  import { page } from '$app/state';
704
- import { useStrivacity, injectScript } from '@strivacity/sdk-svelte/client';
705
-
706
- const { sdk } = useStrivacity();
730
+ import { injectScript } from '@strivacity/sdk-svelte/client';
731
+ import { sdkOptions } from '$lib/options';
707
732
 
708
733
  // injectScript loads the <sty-login>/<sty-notifications>/<sty-language-selector>
709
- // custom element definitions from the auth server
710
- injectScript('sty-components', `${sdk.options.issuer}/assets/components/bundle.js`);
734
+ // custom element definitions from the auth server (it does nothing during server-side rendering)
735
+ injectScript('sty-components', `${sdkOptions.issuer}/assets/components/bundle.js`);
711
736
 
712
737
  // Optional: Resume a session started from an entry URL (e.g., password reset)
713
738
  const sessionId = page.url.searchParams.get('session_id');
@@ -976,7 +1001,7 @@ Then on your login route, read the parameters and pass them to `<sty-login>`:
976
1001
 
977
1002
  **Option 2: Render login on the entry route**
978
1003
 
979
- Pass the parameters directly to `<sty-login>` on the same route:
1004
+ Resolve the entry link with `entry()` inside `src/routes/entry/+page.svelte` and render the native login on the same route - no redirect needed. `useNativeLogin` reads its `params` and starts the flow when the calling component mounts, so call it from a child component that is only rendered once `entry()` has returned the `session_id`:
980
1005
 
981
1006
  ```svelte
982
1007
  <!-- src/routes/entry/+page.svelte -->
@@ -985,43 +1010,60 @@ Pass the parameters directly to `<sty-login>` on the same route:
985
1010
  import { goto } from '$app/navigation';
986
1011
  import { resolve } from '$app/paths';
987
1012
  import { useStrivacity } from '@strivacity/sdk-svelte/client';
988
- import type { EmbeddedFlow } from '@strivacity/sdk-svelte/client';
1013
+ import type { NativeFlow } from '@strivacity/sdk-svelte/client';
1014
+ import EntryNativeLogin from './EntryNativeLogin.svelte';
989
1015
 
990
- const ctx = useStrivacity<EmbeddedFlow>();
1016
+ const sdkCtx = useStrivacity<NativeFlow>();
991
1017
 
992
- let sessionId = $state<string | null>(null);
993
- let shortAppId = $state<string | null>(null);
994
- let language = $state<string | null>(null);
1018
+ let entryData = $state<{ sessionId: string | null; language: string | null } | null>(null);
995
1019
 
996
1020
  onMount(async () => {
997
1021
  try {
998
- const data = await ctx.entry();
999
- // Set state for sty-login component
1000
- sessionId = data.session_id;
1001
- shortAppId = data.short_app_id;
1002
- language = data.language;
1022
+ const data = await sdkCtx.entry();
1023
+ entryData = { sessionId: data.session_id, language: data.language };
1003
1024
  } catch (error) {
1004
1025
  await goto(`${resolve('/error')}?message=${encodeURIComponent(error instanceof Error ? error.message : 'Unknown error')}`);
1005
1026
  }
1006
1027
  });
1007
1028
  </script>
1008
1029
 
1009
- {#if !sessionId}
1030
+ {#if entryData}
1031
+ <EntryNativeLogin sessionId={entryData.sessionId} language={entryData.language} />
1032
+ {:else}
1010
1033
  <section>
1011
1034
  <h1>Loading...</h1>
1012
1035
  </section>
1013
- {:else}
1036
+ {/if}
1037
+ ```
1038
+
1039
+ ```svelte
1040
+ <!-- src/routes/entry/EntryNativeLogin.svelte -->
1041
+ <script lang="ts">
1042
+ import { untrack } from 'svelte';
1043
+ import { goto } from '$app/navigation';
1044
+ import { resolve } from '$app/paths';
1045
+ import { useNativeLogin } from '@strivacity/sdk-svelte/client';
1046
+
1047
+ let { sessionId, language }: { sessionId: string | null; language: string | null } = $props();
1048
+
1049
+ const ctx = useNativeLogin({
1050
+ // The parameters are read once, when the flow starts - `untrack` makes the one-off read explicit
1051
+ params: untrack(() => ({ sessionId, language })),
1052
+ onLogin: async () => {
1053
+ await goto(resolve('/profile'));
1054
+ },
1055
+ onError: async (error) => {
1056
+ await goto(`${resolve('/error')}?message=${encodeURIComponent(error.message)}`);
1057
+ },
1058
+ });
1059
+ </script>
1060
+
1061
+ {#if ctx.loading || !ctx.state.screen}
1014
1062
  <section>
1015
- <sty-notifications></sty-notifications>
1016
- <sty-login
1017
- {sessionId}
1018
- {shortAppId}
1019
- lang={language}
1020
- onlogin={() => goto(resolve('/profile'))}
1021
- onerror={(event: CustomEvent<string>) => goto(`${resolve('/error')}?message=${encodeURIComponent(event.detail)}`)}
1022
- ></sty-login>
1023
- <sty-language-selector></sty-language-selector>
1063
+ <h1>Loading...</h1>
1024
1064
  </section>
1065
+ {:else}
1066
+ <!-- ...render based on `ctx.state.screen` as shown in the Login / Register example above -->
1025
1067
  {/if}
1026
1068
  ```
1027
1069
 
@@ -1034,14 +1076,14 @@ Call this to clear the session and redirect to the Strivacity end-session endpoi
1034
1076
  ```svelte
1035
1077
  <!-- src/routes/logout/+page.svelte -->
1036
1078
  <script lang="ts">
1037
- import { onMount } from 'svelte';
1038
1079
  import { useStrivacity } from '@strivacity/sdk-svelte/client';
1039
1080
 
1040
1081
  const ctx = useStrivacity();
1041
1082
 
1042
- onMount(async () => {
1043
- if (ctx.isAuthenticated) {
1044
- await ctx.logout();
1083
+ // Wait until the SDK has restored the session: on a direct page load `isAuthenticated` is still `false` while `loading` is `true`
1084
+ $effect(() => {
1085
+ if (!ctx.loading && ctx.isAuthenticated) {
1086
+ void ctx.logout();
1045
1087
  }
1046
1088
  });
1047
1089
  </script>
@@ -1053,7 +1095,7 @@ Call this to clear the session and redirect to the Strivacity end-session endpoi
1053
1095
 
1054
1096
  **Server-managed sessions**:
1055
1097
 
1056
- With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
1098
+ With `serverSessionUri` configured, the `logout()` call above doesn't clear the server session cookie, so redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint, which then sends the user back to `postLogoutRedirectUri`. To land somewhere else, add a same-origin `?returnTo=` (e.g. `/auth/logout?returnTo=/goodbye`); a missing, invalid, or cross-origin value falls back to `postLogoutRedirectUri`. Whichever URL is used must be registered as an allowed post-logout redirect URI:
1057
1099
 
1058
1100
  ```svelte
1059
1101
  <!-- src/routes/logout/+page.svelte -->
@@ -1100,7 +1142,7 @@ Call these methods to manage the session and access token client-side.
1100
1142
 
1101
1143
  **Server-managed sessions**:
1102
1144
 
1103
- With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the auth routes, then let the hook redirect back:
1145
+ With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the auth routes, then let the hook redirect back. `ctx.sdk.refresh({ returnTo: '/profile' })` performs the same refresh navigation for you:
1104
1146
 
1105
1147
  ```svelte
1106
1148
  <script lang="ts">
@@ -1147,7 +1189,6 @@ You build the entire login UI with your own components. `useNativeLogin()` drive
1147
1189
  params: {
1148
1190
  prompt: 'login', // use 'create' to open the registration flow instead
1149
1191
  language: 'en-US', // set the UI language (BCP 47 language tag)
1150
- sdk: 'web-minimal', // rendering mode: 'web-minimal' for simplified rendering, 'web' (default) for full rendering hints and branding
1151
1192
  sessionId: page.url.searchParams.get('session_id'), // pass a session ID to resume an existing flow
1152
1193
  },
1153
1194
  onLogin: async () => {
@@ -1352,14 +1393,14 @@ Call this to clear the session and redirect to the Strivacity end-session endpoi
1352
1393
  ```svelte
1353
1394
  <!-- src/routes/logout/+page.svelte -->
1354
1395
  <script lang="ts">
1355
- import { onMount } from 'svelte';
1356
1396
  import { useStrivacity } from '@strivacity/sdk-svelte/client';
1357
1397
 
1358
1398
  const ctx = useStrivacity();
1359
1399
 
1360
- onMount(async () => {
1361
- if (ctx.isAuthenticated) {
1362
- await ctx.logout();
1400
+ // Wait until the SDK has restored the session: on a direct page load `isAuthenticated` is still `false` while `loading` is `true`
1401
+ $effect(() => {
1402
+ if (!ctx.loading && ctx.isAuthenticated) {
1403
+ void ctx.logout();
1363
1404
  }
1364
1405
  });
1365
1406
  </script>
@@ -1371,7 +1412,7 @@ Call this to clear the session and redirect to the Strivacity end-session endpoi
1371
1412
 
1372
1413
  **Server-managed sessions**:
1373
1414
 
1374
- With `serverSessionUri` configured, redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint:
1415
+ With `serverSessionUri` configured, the `logout()` call above doesn't clear the server session cookie, so redirect to `/auth/logout` instead - the Server SDK clears the session and redirects to the IDP end-session endpoint, which then sends the user back to `postLogoutRedirectUri`. To land somewhere else, add a same-origin `?returnTo=` (e.g. `/auth/logout?returnTo=/goodbye`); a missing, invalid, or cross-origin value falls back to `postLogoutRedirectUri`. Whichever URL is used must be registered as an allowed post-logout redirect URI:
1375
1416
 
1376
1417
  ```svelte
1377
1418
  <!-- src/routes/logout/+page.svelte -->
@@ -1422,7 +1463,7 @@ Call these methods to manage the session and access token client-side.
1422
1463
 
1423
1464
  **Server-managed sessions**:
1424
1465
 
1425
- With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the auth routes, then let the hook redirect back:
1466
+ With `serverSessionUri` configured, tokens are refreshed/revoked by the Server SDK - trigger it by navigating to the auth routes, then let the hook redirect back. `ctx.sdk.refresh({ returnTo: '/profile' })` performs the same refresh navigation for you:
1426
1467
 
1427
1468
  ```svelte
1428
1469
  <script lang="ts">
@@ -1475,14 +1516,19 @@ const ctx = useStrivacity<RedirectFlow>();
1475
1516
  readonly accessTokenExpired: boolean; // True once the access token has expired
1476
1517
  readonly accessTokenExpirationDate: number | null; // Access token expiration timestamp
1477
1518
 
1478
- // Methods (all are async)
1479
- login(params?: LoginParams): Promise<void>; // Start login flow
1480
- register(params?: LoginParams): Promise<void>; // Start registration flow
1481
- handleCallback(url?: string): Promise<void>; // Handle OAuth callback
1519
+ // Methods (proxied to `sdk`; parameter/return types follow the flow type)
1520
+ init(): Promise<void>; // Load metadata and restore any persisted session
1521
+ checkAuthentication(params?: { autoRefresh: boolean }): Promise<boolean>; // Check the session, optionally refreshing tokens (never when serverSessionUri is set)
1522
+ login(params?: RedirectParams | PopupParams): Promise<void>; // Start login flow (redirect: `returnTo` sets where handleCallback() sends the user) (embedded/native: deprecated, returns a login flow instance - use `sdk.startSession()`)
1523
+ register(params?: RedirectParams | PopupParams): Promise<void>; // Start registration flow (same caveat as `login`)
1524
+ handleCallback(url?: string | URL): Promise<{ returnTo?: string }>; // Handle OAuth callback; resolves to the `returnTo` given to login()
1482
1525
  logout(params?: LogoutParams): Promise<void>; // End session
1483
- refresh(): Promise<void>; // Refresh access token
1526
+ refresh(params?: { returnTo?: string }): Promise<void>; // Refresh access token (server-managed: navigates to the server's refresh route, `${authUrlPrefix}/refresh`, instead; `returnTo` sets where it returns, default: the current URL)
1484
1527
  revoke(): Promise<void>; // Revoke tokens
1485
- entry(): Promise<EntryData>; // Handle external entry (embedded/native only)
1528
+ tokenExchange(params?: Record<string, string>): Promise<void>; // Perform a token exchange
1529
+ entry(url?: string | URL): Promise<EntryResponse>; // Handle external entry (embedded/native; resolves `{ session_id, short_app_id, language }`). Redirect/popup: `entry(url?, params?: RedirectOptions): Promise<void>`
1530
+ subscribeToEvent(eventName, callbackFn): { dispose: () => void }; // Subscribe to a single SDK event
1531
+ subscribeToAllEvents(callbackFn): { dispose: () => void }; // Subscribe to all SDK events
1486
1532
  }
1487
1533
  ```
1488
1534
 
@@ -1508,10 +1554,10 @@ const ctx = useNativeLogin({
1508
1554
  {
1509
1555
  params?: NativeParams; // Initial flow parameters
1510
1556
  onLogin?: (session: SessionData) => void | Promise<void>; // Called on successful login
1511
- onClose?: () => void; // Called when user closes the flow
1512
- onError?: (error: unknown) => void; // Called on error
1513
- onFallback?: (error: FallbackError) => void; // Called when fallback needed
1514
- onGlobalMessage?: (message: NativeFlowMessage) => void; // Called for global messages
1557
+ onClose?: () => void | Promise<void>; // Called when user closes the flow
1558
+ onError?: (error: Error) => void | Promise<void>; // Called on error
1559
+ onFallback?: (error: FallbackError) => void | Promise<void>; // Called when fallback needed
1560
+ onGlobalMessage?: (message: NativeFlowMessage) => void | Promise<void>; // Called for global messages
1515
1561
  }
1516
1562
  ```
1517
1563
 
@@ -1538,18 +1584,19 @@ const ctx = useNativeLogin({
1538
1584
 
1539
1585
  ## Server SDK
1540
1586
 
1541
- This is the same backend-for-frontend ([BFF](../../README.md#bff)) server implementation as the [core Server SDK](../sdk-core/README.md#server-sdk), pre-wired for SvelteKit: `createServerSDK` provides a SvelteKit `ServerAdapter` and a default encrypted-cookie storage built on `event.cookies`.
1587
+ This is the same backend-for-frontend ([BFF](../../README.md#bff)) server implementation as the [core Server SDK](../sdk-core/README.md#server-sdk), pre-wired for SvelteKit: `createServerSDK` provides a SvelteKit `ServerAdapter` and the default encrypted-cookie storage. The storage reads the session from the request's `cookie` header, and the `handle` hook writes the queued `Set-Cookie` headers to the response.
1542
1588
 
1543
1589
  ### Setup
1544
1590
 
1545
1591
  ```ts
1546
1592
  // src/lib/server/strivacity.ts
1547
1593
  import { createServerSDK } from '@strivacity/sdk-svelte/server';
1594
+ import { env } from '$env/dynamic/private';
1548
1595
  import { sdkOptions } from '../options';
1549
1596
 
1550
1597
  export const sdk = createServerSDK({
1551
1598
  ...sdkOptions,
1552
- secret: process.env.SECRET, // required unless you provide a custom `storage`
1599
+ secret: env.SECRET, // required unless you provide a custom `storage`
1553
1600
  postLoginRedirectUri: '/profile',
1554
1601
  });
1555
1602
  ```
@@ -1572,16 +1619,19 @@ Call `getSession(event)` from any `load()` function, form action, or `+server.ts
1572
1619
  ```ts
1573
1620
  // src/routes/+layout.server.ts
1574
1621
  import type { LayoutServerLoad } from './$types';
1622
+ import { toClientSession } from '@strivacity/sdk-svelte/server';
1575
1623
  import { sdk } from '$lib/server/strivacity';
1576
1624
 
1577
1625
  export const load: LayoutServerLoad = async (event) => {
1578
1626
  return {
1579
- session: await sdk.getSession(event),
1627
+ session: toClientSession(await sdk.getSession(event)),
1580
1628
  };
1581
1629
  };
1582
1630
  ```
1583
1631
 
1584
- Guard a route by calling `requireSession(event, options?)` from its `load()` - it returns the current session, or throws SvelteKit's own `redirect()` to the login page (default `/login`, or `${authUrlPrefix}/login` for `redirect`/`popup` modes) when there's no active session:
1632
+ When the access token is within `refreshSkew` seconds of expiry and the session has a refresh token, `getSession(event)` refreshes it before returning. The refresh runs at most once per request, and every caller of the same request - the layout and page `load()` functions SvelteKit runs concurrently, form actions, `+server.ts` handlers, `requireSession` - gets the same rotated session. The rotated session cookie is written to the response by `sdk.handle`, so there is no need to send the browser through `/auth/refresh` first. If the refresh fails, `getSession(event)` returns `null`. Set `autoRefresh: false` in the Server SDK options to turn this off.
1633
+
1634
+ Guard a route by calling `requireSession(event, options?)` from its `load()` - it returns the current session (refreshed through `getSession(event)` if it's about to expire), or throws SvelteKit's own `redirect()` to the login page (`loginUri`, default `/login`, in `embedded`/`native` modes; `${authUrlPrefix}/login` in `redirect`/`popup` modes) when there's no active session or its refresh failed:
1585
1635
 
1586
1636
  ```ts
1587
1637
  // src/routes/profile/+page.server.ts
@@ -1591,11 +1641,12 @@ import { sdk } from '$lib/server/strivacity';
1591
1641
  export const load: PageServerLoad = async (event) => {
1592
1642
  const session = await sdk.requireSession(event, { returnTo: '/profile' });
1593
1643
 
1594
- return { session };
1644
+ // Page data is serialized into the HTML, so return only what the page needs - never the raw tokens.
1645
+ return { claims: session.claims };
1595
1646
  };
1596
1647
  ```
1597
1648
 
1598
- > **Caveat**: `requireSession`'s `redirect()` only works as a genuine HTTP redirect when called from a `load()` whose page is actually rendered server-side. If your app disables SSR (`export const ssr = false`) and the login target isn't a real page route (the default `${authUrlPrefix}/login` for `redirect`/`popup` modes isn't - only `embedded`/`native` modes' `/login` page is), call `requireSession` from `hooks.server.ts`'s `handle` instead of a page's own `load()`, before/alongside `sdk.handle`. See the [example app](../../apps/sveltekit/src/routes/profile/+page.server.ts) for a working setup.
1649
+ > **Note**: call `requireSession` from a server `load()` (`+page.server.ts`/`+layout.server.ts`) - it needs the server `RequestEvent`, so a universal `+page.ts`/`+layout.ts` `load()` can't use it. This also works when SSR is disabled (`export const ssr = false`, as in the example app): SvelteKit still runs server `load()`s (via its `__data.json` request) and the client router follows the thrown `redirect()`. When the target isn't a page route (e.g. `${authUrlPrefix}/login` in `redirect`/`popup` modes), SvelteKit falls back to a full-page navigation, which `sdk.handle` in `hooks.server.ts` then serves. See the [example app](../../apps/sveltekit/src/routes/profile/+page.server.ts) for a working setup.
1599
1650
 
1600
1651
  <a id="server-storages"></a>
1601
1652
  ### Storages
@@ -1604,8 +1655,8 @@ By default the Server SDK stores tokens encrypted in http-only cookies and login
1604
1655
 
1605
1656
  #### Built-in session storages
1606
1657
 
1607
- - **`createEncryptedCookieStorage(secret, options?)`** - default storage that keeps the session encrypted in an http-only cookie.
1608
- - **`createSessionIdCookieStorage(storage, options?)`** - puts only a small, random session-id cookie on the client and keeps the actual session payload in the `storage` you provide. This supports back-channel logout out of the box.
1658
+ - **Encrypted cookie storage** - the default, used when no `storage` is provided: keeps the whole session encrypted with `secret` in an http-only cookie.
1659
+ - **`createSessionIdCookieStorage(storage, options?)`** - puts only a small, random session-id cookie on the client and keeps the actual session payload in the `storage` you provide. It supports back-channel logout when `storage` implements `deleteByLogoutToken`.
1609
1660
 
1610
1661
  ```ts
1611
1662
  // src/lib/server/storage.ts
@@ -1634,37 +1685,40 @@ export const sdk = createServerSDK({
1634
1685
 
1635
1686
  #### Custom storage
1636
1687
 
1637
- For example you can use Redis via [unstorage](https://npmjs.com/package/unstorage):
1688
+ For example you can use Redis via [unstorage](https://npmjs.com/package/unstorage).
1689
+
1690
+ The SDK always reads and writes the session under the same key (`storageTokenName`), and passes the current request event as the last argument. A storage that ignores the event would therefore share a single session across all users, so key the data per browser session instead. The easiest way is to wrap your key-value store with `createSessionIdCookieStorage`, which puts a random session id in a cookie and calls your store with that id as the key:
1638
1691
 
1639
1692
  ```ts
1640
1693
  // src/lib/server/storage.ts
1641
1694
  import { createStorage } from 'unstorage';
1642
1695
  import redisDriver from 'unstorage/drivers/redis';
1643
- import type { SvelteKitServerStorage, SDKStorage } from '@strivacity/sdk-svelte/server';
1696
+ import type { ServerStorage, SDKStorage } from '@strivacity/sdk-svelte/server';
1644
1697
 
1645
1698
  const unstorageInstance = createStorage({ driver: redisDriver({ url: process.env.REDIS_URL }) });
1646
1699
 
1647
- // Custom session storage for tokens
1648
- export const sessionStorage: SvelteKitServerStorage = {
1700
+ // Session payload store - `key` is the per-browser session id provided by `createSessionIdCookieStorage`
1701
+ // (raw get/set keep the serialized session string as-is)
1702
+ export const sessionStore: ServerStorage = {
1649
1703
  async get(key) {
1650
- return unstorageInstance.getItem<string>(key);
1704
+ return unstorageInstance.getItemRaw<string>(`session:${key}`);
1651
1705
  },
1652
1706
  async set(key, value) {
1653
- await unstorageInstance.setItem(key, value);
1707
+ await unstorageInstance.setItemRaw(`session:${key}`, value);
1654
1708
  },
1655
1709
  async delete(key) {
1656
- await unstorageInstance.removeItem(key);
1710
+ await unstorageInstance.removeItem(`session:${key}`);
1657
1711
  },
1658
1712
  // Required for back-channel logout support - see below.
1659
- // Scans all stored sessions and removes those matching the logout token's sid or sub claim.
1713
+ // Scans all stored sessions and removes those whose ID token claims match the logout token's sid or sub claim.
1660
1714
  async deleteByLogoutToken(logoutToken) {
1661
- const keys = await unstorageInstance.getKeys();
1715
+ const keys = await unstorageInstance.getKeys('session');
1662
1716
  await Promise.all(
1663
1717
  keys.map(async (key) => {
1664
- const raw = await unstorageInstance.getItem<string>(key);
1718
+ const raw = await unstorageInstance.getItemRaw<string>(key);
1665
1719
  if (!raw) return;
1666
1720
  const session = JSON.parse(raw);
1667
- if ((logoutToken.sid && session.sid === logoutToken.sid) || (logoutToken.sub && session.sub === logoutToken.sub)) {
1721
+ if ((logoutToken.sid && session.claims?.sid === logoutToken.sid) || (logoutToken.sub && session.claims?.sub === logoutToken.sub)) {
1668
1722
  await unstorageInstance.removeItem(key);
1669
1723
  }
1670
1724
  }),
@@ -1689,12 +1743,13 @@ export const stateStorage: SDKStorage = {
1689
1743
  ```ts
1690
1744
  // src/lib/server/strivacity.ts
1691
1745
  import { createServerSDK } from '@strivacity/sdk-svelte/server';
1746
+ import { createSessionIdCookieStorage } from '@strivacity/sdk-svelte/server/storages';
1692
1747
  import { sdkOptions } from '../options';
1693
- import { sessionStorage, stateStorage } from './storage';
1748
+ import { sessionStore, stateStorage } from './storage';
1694
1749
 
1695
1750
  export const sdk = createServerSDK({
1696
1751
  ...sdkOptions,
1697
- storage: sessionStorage, // Custom Redis-backed session storage
1752
+ storage: createSessionIdCookieStorage(sessionStore), // Custom Redis-backed session storage, keyed by a session-id cookie
1698
1753
  stateStorage, // Custom Redis-backed state storage
1699
1754
  });
1700
1755
  ```
@@ -1705,9 +1760,9 @@ export const sdk = createServerSDK({
1705
1760
 
1706
1761
  OIDC back-channel logout lets the authorization server terminate sessions server-to-server, without involving the browser. When the IDP sends a logout event (e.g. an admin terminates a session, or the user logs out from a different device), it POSTs a signed `logout_token` JWT to `/auth/backchannel-logout` - already wired up by the `handle` hook from [Setup](#setup) - which routes it to `sdk.handleBackChannelLogout(event)`.
1707
1762
 
1708
- The handler verifies the token's signature against the IDP's JWKS, validates the `iss`, `aud`, `iat` (freshness), and `jti` (replay protection) claims, requires the `http://schemas.openid.net/event/backchannel-logout` event and a `sid` or `sub` claim, then calls `storage.deleteByLogoutToken({ sid?, sub? })` to remove the matching session(s). It responds `200` on success, `400` for an invalid or malformed `logout_token`, and `501` if the configured storage doesn't implement `deleteByLogoutToken`.
1763
+ The handler verifies the token's signature against the IDP's JWKS, validates the `iss`, `aud`, `iat` (freshness), and `jti` (replay protection) claims, requires the `http://schemas.openid.net/event/backchannel-logout` event and a `sid` or `sub` claim, then calls `storage.deleteByLogoutToken({ sid?, sub? })` to remove the matching session(s). It responds `200` on success, `400` for an invalid or malformed `logout_token`, `501` if the configured storage doesn't implement `deleteByLogoutToken`, and `500` if deleting the session fails.
1709
1764
 
1710
- > **The default encrypted-cookie storage does not support back-channel logout** because each cookie is bound to a single browser session - there is no server-side index to look up by `sid` or `sub`. To support back-channel logout, use [`createSessionIdCookieStorage`](#server-storages) with a `storage` that implements `deleteByLogoutToken` (e.g. `createServerMemoryStorage()` for local testing), or a fully custom server storage as shown in the [Custom storage](#server-storages) example above.
1765
+ > **The default encrypted-cookie storage does not support back-channel logout** because each cookie is bound to a single browser session - there is no server-side index to look up by `sid` or `sub`. To support back-channel logout, use [`createSessionIdCookieStorage`](#server-storages) with a `storage` that implements `deleteByLogoutToken`, e.g. `createServerMemoryStorage()` for local testing or the Redis-backed store from the [Custom storage](#server-storages) example above.
1711
1766
 
1712
1767
  Configure the **Back-channel logout URI** in your Strivacity application settings to:
1713
1768
 
@@ -1724,9 +1779,9 @@ For a complete explanation of the handshake and validation performed, see the co
1724
1779
  options: ServerSDKOptions; // resolved server SDK configuration
1725
1780
 
1726
1781
  // Session management
1727
- getSession(event?): Promise<SessionData | null>; // read the current session
1782
+ getSession(event?): Promise<SessionData | null>; // read the current session; with `autoRefresh` (default) it refreshes a near-expiry session once per request; null if the refresh fails
1728
1783
  updateSession(session, event?): Promise<void>; // persist new session data
1729
- refreshSession(event?): Promise<SessionData>; // refresh tokens using the refresh token
1784
+ refreshSession(event?): Promise<SessionData>; // force a token refresh using the refresh token (getSession already refreshes near-expiry sessions)
1730
1785
  revokeSession(event?): Promise<void>; // revoke tokens and clear the session
1731
1786
  getEntrySession(entryUrl): Promise<Record<string, string>>; // resolve an externally-initiated (embedded/native) entry URL
1732
1787
  completeLogin(params, event?): Promise<SessionData>; // exchange an authorization code for tokens
@@ -1759,16 +1814,16 @@ The Server SDK accepts the same configuration as the client SDK (see [Configurat
1759
1814
  | `stateStorage` | `SDKStorage` | No | In-memory `Map` | Custom OAuth2 state storage |
1760
1815
  | `authUrlPrefix` | `string` | No | `'/auth'` | URL prefix matched by `handle` |
1761
1816
  | `loginUri` | `string` | No | `'/login'` | Route `requireSession` redirects to when there's no session (`embedded`/`native` modes only - `redirect`/`popup` modes always use `${authUrlPrefix}/login`) |
1762
- | `postLoginRedirectUri` | `string` | No | - | Default redirect after login when no `?returnTo=` is given |
1763
- | `postLogoutRedirectUri` | `string` | No | - | Default redirect after logout |
1817
+ | `postLoginRedirectUri` | `string` | No | origin of `redirectUri` | Default redirect after login when no `?returnTo=` is given |
1818
+ | `postLogoutRedirectUri` | `string` | No | origin of `redirectUri` | Default redirect after logout |
1764
1819
  | `cookieMaxAge` | `number` | No | `2592000` (30 days) | Max age of the session cookie in seconds |
1765
1820
 
1766
1821
  ---
1767
1822
 
1768
1823
  ## Route guards
1769
1824
 
1770
- - **Client-side**: there's no dedicated guard helper - check `ctx.isAuthenticated` (after `ctx.loading` becomes `false`) in `onMount` and redirect with `goto()`/`location.href` if it's `false`.
1771
- - **Server-side**: call [`requireSession(event, options?)`](#accessing-the-session-server-side) from a `+page.server.ts`/`+layout.server.ts` `load()` - see the caveat about non-page login targets under SSR-disabled apps documented there.
1825
+ - **Client-side**: there's no dedicated guard helper - check `ctx.isAuthenticated` in an `$effect` once `ctx.loading` is `false`, and redirect with `goto()`/`location.href` if it's `false`. `onMount` is too early on a direct page load, because the session is still being restored then.
1826
+ - **Server-side**: call [`requireSession(event, options?)`](#accessing-the-session-server-side) from a `+page.server.ts`/`+layout.server.ts` `load()` - this works with SSR disabled too (see the note there).
1772
1827
 
1773
1828
  ---
1774
1829
 
@@ -1823,7 +1878,7 @@ import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
1823
1878
 
1824
1879
  export function createCustomFlow(initConfig: SDKInitConfig) {
1825
1880
  const state = getDefaultFlowState();
1826
- const options = getSDKOptions<SDKOptions>(state, initConfig);
1881
+ const options = getSDKOptions<SDKOptions>(initConfig);
1827
1882
  const base = createBaseFlow(state, options);
1828
1883
 
1829
1884
  async function refresh(): Promise<void> {
@@ -1845,11 +1900,12 @@ v4 replaces that with the Server SDK shown in [Quick start](#quick-start) above:
1845
1900
  ```ts
1846
1901
  // src/lib/server/strivacity.ts
1847
1902
  import { createServerSDK } from '@strivacity/sdk-svelte/server';
1903
+ import { env } from '$env/dynamic/private';
1848
1904
  import { sdkOptions } from '../options';
1849
1905
 
1850
1906
  export const sdk = createServerSDK({
1851
1907
  ...sdkOptions,
1852
- secret: process.env.SECRET, // http-only cookie encryption key (random 32+ characters)
1908
+ secret: env.SECRET, // http-only cookie encryption key (random 32+ characters)
1853
1909
  });
1854
1910
  ```
1855
1911