@strivacity/sdk-core 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 (98) hide show
  1. package/README.md +228 -87
  2. package/dist/assets/oidc.cjs +1 -1
  3. package/dist/assets/oidc.cjs.map +1 -1
  4. package/dist/assets/oidc.mjs +1 -1
  5. package/dist/assets/oidc.mjs.map +1 -1
  6. package/dist/assets/server.cjs +1 -1
  7. package/dist/assets/server.cjs.map +1 -1
  8. package/dist/assets/server.mjs +1 -1
  9. package/dist/assets/server.mjs.map +1 -1
  10. package/dist/flows/base.cjs +1 -1
  11. package/dist/flows/base.cjs.map +1 -1
  12. package/dist/flows/base.d.ts +4 -2
  13. package/dist/flows/base.mjs +1 -1
  14. package/dist/flows/base.mjs.map +1 -1
  15. package/dist/flows/popup.cjs +1 -1
  16. package/dist/flows/popup.cjs.map +1 -1
  17. package/dist/flows/popup.mjs +1 -1
  18. package/dist/flows/popup.mjs.map +1 -1
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.ts +6 -2
  22. package/dist/index.mjs +1 -1
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/server/base.cjs +1 -1
  25. package/dist/server/base.cjs.map +1 -1
  26. package/dist/server/base.d.ts +2 -4
  27. package/dist/server/base.mjs +1 -1
  28. package/dist/server/base.mjs.map +1 -1
  29. package/dist/server/index.cjs +1 -1
  30. package/dist/server/index.mjs +1 -1
  31. package/dist/storages/cache.cjs.map +1 -1
  32. package/dist/storages/cache.d.ts +1 -1
  33. package/dist/storages/cache.mjs.map +1 -1
  34. package/dist/storages/index.cjs +1 -1
  35. package/dist/storages/index.mjs +1 -1
  36. package/dist/storages/server.cjs +1 -1
  37. package/dist/storages/server.d.ts +29 -1
  38. package/dist/storages/server.mjs +1 -1
  39. package/dist/storages/worker.cjs.map +1 -1
  40. package/dist/storages/worker.d.ts +1 -1
  41. package/dist/storages/worker.mjs.map +1 -1
  42. package/dist/types/native.d.ts +67 -2
  43. package/dist/types/oidc.cjs.map +1 -1
  44. package/dist/types/oidc.d.ts +41 -6
  45. package/dist/types/oidc.mjs.map +1 -1
  46. package/dist/types/popup.d.ts +1 -1
  47. package/dist/types/server.d.ts +21 -9
  48. package/dist/utils/index.cjs +1 -1
  49. package/dist/utils/index.d.ts +1 -0
  50. package/dist/utils/index.mjs +1 -1
  51. package/dist/utils/native-login.cjs +2 -0
  52. package/dist/utils/native-login.cjs.map +1 -0
  53. package/dist/utils/native-login.d.ts +13 -0
  54. package/dist/utils/native-login.mjs +2 -0
  55. package/dist/utils/native-login.mjs.map +1 -0
  56. package/dist/utils/oidc.d.ts +3 -4
  57. package/dist/utils/path.cjs +2 -0
  58. package/dist/utils/path.cjs.map +1 -0
  59. package/dist/utils/path.d.ts +16 -0
  60. package/dist/utils/path.mjs +2 -0
  61. package/dist/utils/path.mjs.map +1 -0
  62. package/dist/utils/server.d.ts +5 -4
  63. package/dist/utils/session.cjs +1 -1
  64. package/dist/utils/session.cjs.map +1 -1
  65. package/dist/utils/session.d.ts +9 -1
  66. package/dist/utils/session.mjs +1 -1
  67. package/dist/utils/session.mjs.map +1 -1
  68. package/package.json +1 -1
  69. package/testing/tests/flows/base.spec.ts +0 -1030
  70. package/testing/tests/flows/embedded.spec.ts +0 -443
  71. package/testing/tests/flows/index.spec.ts +0 -8
  72. package/testing/tests/flows/native.spec.ts +0 -487
  73. package/testing/tests/flows/popup.spec.ts +0 -400
  74. package/testing/tests/flows/redirect.spec.ts +0 -369
  75. package/testing/tests/handlers/embedded.spec.ts +0 -199
  76. package/testing/tests/handlers/index.spec.ts +0 -8
  77. package/testing/tests/handlers/native.spec.ts +0 -283
  78. package/testing/tests/handlers/popup.spec.ts +0 -206
  79. package/testing/tests/handlers/redirect.spec.ts +0 -70
  80. package/testing/tests/index.spec.ts +0 -78
  81. package/testing/tests/server/base.spec.ts +0 -846
  82. package/testing/tests/server/index.spec.ts +0 -15
  83. package/testing/tests/storages/cache.spec.ts +0 -107
  84. package/testing/tests/storages/index.spec.ts +0 -22
  85. package/testing/tests/storages/indexeddb.spec.ts +0 -113
  86. package/testing/tests/storages/local.spec.ts +0 -46
  87. package/testing/tests/storages/memory.spec.ts +0 -51
  88. package/testing/tests/storages/server.spec.ts +0 -435
  89. package/testing/tests/storages/session.spec.ts +0 -46
  90. package/testing/tests/storages/worker.spec.ts +0 -106
  91. package/testing/tests/utils/common.spec.ts +0 -77
  92. package/testing/tests/utils/credentials.spec.ts +0 -195
  93. package/testing/tests/utils/dom.spec.ts +0 -37
  94. package/testing/tests/utils/index.spec.ts +0 -71
  95. package/testing/tests/utils/logging.spec.ts +0 -82
  96. package/testing/tests/utils/oidc.spec.ts +0 -745
  97. package/testing/tests/utils/server.spec.ts +0 -235
  98. package/testing/tests/utils/session.spec.ts +0 -87
package/README.md CHANGED
@@ -22,6 +22,7 @@ Use it directly in vanilla JS/TS projects or use one of the [framework-specific
22
22
  - [Server SDK](#server-sdk)
23
23
  - [Usage](#server-usage)
24
24
  - [Storages](#server-storages)
25
+ - [Session helpers](#server-session-helpers)
25
26
  - [Back-channel logout](#server-backchannel-logout)
26
27
  - [Configuration Options](#server-configuration-options)
27
28
  - [Shared features](#shared-features)
@@ -130,10 +131,12 @@ Call this on your redirect URI page after the IDP sends the user back. It parses
130
131
  // callback.ts
131
132
  import { sdk } from './sdk';
132
133
 
133
- await sdk.handleCallback();
134
- window.location.href = '/profile';
134
+ const { returnTo } = await sdk.handleCallback();
135
+ window.location.href = returnTo ?? '/profile';
135
136
  ```
136
137
 
138
+ To come back to the page that started the login (for example the one a route guard protected), pass it to `login({ returnTo: '/orders' })`. It must be a path or a URL on the current origin, otherwise `login()` throws. The SDK keeps it with the login state, not in the URL, and `handleCallback()` resolves to it. With `serverSessionUri` it is forwarded to the Server SDK login route, which redirects there after its callback.
139
+
137
140
  > The callback URL is automatically read from `window.location.href` if not provided. You can pass a custom URL as the first parameter: `await sdk.handleCallback(customUrl)`.
138
141
 
139
142
  #### Registration
@@ -169,6 +172,8 @@ await sdk.logout({
169
172
 
170
173
  > The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
171
174
 
175
+ With [server-managed sessions](#logging-out), `logout()` doesn't end the server session - send the browser to your server's logout route instead.
176
+
172
177
  #### Token management
173
178
 
174
179
  Call these methods to manage the session and access token.
@@ -218,7 +223,7 @@ const sdk = initFlow({
218
223
 
219
224
  Call this to start the login flow. It opens a popup window by default with the Strivacity login page, where the user authenticates. After that the popup closes itself and the session is stored in the [configured storage](#storages).
220
225
 
221
- By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and position:
226
+ By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and other window features (`left`/`top` are ignored - the SDK always recalculates them to center the popup over the current window):
222
227
 
223
228
  ```ts
224
229
  // login.ts
@@ -233,8 +238,6 @@ await sdk.login({
233
238
  popupWindowFeatures: {
234
239
  width: 500,
235
240
  height: 700,
236
- left: 100,
237
- top: 100,
238
241
  toolbar: false,
239
242
  location: false,
240
243
  resizable: true,
@@ -250,7 +253,16 @@ if (await sdk.isAuthenticated) {
250
253
 
251
254
  #### Handle the callback
252
255
 
253
- 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.
256
+ 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:
257
+
258
+ ```ts
259
+ // callback.ts
260
+ import { sdk } from './sdk';
261
+
262
+ void sdk.handleCallback();
263
+ ```
264
+
265
+ `login()` resolves in the opener once the result has been received and, for client-managed sessions, the tokens have been exchanged.
254
266
 
255
267
  #### Registration
256
268
 
@@ -269,8 +281,6 @@ await sdk.register({
269
281
  popupWindowFeatures: {
270
282
  width: 500,
271
283
  height: 700,
272
- left: 100,
273
- top: 100,
274
284
  toolbar: false,
275
285
  location: false,
276
286
  resizable: true,
@@ -299,6 +309,8 @@ await sdk.logout({
299
309
 
300
310
  > The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
301
311
 
312
+ With [server-managed sessions](#logging-out), `logout()` doesn't end the server session - send the browser to your server's logout route instead.
313
+
302
314
  #### Token management
303
315
 
304
316
  Call these methods to manage the session and access token.
@@ -456,7 +468,9 @@ login.lang = 'en-US'; // Switch to English
456
468
 
457
469
  #### Handle the callback
458
470
 
459
- No separate callback page is needed. The `<sty-login>` component handles the entire authentication flow automatically, including token exchange, and dispatches a `login` event when authentication completes successfully.
471
+ No separate callback page is needed for client-managed sessions. The `<sty-login>` component handles the entire authentication flow automatically, including token exchange, and dispatches a `login` event when authentication completes successfully.
472
+
473
+ With [server-managed sessions](#server-side-session-management) (`serverSessionUri` set), the SDK instead redirects the browser to your `redirectUri` with `?code=…`, so you need a callback page there that forwards the browser to your server's callback route (e.g. `/auth/callback`).
460
474
 
461
475
  #### Externally-initiated flows (entry)
462
476
 
@@ -497,7 +511,7 @@ const params = new URLSearchParams(window.location.search);
497
511
  const login = document.querySelector('sty-login[lazy]');
498
512
  login.sessionId = params.get('session_id');
499
513
  login.shortAppId = params.get('short_app_id');
500
- login.language = params.get('language');
514
+ login.lang = params.get('language') ?? navigator.language;
501
515
  login.start();
502
516
 
503
517
  login.addEventListener('login', () => {
@@ -523,7 +537,7 @@ const data = await sdk.entry();
523
537
  const login = document.querySelector('sty-login[lazy]');
524
538
  login.sessionId = data.session_id;
525
539
  login.shortAppId = data.short_app_id;
526
- login.language = data.language;
540
+ login.lang = data.language;
527
541
  login.start();
528
542
 
529
543
  login.addEventListener('login', () => {
@@ -547,6 +561,8 @@ await sdk.logout({
547
561
 
548
562
  > The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
549
563
 
564
+ With [server-managed sessions](#logging-out), `logout()` doesn't end the server session - send the browser to your server's logout route instead.
565
+
550
566
  #### Token management
551
567
 
552
568
  Call these methods to manage the session and access token.
@@ -576,7 +592,7 @@ const refreshToken = sdk.refreshToken; // read-only property - returns the curre
576
592
 
577
593
  > For details on how this mode works, see the [native journey documentation](https://docs.strivacity.com/reference/native-journey).
578
594
 
579
- You build the entire login UI with your own components. Call `startSession()` on the SDK instance to get the initial `NativeFlowState`, render the widgets, submit each form step with `submitForm()`, and repeat until `state.finalizeUrl` is set - then call `finalizeSession()`.
595
+ You build the entire login UI with your own components. Call `startSession()` on the SDK instance to get the initial `NativeFlowState`, render the widgets, submit each form step with `submitForm()`, and repeat until `state.finalizeUrl` is set. When a `submitForm()` (or `startSession()`) response contains `finalizeUrl`, the SDK finalizes the session automatically - you don't need to call `finalizeSession()` yourself.
580
596
 
581
597
  > [Each example app](https://github.com/Strivacity/sdk-js/tree/main/apps) in this repository includes a framework-specific renderer implementation that you can use as a reference for your own integration.
582
598
 
@@ -590,19 +606,20 @@ const nativeFlowState = await sdk.startSession({
590
606
  // Optional parameters
591
607
  prompt: 'login', // use 'create' to open the registration flow instead
592
608
  language: 'en-US', // set the UI language (BCP 47 language tag)
593
- sdk: 'web-minimal', // rendering mode: 'web-minimal' for simplified rendering (see below), 'web' (default) for full rendering hints and branding
594
609
  sessionId: null, // pass a session ID to resume an existing flow
595
610
  });
596
611
 
597
612
  // NativeFlowState structure:
598
613
  // - web mode (default): Full response with all features
599
614
  // - web-minimal mode: Minimal response with layout, branding, and render hints excluded
615
+ // The client SDK always requests `web` mode; `web-minimal` is only available through the Server SDK (BFF),
616
+ // whose login endpoint (`handleLogin`) forwards an `?sdk=web-minimal` query parameter in embedded and native modes
600
617
 
601
618
  // nativeFlowState.branding - branding information (logos, etc.) (`web` mode only)
602
619
  // nativeFlowState.forms - array of form definitions with widgets
603
620
  // nativeFlowState.layout - tree structure defining widget rendering order (`web` mode only)
604
621
  // nativeFlowState.screen - current step identifier (e.g., 'identifier', 'password')
605
- // nativeFlowState.finalizeUrl - set when authentication completes, pass to finalizeSession()
622
+ // nativeFlowState.finalizeUrl - set when authentication completes (the SDK has already finalized the session by then)
606
623
 
607
624
  // Render the form based on nativeFlowState.screen, collect user input, then submit:
608
625
  const nextNativeFlowState = await sdk.submitForm('identifier', {
@@ -610,8 +627,7 @@ const nextNativeFlowState = await sdk.submitForm('identifier', {
610
627
  });
611
628
 
612
629
  if (nextNativeFlowState.finalizeUrl) {
613
- // Finalize the session at the IDP and exchange the authorization code for tokens and store them in the configured storage
614
- await sdk.finalizeSession(nextNativeFlowState.finalizeUrl);
630
+ // submitForm() has already finalized the session at the IDP, exchanged the authorization code for tokens and stored them in the configured storage
615
631
 
616
632
  // Redirect to a protected page after successful authentication
617
633
  if (await sdk.isAuthenticated) {
@@ -624,7 +640,9 @@ if (nextNativeFlowState.finalizeUrl) {
624
640
 
625
641
  #### Handle the callback
626
642
 
627
- No separate callback page is needed. Once `nativeFlowState.finalizeUrl` is set, call `sdk.finalizeSession(nativeFlowState.finalizeUrl)` to exchange the authorization code for tokens and store the session (see the Login / Register example above).
643
+ No separate callback page is needed for client-managed sessions. Once a response contains `finalizeUrl`, the SDK finalizes the session automatically - it exchanges the authorization code for tokens and stores the session (see the Login / Register example above).
644
+
645
+ With [server-managed sessions](#server-side-session-management) (`serverSessionUri` set), the SDK instead redirects the browser to your `redirectUri` with `?code=…`, so you need a callback page there that forwards the browser to your server's callback route (e.g. `/auth/callback`).
628
646
 
629
647
  #### Externally-initiated flows (entry)
630
648
 
@@ -698,6 +716,33 @@ await sdk.logout({
698
716
 
699
717
  > The `postLogoutRedirectUri` must be configured in your Strivacity application client settings as an allowed post-logout redirect URI. If the URL is invalid or not provided, the user remains on the Strivacity-hosted logged out page.
700
718
 
719
+ With [server-managed sessions](#logging-out), `logout()` doesn't end the server session - send the browser to your server's logout route instead.
720
+
721
+ #### Native login controller
722
+
723
+ To build a native login integration for a framework that has no Strivacity package, use `createNativeLoginController`. It holds the state the framework packages expose through `useNativeLogin` (`loading`, `forms`, `messages` and `state`) and publishes every change as a new immutable snapshot. Bind the snapshots to your framework's reactivity, and call `start()` once when the login UI mounts.
724
+
725
+ ```ts
726
+ import { createNativeLoginController } from '@strivacity/sdk-core/utils';
727
+ import { sdk } from './sdk';
728
+
729
+ // The SDK is passed as a getter: it is resolved on every use, so it may be created after the controller
730
+ const controller = createNativeLoginController(() => sdk, {
731
+ onLogin: () => (window.location.href = '/profile'),
732
+ onFallback: (error) => (window.location.href = error.url.toString()),
733
+ onError: (error) => console.error(error),
734
+ });
735
+
736
+ const unsubscribe = controller.subscribe(({ loading, state, forms, messages }) => render({ loading, state, forms, messages }));
737
+
738
+ await controller.start(); // sdk.init() + startSession(options.params)
739
+
740
+ controller.setFormValue('identifier', 'identifier', 'user@example.com');
741
+ await controller.submitForm('identifier');
742
+ ```
743
+
744
+ `setOptions(options)` replaces the callbacks, for example on every render. `triggerFallback()`, `triggerClose()` and `setMessage()` work the same way as in `useNativeLogin`.
745
+
701
746
  #### Token management
702
747
 
703
748
  Call these methods to manage the session and access token.
@@ -749,11 +794,40 @@ In this mode, the SDK relies on your backend to:
749
794
  - Store tokens in server-side storage (e.g., Redis, encrypted cookies)
750
795
  - Complete the OAuth2 code exchange and redirect the browser back to your app
751
796
 
797
+ #### Logging out
798
+
799
+ `logout()` doesn't end a server-managed session: it clears the local client state and redirects to the IDP end-session endpoint only if the client session holds an ID token, but the server session is left in place. Send the browser to your server's logout route (`handleLogout`, such as `/api/auth/logout`) instead, which clears the server session and redirects to the IDP end-session endpoint (or straight to `returnTo` / `postLogoutRedirectUri` when the session holds no ID token):
800
+
801
+ ```ts
802
+ // logout.ts
803
+ window.location.assign('/api/auth/logout?returnTo=/goodbye');
804
+ ```
805
+
806
+ The logout route accepts an optional `returnTo` query param, and only a same-origin one. A missing, invalid, or cross-origin value falls back to the server's `postLogoutRedirectUri`. Whichever URL is used must also be registered as an allowed post-logout redirect URI in your Strivacity application client settings.
807
+
808
+ #### Refreshing the session
809
+
810
+ The browser never refreshes tokens in this mode - the refresh token stays on the server, and `autoRefresh` is ignored. Refresh the session on the server instead: during server rendering with the Server SDK's [`getSession(request)`](#server-session-helpers), or by sending the browser to your server's refresh route (`handleRefresh`, such as `/api/auth/refresh`), which rotates the session cookie and redirects back to a same-origin `returnTo`. On the Server SDK, `autoRefresh: false` turns off the refresh in `getSession` (and in the integrations built on it).
811
+
812
+ `refresh()` does the navigation for you: it sends the browser (as a full-page navigation, also in `popup` mode) to `${authUrlPrefix}/refresh?returnTo=<url>`, where `returnTo` defaults to the current URL. `authUrlPrefix` defaults to `/auth`, the Server SDK's own default, so set it on the client only when your server uses another prefix.
813
+
814
+ ```ts
815
+ // sdk.ts
816
+ const sdk = initFlow({
817
+ // ...
818
+ serverSessionUri: '/api/auth/login',
819
+ authUrlPrefix: '/api/auth', // Where your server serves the auth routes: refresh() and the guards use `${authUrlPrefix}/refresh`
820
+ });
821
+
822
+ // Navigates to /api/auth/refresh?returnTo=/profile, then the server redirects back to /profile
823
+ await sdk.refresh({ returnTo: '/profile' });
824
+ ```
825
+
752
826
  #### Accessing the session on the client-side
753
827
 
754
828
  Tokens never reach the client SDK in this mode, so expose only what the UI needs - typically the decoded `claims`, not the raw tokens - through your own backend:
755
829
 
756
- - **Server-rendered pages** - fetch the session once per request in your server-side loader/layout via the Server SDK's `getSession(request)`, then pass the safe subset down to your client-rendered components. Framework-specific SDK wrappers already handle this for you internally.
830
+ - **Server-rendered pages** - fetch the session once per request in your server-side loader/layout via the Server SDK's `getSession(request)` (it refreshes a near-expiry session first with `autoRefresh`, so call it where the response cookies can still be written), then pass the safe subset down to your client-rendered components. Framework-specific SDK wrappers already handle this for you internally.
757
831
  - **Client-rendered pages** - same idea, but triggered from the client: expose a small endpoint (e.g. `/api/auth/session`) that calls `getSession(req)` server-side and returns the safe subset as JSON, then fetch it on the client and store it wherever your client-side session state lives.
758
832
 
759
833
  ```ts
@@ -766,14 +840,37 @@ export async function GET(req: Request) {
766
840
  }
767
841
  ```
768
842
 
843
+ ```ts
844
+ // main.ts (client)
845
+ const response = await fetch('/api/auth/session', { credentials: 'include' });
846
+ const { claims } = (await response.json()) ?? {};
847
+
848
+ // Keep `claims` in your own client-side state (store, context, ...) and render from it
849
+ ```
850
+
851
+ Note that the client SDK's `isAuthenticated` / `isAuthenticatedSync` only report `true` for a session that has an `access_token` and a future `expires_at`, so loading a claims-only object into the SDK won't mark the user as authenticated. If you want the client SDK state (`isAuthenticated`, `idTokenClaims`, events) to reflect the server session - as the [example apps](../../apps) do - return the full session from your endpoint and load it with `updateSession()`. This exposes the access token to the browser (it's still never written to client-side storage), so only do it when that trade-off is acceptable. Strip the refresh and ID tokens on the server with `toClientSession(session)` (from `@strivacity/sdk-core/utils`), which returns a copy without `refresh_token` and `id_token`:
852
+
853
+ ```ts
854
+ // api/auth/session.ts (server route)
855
+ import { toClientSession } from '@strivacity/sdk-core/utils';
856
+ import { sdk } from './server';
857
+
858
+ export async function GET(req: Request) {
859
+ return Response.json(toClientSession(await sdk.getSession(req)));
860
+ }
861
+ ```
862
+
769
863
  ```ts
770
864
  // main.ts (client)
771
865
  import { sdk } from './client';
772
866
 
773
- const response = await fetch('/api/auth/session');
774
- const session = await response.json();
867
+ const response = await fetch('/api/auth/session', { credentials: 'include' });
775
868
 
776
- sdk.session = session;
869
+ const session = response.ok ? await response.json() : null; // endpoint returns `toClientSession(session)`, or null
870
+
871
+ if (session?.access_token) {
872
+ await sdk.updateSession(session);
873
+ }
777
874
  ```
778
875
 
779
876
  ---
@@ -798,7 +895,7 @@ const sdk = createBaseServerSDK(
798
895
  // Server adapter
799
896
  {
800
897
  toRequest: (req) => toWebRequest(req), // converts framework-specific request to standard Web Request
801
- redirect: (res, url) => res.redirect(url), // optional - override default redirect behavior
898
+ redirect: (url, status) => Response.redirect(url, status), // optional - (url, status, event?) => Response; override default redirect behavior
802
899
  },
803
900
 
804
901
  // Server configuration
@@ -835,7 +932,7 @@ mount('get', '/api/auth/register', (req) => sdk.handleRegister(req));
835
932
  /**
836
933
  * Handles external flow entry (e.g., password reset link).
837
934
  * @modes embedded, native
838
- * @returns JSON with session data
935
+ * @returns 302 redirect to `loginUri`
839
936
  */
840
937
  mount('get', '/api/auth/entry', (req) => sdk.handleEntry(req));
841
938
 
@@ -856,21 +953,22 @@ mount('get', '/api/auth/refresh', (req) => sdk.handleRefresh(req));
856
953
  /**
857
954
  * Revokes tokens and clears the session.
858
955
  * @modes all
859
- * @returns 204 No Content
956
+ * @returns 302 redirect to `postLogoutRedirectUri`
860
957
  */
861
958
  mount('get', '/api/auth/revoke', (req) => sdk.handleRevoke(req));
862
959
 
863
960
  /**
864
961
  * Ends the session and redirects to the IDP logout page.
962
+ * Accepts an optional same-origin `?returnTo=` (falls back to `postLogoutRedirectUri`).
865
963
  * @modes all
866
- * @returns 302 redirect to IDP logout
964
+ * @returns 302 redirect to IDP logout (or straight to `returnTo` when the session has no ID token)
867
965
  */
868
966
  mount('get', '/api/auth/logout', (req) => sdk.handleLogout(req));
869
967
 
870
968
  /**
871
969
  * Processes back-channel logout requests from the IDP.
872
970
  * @modes all
873
- * @returns 204 No Content
971
+ * @returns 200 OK (400 invalid token, 501 storage without deleteByLogoutToken, 500 delete failed)
874
972
  */
875
973
  mount('post', '/api/auth/backchannel-logout', (req) => sdk.handleBackChannelLogout(req));
876
974
  ```
@@ -957,8 +1055,8 @@ By default, the server SDK stores:
957
1055
 
958
1056
  #### Built-in session storages
959
1057
 
960
- - **`createEncryptedCookieStorage(secret, options?)`** - default storage that keeps the session encrypted in an http-only cookie.
961
- - **`createSessionIdCookieStorage(adapter, 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.
1058
+ - **`createEncryptedCookieStorage(secret, adapter, options?)`** - default storage that keeps the session encrypted in an http-only cookie.
1059
+ - **`createSessionIdCookieStorage(adapter, 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`.
962
1060
 
963
1061
  ```ts
964
1062
  // server.ts
@@ -982,7 +1080,9 @@ const sdk = createBaseServerSDK(adapter, {
982
1080
  #### Custom storage
983
1081
 
984
1082
  You can customize storage by providing `storage` and `stateStorage` options.
985
- For example you can use [unstorage](https://npmjs.com/package/unstorage) which provides a unified async key-value API with dozens of built-in drivers (Redis, Cloudflare KV, filesystem, memory, and more):
1083
+ For example you can use [unstorage](https://npmjs.com/package/unstorage) which provides a unified async key-value API with dozens of built-in drivers (Redis, Cloudflare KV, filesystem, memory, and more).
1084
+
1085
+ The `get` / `set` / `delete` methods of a `ServerStorage` receive the incoming request event as their last argument, while the SDK always calls it with the same key (`storageTokenName`). A storage that ignores the event would therefore share a single session across all users - 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:
986
1086
 
987
1087
  ```ts
988
1088
  // storage.ts
@@ -994,29 +1094,30 @@ const unstorageInstance = createStorage({
994
1094
  driver: redisDriver({ url: process.env.REDIS_URL }),
995
1095
  });
996
1096
 
997
- // Custom session storage for tokens
998
- export const sessionStorage: ServerStorage = {
1097
+ // Session payload store - `key` is the per-browser session id provided by `createSessionIdCookieStorage`
1098
+ // (raw get/set keep the serialized session string as-is)
1099
+ export const sessionStore: ServerStorage = {
999
1100
  async get(key) {
1000
- return unstorageInstance.getItem<string>(key);
1101
+ return unstorageInstance.getItemRaw<string>(`session:${key}`);
1001
1102
  },
1002
1103
  async set(key, value) {
1003
- await unstorageInstance.setItem(key, value);
1104
+ await unstorageInstance.setItemRaw(`session:${key}`, value);
1004
1105
  },
1005
1106
  async delete(key) {
1006
- await unstorageInstance.removeItem(key);
1107
+ await unstorageInstance.removeItem(`session:${key}`);
1007
1108
  },
1008
1109
  // Required for OIDC back-channel logout support.
1009
- // Scans all stored sessions and removes those matching the logout token's sid or sub claim.
1110
+ // Scans all stored sessions and removes those whose ID token claims match the logout token's sid or sub claim.
1010
1111
  async deleteByLogoutToken(logoutToken) {
1011
- const keys = await unstorageInstance.getKeys();
1112
+ const keys = await unstorageInstance.getKeys('session');
1012
1113
  await Promise.all(
1013
1114
  keys.map(async (key) => {
1014
- const raw = await unstorageInstance.getItem<string>(key);
1115
+ const raw = await unstorageInstance.getItemRaw<string>(key);
1015
1116
  if (!raw) return;
1016
1117
  const session = JSON.parse(raw);
1017
1118
  if (
1018
- (logoutToken.sid && session.sid === logoutToken.sid) ||
1019
- (logoutToken.sub && session.sub === logoutToken.sub)
1119
+ (logoutToken.sid && session.claims?.sid === logoutToken.sid) ||
1120
+ (logoutToken.sub && session.claims?.sub === logoutToken.sub)
1020
1121
  ) {
1021
1122
  await unstorageInstance.removeItem(key);
1022
1123
  }
@@ -1042,19 +1143,42 @@ export const stateStorage: SDKStorage = {
1042
1143
  ```ts
1043
1144
  // server.ts
1044
1145
  import { createBaseServerSDK } from '@strivacity/sdk-core/server';
1146
+ import { createSessionIdCookieStorage } from '@strivacity/sdk-core/storages';
1045
1147
  import { toWebRequest } from './utils';
1046
- import { sessionStorage, stateStorage } from './storage';
1148
+ import { sessionStore, stateStorage } from './storage';
1047
1149
 
1048
- const sdk = createBaseServerSDK(
1049
- {
1050
- toRequest: (req) => toWebRequest(req),
1051
- },
1052
- {
1053
- // other configuration options
1054
- storage: sessionStorage, // Custom Redis-backed session storage
1055
- stateStorage: stateStorage, // Custom Redis-backed state storage
1056
- }
1057
- );
1150
+ const adapter = { toRequest: (req) => toWebRequest(req) };
1151
+
1152
+ const sdk = createBaseServerSDK(adapter, {
1153
+ // other configuration options
1154
+ storage: createSessionIdCookieStorage(adapter, sessionStore), // Custom Redis-backed session storage, keyed by a session-id cookie
1155
+ stateStorage: stateStorage, // Custom Redis-backed state storage
1156
+ });
1157
+ ```
1158
+
1159
+ <a id="server-session-helpers"></a>
1160
+ ### Session helpers
1161
+
1162
+ Besides the route handlers, the Server SDK exposes the session directly, for your own routes, loaders and middleware. Each method takes the framework-native request event:
1163
+
1164
+ | Method | Description |
1165
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1166
+ | `getSession(event)` | Returns the session, or `null`. With `autoRefresh` (the default) it refreshes the session first when the access token is expired or within `refreshSkew` of expiry and a refresh token exists, like the client SDK. Refreshes at most once per request event - concurrent and later calls with the same event share the result. A failed refresh resolves to `null`. With `autoRefresh: false` - the option, or `getSession(event, { autoRefresh: false })` for one call - it returns the stored session |
1167
+ | `updateSession(session, event)` | Stores a session |
1168
+ | `refreshSession(event)` | Refreshes the session with its refresh token (throws on failure and clears the session) |
1169
+ | `revokeSession(event)` | Revokes the refresh token (or the access token when there is none) and clears the session |
1170
+
1171
+ `getSession` queues the rotated session cookie on the event instead of writing it to a response, so write the queued cookies to the response yourself with `flushSetCookies(event, response)` (from `@strivacity/sdk-core/utils`). The framework SDKs do this for you in their middleware and SSR integrations.
1172
+
1173
+ ```ts
1174
+ // api/me.ts (server route)
1175
+ import { flushSetCookies } from '@strivacity/sdk-core/utils';
1176
+ import { sdk } from './server';
1177
+
1178
+ export async function GET(req: Request) {
1179
+ const session = await sdk.getSession(req);
1180
+ return flushSetCookies(req, Response.json(session ? { claims: session.claims } : null, { status: session ? 200 : 401 }));
1181
+ }
1058
1182
  ```
1059
1183
 
1060
1184
  <a id="server-backchannel-logout"></a>
@@ -1062,7 +1186,7 @@ const sdk = createBaseServerSDK(
1062
1186
 
1063
1187
  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 the `/api/auth/backchannel-logout` route you mounted earlier, which the SDK routes to `sdk.handleBackChannelLogout(req)`.
1064
1188
 
1065
- 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`.
1189
+ 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.
1066
1190
 
1067
1191
  > **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`, or a fully custom server storage as shown in the [Custom storage](#server-storages) example above.
1068
1192
 
@@ -1086,15 +1210,16 @@ The server SDK accepts the same configuration as the client SDK, plus server-spe
1086
1210
  scopes: string[],
1087
1211
  storage: ServerStorage, // Custom session storage
1088
1212
  stateStorage: SDKStorage, // Custom OAuth2 state storage
1089
- serverSessionUri: string, // Optional - route login requests through your own server endpoint
1213
+ serverSessionUri: string, // Optional - defaults to `${authUrlPrefix}/login`
1090
1214
  // ... other client SDK options
1091
1215
 
1092
1216
  // server-specific options
1093
1217
  secret: '<your-encryption-key>', // encrypts http-only cookies
1094
- authPrefix: '/auth', // Prefix for auth routes
1095
- cookieMaxAge: 43200, // Max age for session cookies in seconds (default: 30 days)
1096
- postLoginRedirectUri: '/', // Default redirect after login
1097
- postLogoutRedirectUri: '/', // Default redirect after logout
1218
+ authUrlPrefix: '/auth', // Prefix for auth routes (default: '/auth')
1219
+ loginUri: '/login', // App's login page; the entry route (`handleEntry`) redirects there with `session_id`, `short_app_id` and `language` (default: '/login')
1220
+ cookieMaxAge: 2592000, // Max age for session cookies in seconds (default: 30 days)
1221
+ postLoginRedirectUri: '/profile', // Redirect after login when no `?returnTo=` is given (default: origin of `redirectUri`)
1222
+ postLogoutRedirectUri: '/', // Redirect after logout (default: origin of `redirectUri`)
1098
1223
  }
1099
1224
  ```
1100
1225
 
@@ -1130,7 +1255,7 @@ The SDK stores login state parameters separately from session tokens. Override `
1130
1255
 
1131
1256
  ```ts
1132
1257
  import { initFlow } from '@strivacity/sdk-core';
1133
- import { createSessionStorage } from '@strivacity/sdk-core/storages';
1258
+ import { createLocalStorage, createSessionStorage } from '@strivacity/sdk-core/storages';
1134
1259
 
1135
1260
  const sdk = initFlow({
1136
1261
  mode: 'redirect',
@@ -1175,7 +1300,7 @@ const sdk = createBaseServerSDK(adapter, {
1175
1300
 
1176
1301
  ##### Worker Storage
1177
1302
 
1178
- For enhanced security isolation, you can offload all storage operations to a Web Worker via `postMessage`, effectively keeping sensitive tokens off the main thread. Simply instantiate the Worker and bridge it using `handleWorkerStorageRequests(storage)` inside the worker context:
1303
+ For enhanced security isolation, you can offload all storage operations to a Web Worker via `postMessage`, so the persisted session lives in the worker's storage (e.g. IndexedDB) instead of `localStorage`. Note that the SDK still keeps the active session in main-thread memory while the page is running. Simply instantiate the Worker and bridge it using `handleWorkerStorageRequests(storage)` inside the worker context:
1179
1304
 
1180
1305
  ```ts
1181
1306
  // storage.worker.ts
@@ -1204,7 +1329,7 @@ const sdk = initFlow({
1204
1329
  All flow instances expose the same set of getters:
1205
1330
 
1206
1331
  ```ts
1207
- // Async - waits for init, optionally auto-refreshes an expired token
1332
+ // Async - waits for init, optionally auto-refreshes an expired token (never when serverSessionUri is set)
1208
1333
  const isAuthenticated: boolean = await sdk.isAuthenticated;
1209
1334
 
1210
1335
  // Sync - true only if a non-expired session is already in memory
@@ -1224,12 +1349,14 @@ const sessionId: string | null = sdk.sessionId; // current session identifier
1224
1349
  const shortAppId: string | null = sdk.shortAppId; // short application identifier
1225
1350
  const language: string = sdk.language; // current UI language code
1226
1351
 
1227
- // Verify authentication status (auto-refreshes an expired token if a refresh token is available)
1352
+ // Verify authentication status (auto-refreshes an expired token if a refresh token is available, unless serverSessionUri is set)
1228
1353
  const ok: boolean = await sdk.checkAuthentication();
1229
1354
  // Verify authentication status without attempting a refresh
1230
1355
  const ok: boolean = await sdk.checkAuthentication({ autoRefresh: false });
1231
1356
  ```
1232
1357
 
1358
+ Tabs that share the session storage (`localStorage`, IndexedDB, ...) never refresh at the same time: `refresh()` (also the automatic one) runs under a [Web Lock](https://developer.mozilla.org/docs/Web/API/Web_Locks_API) per client and storage key. A tab that waited for the lock first re-reads the storage and uses the session another tab has just refreshed, instead of sending the old refresh token again, which would fail with refresh token rotation and clear the session. Without the Web Locks API, refresh runs directly.
1359
+
1233
1360
  ---
1234
1361
 
1235
1362
  ### SDK events
@@ -1243,7 +1370,7 @@ Subscribe to authentication lifecycle events via `subscribeToEvent` (one specifi
1243
1370
  | `sessionUpdated` | `{ idToken, accessToken, refreshToken, claims }` | The session was updated (e.g., after a token refresh) |
1244
1371
  | `sessionCleared` | - | The session was cleared (e.g., after logout or token revocation) |
1245
1372
  | `loginInitiated` | - | A login or registration redirect / popup has started |
1246
- | `flowInitiated` | `{ sessionId, shortAppId, language }` | A native or embedded flow has started |
1373
+ | `flowInitiated` | - | A native or embedded flow has started |
1247
1374
  | `loggedIn` | `{ idToken, accessToken, refreshToken, claims }` | Tokens were received and stored after a successful login |
1248
1375
  | `logoutInitiated` | `{ idToken, claims }` | Logout was initiated, before the redirect to the IDP end-session endpoint |
1249
1376
  | `tokenRefreshed` | `{ idToken, accessToken, refreshToken, claims }` | Access token was silently refreshed |
@@ -1378,25 +1505,25 @@ For cases where none of the built-in modes fit-for example, when all authenticat
1378
1505
 
1379
1506
  `createBaseFlow` is the base factory function used by all built-in flows. It gives you the full shared method set (`init`, `checkAuthentication`, `refresh`, `revoke`, `logout`, `handleCallback`, `getSession`, `updateSession`, `cleanupSession`, `subscribeToEvent`, `subscribeToAllEvents`, `tokenExchange`) and lets you add your own login logic on top.
1380
1507
 
1381
- Wire it up through `initFlow` by passing your factory function as the `factory` option - when present, `initFlow` calls it with the resolved options instead of dispatching to the built-in `redirect`/`popup`/`embedded`/`native` flows.
1508
+ Wire it up through `initFlow` by passing your factory function as the `factory` option - when present, `initFlow` calls it with the raw options you passed to `initFlow` (defaults not applied - call `getSDKOptions()` yourself, as below) instead of dispatching to the built-in `redirect`/`popup`/`embedded`/`native` flows.
1382
1509
 
1383
1510
  ```ts
1384
1511
  import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
1385
1512
  import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
1386
1513
  import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
1387
1514
 
1388
- type CustomFlow = {
1389
- init: ReturnType<typeof createBaseFlow>['init'],
1390
- login (params?: Record<string, unknown>) => Promise<void>,
1391
- refresh () => Promise<void>,
1392
- revoke () => Promise<void>,
1393
- logout: () => Promise<void>,
1515
+ export type CustomFlow = {
1516
+ init: ReturnType<typeof createBaseFlow>['init'];
1517
+ login: (params?: Record<string, unknown>) => Promise<void>;
1518
+ refresh: () => Promise<void>;
1519
+ revoke: () => Promise<void>;
1520
+ logout: () => Promise<void>;
1394
1521
  // Add your own methods here
1395
- }
1522
+ };
1396
1523
 
1397
1524
  export function createCustomFlow(initConfig: SDKInitConfig): CustomFlow {
1398
1525
  const state = getDefaultFlowState();
1399
- const options = getSDKOptions<SDKOptions>(state, initConfig);
1526
+ const options = getSDKOptions<SDKOptions>(initConfig);
1400
1527
  const base = createBaseFlow(state, options);
1401
1528
 
1402
1529
  async function login(params: Record<string, unknown> = {}): Promise<void> {
@@ -1421,8 +1548,9 @@ export function createCustomFlow(initConfig: SDKInitConfig): CustomFlow {
1421
1548
  });
1422
1549
 
1423
1550
  if (!response.ok) {
1551
+ const failedRefreshToken = state.session?.refresh_token;
1424
1552
  await base.cleanupSession();
1425
- base.dispatchEvent('tokenRefreshFailed', [{}]);
1553
+ base.dispatchEvent('tokenRefreshFailed', [{ refreshToken: failedRefreshToken }]);
1426
1554
  return;
1427
1555
  }
1428
1556
 
@@ -1437,15 +1565,16 @@ export function createCustomFlow(initConfig: SDKInitConfig): CustomFlow {
1437
1565
  credentials: 'include',
1438
1566
  });
1439
1567
 
1568
+ const token = state.session?.refresh_token ?? state.session?.access_token;
1569
+
1440
1570
  if (!response.ok) {
1441
1571
  await base.cleanupSession();
1442
- base.dispatchEvent('tokenRevokeFailed', [{}]);
1572
+ base.dispatchEvent('tokenRevokeFailed', [{ token }]);
1443
1573
  return;
1444
1574
  }
1445
1575
 
1446
- const session = await response.json();
1447
- await base.updateSession(session);
1448
- base.dispatchEvent('tokenRefreshed', [{ accessToken: session.access_token, claims: session.claims }]);
1576
+ await base.cleanupSession();
1577
+ base.dispatchEvent('tokenRevoked', [{ token }]);
1449
1578
  }
1450
1579
 
1451
1580
  async function logout(): Promise<void> {
@@ -1486,7 +1615,7 @@ Or wire it through `initFlow` via the `factory` option:
1486
1615
 
1487
1616
  ```ts
1488
1617
  import { initFlow } from '@strivacity/sdk-core';
1489
- import { createCustomFlow } from './custom-flow';
1618
+ import { createCustomFlow, type CustomFlow } from './custom-flow';
1490
1619
 
1491
1620
  const sdk = initFlow<CustomFlow>({
1492
1621
  mode: 'native',
@@ -1582,7 +1711,7 @@ try {
1582
1711
  // error.recoverable === true - retry might work
1583
1712
  console.error('Network error - retrying...', error.message);
1584
1713
  } else if (error instanceof ServerError) {
1585
- // Server returned 5xx error
1714
+ // Server returned a 5xx or 429 error, or the discovery document / JWKS request returned a non-OK response
1586
1715
  // error.recoverable === true - temporary server issue
1587
1716
  // error.status contains HTTP status code
1588
1717
  console.error(`Server error ${error.status}:`, error.message);
@@ -1622,10 +1751,10 @@ try {
1622
1751
 
1623
1752
  **Error categories:**
1624
1753
  - `Network` - Network connectivity issues (recoverable)
1625
- - `Server` - Server errors 5xx (recoverable)
1754
+ - `Server` - Server errors: 5xx and 429 responses, and any non-OK discovery document / JWKS response (recoverable)
1626
1755
  - `Oidc` - OIDC protocol errors from authorization server (not recoverable)
1627
1756
  - `Protocol` - Protocol violations or invalid responses (not recoverable)
1628
- - `ConfigurationError` - Invalid SDK configuration (not recoverable)
1757
+ - `InvalidRequest` - Invalid SDK configuration, thrown as `ConfigurationError` (not recoverable)
1629
1758
  - `Internal` - Unexpected internal SDK errors (not recoverable)
1630
1759
 
1631
1760
  ---
@@ -1736,7 +1865,7 @@ try {
1736
1865
  - Verifying tokens received from external sources (e.g., webhooks, API callbacks)
1737
1866
  - Server-side token verification when building custom server flows
1738
1867
 
1739
- > **Note:** The SDK automatically verifies ID tokens during login and refresh flows. You only need `verifyJwt` when handling tokens from external sources.
1868
+ > **Note:** The SDK automatically verifies ID tokens during login and refresh flows. The ID token's `exp` claim is checked with a 60-second clock-skew tolerance, so a device clock that runs slightly fast doesn't reject a freshly issued token with `ProtocolError('Token has expired')`. You only need `verifyJwt` when handling tokens from external sources.
1740
1869
 
1741
1870
  #### PKCE code verifier generation
1742
1871
 
@@ -1781,13 +1910,14 @@ injectScript('my-script', 'https://example.com/script.js');
1781
1910
  | `responseType` | `'code'` | No | `'code'` | OAuth2 response type |
1782
1911
  | `responseMode` | `'query' \| 'fragment'` | No | `'query'` | OAuth2 response mode |
1783
1912
  | `storage` | `SDKStorage` | No | `localStorage` | [Session storage](#storages) |
1784
- | `stateStorage` | `SDKStorage` | No | same as `storage` | OAuth2 PKCE / state storage |
1913
+ | `stateStorage` | `SDKStorage` | No | `createSessionStorage()` | OAuth2 PKCE / state storage |
1785
1914
  | `storageTokenName` | `string` | No | `'sty.session'` | Key under which the session is stored in the storage |
1786
- | `autoRefresh` | `boolean` | No | `true` | Automatically refreshes the access token before it expires. Set to `false` to manage refresh manually |
1787
- | `refreshSkew` | `number` | No | `60` | Number of seconds before the access token's actual expiration that it is treated as expired, so it gets refreshed early |
1915
+ | `autoRefresh` | `boolean` | No | `true` | Refreshes an expired (or about-to-expire, see `refreshSkew`) access token when authentication is checked (`isAuthenticated`, `checkAuthentication()`, route guards). There is no background timer. Set to `false` to manage refresh manually. Ignored in the browser with `serverSessionUri` - the server refreshes the session instead. On the Server SDK, `false` turns off the refresh in `getSession`. Browser tabs that share the session storage refresh, revoke and log out one at a time (Web Locks API) |
1916
+ | `refreshSkew` | `number` | No | `60` | Number of seconds before the access token's actual expiration that it is treated as expired, so it gets refreshed early (capped at half of the token lifetime, so short-lived tokens are not refreshed on every check) |
1917
+ | `clockSkew` | `number` | No | `60` | Number of seconds of clock difference tolerated between the device and the authorization server when validating the ID token's expiration |
1788
1918
  | `lazyLoad` | `boolean` | No | `false` | When `true`, defers initialization until the first method call and returns the instance synchronously |
1789
- | `serverSessionUri` | `string` | No | - | Routes login requests through your server and enables server-managed sessions; see [Server-side session management](#server-side-session-management) |
1790
- | `loginUri` | `string` | No | `'/login'` | URI of the app's login page; used in `embedded` and `native` modes to redirect the user when a new login is required |
1919
+ | `serverSessionUri` | `string \| boolean \| null` | No | `null` | Routes login requests through your server and enables server-managed sessions; `false` / `null` keeps client-managed sessions. The Server SDK defaults it to `` `${authUrlPrefix}/login` ``; see [Server-side session management](#server-side-session-management) |
1920
+ | `authUrlPrefix` | `string` | No | `'/auth'` | URL prefix of the Server SDK auth routes. With server-managed sessions `refresh()` navigates the browser to `${authUrlPrefix}/refresh` with `returnTo` set to the current URL (or `refresh({ returnTo })`), so the server rotates the session cookie and redirects back; see [Refreshing the session](#refreshing-the-session) |
1791
1921
  | `logging` | `SDKLogging` | No | - | Logging adapter; see [Logging](#logging) |
1792
1922
  | `httpClient` | `SDKHttpClient` | No | fetch | Custom HTTP client adapter; see [HTTP client](#http-client) |
1793
1923
  | `discoveryDocumentCacheTTL` | `number` | No | `3600` | OIDC metadata cache duration in seconds (1 hour by default) |
@@ -1996,7 +2126,7 @@ import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
1996
2126
 
1997
2127
  export function createCustomFlow(initConfig: SDKInitConfig) {
1998
2128
  const state = getDefaultFlowState();
1999
- const options = getSDKOptions(state, initConfig);
2129
+ const options = getSDKOptions(initConfig);
2000
2130
  const base = createBaseFlow(state, options);
2001
2131
 
2002
2132
  async function refresh(): Promise<void> {
@@ -2047,11 +2177,22 @@ v4 moves `startSession()`, `submitForm()`, and `finalizeSession()` directly onto
2047
2177
  // v4
2048
2178
  const state = await sdk.startSession({ sessionId });
2049
2179
  const nextState = await sdk.submitForm('formId', { identifier: 'user@example.com' });
2050
- await sdk.finalizeSession(nextState.finalizeUrl);
2180
+ // When nextState.finalizeUrl is set, submitForm() has already finalized the session - no finalizeSession() call needed
2051
2181
  ```
2052
2182
 
2053
2183
  See [native mode](#client-native-mode) above for the full rendering loop. Update any code that calls `login()`/`register()` and drives the returned handler to call `sdk.startSession()`/`sdk.submitForm()`/`sdk.finalizeSession()` directly instead. Framework-specific wrappers already wrap this for you.
2054
2184
 
2185
+ #### Other breaking changes
2186
+
2187
+ - **`mode` is required.** v3 defaulted to `redirect` when `mode` was omitted; v4 throws `UnsupportedFlowError` (see [Unsupported flow error](#unsupported-flow-error)).
2188
+ - **`storage`, `logging`, and `httpClient` take plain objects instead of classes.** v3 expected constructors (`storage: LocalStorage`, `logging: MyLogger`) that it instantiated itself; v4 expects instances, e.g. `storage: createLocalStorage()`, `logging: createDefaultLogging()`. The `SDKStorage`/`SDKLogging`/`SDKHttpClient` abstract classes are now plain types, and `SDKStorageType`/`SDKLoggingType`/`SDKHttpClientType` were removed.
2189
+ - **Custom HTTP clients must implement `sendTokenRequest`** in addition to `request`, and the optional logger property was renamed from `logging` to `logger` (see [HTTP client](#http-client)).
2190
+ - **Error classes are no longer exported from the package root.** Import them from `@strivacity/sdk-core/utils` instead of `@strivacity/sdk-core`.
2191
+ - **Events are scoped per SDK instance.** In v3, event subscriptions were shared across all flow instances; in v4 each instance has its own subscribers.
2192
+ - **PKCE / login state is stored in `stateStorage`** (default: `createSessionStorage()`), separately from the session `storage`. v3 stored it in the session storage.
2193
+ - **`waitToInitialize()` was replaced by `init()`.**
2194
+ - **Type renames:** `LoginFlowState` → `NativeFlowState`, `LoginFlowMessage` → `NativeFlowMessage`, `MetadataOptions` → `DiscoveryDocument`.
2195
+
2055
2196
  ### Migrating to v3.0
2056
2197
 
2057
2198
  ##### Entry API Major Changes