@strivacity/sdk-core 3.0.3 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +1873 -340
  2. package/dist/assets/oidc.cjs +2 -0
  3. package/dist/assets/oidc.cjs.map +1 -0
  4. package/dist/assets/oidc.mjs +2 -0
  5. package/dist/assets/oidc.mjs.map +1 -0
  6. package/dist/assets/server.cjs +2 -0
  7. package/dist/assets/server.cjs.map +1 -0
  8. package/dist/assets/server.mjs +2 -0
  9. package/dist/assets/server.mjs.map +1 -0
  10. package/dist/flows/base.cjs +2 -0
  11. package/dist/flows/base.cjs.map +1 -0
  12. package/dist/flows/base.d.ts +26 -0
  13. package/dist/flows/base.mjs +2 -0
  14. package/dist/flows/base.mjs.map +1 -0
  15. package/dist/flows/embedded.cjs +2 -0
  16. package/dist/flows/embedded.cjs.map +1 -0
  17. package/dist/flows/embedded.d.ts +9 -0
  18. package/dist/flows/embedded.mjs +2 -0
  19. package/dist/flows/embedded.mjs.map +1 -0
  20. package/dist/flows/index.cjs +1 -0
  21. package/dist/flows/index.d.ts +5 -0
  22. package/dist/flows/index.mjs +1 -0
  23. package/dist/flows/native.cjs +2 -0
  24. package/dist/flows/native.cjs.map +1 -0
  25. package/dist/flows/native.d.ts +9 -0
  26. package/dist/flows/native.mjs +2 -0
  27. package/dist/flows/native.mjs.map +1 -0
  28. package/dist/flows/popup.cjs +2 -0
  29. package/dist/flows/popup.cjs.map +1 -0
  30. package/dist/flows/popup.d.ts +9 -0
  31. package/dist/flows/popup.mjs +2 -0
  32. package/dist/flows/popup.mjs.map +1 -0
  33. package/dist/flows/redirect.cjs +2 -0
  34. package/dist/flows/redirect.cjs.map +1 -0
  35. package/dist/flows/redirect.d.ts +9 -0
  36. package/dist/flows/redirect.mjs +2 -0
  37. package/dist/flows/redirect.mjs.map +1 -0
  38. package/dist/handlers/embedded.cjs +2 -0
  39. package/dist/handlers/embedded.cjs.map +1 -0
  40. package/dist/handlers/embedded.d.ts +37 -0
  41. package/dist/handlers/embedded.mjs +2 -0
  42. package/dist/handlers/embedded.mjs.map +1 -0
  43. package/dist/handlers/index.cjs +1 -0
  44. package/dist/handlers/index.d.ts +2 -0
  45. package/dist/handlers/index.mjs +1 -0
  46. package/dist/handlers/native.cjs +2 -0
  47. package/dist/handlers/native.cjs.map +1 -0
  48. package/dist/handlers/native.d.ts +56 -0
  49. package/dist/handlers/native.mjs +2 -0
  50. package/dist/handlers/native.mjs.map +1 -0
  51. package/dist/handlers/popup.cjs +2 -0
  52. package/dist/handlers/popup.cjs.map +1 -0
  53. package/dist/handlers/popup.d.ts +21 -0
  54. package/dist/handlers/popup.mjs +2 -0
  55. package/dist/handlers/popup.mjs.map +1 -0
  56. package/dist/handlers/redirect.cjs +2 -0
  57. package/dist/handlers/redirect.cjs.map +1 -0
  58. package/dist/handlers/redirect.d.ts +18 -0
  59. package/dist/handlers/redirect.mjs +2 -0
  60. package/dist/handlers/redirect.mjs.map +1 -0
  61. package/dist/index.cjs +2 -2
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.d.ts +16 -21
  64. package/dist/index.mjs +2 -2
  65. package/dist/index.mjs.map +1 -1
  66. package/dist/server/base.cjs +2 -0
  67. package/dist/server/base.cjs.map +1 -0
  68. package/dist/server/base.d.ts +15 -0
  69. package/dist/server/base.mjs +2 -0
  70. package/dist/server/base.mjs.map +1 -0
  71. package/dist/server/index.cjs +1 -0
  72. package/dist/server/index.d.ts +1 -0
  73. package/dist/server/index.mjs +1 -0
  74. package/dist/storages/cache.cjs +2 -0
  75. package/dist/storages/cache.cjs.map +1 -0
  76. package/dist/storages/cache.d.ts +11 -0
  77. package/dist/storages/cache.mjs +2 -0
  78. package/dist/storages/cache.mjs.map +1 -0
  79. package/dist/storages/index.cjs +1 -0
  80. package/dist/storages/index.d.ts +7 -0
  81. package/dist/storages/index.mjs +1 -0
  82. package/dist/storages/indexeddb.cjs +2 -0
  83. package/dist/storages/indexeddb.cjs.map +1 -0
  84. package/dist/storages/indexeddb.d.ts +10 -0
  85. package/dist/storages/indexeddb.mjs +2 -0
  86. package/dist/storages/indexeddb.mjs.map +1 -0
  87. package/dist/storages/local.cjs +2 -0
  88. package/dist/storages/local.cjs.map +1 -0
  89. package/dist/storages/local.d.ts +7 -0
  90. package/dist/storages/local.mjs +2 -0
  91. package/dist/storages/local.mjs.map +1 -0
  92. package/dist/storages/memory.cjs +2 -0
  93. package/dist/storages/memory.cjs.map +1 -0
  94. package/dist/storages/memory.d.ts +8 -0
  95. package/dist/storages/memory.mjs +2 -0
  96. package/dist/storages/memory.mjs.map +1 -0
  97. package/dist/storages/server.cjs +1 -0
  98. package/dist/storages/server.d.ts +39 -0
  99. package/dist/storages/server.mjs +1 -0
  100. package/dist/storages/session.cjs +2 -0
  101. package/dist/storages/session.cjs.map +1 -0
  102. package/dist/storages/session.d.ts +7 -0
  103. package/dist/storages/session.mjs +2 -0
  104. package/dist/storages/session.mjs.map +1 -0
  105. package/dist/storages/worker.cjs +2 -0
  106. package/dist/storages/worker.cjs.map +1 -0
  107. package/dist/storages/worker.d.ts +46 -0
  108. package/dist/storages/worker.mjs +2 -0
  109. package/dist/storages/worker.mjs.map +1 -0
  110. package/dist/types/common.cjs +0 -0
  111. package/dist/types/common.d.ts +145 -0
  112. package/dist/types/common.mjs +0 -0
  113. package/dist/types/embedded.cjs +0 -0
  114. package/dist/types/embedded.d.ts +103 -0
  115. package/dist/types/embedded.mjs +0 -0
  116. package/dist/types/index.cjs +1 -0
  117. package/dist/types/index.d.ts +7 -0
  118. package/dist/types/index.mjs +1 -0
  119. package/dist/types/native.cjs +0 -0
  120. package/dist/types/native.d.ts +385 -0
  121. package/dist/types/native.mjs +0 -0
  122. package/dist/types/oidc.cjs +2 -0
  123. package/dist/types/oidc.cjs.map +1 -0
  124. package/dist/types/oidc.d.ts +768 -0
  125. package/dist/types/oidc.mjs +2 -0
  126. package/dist/types/oidc.mjs.map +1 -0
  127. package/dist/types/popup.cjs +0 -0
  128. package/dist/types/popup.d.ts +69 -0
  129. package/dist/types/popup.mjs +0 -0
  130. package/dist/types/redirect.cjs +0 -0
  131. package/dist/types/redirect.d.ts +21 -0
  132. package/dist/types/redirect.mjs +0 -0
  133. package/dist/types/server.cjs +0 -0
  134. package/dist/types/server.d.ts +200 -0
  135. package/dist/types/server.mjs +0 -0
  136. package/dist/utils/base64url.cjs +2 -0
  137. package/dist/utils/base64url.cjs.map +1 -0
  138. package/dist/utils/base64url.d.ts +14 -0
  139. package/dist/utils/base64url.mjs +2 -0
  140. package/dist/utils/base64url.mjs.map +1 -0
  141. package/dist/utils/common.cjs +2 -0
  142. package/dist/utils/common.cjs.map +1 -0
  143. package/dist/utils/common.d.ts +23 -0
  144. package/dist/utils/common.mjs +2 -0
  145. package/dist/utils/common.mjs.map +1 -0
  146. package/dist/utils/credentials.cjs +2 -2
  147. package/dist/utils/credentials.cjs.map +1 -1
  148. package/dist/utils/credentials.d.ts +19 -3
  149. package/dist/utils/credentials.mjs +2 -2
  150. package/dist/utils/credentials.mjs.map +1 -1
  151. package/dist/utils/crypto.cjs +2 -2
  152. package/dist/utils/crypto.cjs.map +1 -1
  153. package/dist/utils/crypto.d.ts +16 -26
  154. package/dist/utils/crypto.mjs +2 -2
  155. package/dist/utils/crypto.mjs.map +1 -1
  156. package/dist/utils/dom.cjs +2 -0
  157. package/dist/utils/dom.cjs.map +1 -0
  158. package/dist/utils/dom.d.ts +7 -0
  159. package/dist/utils/dom.mjs +2 -0
  160. package/dist/utils/dom.mjs.map +1 -0
  161. package/dist/utils/errors.cjs +2 -2
  162. package/dist/utils/errors.cjs.map +1 -1
  163. package/dist/utils/errors.d.ts +62 -1
  164. package/dist/utils/errors.mjs +2 -2
  165. package/dist/utils/errors.mjs.map +1 -1
  166. package/dist/utils/httpClient.cjs +2 -0
  167. package/dist/utils/httpClient.cjs.map +1 -0
  168. package/dist/utils/httpClient.d.ts +2 -0
  169. package/dist/utils/httpClient.mjs +2 -0
  170. package/dist/utils/httpClient.mjs.map +1 -0
  171. package/dist/utils/index.cjs +1 -0
  172. package/dist/utils/index.d.ts +12 -0
  173. package/dist/utils/index.mjs +1 -0
  174. package/dist/utils/logging.cjs +2 -0
  175. package/dist/utils/logging.cjs.map +1 -0
  176. package/dist/utils/logging.d.ts +2 -0
  177. package/dist/utils/logging.mjs +2 -0
  178. package/dist/utils/logging.mjs.map +1 -0
  179. package/dist/utils/oidc.cjs +1 -0
  180. package/dist/utils/oidc.d.ts +264 -0
  181. package/dist/utils/oidc.mjs +1 -0
  182. package/dist/utils/server.cjs +1 -0
  183. package/dist/utils/server.d.ts +72 -0
  184. package/dist/utils/server.mjs +1 -0
  185. package/dist/utils/session.cjs +2 -0
  186. package/dist/utils/session.cjs.map +1 -0
  187. package/dist/utils/session.d.ts +29 -0
  188. package/dist/utils/session.mjs +2 -0
  189. package/dist/utils/session.mjs.map +1 -0
  190. package/dist/utils/state.cjs +1 -0
  191. package/dist/utils/state.d.ts +23 -0
  192. package/dist/utils/state.mjs +1 -0
  193. package/package.json +53 -1
  194. package/testing/tests/flows/base.spec.ts +1030 -0
  195. package/testing/tests/flows/embedded.spec.ts +443 -0
  196. package/testing/tests/flows/index.spec.ts +8 -0
  197. package/testing/tests/flows/native.spec.ts +487 -0
  198. package/testing/tests/flows/popup.spec.ts +400 -0
  199. package/testing/tests/flows/redirect.spec.ts +369 -0
  200. package/testing/tests/handlers/embedded.spec.ts +199 -0
  201. package/testing/tests/handlers/index.spec.ts +8 -0
  202. package/testing/tests/handlers/native.spec.ts +283 -0
  203. package/testing/tests/handlers/popup.spec.ts +206 -0
  204. package/testing/tests/handlers/redirect.spec.ts +70 -0
  205. package/testing/tests/index.spec.ts +78 -0
  206. package/testing/tests/server/base.spec.ts +846 -0
  207. package/testing/tests/server/index.spec.ts +15 -0
  208. package/testing/tests/storages/cache.spec.ts +107 -0
  209. package/testing/tests/storages/index.spec.ts +22 -0
  210. package/testing/tests/storages/indexeddb.spec.ts +113 -0
  211. package/testing/tests/storages/local.spec.ts +46 -0
  212. package/testing/tests/storages/memory.spec.ts +51 -0
  213. package/testing/tests/storages/server.spec.ts +435 -0
  214. package/testing/tests/storages/session.spec.ts +46 -0
  215. package/testing/tests/storages/worker.spec.ts +106 -0
  216. package/testing/tests/utils/common.spec.ts +77 -0
  217. package/testing/tests/utils/credentials.spec.ts +195 -0
  218. package/testing/tests/utils/dom.spec.ts +37 -0
  219. package/testing/tests/utils/index.spec.ts +71 -0
  220. package/testing/tests/utils/logging.spec.ts +82 -0
  221. package/testing/tests/utils/oidc.spec.ts +744 -0
  222. package/testing/tests/utils/server.spec.ts +235 -0
  223. package/dist/flows/BaseFlow.cjs +0 -2
  224. package/dist/flows/BaseFlow.cjs.map +0 -1
  225. package/dist/flows/BaseFlow.d.ts +0 -191
  226. package/dist/flows/BaseFlow.mjs +0 -2
  227. package/dist/flows/BaseFlow.mjs.map +0 -1
  228. package/dist/flows/EmbeddedFlow.cjs +0 -2
  229. package/dist/flows/EmbeddedFlow.cjs.map +0 -1
  230. package/dist/flows/EmbeddedFlow.d.ts +0 -26
  231. package/dist/flows/EmbeddedFlow.mjs +0 -2
  232. package/dist/flows/EmbeddedFlow.mjs.map +0 -1
  233. package/dist/flows/NativeFlow.cjs +0 -2
  234. package/dist/flows/NativeFlow.cjs.map +0 -1
  235. package/dist/flows/NativeFlow.d.ts +0 -34
  236. package/dist/flows/NativeFlow.mjs +0 -2
  237. package/dist/flows/NativeFlow.mjs.map +0 -1
  238. package/dist/flows/PopupFlow.cjs +0 -2
  239. package/dist/flows/PopupFlow.cjs.map +0 -1
  240. package/dist/flows/PopupFlow.d.ts +0 -36
  241. package/dist/flows/PopupFlow.mjs +0 -2
  242. package/dist/flows/PopupFlow.mjs.map +0 -1
  243. package/dist/flows/RedirectFlow.cjs +0 -2
  244. package/dist/flows/RedirectFlow.cjs.map +0 -1
  245. package/dist/flows/RedirectFlow.d.ts +0 -38
  246. package/dist/flows/RedirectFlow.mjs +0 -2
  247. package/dist/flows/RedirectFlow.mjs.map +0 -1
  248. package/dist/handlers/BaseFlowHandler.cjs +0 -2
  249. package/dist/handlers/BaseFlowHandler.cjs.map +0 -1
  250. package/dist/handlers/BaseFlowHandler.d.ts +0 -61
  251. package/dist/handlers/BaseFlowHandler.mjs +0 -2
  252. package/dist/handlers/BaseFlowHandler.mjs.map +0 -1
  253. package/dist/handlers/EmbeddedFlowHandler.cjs +0 -2
  254. package/dist/handlers/EmbeddedFlowHandler.cjs.map +0 -1
  255. package/dist/handlers/EmbeddedFlowHandler.d.ts +0 -28
  256. package/dist/handlers/EmbeddedFlowHandler.mjs +0 -2
  257. package/dist/handlers/EmbeddedFlowHandler.mjs.map +0 -1
  258. package/dist/handlers/NativeFlowHandler.cjs +0 -2
  259. package/dist/handlers/NativeFlowHandler.cjs.map +0 -1
  260. package/dist/handlers/NativeFlowHandler.d.ts +0 -31
  261. package/dist/handlers/NativeFlowHandler.mjs +0 -2
  262. package/dist/handlers/NativeFlowHandler.mjs.map +0 -1
  263. package/dist/storages/LocalStorage.cjs +0 -2
  264. package/dist/storages/LocalStorage.cjs.map +0 -1
  265. package/dist/storages/LocalStorage.d.ts +0 -23
  266. package/dist/storages/LocalStorage.mjs +0 -2
  267. package/dist/storages/LocalStorage.mjs.map +0 -1
  268. package/dist/storages/SessionStorage.cjs +0 -2
  269. package/dist/storages/SessionStorage.cjs.map +0 -1
  270. package/dist/storages/SessionStorage.d.ts +0 -23
  271. package/dist/storages/SessionStorage.mjs +0 -2
  272. package/dist/storages/SessionStorage.mjs.map +0 -1
  273. package/dist/types.cjs +0 -2
  274. package/dist/types.cjs.map +0 -1
  275. package/dist/types.d.ts +0 -1263
  276. package/dist/types.mjs +0 -2
  277. package/dist/types.mjs.map +0 -1
  278. package/dist/utils/HttpClient.cjs +0 -2
  279. package/dist/utils/HttpClient.cjs.map +0 -1
  280. package/dist/utils/HttpClient.d.ts +0 -4
  281. package/dist/utils/HttpClient.mjs +0 -2
  282. package/dist/utils/HttpClient.mjs.map +0 -1
  283. package/dist/utils/Logging.cjs +0 -2
  284. package/dist/utils/Logging.cjs.map +0 -1
  285. package/dist/utils/Logging.d.ts +0 -8
  286. package/dist/utils/Logging.mjs +0 -2
  287. package/dist/utils/Logging.mjs.map +0 -1
  288. package/dist/utils/Metadata.cjs +0 -2
  289. package/dist/utils/Metadata.cjs.map +0 -1
  290. package/dist/utils/Metadata.d.ts +0 -98
  291. package/dist/utils/Metadata.mjs +0 -2
  292. package/dist/utils/Metadata.mjs.map +0 -1
  293. package/dist/utils/Session.cjs +0 -2
  294. package/dist/utils/Session.cjs.map +0 -1
  295. package/dist/utils/Session.d.ts +0 -82
  296. package/dist/utils/Session.mjs +0 -2
  297. package/dist/utils/Session.mjs.map +0 -1
  298. package/dist/utils/State.cjs +0 -2
  299. package/dist/utils/State.cjs.map +0 -1
  300. package/dist/utils/State.d.ts +0 -43
  301. package/dist/utils/State.mjs +0 -2
  302. package/dist/utils/State.mjs.map +0 -1
  303. package/dist/utils/base64Url.cjs +0 -2
  304. package/dist/utils/base64Url.cjs.map +0 -1
  305. package/dist/utils/base64Url.d.ts +0 -27
  306. package/dist/utils/base64Url.mjs +0 -2
  307. package/dist/utils/base64Url.mjs.map +0 -1
  308. package/dist/utils/date.cjs +0 -2
  309. package/dist/utils/date.cjs.map +0 -1
  310. package/dist/utils/date.d.ts +0 -6
  311. package/dist/utils/date.mjs +0 -2
  312. package/dist/utils/date.mjs.map +0 -1
  313. package/dist/utils/handlers.cjs +0 -2
  314. package/dist/utils/handlers.cjs.map +0 -1
  315. package/dist/utils/handlers.d.ts +0 -35
  316. package/dist/utils/handlers.mjs +0 -2
  317. package/dist/utils/handlers.mjs.map +0 -1
  318. package/dist/utils/jwt.cjs +0 -2
  319. package/dist/utils/jwt.cjs.map +0 -1
  320. package/dist/utils/jwt.d.ts +0 -34
  321. package/dist/utils/jwt.mjs +0 -2
  322. package/dist/utils/jwt.mjs.map +0 -1
  323. package/dist/utils/object.cjs +0 -2
  324. package/dist/utils/object.cjs.map +0 -1
  325. package/dist/utils/object.d.ts +0 -1
  326. package/dist/utils/object.mjs +0 -2
  327. package/dist/utils/object.mjs.map +0 -1
package/README.md CHANGED
@@ -1,538 +1,2071 @@
1
1
  # @strivacity/sdk-core
2
2
 
3
- A framework-agnostic JavaScript/TypeScript client that integrates Strivacity's policy-driven authentication journeys into any application using the OAuth 2.0 PKCE flow. Supports `redirect`, `popup`, `native`, and `embedded` modes.
3
+ Framework-agnostic JavaScript/TypeScript SDK for [Strivacity](https://www.strivacity.com) - adds PKCE-protected OIDC authentication to any web application with no framework dependencies.
4
+
5
+ Use it directly in vanilla JS/TS projects or use one of the [framework-specific wrappers](https://docs.strivacity.com/reference/javascript-sdks).
6
+
7
+ **See also:**
8
+ - [Full Documentation](https://docs.strivacity.com/reference/overview) - Complete guide for all authentication modes
9
+ - [Example Apps](../../apps/) - Working examples for different frameworks
10
+
11
+ ## Table of contents
12
+
13
+ - [Prerequisites](#prerequisites)
14
+ - [Installation](#installation)
15
+ - [Choosing a mode](#choosing-a-mode)
16
+ - [Client SDK](#client-sdk)
17
+ - [redirect mode](#client-redirect-mode)
18
+ - [popup mode](#client-popup-mode)
19
+ - [embedded mode](#client-embedded-mode)
20
+ - [native mode](#client-native-mode)
21
+ - [Server-side session management](#server-side-session-management)
22
+ - [Server SDK](#server-sdk)
23
+ - [Usage](#server-usage)
24
+ - [Storages](#server-storages)
25
+ - [Back-channel logout](#server-backchannel-logout)
26
+ - [Configuration Options](#server-configuration-options)
27
+ - [Shared features](#shared-features)
28
+ - [Storages](#storages)
29
+ - [SDK events](#sdk-events)
30
+ - [Logging](#logging)
31
+ - [HTTP client](#http-client)
32
+ - [Session state](#session-state)
33
+ - [Advanced](#advanced)
34
+ - [Custom flow](#custom-flow)
35
+ - [Error handling](#error-handling)
36
+ - [WebAuthn support](#webauthn-support)
37
+ - [Utility functions](#utility-functions)
38
+ - [Configuration reference](#configuration-reference)
39
+ - [Configuration examples](#configuration-examples)
40
+ - [Caching](#caching)
41
+ - [Migration guide](#migration-guide)
42
+ - [Vulnerability Reporting](#vulnerability-reporting)
43
+ - [License](#license)
44
+ - [Contributing](#contributing)
4
45
 
5
- See our [Developer Portal](https://www.strivacity.com/learn-support/developer-hub) to get started with developing with the Strivacity product.
6
46
 
7
- ## Overview
47
+ ---
8
48
 
9
- This SDK provides the core authentication primitives for Strivacity's policy-driven journeys. It is a standalone JavaScript/TypeScript library with no framework dependencies that can be used directly or as the foundation for framework-specific SDKs (React, Vue, Angular, etc.). The SDK uses the OAuth 2.0 PKCE flow to authenticate with Strivacity.
49
+ ## Prerequisites
10
50
 
11
- ## Demo Application
51
+ - A Strivacity tenant with an application configured (issuer URL, client ID, redirect URI)
12
52
 
13
- - [Example app](https://github.com/Strivacity/sdk-js/tree/main/apps/web-component)
53
+ ---
14
54
 
15
- ## Install
55
+ ## Installation
16
56
 
17
57
  ```bash
18
58
  npm install @strivacity/sdk-core
19
59
  ```
20
60
 
21
- ## Usage
61
+ ---
62
+
63
+ ## Choosing a mode
64
+
65
+ The SDK can be used in **two environments**:
66
+ - **[Client SDK](#client-sdk)** - runs in the browser, handles authentication flow directly
67
+ - **[Server SDK](#server-sdk)** - runs on your backend, proxies authentication requests to the Strivacity IDP, and manages session state server-side
68
+
69
+ Both support the same **four authentication modes**:
70
+
71
+ | Mode | Login UI | Best for |
72
+ | ---------- | ---------------------------------------- | -------------------------------------------- |
73
+ | `redirect` | Strivacity hosted page | Standard web apps |
74
+ | `popup` | Strivacity hosted page in a popup | Web apps that must stay on the current page |
75
+ | `embedded` | Strivacity web components in your page | Branded login inside your own layout |
76
+ | `native` | Your own components driven by flow state | Full UI control, step-by-step form rendering |
77
+
78
+ > All modes use the same PKCE-protected OIDC flow under the hood. The `mode` option only controls where the login UI lives and how the flow state is consumed.
22
79
 
23
- ### Initialization
80
+ ---
81
+
82
+ ## Client SDK
83
+
84
+ The client SDK (`@strivacity/sdk-core`) runs directly in the browser and handles authentication flows, token management, and session storage client-side.
85
+
86
+ <a id="client-redirect-mode"></a>
87
+ ### redirect mode
24
88
 
25
- Create a flow instance using the `initFlow` factory function:
89
+ > For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
26
90
 
27
- ```js
91
+ The current browser tab navigates to the Strivacity-hosted login page and back to the configured `redirectUri` after authentication.
92
+
93
+ ```ts
94
+ // sdk.ts
28
95
  import { initFlow } from '@strivacity/sdk-core';
29
96
 
97
+ // Default - session loading starts in the background immediately
30
98
  const sdk = initFlow({
31
- mode: 'redirect', // or 'popup', 'native', 'embedded'
32
- issuer: 'https://<YOUR_DOMAIN>',
33
- scopes: ['openid', 'profile'],
34
- clientId: '<YOUR_CLIENT_ID>',
35
- redirectUri: '<YOUR_REDIRECT_URI>',
99
+ mode: 'redirect', // authentication mode
100
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
101
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
102
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
103
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
36
104
  });
37
105
  ```
38
106
 
39
- ### Redirect / Popup mode
40
-
41
- In `redirect` mode the user is taken to the identity provider in the same window; in `popup` mode authentication happens in a popup. Both are initiated the same way from code.
42
-
43
107
  #### Login
44
108
 
45
- ```js
46
- await sdk.login();
109
+ Call this to start the login flow. It redirects the user to the Strivacity login page in current browser tab, where they authenticate.
110
+
111
+ ```ts
112
+ // login.ts
113
+ import { sdk } from './sdk';
114
+
115
+ await sdk.login({
116
+ // Optional parameters
117
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
118
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
119
+ audiences: ['https://api.example.com'], // target resources for the access token
120
+ locationMethod: 'assign', // window.location method: 'assign' adds to browser history, 'replace' doesn't (default: 'assign')
121
+ targetWindow: 'self', // 'self' redirects current window, 'top' redirects top-level window (default: 'self')
122
+ });
47
123
  ```
48
124
 
49
- #### Callback handling
125
+ #### Handle the callback
126
+
127
+ Call this on your redirect URI page after the IDP sends the user back. It parses the query parameters from the callback URL, verifies the state matches what was stored during login (CSRF protection), exchanges the authorization code for tokens using PKCE, validates the ID token, and stores the session in the [configured storage](#storages). After that you can redirect to a protected page or render your app.
50
128
 
51
- Process the authorization callback after the identity provider redirects back to your application:
129
+ ```ts
130
+ // callback.ts
131
+ import { sdk } from './sdk';
52
132
 
53
- ```js
54
133
  await sdk.handleCallback();
134
+ window.location.href = '/profile';
55
135
  ```
56
136
 
57
- #### Logout
137
+ > 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)`.
58
138
 
59
- The `postLogoutRedirectUri` parameter is optional and specifies where users are redirected after logout. This URI must be configured in the Admin Console as an allowed post-logout redirect URI.
139
+ #### Registration
60
140
 
61
- ```js
62
- await sdk.logout({ postLogoutRedirectUri: location.origin });
141
+ Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
142
+
143
+ ```ts
144
+ // register.ts
145
+ import { sdk } from './sdk';
146
+
147
+ await sdk.register({
148
+ // Optional parameters
149
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
150
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
151
+ audiences: ['https://api.example.com'], // target resources for the access token
152
+ locationMethod: 'assign', // window.location method: 'assign' adds to browser history, 'replace' doesn't (default: 'assign')
153
+ targetWindow: 'self', // 'self' redirects current window, 'top' redirects top-level window (default: 'self')
154
+ });
63
155
  ```
64
156
 
65
- #### Token refresh
157
+ #### Logout
158
+
159
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
66
160
 
67
- Refreshes the access token using the stored refresh token, keeping the session alive without requiring the user to log in again.
161
+ ```ts
162
+ // logout.ts
163
+ import { sdk } from './sdk';
68
164
 
69
- ```js
70
- await sdk.refresh();
165
+ await sdk.logout({
166
+ postLogoutRedirectUri: window.location.origin,
167
+ });
71
168
  ```
72
169
 
73
- #### Token revocation
170
+ > 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.
74
171
 
75
- Revokes the current access and refresh tokens on the server, invalidating the session immediately.
172
+ #### Token management
76
173
 
77
- ```js
174
+ Call these methods to manage the session and access token.
175
+
176
+ ```ts
177
+ import { sdk } from './sdk';
178
+
179
+ // Refresh the access token using the refresh token
180
+ // (automatically waits for SDK initialization and session loading)
181
+ await sdk.refresh();
182
+
183
+ // Revoke all tokens at the authorization server and clear the local session
184
+ // (automatically waits for SDK initialization and session loading)
78
185
  await sdk.revoke();
186
+
187
+ // Get the current access and refresh tokens (synchronous property - ensure SDK is initialized first)
188
+ await sdk.init();
189
+ const idToken = sdk.idTokenClaims // read-only property - returns the current ID token claims or null if not authenticated
190
+ const accessToken = sdk.accessToken; // read-only property - returns the current access token or null if not authenticated
191
+ const refreshToken = sdk.refreshToken; // read-only property - returns the current refresh token or null if not authenticated
79
192
  ```
80
193
 
81
- ### Native mode
194
+ ---
82
195
 
83
- In `native` mode, `login()` returns a `NativeFlowHandler` instance that drives the authentication UI step by step.
196
+ <a id="client-popup-mode"></a>
197
+ ### popup mode
84
198
 
85
- #### Login page example
199
+ > For details on how this mode works, see the [hosted journey documentation](https://docs.strivacity.com/reference/hosted-journey).
86
200
 
87
- The login page extracts `session_id` and optionally `language` from the URL on load and passes them to `startSession()` to resume an existing flow or start a new one. When a `language` parameter is present it overrides `uiLocales` to display the authentication UI in the specified language:
201
+ The Strivacity login page opens in a separate window or tab. After authentication the opened window or tab closes itself and the parent page receives the session - no full-page navigation required.
88
202
 
89
- ```js
203
+ ```ts
204
+ // sdk.ts
90
205
  import { initFlow } from '@strivacity/sdk-core';
91
206
 
207
+ // Default - session loading starts in the background immediately
92
208
  const sdk = initFlow({
93
- mode: 'native',
94
- issuer: 'https://<YOUR_DOMAIN>',
95
- scopes: ['openid', 'profile'],
96
- clientId: '<YOUR_CLIENT_ID>',
97
- redirectUri: '<YOUR_REDIRECT_URI>',
209
+ mode: 'popup', // authentication mode
210
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
211
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
212
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
213
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
98
214
  });
215
+ ```
99
216
 
100
- // Returns a NativeFlowHandler instance
101
- const handler = await sdk.login();
217
+ #### Login
102
218
 
103
- // Extract session_id from URL if present
104
- const url = new URL(window.location.href);
105
- const sessionId = url.searchParams.get('session_id');
219
+ 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).
106
220
 
107
- // Start a new session or resume an existing one
108
- const state = await handler.startSession(sessionId);
221
+ By default a centered popup window opens. Pass `popupWindowTarget` to change where the window opens, and `popupWindowFeatures` to control its size and position:
109
222
 
110
- if (state.finalizeUrl) {
111
- // Finalize the session when the flow is complete
112
- await handler.finalizeSession(state.finalizeUrl);
113
- } else {
114
- // Render the form from state.widgets and handle user input
115
- console.log('Current form state:', state);
223
+ ```ts
224
+ // login.ts
225
+ import { sdk } from './sdk';
226
+
227
+ await sdk.login({
228
+ // Optional parameters
229
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
230
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
231
+ audiences: ['https://api.example.com'], // target resources for the access token
232
+ popupWindowTarget: '_blank', // any valid browsing context name
233
+ popupWindowFeatures: {
234
+ width: 500,
235
+ height: 700,
236
+ left: 100,
237
+ top: 100,
238
+ toolbar: false,
239
+ location: false,
240
+ resizable: true,
241
+ scrollbars: true,
242
+ },
243
+ });
244
+
245
+ if (await sdk.isAuthenticated) {
246
+ // User authenticated - navigate to a protected page
247
+ window.location.href = '/profile';
116
248
  }
117
249
  ```
118
250
 
119
- #### Submitting a form
251
+ #### Handle the callback
252
+
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.
254
+
255
+ #### Registration
120
256
 
121
- ```js
122
- const formState = await handler.submitForm('formId', {
123
- email: 'user@example.com',
124
- password: 'secret',
257
+ Call this to start the registration flow. It works the same way as `login()` but opens the registration form instead.
258
+
259
+ ```ts
260
+ // register.ts
261
+ import { sdk } from './sdk';
262
+
263
+ await sdk.register({
264
+ // Optional parameters
265
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
266
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
267
+ audiences: ['https://api.example.com'], // target resources for the access token
268
+ popupWindowTarget: '_blank', // any valid browsing context name
269
+ popupWindowFeatures: {
270
+ width: 500,
271
+ height: 700,
272
+ left: 100,
273
+ top: 100,
274
+ toolbar: false,
275
+ location: false,
276
+ resizable: true,
277
+ scrollbars: true,
278
+ },
125
279
  });
126
280
 
127
- if (formState.finalizeUrl) {
128
- await handler.finalizeSession(formState.finalizeUrl);
281
+ if (await sdk.isAuthenticated) {
282
+ // User authenticated - navigate to a protected page
283
+ window.location.href = '/profile';
129
284
  }
130
285
  ```
131
286
 
132
- #### Callback page example
287
+ #### Logout
133
288
 
134
- When a `session_id` is present in the URL the native flow is resumed by forwarding it to the login page. Otherwise the standard `handleCallback()` path is used:
289
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
135
290
 
136
- ```js
137
- const url = new URL(location.href);
138
- const sessionId = url.searchParams.get('session_id');
291
+ ```ts
292
+ // logout.ts
293
+ import { sdk } from './sdk';
139
294
 
140
- if (sessionId) {
141
- // Forward to login page with session_id to resume the native flow
142
- window.location.href = `/login?session_id=${sessionId}`;
143
- } else {
144
- try {
145
- await sdk.handleCallback();
146
- window.location.href = '/profile';
147
- } catch (error) {
148
- console.error('Error during callback handling:', error);
149
- }
150
- }
295
+ await sdk.logout({
296
+ postLogoutRedirectUri: window.location.origin,
297
+ });
151
298
  ```
152
299
 
153
- #### Entry page example
300
+ > 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.
154
301
 
155
- The entry page processes flows started by an external process (e.g. password reset) by calling `entry()` to extract the necessary parameters to resume the flow:
302
+ #### Token management
156
303
 
157
- ```js
158
- try {
159
- const data = await sdk.entry();
304
+ Call these methods to manage the session and access token.
160
305
 
161
- if (data && Object.keys(data).length > 0) {
162
- window.location.href = `/callback?${new URLSearchParams(data).toString()}`;
163
- } else {
164
- window.location.href = '/';
165
- }
166
- } catch (error) {
167
- console.error('Entry failed:', error);
168
- window.location.href = '/';
169
- }
306
+ ```ts
307
+ import { sdk } from './sdk';
308
+
309
+ // Refresh the access token using the refresh token
310
+ // (automatically waits for SDK initialization and session loading)
311
+ await sdk.refresh();
312
+
313
+ // Revoke all tokens at the authorization server and clear the local session
314
+ // (automatically waits for SDK initialization and session loading)
315
+ await sdk.revoke();
316
+
317
+ // Get the current access and refresh tokens (synchronous property - ensure SDK is initialized first)
318
+ await sdk.init();
319
+ const idToken = sdk.idTokenClaims // read-only property - returns the current ID token claims or null if not authenticated
320
+ const accessToken = sdk.accessToken; // read-only property - returns the current access token or null if not authenticated
321
+ const refreshToken = sdk.refreshToken; // read-only property - returns the current refresh token or null if not authenticated
170
322
  ```
171
323
 
172
- ### Custom mode
324
+ ---
325
+
326
+ <a id="client-embedded-mode"></a>
327
+ ### embedded mode
173
328
 
174
- Custom mode lets you replace the built-in flow implementations with your own classes. This is useful when you need to proxy authentication through a backend server (BFF pattern) rather than communicating with the identity provider directly from the browser.
329
+ > For details on how this mode works, see the [embedded journey documentation](https://docs.strivacity.com/reference/embedded-journey).
175
330
 
176
- Pass `mode: 'custom'` together with a `customFlow` class that extends one of the built-in flow base classes (`NativeFlow`, `EmbeddedFlow`, etc.):
331
+ The login UI renders inside your own page using Strivacity web components (`<sty-login>`, `<sty-notifications>`, `<sty-language-selector>`). The component bundle isn't an npm package - load it dynamically from your Strivacity tenant cluster once during application bootstrap, alongside SDK initialization:
177
332
 
178
333
  ```ts
179
334
  import { initFlow } from '@strivacity/sdk-core';
180
- import { CustomNativeFlow } from './sdk/CustomNativeFlow';
335
+ import { injectScript } from '@strivacity/sdk-core/utils';
336
+
337
+ // Load web components bundle
338
+ injectScript('sty-components', 'https://<YOUR_TENANT_DOMAIN>/assets/components/bundle.js');
181
339
 
182
340
  const sdk = initFlow({
183
- mode: 'custom',
184
- issuer: 'https://<YOUR_DOMAIN>',
185
- scopes: ['openid', 'profile'],
186
- clientId: '<YOUR_CLIENT_ID>',
187
- redirectUri: '<YOUR_REDIRECT_URI>',
188
- customFlow: CustomNativeFlow,
341
+ mode: 'embedded', // authentication mode
342
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
343
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
344
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
345
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
189
346
  });
190
347
  ```
191
348
 
192
- #### Creating a custom flow class
349
+ #### Login / Register
193
350
 
194
- Extend `NativeFlow` (or `EmbeddedFlow`) and override the methods you need to change. The example below delegates session management to a backend API instead of calling the identity provider directly:
351
+ In embedded mode `sty-login` web component does not take `issuer`, `clientId`, or `redirectUri` as attributes - those come from the SDK configuration above. Place it on your login page together with `sty-notifications` (toast-style system notifications) and `sty-language-selector` (a language switcher for the login flow):
195
352
 
196
353
  ```ts
197
- import type { NativeParams } from '@strivacity/sdk-core';
198
- import { NativeFlow } from '@strivacity/sdk-core/flows/NativeFlow';
199
- import { Session } from '@strivacity/sdk-core/utils/Session';
200
- import { CustomNativeFlowHandler } from './CustomNativeFlowHandler';
354
+ // login.ts
355
+ import { sdk } from './sdk';
201
356
 
202
- export class CustomNativeFlow extends NativeFlow {
203
- override login(params: NativeParams = {}): CustomNativeFlowHandler {
204
- this.dispatchEvent('loginInitiated', []);
205
- return new CustomNativeFlowHandler(this, params);
206
- }
357
+ // Mount your web components - they drive themselves from here
358
+ // <sty-notifications></sty-notifications>
359
+ // <sty-login></sty-login>
360
+ // <sty-language-selector></sty-language-selector>
361
+ ```
207
362
 
208
- override register(params: NativeParams = {}): CustomNativeFlowHandler {
209
- params.prompt = 'create';
210
- return this.login(params);
211
- }
363
+ ##### Controlling when the flow starts
212
364
 
213
- async fetchSessionData(): Promise<void> {
214
- const response = await this.httpClient.request(new URL('/api/session/info', location.origin).toString(), { method: 'GET', credentials: 'include' });
365
+ By default the login flow starts automatically as soon as `sty-login` connects to the DOM. Add the `lazy` attribute to take manual control, then call `start()` when ready. `start()` accepts an optional params object forwarded to the authorization request, or you can set params via the `params` property before the component mounts:
215
366
 
216
- this.session = Object.assign(new Session(), await response.json());
367
+ ```ts
368
+ // Mount your web component with lazy attribute
369
+ // <sty-login id="login" lazy></sty-login>
217
370
 
218
- if (!this.accessTokenExpired) {
219
- this.dispatchEvent('loggedIn', [{ claims: this.idTokenClaims! }]);
220
- }
221
- }
371
+ const login = document.querySelector('sty-login[lazy]');
222
372
 
223
- override async refresh(): Promise<void> {
224
- const response = await this.httpClient.request(new URL('/api/session/refresh', location.origin).toString(), { method: 'POST', credentials: 'include' });
373
+ // Option 1: Start without params (basic usage)
374
+ await login.start();
225
375
 
226
- if (!response.ok) {
227
- this.session = null;
228
- this.dispatchEvent('tokenRefreshFailed', [{}]);
229
- return;
230
- }
376
+ // Option 2: Pass params to start()
377
+ await login.start({
378
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
379
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
380
+ audiences: ['https://api.example.com'], // target resources for the access token
381
+ language: 'en-US', // set the UI language (BCP 47 language tag)
382
+ prompt: 'login', // use 'create' to open the registration flow instead
383
+ });
231
384
 
232
- this.session = Object.assign(new Session(), await response.json());
233
- this.dispatchEvent('tokenRefreshed', [{ claims: this.idTokenClaims! }]);
234
- }
385
+ // Option 3: Set params property before calling start()
386
+ login.params = {
387
+ loginHint: 'user@example.com', // identifier or JWT-encoded data to hint the login flow
388
+ acrValues: ['urn:strivacity:loa:2'], // request specific authentication context
389
+ audiences: ['https://api.example.com'], // target resources for the access token
390
+ language: 'en-US', // set the UI language (BCP 47 language tag)
391
+ prompt: 'login', // use 'create' to open the registration flow instead
392
+ };
393
+ await login.start();
394
+ ```
395
+
396
+ The web components communicate via custom events:
397
+
398
+ **Login events:**
399
+
400
+ The `<sty-login>` element dispatches `login`, `close`, and `error` custom events. Listen to them and react accordingly:
401
+
402
+ ```ts
403
+ const login = document.querySelector('sty-login');
404
+
405
+ login.addEventListener('login', () => {
406
+ // User authenticated - navigate to a protected page
407
+ window.location.href = '/profile';
408
+ });
409
+
410
+ login.addEventListener('close', () => {
411
+ // User cancelled or closed the login flow
412
+ location.reload();
413
+ });
414
+
415
+ login.addEventListener('error', (event) => {
416
+ // A fatal error occurred - the message is available in event.detail
417
+ console.error(event.detail);
418
+ });
419
+ ```
235
420
 
236
- override async logout(): Promise<void> {
237
- this.dispatchEvent('logoutInitiated', []);
421
+ **Notification events:**
238
422
 
239
- await this.httpClient.request(new URL('/api/session/logout', location.origin).toString(), { method: 'POST', credentials: 'include' });
423
+ The components dispatch `notification` events on the `document` that the `<sty-notifications>` component automatically displays. If you don't want to use `<sty-notifications>`, you can listen to these events and handle them yourself:
240
424
 
241
- this.session = null;
425
+ ```ts
426
+ function onNotification(event: CustomEvent) {
427
+ if (event.detail.action === 'show') {
428
+ // Add new notification to your custom notification system
429
+ const notification = event.detail.notification;
430
+ console.log('New notification:', notification);
431
+ // Handle the notification display in your own UI
432
+ } else if (event.detail.action === 'clear') {
433
+ // Clear all notifications
434
+ console.log('Clear all notifications');
242
435
  }
243
436
  }
437
+
438
+ document.addEventListener('notification', onNotification);
439
+
440
+ // Don't forget to clean up when unmounting
441
+ document.removeEventListener('notification', onNotification);
244
442
  ```
245
443
 
246
- #### Creating a custom flow handler
444
+ **Dynamic language switching:**
247
445
 
248
- Extend `NativeFlowHandler` and override `startSession` and `finalizeSession` to communicate with your backend:
446
+ The `<sty-language-selector>` component provides a built-in UI for language switching. If you don't want to use it, you can change the UI language dynamically by updating the `lang` property on the `<sty-login>` component:
249
447
 
250
448
  ```ts
251
- import type { LoginFlowState } from '@strivacity/sdk-core';
252
- import { NativeFlowHandler } from '@strivacity/sdk-core/handlers/NativeFlowHandler';
253
- import { CustomNativeFlow } from './CustomNativeFlow';
449
+ const login = document.querySelector('sty-login');
254
450
 
255
- export class CustomNativeFlowHandler extends NativeFlowHandler {
256
- declare sdk: CustomNativeFlow;
451
+ // Change language programmatically (e.g., from your own language selector)
452
+ login.lang = 'fr-FR'; // Switch to French
453
+ login.lang = 'de-DE'; // Switch to German
454
+ login.lang = 'en-US'; // Switch to English
455
+ ```
257
456
 
258
- override async startSession(sessionId?: string | null, language?: string | null): Promise<LoginFlowState | void> {
259
- if (sessionId) {
260
- this.sessionId = sessionId;
261
- return this.submitForm();
262
- }
457
+ #### Handle the callback
263
458
 
264
- if (language) {
265
- this.language = language;
266
- }
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.
267
460
 
268
- const response = await this.sdk.httpClient.request(new URL('/api/session/start', location.origin).toString(), {
269
- method: 'POST',
270
- credentials: 'include',
271
- headers: { 'Content-Type': 'application/json' },
272
- body: JSON.stringify(this.params),
273
- });
461
+ #### Externally-initiated flows (entry)
274
462
 
275
- const data = await response.json();
276
- this.sessionId = data.session_id;
463
+ For flows started externally (e.g. a password reset email link), the user lands on the entry URL you configured in your Strivacity application native client settings. Call `entry()` on that landing page to resolve the flow parameters from the IDP (`session_id`, `short_app_id`, `language`).
277
464
 
278
- return this.submitForm();
279
- }
465
+ You have two options:
280
466
 
281
- override async finalizeSession(finalizeUrl: string): Promise<void> {
282
- const finalizeResponse = await this.sdk.httpClient.request(finalizeUrl, {
283
- method: 'GET',
284
- headers: { Authorization: `Bearer ${this.sessionId}` },
285
- credentials: 'include',
286
- });
467
+ **Option 1: Redirect to a separate login page**
287
468
 
288
- const redirectUri = new URL(await finalizeResponse.text());
469
+ Forward the parameters as query params to your login page:
289
470
 
290
- await this.sdk.httpClient.request(new URL('/api/session/finalize', location.origin).toString(), {
291
- method: 'POST',
292
- headers: {
293
- 'Content-Type': 'application/json',
294
- Authorization: `Bearer ${this.sessionId}`,
295
- },
296
- body: JSON.stringify(Object.fromEntries(redirectUri.searchParams)),
297
- credentials: 'include',
298
- });
471
+ ```ts
472
+ // entry.ts
473
+ import { sdk } from './sdk';
299
474
 
300
- await this.sdk.fetchSessionData();
301
- }
302
- }
303
- ```
475
+ const data = await sdk.entry();
304
476
 
305
- ### Event subscription
477
+ // Redirect to login page with flow parameters
478
+ const params = new URLSearchParams({
479
+ session_id: data.session_id,
480
+ short_app_id: data.short_app_id,
481
+ language: data.language,
482
+ });
483
+ window.location.href = `/login?${params}`;
484
+ ```
306
485
 
307
- The SDK provides an event system for listening to authentication lifecycle events:
486
+ Then on your login page, read the parameters and pass them to `sty-login`:
308
487
 
309
- ```js
310
- const { dispose } = sdk.subscribeToEvent('loggedIn', (eventData) => {
311
- console.log('User logged in:', eventData);
488
+ ```ts
489
+ // login.ts
490
+ import { sdk } from './sdk';
491
+
492
+ // Mount your web component
493
+ // <sty-login id="login" lazy></sty-login>
494
+
495
+ // Read parameters from URL
496
+ const params = new URLSearchParams(window.location.search);
497
+ const login = document.querySelector('sty-login[lazy]');
498
+ login.sessionId = params.get('session_id');
499
+ login.shortAppId = params.get('short_app_id');
500
+ login.language = params.get('language');
501
+ login.start();
502
+
503
+ login.addEventListener('login', () => {
504
+ // User authenticated - navigate to a protected page
505
+ window.location.href = '/profile';
312
506
  });
313
-
314
- // Later: unsubscribe
315
- dispose();
316
507
  ```
317
508
 
318
- Available events:
509
+ **Option 2: Render login on the entry page**
510
+
511
+ Pass the parameters directly to `sty-login` on the same page:
512
+
513
+ ```ts
514
+ // entry.ts
515
+ import { sdk } from './sdk';
319
516
 
320
- - **`init`**: Fired when the SDK is initialized.
321
- - **`loggedIn`**: Fired when a user successfully logs in.
322
- - **`loginInitiated`**: Fired when a login flow is initiated.
323
- - **`sessionLoaded`**: Fired when an existing session is loaded.
324
- - **`accessTokenExpired`**: Fired when the access token expires.
325
- - **`tokenRefreshed`**: Fired when access tokens are successfully refreshed.
326
- - **`tokenRefreshFailed`**: Fired when token refresh fails.
327
- - **`logoutInitiated`**: Fired when logout is initiated.
328
- - **`tokenRevoked`**: Fired when tokens are successfully revoked.
329
- - **`tokenRevokeFailed`**: Fired when token revocation fails.
517
+ const data = await sdk.entry();
330
518
 
331
- ## Logging
519
+ // Mount your web component
520
+ // <sty-login id="login" lazy></sty-login>
332
521
 
333
- The SDK supports optional logging to help you debug authentication flows and monitor SDK behavior. You can enable the built-in console logger or provide your own custom logger implementation.
522
+ // Set properties on the existing sty-login component
523
+ const login = document.querySelector('sty-login[lazy]');
524
+ login.sessionId = data.session_id;
525
+ login.shortAppId = data.short_app_id;
526
+ login.language = data.language;
527
+ login.start();
334
528
 
335
- ### Using the Default Logger
529
+ login.addEventListener('login', () => {
530
+ // User authenticated - navigate to a protected page
531
+ window.location.href = window.location.origin;
532
+ });
533
+ ```
336
534
 
337
- Enable the default console logger by adding the `logging` option when calling `initFlow`:
535
+ #### Logout
338
536
 
339
- ```js
340
- import { initFlow, DefaultLogging } from '@strivacity/sdk-core';
537
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
341
538
 
342
- const sdk = initFlow({
343
- mode: 'redirect',
344
- issuer: 'https://<YOUR_DOMAIN>',
345
- scopes: ['openid', 'profile'],
346
- clientId: '<YOUR_CLIENT_ID>',
347
- redirectUri: '<YOUR_REDIRECT_URI>',
348
- logging: DefaultLogging,
539
+ ```ts
540
+ // logout.ts
541
+ import { sdk } from './sdk';
542
+
543
+ await sdk.logout({
544
+ postLogoutRedirectUri: window.location.origin,
349
545
  });
350
546
  ```
351
547
 
352
- The default logger writes to the browser console and automatically prefixes messages with a correlation ID when available (via the `xEventId` property).
548
+ > 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.
353
549
 
354
- ### Creating a Custom Logger
550
+ #### Token management
355
551
 
356
- Implement the `SDKLogging` interface and pass your class to the `logging` option:
552
+ Call these methods to manage the session and access token.
357
553
 
358
- ```typescript
359
- import type { SDKLogging } from '@strivacity/sdk-core';
554
+ ```ts
555
+ import { sdk } from './sdk';
360
556
 
361
- export class MyLogger implements SDKLogging {
362
- xEventId?: string;
557
+ // Refresh the access token using the refresh token
558
+ // (automatically waits for SDK initialization and session loading)
559
+ await sdk.refresh();
363
560
 
364
- debug(message: string): void {
365
- console.debug(this.xEventId ? `[${this.xEventId}] ${message}` : message);
366
- }
561
+ // Revoke all tokens at the authorization server and clear the local session
562
+ // (automatically waits for SDK initialization and session loading)
563
+ await sdk.revoke();
367
564
 
368
- info(message: string): void {
369
- console.info(this.xEventId ? `[${this.xEventId}] ${message}` : message);
370
- }
565
+ // Get the current access and refresh tokens (synchronous property - ensure SDK is initialized first)
566
+ await sdk.init();
567
+ const idToken = sdk.idTokenClaims // read-only property - returns the current ID token claims or null if not authenticated
568
+ const accessToken = sdk.accessToken; // read-only property - returns the current access token or null if not authenticated
569
+ const refreshToken = sdk.refreshToken; // read-only property - returns the current refresh token or null if not authenticated
570
+ ```
371
571
 
372
- warn(message: string): void {
373
- console.warn(this.xEventId ? `[${this.xEventId}] ${message}` : message);
374
- }
572
+ ---
573
+
574
+ <a id="client-native-mode"></a>
575
+ ### native mode
576
+
577
+ > For details on how this mode works, see the [native journey documentation](https://docs.strivacity.com/reference/native-journey).
578
+
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()`.
580
+
581
+ > [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
+
583
+ #### Login / Register
584
+
585
+ ```ts
586
+ // login.ts
587
+ import { sdk } from './sdk';
588
+
589
+ const nativeFlowState = await sdk.startSession({
590
+ // Optional parameters
591
+ prompt: 'login', // use 'create' to open the registration flow instead
592
+ 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
+ sessionId: null, // pass a session ID to resume an existing flow
595
+ });
596
+
597
+ // NativeFlowState structure:
598
+ // - web mode (default): Full response with all features
599
+ // - web-minimal mode: Minimal response with layout, branding, and render hints excluded
600
+
601
+ // nativeFlowState.branding - branding information (logos, etc.) (`web` mode only)
602
+ // nativeFlowState.forms - array of form definitions with widgets
603
+ // nativeFlowState.layout - tree structure defining widget rendering order (`web` mode only)
604
+ // nativeFlowState.screen - current step identifier (e.g., 'identifier', 'password')
605
+ // nativeFlowState.finalizeUrl - set when authentication completes, pass to finalizeSession()
606
+
607
+ // Render the form based on nativeFlowState.screen, collect user input, then submit:
608
+ const nextNativeFlowState = await sdk.submitForm('identifier', {
609
+ identifier: 'user@example.com',
610
+ });
611
+
612
+ 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);
375
615
 
376
- error(message: string, error: Error): void {
377
- console.error(this.xEventId ? `[${this.xEventId}] ${message}` : message, error);
616
+ // Redirect to a protected page after successful authentication
617
+ if (await sdk.isAuthenticated) {
618
+ window.location.href = '/profile';
378
619
  }
620
+ } else {
621
+ // Re-render for nextNativeFlowState.screen and repeat
379
622
  }
380
623
  ```
381
624
 
382
- ```js
383
- import { initFlow } from '@strivacity/sdk-core';
384
- import { MyLogger } from './logging/MyLogger';
625
+ #### Handle the callback
385
626
 
386
- const sdk = initFlow({
387
- // ...other options
388
- logging: MyLogger,
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).
628
+
629
+ #### Externally-initiated flows (entry)
630
+
631
+ For flows started externally (e.g. a password reset email link), the user lands on the entry URL you configured in your Strivacity application native client settings. Call `entry()` on that landing page to resolve the flow parameters from the IDP (`session_id`, `short_app_id`, `language`).
632
+
633
+ You have two options:
634
+
635
+ **Option 1: Redirect to a separate login page**
636
+
637
+ Forward the parameters as query params to your login page:
638
+
639
+ ```ts
640
+ // entry.ts
641
+ import { sdk } from './sdk';
642
+
643
+ const data = await sdk.entry();
644
+
645
+ // Redirect to login page with flow parameters
646
+ const params = new URLSearchParams({
647
+ session_id: data.session_id,
648
+ language: data.language,
649
+ });
650
+ window.location.href = `/login?${params}`;
651
+ ```
652
+
653
+ Then on your login page, read the parameters and pass them to `startSession()`:
654
+
655
+ ```ts
656
+ // login.ts
657
+ import { sdk } from './sdk';
658
+
659
+ // Read parameters from URL
660
+ const params = new URLSearchParams(window.location.search);
661
+ const state = await sdk.startSession({
662
+ sessionId: params.get('session_id'),
663
+ language: params.get('language'),
389
664
  });
665
+
666
+ // Continue with the rendering loop (see minimal rendering example above)
390
667
  ```
391
668
 
392
- The `SDKLogging` interface requires `debug`, `info`, `warn`, and `error` methods. The optional `xEventId` property, when set by the SDK, provides a correlation ID to trace related log messages across the authentication flow.
669
+ **Option 2: Start session on the entry page**
393
670
 
394
- ## HTTP Client
671
+ Pass the parameters directly to `startSession()` on the same page:
395
672
 
396
- The SDK uses a built-in `fetch`-based HTTP client for all requests. You can replace it with your own implementation by extending `SDKHttpClient` and passing your class via the `httpClient` option. This is useful when you need to attach custom headers (e.g. `x-sty-app-id`) to every outgoing request, route traffic through a proxy, or use a platform-specific transport such as Capacitor's `CapacitorHttp`.
673
+ ```ts
674
+ // entry.ts
675
+ import { sdk } from './sdk';
397
676
 
398
- ### Adding custom headers to every request
677
+ const data = await sdk.entry();
678
+ const state = await sdk.startSession({
679
+ sessionId: data.session_id,
680
+ language: data.language,
681
+ });
399
682
 
400
- ```typescript
401
- import { initFlow, SDKHttpClient, type HttpClientResponse } from '@strivacity/sdk-core';
683
+ // Continue with the rendering loop (see minimal rendering example above)
684
+ ```
402
685
 
403
- class CustomHttpClient extends SDKHttpClient {
404
- async request<T>(url: string, options?: RequestInit): Promise<HttpClientResponse<T>> {
405
- const mergedOptions: RequestInit = {
406
- ...options,
407
- headers: {
408
- 'x-sty-app-id': 'my-app',
409
- ...(options?.headers as Record<string, string>),
410
- },
411
- };
686
+ #### Logout
412
687
 
413
- const response = await fetch(url, mergedOptions);
688
+ Call this to clear the session and redirect to the Strivacity end-session endpoint. After that the user is redirected back to your app at `postLogoutRedirectUri`.
414
689
 
415
- return {
416
- headers: response.headers,
417
- ok: response.ok,
418
- status: response.status,
419
- statusText: response.statusText,
420
- url: response.url,
421
- json: async () => (await response.json()) as T,
422
- text: async () => await response.text(),
423
- };
424
- }
425
- }
690
+ ```ts
691
+ // logout.ts
692
+ import { sdk } from './sdk';
693
+
694
+ await sdk.logout({
695
+ postLogoutRedirectUri: window.location.origin,
696
+ });
697
+ ```
698
+
699
+ > 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
+
701
+ #### Token management
702
+
703
+ Call these methods to manage the session and access token.
704
+
705
+ ```ts
706
+ import { sdk } from './sdk';
707
+
708
+ // Refresh the access token using the refresh token
709
+ // (automatically waits for SDK initialization and session loading)
710
+ await sdk.refresh();
711
+
712
+ // Revoke all tokens at the authorization server and clear the local session
713
+ // (automatically waits for SDK initialization and session loading)
714
+ await sdk.revoke();
715
+
716
+ // Get the current access and refresh tokens (synchronous property - ensure SDK is initialized first)
717
+ await sdk.init();
718
+ const idToken = sdk.idTokenClaims // read-only property - returns the current ID token claims or null if not authenticated
719
+ const accessToken = sdk.accessToken; // read-only property - returns the current access token or null if not authenticated
720
+ const refreshToken = sdk.refreshToken; // read-only property - returns the current refresh token or null if not authenticated
721
+ ```
722
+
723
+ ---
724
+
725
+ ### Server-side session management
726
+
727
+ Set `serverSessionUri` on the SDK options when using a backend-for-frontend ([BFF](../../README.md#bff)) architecture, to route login requests through your own server-side endpoint instead of the SDK's default IDP endpoint - useful when the request must be built/signed server-side. Setting it also switches the client SDK into server-managed session mode: tokens are never read from or written to client-side storage.
728
+
729
+ ```ts
730
+ // sdk.ts
731
+ import { initFlow } from '@strivacity/sdk-core';
426
732
 
427
733
  const sdk = initFlow({
428
- // ...other options
429
- httpClient: CustomHttpClient,
734
+ mode: 'redirect', // works the same way in popup, embedded, and native modes
735
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
736
+ clientId: 'YOUR_CLIENT_ID',
737
+ redirectUri: 'https://your-app.example.com/callback',
738
+ scopes: ['openid', 'profile', 'email'],
739
+ serverSessionUri: '/api/auth/login', // Requests are routed through your server; tokens are NOT written to client storage
430
740
  });
741
+
742
+ await sdk.login();
431
743
  ```
432
744
 
433
- Any header you add inside `request()` is automatically included in every SDK request
745
+ Sensitive OAuth2 parameters (`client_id`, `redirect_uri`, `scope`, ...) are never sent to `serverSessionUri` - your endpoint is responsible for adding those itself; only the non-sensitive extras (`prompt`, `display`, `acrValues`, `loginHint`, `uiLocales`, `audiences`) are appended as query params.
746
+
747
+ In this mode, the SDK relies on your backend to:
748
+ - Build/sign the authorization request and proxy it to the IDP
749
+ - Store tokens in server-side storage (e.g., Redis, encrypted cookies)
750
+ - Complete the OAuth2 code exchange and redirect the browser back to your app
434
751
 
435
- ### CORS configuration
752
+ #### Accessing the session on the client-side
436
753
 
437
- For custom request headers to reach the Strivacity cluster, the cluster must be configured to explicitly allow them. Add the header name(s) to the **Access-Control-Allow-Headers** list in the cluster settings. Without this, browsers will block the preflight `OPTIONS` request and the SDK call will fail with a CORS error.
754
+ 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:
438
755
 
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.
757
+ - **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
+
759
+ ```ts
760
+ // api/auth/session.ts (server route)
761
+ import { sdk } from './server';
762
+
763
+ export async function GET(req: Request) {
764
+ const session = await sdk.getSession(req);
765
+ return Response.json(session ? { claims: session.claims } : null);
766
+ }
439
767
  ```
440
- Access-Control-Allow-Headers: x-sty-app-id, <any other custom headers>
768
+
769
+ ```ts
770
+ // main.ts (client)
771
+ import { sdk } from './client';
772
+
773
+ const response = await fetch('/api/auth/session');
774
+ const session = await response.json();
775
+
776
+ sdk.session = session;
441
777
  ```
442
778
 
443
- ## API Documentation
779
+ ---
780
+
781
+ ## Server SDK
782
+
783
+ This package provides a backend-for-frontend ([BFF](../../README.md#bff)) server implementation that handles OAuth2 state and PKCE management automatically. Use it when you need to route authentication traffic through your own server for request signing, custom headers, or additional validation.
784
+
785
+ <a id="server-usage"></a>
786
+ ### Usage
444
787
 
445
- ### `initFlow(options)`
788
+ Here's a minimal example of how to set up the server SDK with Express. The SDK handles all the OAuth2 flow and session management for you, so your routes can be very simple.
789
+
790
+ ```ts
791
+ // server.ts
792
+ import { createBaseServerSDK } from '@strivacity/sdk-core/server';
793
+ import { toWebRequest } from './utils';
794
+
795
+ // Initialize the server SDK
796
+ const sdk = createBaseServerSDK(
797
+
798
+ // Server adapter
799
+ {
800
+ toRequest: (req) => toWebRequest(req), // converts framework-specific request to standard Web Request
801
+ redirect: (res, url) => res.redirect(url), // optional - override default redirect behavior
802
+ },
803
+
804
+ // Server configuration
805
+ {
806
+ mode: 'redirect', // authentication mode
807
+ issuer: 'https://<YOUR_TENANT_DOMAIN>', // OIDC provider URL
808
+ clientId: 'YOUR_CLIENT_ID', // OAuth2 client ID
809
+ redirectUri: 'https://your-app.example.com/callback', // callback URL after authentication
810
+ scopes: ['openid', 'profile', 'email'], // requested user permissions/data
811
+ secret: process.env.SECRET, // http-only cookie encryption key (random 32+ characters); Required if using cookie storage (default)
812
+ }
813
+ );
814
+ ```
446
815
 
447
- ```typescript
448
- initFlow(options: SDKOptions): PopupFlow | RedirectFlow | NativeFlow | EmbeddedFlow
816
+ ```ts
817
+ // routes.ts
818
+ import { sdk } from './server';
819
+ import { mount } from './utils';
820
+
821
+ /**
822
+ * Starts the login flow and redirects to the IDP.
823
+ * @modes all
824
+ * @returns 302 redirect to IDP
825
+ */
826
+ mount('get', '/api/auth/login', (req) => sdk.handleLogin(req));
827
+
828
+ /**
829
+ * Starts the registration flow and redirects to the IDP.
830
+ * @modes all
831
+ * @returns 302 redirect to IDP
832
+ */
833
+ mount('get', '/api/auth/register', (req) => sdk.handleRegister(req));
834
+
835
+ /**
836
+ * Handles external flow entry (e.g., password reset link).
837
+ * @modes embedded, native
838
+ * @returns JSON with session data
839
+ */
840
+ mount('get', '/api/auth/entry', (req) => sdk.handleEntry(req));
841
+
842
+ /**
843
+ * Completes authentication (handles the callback from the IDP).
844
+ * @modes all
845
+ * @returns 302 redirect or popup close script
846
+ */
847
+ mount('get', '/api/auth/callback', (req) => sdk.handleCallback(req));
848
+
849
+ /**
850
+ * Refreshes the access token.
851
+ * @modes all
852
+ * @returns 204 No Content or 302 redirect
853
+ */
854
+ mount('get', '/api/auth/refresh', (req) => sdk.handleRefresh(req));
855
+
856
+ /**
857
+ * Revokes tokens and clears the session.
858
+ * @modes all
859
+ * @returns 204 No Content
860
+ */
861
+ mount('get', '/api/auth/revoke', (req) => sdk.handleRevoke(req));
862
+
863
+ /**
864
+ * Ends the session and redirects to the IDP logout page.
865
+ * @modes all
866
+ * @returns 302 redirect to IDP logout
867
+ */
868
+ mount('get', '/api/auth/logout', (req) => sdk.handleLogout(req));
869
+
870
+ /**
871
+ * Processes back-channel logout requests from the IDP.
872
+ * @modes all
873
+ * @returns 204 No Content
874
+ */
875
+ mount('post', '/api/auth/backchannel-logout', (req) => sdk.handleBackChannelLogout(req));
449
876
  ```
450
877
 
451
- The `initFlow` function creates and returns a flow instance based on the `mode` specified in `options`.
878
+ ```ts
879
+ // utils.ts
880
+ import type { Request as ExpressRequest, Response as ExpressResponse } from 'express';
881
+ import { Router } from 'express';
882
+ import { Readable } from 'stream';
452
883
 
453
- **Parameters:**
884
+ const handlers = Router();
454
885
 
455
- | Name | Type | Required | Description |
456
- | ------------------ | ------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------ |
457
- | `issuer` | `string` | ✅ | The issuer URL of the OpenID Provider. |
458
- | `clientId` | `string` | ✅ | The client identifier for the application. |
459
- | `redirectUri` | `string` | ✅ | The URI to redirect to after authentication. |
460
- | `mode` | `'redirect' \| 'popup' \| 'native' \| 'embedded' \| 'custom'` | ⬜ | The authentication flow mode. Defaults to `'redirect'`. |
461
- | `scopes` | `string[]` | ⬜ | The scopes to request. Defaults to `['openid']`. |
462
- | `responseType` | `'code' \| 'id_token'` | ⬜ | The response type. |
463
- | `responseMode` | `'query' \| 'fragment'` | ⬜ | The response mode. |
464
- | `storageTokenName` | `string` | ⬜ | Custom name for the session storage key. Defaults to `'sty.session'`. |
465
- | `storage` | `SDKStorageType` | ⬜ | Custom storage implementation. Defaults to `LocalStorage`. |
466
- | `httpClient` | `SDKHttpClientType` | ⬜ | Custom HTTP client implementation. |
467
- | `logging` | `SDKLoggingType` | ⬜ | Logging implementation class. |
468
- | `customFlow` | `FlowType` | ⬜ | Custom flow class. Required when `mode` is `'custom'`. Must extend `BaseFlow`. |
886
+ // Apply Web Response to Express response
887
+ export async function applyResponse(response: Response, res: ExpressResponse): Promise<void> {
888
+ res.status(response.status);
469
889
 
470
- **Returns:** A flow instance (`PopupFlow`, `RedirectFlow`, `NativeFlow`, `EmbeddedFlow`, or your custom flow class) depending on the configured `mode`.
890
+ for (const [key, value] of response.headers.entries()) {
891
+ if (key.toLowerCase() === 'set-cookie') {
892
+ continue;
893
+ }
471
894
 
472
- ---
895
+ res.setHeader(key, value);
896
+ }
473
897
 
474
- ### Flow instance methods
898
+ const setCookies = response.headers.getSetCookie();
475
899
 
476
- All flow instances share the following methods:
900
+ if (setCookies.length) {
901
+ res.setHeader('set-cookie', setCookies);
902
+ }
477
903
 
478
- - **`login(options?: LoginOptions): Promise<void | NativeFlowHandler>`**: Initiates login. In `native` mode returns a `NativeFlowHandler`.
479
- - **`register(options?: RegisterOptions): Promise<void | NativeFlowHandler>`**: Initiates registration.
480
- - **`entry(url?: string): Promise<Record<string, string>>`**: Processes an externally-initiated flow URL and returns parameters needed to resume the flow.
481
- - **`handleCallback(url?: string): Promise<void>`**: Processes the authorization callback.
482
- - **`refresh(): Promise<void>`**: Refreshes the session tokens.
483
- - **`revoke(): Promise<void>`**: Revokes the current session tokens.
484
- - **`logout(options?: LogoutOptions): Promise<void>`**: Logs the user out.
485
- - **`subscribeToEvent(eventType: string, callback: Function): { dispose(): void }`**: Subscribes to an authentication lifecycle event. Returns an object with a `dispose()` method to unsubscribe.
904
+ if (!response.body) {
905
+ res.end();
906
+ return;
907
+ }
486
908
 
487
- **Session state getters:**
909
+ await new Promise<void>((resolve, reject) => {
910
+ Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0])
911
+ .pipe(res)
912
+ .on('finish', resolve)
913
+ .on('error', reject);
914
+ });
915
+ }
488
916
 
489
- - **`isAuthenticated: Promise<boolean>`**: Resolves to `true` when a valid session exists.
490
- - **`isAuthenticatedSync: boolean`**: Synchronous check for an active session.
491
- - **`idTokenClaims: IdTokenClaims | null`**: Claims from the ID token.
492
- - **`accessToken: string | null`**: The current access token.
493
- - **`refreshToken: string | null`**: The current refresh token.
494
- - **`accessTokenExpired: boolean`**: `true` when the access token has expired.
495
- - **`accessTokenExpirationDate: number | null`**: Expiration timestamp (Unix seconds) of the access token.
917
+ // Mount a handler for a specific HTTP method and path
918
+ export function mount(method: 'get' | 'post', path: string, handler: (req: ExpressRequest) => Promise<Response>): void {
919
+ handlers[method](path, async (req, res, next) => {
920
+ try {
921
+ await applyResponse(await handler(req), res);
922
+ } catch (error) {
923
+ next(error);
924
+ }
925
+ });
926
+ }
496
927
 
497
- ---
928
+ // Convert Express request to Web Request
929
+ export function toWebRequest(req: ExpressRequest): Request {
930
+ const url = new URL(req.originalUrl, `${req.protocol}://${req.get('host')}`);
931
+ const headers = new Headers();
498
932
 
499
- ### `NativeFlowHandler`
933
+ for (const [key, value] of Object.entries(req.headers)) {
934
+ if (value === undefined) continue;
935
+ for (const v of Array.isArray(value) ? value : [value]) {
936
+ headers.append(key, v);
937
+ }
938
+ }
500
939
 
501
- Returned by `NativeFlow.login()` and `NativeFlow.register()` to drive the step-by-step native authentication UI.
940
+ const hasBody = req.method !== 'GET' && req.method !== 'HEAD';
502
941
 
503
- - **`startSession(sessionId?: string | null): Promise<LoginFlowState | void>`**: Starts a new authentication session or resumes an existing one when `sessionId` is provided.
504
- - **`submitForm(formId?: string, body?: Record<string, unknown>): Promise<LoginFlowState>`**: Submits a form step in the authentication flow.
505
- - **`finalizeSession(finalizeUrl: string): Promise<void>`**: Finalizes the session using the URL returned in `LoginFlowState.finalizeUrl`.
942
+ return new Request(url, {
943
+ method: req.method,
944
+ headers,
945
+ ...(hasBody ? { body: Readable.toWeb(req), duplex: 'half' } : {}),
946
+ } as RequestInit);
947
+ }
948
+ ```
506
949
 
507
- ---
950
+ For complete utility implementations, see the [backend example app](../../apps/backend) in this repository.
508
951
 
509
- ### `subscribeToEvent(eventType, callback)`
952
+ <a id="server-storages"></a>
510
953
 
511
- Subscribe to SDK lifecycle events.
954
+ By default, the server SDK stores:
955
+ - **Tokens** (access_token, refresh_token, id_token): encrypted in http-only cookies
956
+ - **Login state**: global in-memory Map
512
957
 
513
- **Parameters:**
958
+ #### Built-in session storages
514
959
 
515
- | Name | Type | Required | Description |
516
- | ----------- | -------------------------- | -------- | ---------------------------------------- |
517
- | `eventType` | `string` | ✅ | The event type to listen for. |
518
- | `callback` | `(eventData: any) => void` | ✅ | The handler called when the event fires. |
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.
519
962
 
520
- **Returns:** `{ dispose(): void }` — call `dispose()` to unsubscribe.
963
+ ```ts
964
+ // server.ts
965
+ import { createBaseServerSDK } from '@strivacity/sdk-core/server';
966
+ import { createSessionIdCookieStorage, createServerMemoryStorage } from '@strivacity/sdk-core/storages';
967
+ import { toWebRequest } from './utils';
968
+
969
+ const adapter = { toRequest: (req) => toWebRequest(req) };
970
+
971
+ const sdk = createBaseServerSDK(adapter, {
972
+ storage: createSessionIdCookieStorage(
973
+ adapter,
974
+ createServerMemoryStorage(),
975
+ {
976
+ // maxAge: 30 * 24 * 60 * 60 // Without maxAge this is a browser-session cookie that gets cleared when the browser closes
977
+ },
978
+ ),
979
+ });
980
+ ```
521
981
 
522
- ## Vulnerability Reporting
982
+ #### Custom storage
523
983
 
524
- The [Guidelines for responsible disclosure](https://www.strivacity.com/report-a-security-issue) details the procedure for disclosing security issues. Please do not report security vulnerabilities on the public issue tracker.
984
+ 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):
525
986
 
526
- ## License
987
+ ```ts
988
+ // storage.ts
989
+ import { createStorage } from 'unstorage';
990
+ import redisDriver from 'unstorage/drivers/redis';
991
+ import type { ServerStorage, SDKStorage } from '@strivacity/sdk-core/types';
992
+
993
+ const unstorageInstance = createStorage({
994
+ driver: redisDriver({ url: process.env.REDIS_URL }),
995
+ });
527
996
 
528
- @strivacity/sdk-core is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
997
+ // Custom session storage for tokens
998
+ export const sessionStorage: ServerStorage = {
999
+ async get(key) {
1000
+ return unstorageInstance.getItem<string>(key);
1001
+ },
1002
+ async set(key, value) {
1003
+ await unstorageInstance.setItem(key, value);
1004
+ },
1005
+ async delete(key) {
1006
+ await unstorageInstance.removeItem(key);
1007
+ },
1008
+ // Required for OIDC back-channel logout support.
1009
+ // Scans all stored sessions and removes those matching the logout token's sid or sub claim.
1010
+ async deleteByLogoutToken(logoutToken) {
1011
+ const keys = await unstorageInstance.getKeys();
1012
+ await Promise.all(
1013
+ keys.map(async (key) => {
1014
+ const raw = await unstorageInstance.getItem<string>(key);
1015
+ if (!raw) return;
1016
+ const session = JSON.parse(raw);
1017
+ if (
1018
+ (logoutToken.sid && session.sid === logoutToken.sid) ||
1019
+ (logoutToken.sub && session.sub === logoutToken.sub)
1020
+ ) {
1021
+ await unstorageInstance.removeItem(key);
1022
+ }
1023
+ }),
1024
+ );
1025
+ },
1026
+ };
1027
+
1028
+ // Custom state storage for OAuth2 state parameter
1029
+ export const stateStorage: SDKStorage = {
1030
+ async get(key) {
1031
+ return unstorageInstance.getItem<string>(key);
1032
+ },
1033
+ async set(key, value) {
1034
+ await unstorageInstance.setItem(key, value);
1035
+ },
1036
+ async delete(key) {
1037
+ await unstorageInstance.removeItem(key);
1038
+ },
1039
+ };
1040
+ ```
529
1041
 
530
- ## Contributing
1042
+ ```ts
1043
+ // server.ts
1044
+ import { createBaseServerSDK } from '@strivacity/sdk-core/server';
1045
+ import { toWebRequest } from './utils';
1046
+ import { sessionStorage, stateStorage } from './storage';
1047
+
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
+ );
1058
+ ```
531
1059
 
532
- Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).
1060
+ <a id="server-backchannel-logout"></a>
1061
+ ### Back-channel logout
1062
+
1063
+ 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)`.
533
1064
 
534
- ## Migrating to v3.0
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`.
535
1066
 
536
- ### Entry API Major Changes
1067
+ > **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.
537
1068
 
538
- Strivacity SDK's `entry()` API now returns a structured object instead of a plain string. Check the example above in the usage section for more details.
1069
+ Configure the **Back-channel logout URI** in your Strivacity application settings to:
1070
+
1071
+ ```
1072
+ https://your-app.example.com/api/auth/backchannel-logout
1073
+ ```
1074
+
1075
+ <a id="server-configuration-options"></a>
1076
+ ### Configuration Options
1077
+
1078
+ The server SDK accepts the same configuration as the client SDK, plus server-specific options:
1079
+
1080
+ ```ts
1081
+ {
1082
+ mode: 'redirect' | 'popup' | 'embedded' | 'native',
1083
+ issuer: string,
1084
+ clientId: string,
1085
+ redirectUri: string,
1086
+ scopes: string[],
1087
+ storage: ServerStorage, // Custom session storage
1088
+ stateStorage: SDKStorage, // Custom OAuth2 state storage
1089
+ serverSessionUri: string, // Optional - route login requests through your own server endpoint
1090
+ // ... other client SDK options
1091
+
1092
+ // server-specific options
1093
+ 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
1098
+ }
1099
+ ```
1100
+
1101
+ ---
1102
+
1103
+ ## Shared features
1104
+
1105
+ These features are available in both client and server SDK implementations.
1106
+
1107
+ ### Storages
1108
+
1109
+ The SDK provides several built-in storage drivers to help you persist session state and tokens. Choose the driver that fits your environment, target lifetime, and runtime constraints (Client vs. Server).
1110
+
1111
+ | Storage | Export | Persists Across | Environment | Default |
1112
+ | :--- | :--- | :--- | :--- | :--- |
1113
+ | **`localStorage`** | `createLocalStorage()` | Browser restarts | Client-side only | `storage` (client) |
1114
+ | **`sessionStorage`** | `createSessionStorage()` | Tab lifetime | Client-side only | `stateStorage` (client) |
1115
+ | **`IndexedDB`** | `createIndexedDBStorage()` | Browser restarts (larger quota) | Client-side only | - |
1116
+ | **`Cache API`** | `createCacheAPIStorage()` | Browser restarts (works in Service Workers too) | Client-side only | - |
1117
+ | **`Memory`** | `createMemoryStorage()` | Page / Process lifetime | Client-side & Server-side | - |
1118
+ | **`Worker`** | `createWorkerStorage(worker)` | Depends on the backing storage inside the Worker | Client-side & Server-side | - |
1119
+ | **`globalStorage`** | `createServerStateStorage()` | Process/Server runtime lifetime | Server-side only | `stateStorage` (server) |
1120
+ | **`cookieStorage`** | `createEncryptedCookieStorage()` | Cookie expiration / Browser restarts | Server-side only | `storage` (server) |
1121
+ | **`sessionIdStorage`** | `createSessionIdCookieStorage()` | Session cookie / Browser restarts | Server-side only | `storage` (server) |
1122
+
1123
+ #### Session Storages
1124
+
1125
+ Tokens and session data can be configured depending on your security and persistence needs. Swap out the default by passing any of the built-in factory functions - or any custom implementation of `SDKStorage` (`get`, `set`, `delete`) - as the `storage` option in `initFlow` (client) or when creating the server SDK.
1126
+
1127
+ #### State storages
1128
+
1129
+ The SDK stores login state parameters separately from session tokens. Override `stateStorage` independently from `storage`:
1130
+
1131
+ ```ts
1132
+ import { initFlow } from '@strivacity/sdk-core';
1133
+ import { createSessionStorage } from '@strivacity/sdk-core/storages';
1134
+
1135
+ const sdk = initFlow({
1136
+ mode: 'redirect',
1137
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
1138
+ clientId: 'YOUR_CLIENT_ID',
1139
+ redirectUri: 'https://your-app.example.com/callback',
1140
+ scopes: ['openid', 'profile', 'email'],
1141
+ storage: createLocalStorage(), // session tokens in localStorage
1142
+ stateStorage: createSessionStorage(), // OAuth2 state in sessionStorage (cleared when tab closes)
1143
+ });
1144
+ ```
1145
+
1146
+ **Why separate state storage?**
1147
+
1148
+ Login state parameters are temporary - they are only needed between starting the flow and completing the callback. Using `sessionStorage` (client-side) or `globalStorage` (server-side) for state improves security by ensuring these transient values do not persist across browser sessions, while tokens can safely remain in `localStorage` for persistent authentication.
1149
+
1150
+ Any storage implementing `SDKStorage` can be used for state storage.
1151
+
1152
+ #### Advanced Storage Configurations
1153
+
1154
+ Beyond standard drivers, the SDK supports isolated runtime environments and custom storage contracts.
1155
+
1156
+ ##### Session-id Cookie Storage
1157
+
1158
+ Server-side only. `createSessionIdCookieStorage(adapter, storage, options?)` puts only a small, random session-id cookie on the client and delegates the actual session payload to the `storage` you provide - keeping large sessions out of cookies entirely, and enabling [back-channel logout](#server-backchannel-logout) as long as `storage` implements `deleteByLogoutToken`:
1159
+
1160
+ ```ts
1161
+ // server.ts
1162
+ import { createBaseServerSDK } from '@strivacity/sdk-core/server';
1163
+ import { createSessionIdCookieStorage, createServerMemoryStorage } from '@strivacity/sdk-core/storages';
1164
+ import { toWebRequest } from './utils';
1165
+
1166
+ const adapter = { toRequest: (req) => toWebRequest(req) };
1167
+
1168
+ const sdk = createBaseServerSDK(adapter, {
1169
+ // ...
1170
+ storage: createSessionIdCookieStorage(adapter, createServerMemoryStorage()),
1171
+ });
1172
+ ```
1173
+
1174
+ > See [Built-in session storages](#server-storages) for more details, including using an external store (e.g. Redis) instead of `createServerMemoryStorage()`.
1175
+
1176
+ ##### Worker Storage
1177
+
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:
1179
+
1180
+ ```ts
1181
+ // storage.worker.ts
1182
+ import { handleWorkerStorageRequests, createIndexedDBStorage } from '@strivacity/sdk-core/storages';
1183
+
1184
+ handleWorkerStorageRequests(createIndexedDBStorage());
1185
+ ```
1186
+
1187
+ ```ts
1188
+ // sdk.ts
1189
+ import { initFlow } from '@strivacity/sdk-core';
1190
+ import { createWorkerStorage } from '@strivacity/sdk-core/storages';
1191
+
1192
+ const worker = new Worker(new URL('./storage.worker.ts', import.meta.url), { type: 'module' });
1193
+
1194
+ const sdk = initFlow({
1195
+ // ...
1196
+ storage: createWorkerStorage(worker),
1197
+ });
1198
+ ```
1199
+
1200
+ ---
1201
+
1202
+ ### Session state
1203
+
1204
+ All flow instances expose the same set of getters:
1205
+
1206
+ ```ts
1207
+ // Async - waits for init, optionally auto-refreshes an expired token
1208
+ const isAuthenticated: boolean = await sdk.isAuthenticated;
1209
+
1210
+ // Sync - true only if a non-expired session is already in memory
1211
+ const isAuthenticated: boolean = sdk.isAuthenticatedSync;
1212
+
1213
+ // Token values (available synchronously after init)
1214
+ const accessToken: string | null = sdk.accessToken;
1215
+ const refreshToken: string | null = sdk.refreshToken;
1216
+ const idTokenClaims: IdTokenClaims | null = sdk.idTokenClaims;
1217
+
1218
+ // Token expiration info (available synchronously after init)
1219
+ const expired: boolean = sdk.accessTokenExpired;
1220
+ const expiresAt: number | null = sdk.accessTokenExpirationDate; // Unix seconds
1221
+
1222
+ // Session metadata (available in embedded and native modes)
1223
+ const sessionId: string | null = sdk.sessionId; // current session identifier
1224
+ const shortAppId: string | null = sdk.shortAppId; // short application identifier
1225
+ const language: string = sdk.language; // current UI language code
1226
+
1227
+ // Verify authentication status (auto-refreshes an expired token if a refresh token is available)
1228
+ const ok: boolean = await sdk.checkAuthentication();
1229
+ // Verify authentication status without attempting a refresh
1230
+ const ok: boolean = await sdk.checkAuthentication({ autoRefresh: false });
1231
+ ```
1232
+
1233
+ ---
1234
+
1235
+ ### SDK events
1236
+
1237
+ Subscribe to authentication lifecycle events via `subscribeToEvent` (one specific event) or `subscribeToAllEvents` (all events). Both return a `{ dispose() }` handle - call `dispose()` to unsubscribe.
1238
+
1239
+ | Event | Payload | When it fires |
1240
+ | -------------------- | --------------------------------------- | ------------------------------------------------------------------------- |
1241
+ | `init` | - | SDK has finished initializing |
1242
+ | `sessionLoaded` | `{ idToken, accessToken, refreshToken, claims }` | An existing session was read from storage on startup |
1243
+ | `sessionUpdated` | `{ idToken, accessToken, refreshToken, claims }` | The session was updated (e.g., after a token refresh) |
1244
+ | `sessionCleared` | - | The session was cleared (e.g., after logout or token revocation) |
1245
+ | `loginInitiated` | - | A login or registration redirect / popup has started |
1246
+ | `flowInitiated` | `{ sessionId, shortAppId, language }` | A native or embedded flow has started |
1247
+ | `loggedIn` | `{ idToken, accessToken, refreshToken, claims }` | Tokens were received and stored after a successful login |
1248
+ | `logoutInitiated` | `{ idToken, claims }` | Logout was initiated, before the redirect to the IDP end-session endpoint |
1249
+ | `tokenRefreshed` | `{ idToken, accessToken, refreshToken, claims }` | Access token was silently refreshed |
1250
+ | `tokenRefreshFailed` | `{ refreshToken }` | A token refresh attempt failed (refresh token may be expired) |
1251
+ | `accessTokenExpired` | `{ accessToken, refreshToken }` | The stored access token has passed its expiration time |
1252
+ | `tokenRevoked` | `{ token, tokenTypeHint }` | A token was successfully revoked at the authorization server |
1253
+ | `tokenRevokeFailed` | `{ token, tokenTypeHint }` | A token revocation attempt failed |
1254
+
1255
+ ```ts
1256
+ const sub = sdk.subscribeToEvent('tokenRefreshed', ({ accessToken }) => {
1257
+ console.log('Token refreshed:', accessToken);
1258
+ });
1259
+
1260
+ // Unsubscribe
1261
+ sub.dispose();
1262
+
1263
+ // Subscribe to all events with a single callback
1264
+ const sub = sdk.subscribeToAllEvents((...args) => {
1265
+ console.log('SDK event:', args);
1266
+ });
1267
+
1268
+ sub.dispose();
1269
+ ```
1270
+
1271
+ ---
1272
+
1273
+ ### Logging
1274
+
1275
+ #### Built-in logger
1276
+
1277
+ Call `createDefaultLogging()` to get a console logger with per-request correlation ID support:
1278
+
1279
+ ```ts
1280
+ import { initFlow } from '@strivacity/sdk-core';
1281
+ import { createDefaultLogging } from '@strivacity/sdk-core/utils';
1282
+
1283
+ const sdk = initFlow({
1284
+ // ...
1285
+ logging: createDefaultLogging(),
1286
+ });
1287
+ ```
1288
+
1289
+ #### Custom logger
1290
+
1291
+ Use a custom logger when you need to integrate SDK logs with your application's logging infrastructure (e.g., Sentry, Datadog, Winston) or when you want to filter, format, or route logs differently than the console.
1292
+
1293
+ Create a factory function that returns an object implementing the `SDKLogging` interface:
1294
+
1295
+ ```ts
1296
+ import type { SDKLogging } from '@strivacity/sdk-core/types';
1297
+
1298
+ export function createMyLogger(): SDKLogging {
1299
+ const logger: SDKLogging = {
1300
+ /** Set by the SDK per request; use to correlate related log lines */
1301
+ xEventId: undefined,
1302
+
1303
+ debug(message: string): void {
1304
+ const msg = logger.xEventId ? `[${logger.xEventId}] ${message}` : message;
1305
+ console.debug(msg);
1306
+ },
1307
+
1308
+ info(message: string): void {
1309
+ const msg = logger.xEventId ? `[${logger.xEventId}] ${message}` : message;
1310
+ console.info(msg);
1311
+ },
1312
+
1313
+ warn(message: string): void {
1314
+ const msg = logger.xEventId ? `[${logger.xEventId}] ${message}` : message;
1315
+ console.warn(msg);
1316
+ },
1317
+
1318
+ error(message: string, error?: unknown): void {
1319
+ const msg = logger.xEventId ? `[${logger.xEventId}] ${message}` : message;
1320
+ console.error(msg, error);
1321
+ },
1322
+ };
1323
+
1324
+ return logger;
1325
+ }
1326
+
1327
+ const sdk = initFlow({
1328
+ // ...
1329
+ logging: createMyLogger(),
1330
+ });
1331
+ ```
1332
+
1333
+ ---
1334
+
1335
+ ### HTTP client
1336
+
1337
+ The SDK uses `fetch` for all requests. Replace it by implementing the `SDKHttpClient` interface and passing an instance via `httpClient` (`request` and `sendTokenRequest` are both required, so it's easiest to wrap the built-in `createHttpClient()` and override just what you need). Useful for attaching custom headers to every request, routing traffic through a proxy, or using a platform-specific transport (e.g. Capacitor's `CapacitorHttp`).
1338
+
1339
+ ```ts
1340
+ import type { SDKHttpClient, HttpClientResponse } from '@strivacity/sdk-core/types';
1341
+ import { initFlow } from '@strivacity/sdk-core';
1342
+ import { createHttpClient } from '@strivacity/sdk-core/utils';
1343
+
1344
+ function createCustomHttpClient(): SDKHttpClient {
1345
+ const base = createHttpClient();
1346
+
1347
+ return {
1348
+ ...base,
1349
+ async request<T>(url: string | URL, options?: RequestInit): Promise<HttpClientResponse<T>> {
1350
+ return base.request<T>(url, {
1351
+ ...options,
1352
+ headers: {
1353
+ 'x-sty-app-id': 'my-app',
1354
+ ...(options?.headers as Record<string, string>),
1355
+ },
1356
+ });
1357
+ },
1358
+ };
1359
+ }
1360
+
1361
+ const sdk = initFlow({
1362
+ // ...
1363
+ httpClient: createCustomHttpClient(),
1364
+ });
1365
+ ```
1366
+
1367
+ > **CORS note:** custom request headers must be explicitly listed in the Strivacity cluster's `Access-Control-Allow-Headers` configuration - otherwise the browser blocks the preflight `OPTIONS` request.
1368
+
1369
+ ---
1370
+
1371
+ ## Advanced
1372
+
1373
+ Advanced SDK customization patterns for specialized use cases.
1374
+
1375
+ ### Custom flow
1376
+
1377
+ For cases where none of the built-in modes fit-for example, when all authentication traffic must go through your own backend server in a specific way - you can build a completely custom flow on top of `createBaseFlow`.
1378
+
1379
+ `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
+
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.
1382
+
1383
+ ```ts
1384
+ import type { SDKInitConfig, SDKOptions } from '@strivacity/sdk-core/types';
1385
+ import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
1386
+ import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
1387
+
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>,
1394
+ // Add your own methods here
1395
+ }
1396
+
1397
+ export function createCustomFlow(initConfig: SDKInitConfig): CustomFlow {
1398
+ const state = getDefaultFlowState();
1399
+ const options = getSDKOptions<SDKOptions>(state, initConfig);
1400
+ const base = createBaseFlow(state, options);
1401
+
1402
+ async function login(params: Record<string, unknown> = {}): Promise<void> {
1403
+ await base.init();
1404
+
1405
+ const response = await fetch('/api/auth/login', {
1406
+ method: 'POST',
1407
+ credentials: 'include',
1408
+ headers: { 'Content-Type': 'application/json' },
1409
+ body: JSON.stringify(params),
1410
+ });
1411
+
1412
+ if (!response.ok) {
1413
+ throw new Error('Login not started');
1414
+ }
1415
+ }
1416
+
1417
+ async function refresh(): Promise<void> {
1418
+ const response = await fetch('/api/auth/refresh', {
1419
+ method: 'POST',
1420
+ credentials: 'include',
1421
+ });
1422
+
1423
+ if (!response.ok) {
1424
+ await base.cleanupSession();
1425
+ base.dispatchEvent('tokenRefreshFailed', [{}]);
1426
+ return;
1427
+ }
1428
+
1429
+ const session = await response.json();
1430
+ await base.updateSession(session);
1431
+ base.dispatchEvent('tokenRefreshed', [{ accessToken: session.access_token, claims: session.claims }]);
1432
+ }
1433
+
1434
+ async function revoke(): Promise<void> {
1435
+ const response = await fetch('/api/auth/revoke', {
1436
+ method: 'POST',
1437
+ credentials: 'include',
1438
+ });
1439
+
1440
+ if (!response.ok) {
1441
+ await base.cleanupSession();
1442
+ base.dispatchEvent('tokenRevokeFailed', [{}]);
1443
+ return;
1444
+ }
1445
+
1446
+ const session = await response.json();
1447
+ await base.updateSession(session);
1448
+ base.dispatchEvent('tokenRefreshed', [{ accessToken: session.access_token, claims: session.claims }]);
1449
+ }
1450
+
1451
+ async function logout(): Promise<void> {
1452
+ await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
1453
+ await base.cleanupSession();
1454
+ }
1455
+
1456
+ return {
1457
+ init: base.init,
1458
+ login,
1459
+ refresh,
1460
+ revoke,
1461
+ logout,
1462
+ // Add your own methods here
1463
+ };
1464
+ }
1465
+ ```
1466
+
1467
+ #### Usage:
1468
+
1469
+ You can call the factory directly:
1470
+
1471
+ ```ts
1472
+ import { createCustomFlow } from './custom-flow';
1473
+
1474
+ const sdk = createCustomFlow({
1475
+ mode: 'native',
1476
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
1477
+ clientId: 'YOUR_CLIENT_ID',
1478
+ redirectUri: 'https://your-app.example.com/callback',
1479
+ scopes: ['openid', 'profile'],
1480
+ });
1481
+
1482
+ await sdk.login();
1483
+ ```
1484
+
1485
+ Or wire it through `initFlow` via the `factory` option:
1486
+
1487
+ ```ts
1488
+ import { initFlow } from '@strivacity/sdk-core';
1489
+ import { createCustomFlow } from './custom-flow';
1490
+
1491
+ const sdk = initFlow<CustomFlow>({
1492
+ mode: 'native',
1493
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
1494
+ clientId: 'YOUR_CLIENT_ID',
1495
+ redirectUri: 'https://your-app.example.com/callback',
1496
+ scopes: ['openid', 'profile'],
1497
+ factory: createCustomFlow,
1498
+ });
1499
+
1500
+ await sdk.login();
1501
+ ```
1502
+
1503
+ ---
1504
+
1505
+ ### Error handling
1506
+
1507
+ The SDK exports specialized error classes to help you handle different failure scenarios. All SDK errors extend the base `Error` class and include additional context.
1508
+
1509
+ #### Unsupported flow error
1510
+
1511
+ `initFlow()` throws `UnsupportedFlowError` when `mode` is missing or isn't one of `'redirect' | 'popup' | 'embedded' | 'native'`. TypeScript consumers get this caught at compile time, but it can still happen at runtime for plain JS consumers or when `mode` comes from an untyped source (e.g. an environment variable or CMS config):
1512
+
1513
+ ```ts
1514
+ import { initFlow } from '@strivacity/sdk-core';
1515
+ import { UnsupportedFlowError } from '@strivacity/sdk-core/utils';
1516
+
1517
+ try {
1518
+ const sdk = initFlow({
1519
+ mode: mode as never, // e.g. loaded from an environment variable - could be an invalid value
1520
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
1521
+ clientId: 'YOUR_CLIENT_ID',
1522
+ redirectUri: 'https://your-app.example.com/callback',
1523
+ });
1524
+ } catch (error) {
1525
+ if (error instanceof UnsupportedFlowError) {
1526
+ console.error(`Invalid mode: "${mode}". Must be one of: redirect, popup, embedded, native`);
1527
+ }
1528
+ }
1529
+ ```
1530
+
1531
+ #### User interaction errors
1532
+
1533
+ These errors occur when the user cancels or blocks the authentication flow:
1534
+
1535
+ ```ts
1536
+ import { PopupBlockedError, PopupClosedError } from '@strivacity/sdk-core/utils';
1537
+
1538
+ try {
1539
+ await sdk.login();
1540
+ } catch (error) {
1541
+ if (error instanceof PopupBlockedError) {
1542
+ // Browser blocked the popup - show instructions to allow popups
1543
+ console.error('Please allow popups for this site');
1544
+ } else if (error instanceof PopupClosedError) {
1545
+ // User closed the popup before completing login
1546
+ console.error('Login cancelled by user');
1547
+ }
1548
+ }
1549
+ ```
1550
+
1551
+ #### SDK state errors
1552
+
1553
+ The SDK may throw `SessionExpiredError` when attempting operations with an expired session:
1554
+
1555
+ ```ts
1556
+ import { SessionExpiredError } from '@strivacity/sdk-core/utils';
1557
+
1558
+ // Example: Session expired during a native flow
1559
+ try {
1560
+ const state = await sdk.startSession();
1561
+ // ... user fills in the form ...
1562
+ const nextState = await sdk.submitForm(state.screen, formData);
1563
+ } catch (error) {
1564
+ if (error instanceof SessionExpiredError) {
1565
+ // Session expired during the flow - try to reload the page
1566
+ }
1567
+ }
1568
+ ```
1569
+
1570
+ #### Network and server errors
1571
+
1572
+ These errors have a `recoverable` flag indicating whether retrying might succeed:
1573
+
1574
+ ```ts
1575
+ import { NetworkError, ServerError, OidcError, ProtocolError } from '@strivacity/sdk-core/utils';
1576
+
1577
+ try {
1578
+ await sdk.login();
1579
+ } catch (error) {
1580
+ if (error instanceof NetworkError) {
1581
+ // Network request failed (offline, timeout, etc.)
1582
+ // error.recoverable === true - retry might work
1583
+ console.error('Network error - retrying...', error.message);
1584
+ } else if (error instanceof ServerError) {
1585
+ // Server returned 5xx error
1586
+ // error.recoverable === true - temporary server issue
1587
+ // error.status contains HTTP status code
1588
+ console.error(`Server error ${error.status}:`, error.message);
1589
+ } else if (error instanceof OidcError) {
1590
+ // OIDC protocol error from authorization server
1591
+ // error.recoverable === false - don't retry
1592
+ // error.error contains OIDC error code
1593
+ // error.errorDescription contains human-readable description
1594
+ console.error(`OIDC error ${error.error}:`, error.errorDescription);
1595
+ } else if (error instanceof ProtocolError) {
1596
+ // Protocol violation or invalid response
1597
+ // error.recoverable === false
1598
+ console.error('Protocol error:', error.message);
1599
+ }
1600
+ }
1601
+ ```
1602
+
1603
+ #### Error categories
1604
+
1605
+ All network/server errors include a `category` property for easier error handling:
1606
+
1607
+ ```ts
1608
+ try {
1609
+ await sdk.refresh();
1610
+ } catch (error) {
1611
+ if (error.category === 'Network') {
1612
+ // Retry with exponential backoff
1613
+ } else if (error.category === 'Server') {
1614
+ // Show maintenance message
1615
+ } else if (error.category === 'Oidc') {
1616
+ // Invalid configuration or expired session
1617
+ } else if (error.category === 'Protocol') {
1618
+ // Invalid response from server
1619
+ }
1620
+ }
1621
+ ```
1622
+
1623
+ **Error categories:**
1624
+ - `Network` - Network connectivity issues (recoverable)
1625
+ - `Server` - Server errors 5xx (recoverable)
1626
+ - `Oidc` - OIDC protocol errors from authorization server (not recoverable)
1627
+ - `Protocol` - Protocol violations or invalid responses (not recoverable)
1628
+ - `ConfigurationError` - Invalid SDK configuration (not recoverable)
1629
+ - `Internal` - Unexpected internal SDK errors (not recoverable)
1630
+
1631
+ ---
1632
+
1633
+ ### Utility functions
1634
+
1635
+ The SDK exports several utility functions for advanced use cases. These are primarily used internally but can be useful for custom integrations.
1636
+
1637
+ #### Base64URL encoding
1638
+
1639
+ Base64URL is a URL-safe variant of Base64 encoding used in OIDC and JWT:
1640
+
1641
+ ```ts
1642
+ import { encodeBase64URL, decodeBase64URL } from '@strivacity/sdk-core/utils';
1643
+
1644
+ // Encode ArrayBuffer to Base64URL string
1645
+ const buffer = new TextEncoder().encode('Hello, World!');
1646
+ const encoded = encodeBase64URL(buffer.buffer);
1647
+ console.log(encoded); // 'SGVsbG8sIFdvcmxkIQ'
1648
+
1649
+ // Decode Base64URL string to ArrayBuffer
1650
+ const decoded = decodeBase64URL(encoded);
1651
+ const text = new TextDecoder().decode(decoded);
1652
+ console.log(text); // 'Hello, World!'
1653
+ ```
1654
+
1655
+ #### String encryption/decryption
1656
+
1657
+ AES-GCM encryption with HKDF key derivation for encrypting sensitive data:
1658
+
1659
+ ```ts
1660
+ import { encryptString, decryptString } from '@strivacity/sdk-core/utils';
1661
+
1662
+ const secret = 'your-encryption-secret';
1663
+ const context = 'user-session'; // binds key to specific purpose
1664
+
1665
+ // Encrypt a string
1666
+ const plaintext = 'sensitive data';
1667
+ const encrypted = await encryptString(plaintext, secret, context);
1668
+ console.log(encrypted); // Base64URL-encoded: salt(16) + iv(12) + ciphertext
1669
+
1670
+ // Decrypt a string
1671
+ const decrypted = await decryptString(encrypted, secret, context);
1672
+ console.log(decrypted); // 'sensitive data' or null if decryption fails
1673
+ ```
1674
+
1675
+ The encryption format is: `Base64URL(salt || iv || ciphertext)` where:
1676
+ - `salt` (16 bytes) - random salt for HKDF key derivation
1677
+ - `iv` (12 bytes) - random initialization vector for AES-GCM
1678
+ - `ciphertext` - encrypted data
1679
+
1680
+ > **Note:** This is the same encryption used internally by the Server SDK for http-only cookies.
1681
+
1682
+ #### JWT decoding
1683
+
1684
+ Decode JWT tokens without verification (useful for debugging or extracting claims):
1685
+
1686
+ ```ts
1687
+ import { decodeJwt } from '@strivacity/sdk-core/utils';
1688
+
1689
+ const idToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
1690
+ const claims = decodeJwt<{ sub: string; email: string }>(idToken);
1691
+
1692
+ console.log(claims.sub); // user ID
1693
+ console.log(claims.email); // user email
1694
+ ```
1695
+
1696
+ > **Warning:** `decodeJwt` does NOT verify the signature. Use it only for debugging or when the token has already been verified by the SDK.
1697
+
1698
+ #### JWT verification
1699
+
1700
+ Verify and decode JWT tokens using RS256 signature verification with JWKS:
1701
+
1702
+ ```ts
1703
+ import type { SDKOptions } from '@strivacity/sdk-core/types';
1704
+ import { verifyJwt } from '@strivacity/sdk-core/utils';
1705
+
1706
+ // Verify an access token or ID token from an external source
1707
+ const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
1708
+ const jwksUri = 'https://<YOUR_TENANT_DOMAIN>/.well-known/jwks.json';
1709
+
1710
+ // You need SDK options for HTTP client and caching
1711
+ const options: SDKOptions = {
1712
+ mode: 'redirect',
1713
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
1714
+ clientId: 'YOUR_CLIENT_ID',
1715
+ redirectUri: 'https://your-app.example.com/callback',
1716
+ scopes: ['openid', 'profile', 'email'],
1717
+ };
1718
+
1719
+ try {
1720
+ const claims = await verifyJwt<{ sub: string; email: string }>({
1721
+ token,
1722
+ jwksUri,
1723
+ options,
1724
+ });
1725
+
1726
+ console.log('Token verified successfully');
1727
+ console.log(claims.sub); // user ID
1728
+ console.log(claims.email); // user email
1729
+ } catch (error) {
1730
+ console.error('Token verification failed:', error.message);
1731
+ // Throws ProtocolError if signature is invalid or token is malformed
1732
+ }
1733
+ ```
1734
+
1735
+ **When to use `verifyJwt`:**
1736
+ - Verifying tokens received from external sources (e.g., webhooks, API callbacks)
1737
+ - Server-side token verification when building custom server flows
1738
+
1739
+ > **Note:** The SDK automatically verifies ID tokens during login and refresh flows. You only need `verifyJwt` when handling tokens from external sources.
1740
+
1741
+ #### PKCE code verifier generation
1742
+
1743
+ Generate a cryptographically secure PKCE code verifier:
1744
+
1745
+ ```ts
1746
+ import { generateCodeVerifier, generateRandomHex } from '@strivacity/sdk-core/utils';
1747
+
1748
+ // Generate PKCE code verifier (43-128 characters, base64url-encoded)
1749
+ const codeVerifier = generateCodeVerifier();
1750
+ console.log(codeVerifier); // e.g., 'a3K8Bx...' (128 chars)
1751
+
1752
+ // Generate random hex string (useful for state parameters)
1753
+ const state = generateRandomHex(16); // 16 bytes = 32 hex chars
1754
+ console.log(state); // e.g., '5f3a8b2c4d1e9f7a6b8c3d2e1f4a5b6c'
1755
+ ```
1756
+
1757
+ #### Script injection
1758
+
1759
+ Dynamically load external scripts (used internally for loading web components):
1760
+
1761
+ ```ts
1762
+ import { injectScript } from '@strivacity/sdk-core/utils';
1763
+
1764
+ // Inject a script tag with the given ID and source URL
1765
+ injectScript('my-script', 'https://example.com/script.js');
1766
+
1767
+ // If a script with the same ID already exists, it won't be injected again
1768
+ ```
1769
+
1770
+ ---
1771
+
1772
+ ## Configuration reference
1773
+
1774
+ | Option | Type | Required | Default | Description |
1775
+ | ------------------------ | ---------------------------------------------------- | -------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
1776
+ | `mode` | `'redirect' \| 'popup' \| 'embedded' \| 'native'` | Yes | - | Authentication flow mode |
1777
+ | `issuer` | `string` | Yes | - | OIDC issuer URL of your Strivacity tenant |
1778
+ | `clientId` | `string` | Yes | - | OAuth2 public client ID |
1779
+ | `redirectUri` | `string` | Yes | - | OAuth2 redirect URI (must match your application configuration) |
1780
+ | `scopes` | `string[]` | No | `['openid']` | Requested OIDC scopes |
1781
+ | `responseType` | `'code'` | No | `'code'` | OAuth2 response type |
1782
+ | `responseMode` | `'query' \| 'fragment'` | No | `'query'` | OAuth2 response mode |
1783
+ | `storage` | `SDKStorage` | No | `localStorage` | [Session storage](#storages) |
1784
+ | `stateStorage` | `SDKStorage` | No | same as `storage` | OAuth2 PKCE / state storage |
1785
+ | `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
+ | `lazyLoad` | `boolean` | No | `false` | When `true`, defers initialization until the first method call and returns the instance synchronously |
1788
+ | `serverSessionUri` | `string` | No | - | Routes login requests through your server and enables server-managed sessions; see [Server-side session management](#server-side-session-management) |
1789
+ | `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 |
1790
+ | `logging` | `SDKLogging` | No | - | Logging adapter; see [Logging](#logging) |
1791
+ | `httpClient` | `SDKHttpClient` | No | fetch | Custom HTTP client adapter; see [HTTP client](#http-client) |
1792
+ | `discoveryDocumentCacheTTL` | `number` | No | `3600` | OIDC metadata cache duration in seconds (1 hour by default) |
1793
+ | `jwksCacheTTL` | `number` | No | `600` | JWKS (JSON Web Key Set) cache duration in seconds (10 minutes by default) |
1794
+ | `urlHandler` | `(url, params?) => Promise<unknown>` | No | - | Custom handler for URL redirects (e.g. to integrate with a router instead of `window.location`) |
1795
+ | `callbackHandler` | `(url, responseMode?) => Promise<unknown>` | No | - | Custom handler for processing the authorization server callback URL |
1796
+ | `factory` | `(options: SDKInitConfig) => unknown` | No | - | Custom flow factory; when set, `initFlow` calls it with the options instead of the built-in flows - see [Custom flow](#custom-flow) |
1797
+
1798
+ ---
1799
+
1800
+ ### Configuration examples
1801
+
1802
+ #### urlHandler
1803
+
1804
+ Override the default redirect behavior (which uses `window.location`) to integrate with client-side routers or custom navigation logic.
1805
+
1806
+ ```ts
1807
+ // Capacitor InAppBrowser example
1808
+ import { Capacitor } from '@capacitor/core';
1809
+ import { InAppBrowser } from '@capgo/inappbrowser';
1810
+
1811
+ const sdk = initFlow({
1812
+ // ... other config
1813
+ urlHandler: async (url) => {
1814
+ if (Capacitor.getPlatform() === 'web') {
1815
+ // On web: use standard window.location redirect
1816
+ window.location.href = url;
1817
+ } else {
1818
+ // On native (iOS/Android): open in InAppBrowser
1819
+ await InAppBrowser.openInWebView({
1820
+ url,
1821
+ options: { /* Configure InAppBrowser options*/ },
1822
+ });
1823
+ }
1824
+ },
1825
+ });
1826
+ ```
1827
+
1828
+ #### callbackHandler
1829
+
1830
+ Override the default callback URL parsing to handle custom redirect flows or extract the callback URL from a non-standard location.
1831
+
1832
+ ```ts
1833
+ // Capacitor InAppBrowser callback handling
1834
+ import { Capacitor } from '@capacitor/core';
1835
+ import { InAppBrowser } from '@capgo/inappbrowser';
1836
+ import type { PluginListenerHandle } from '@capacitor/core';
1837
+
1838
+ const sdk = initFlow({
1839
+ // ... other config
1840
+ callbackHandler: async (url, responseMode) => {
1841
+ if (Capacitor.getPlatform() === 'web') {
1842
+ // On web: use default callback handling
1843
+ const urlObj = new URL(window.location.href);
1844
+ const params = responseMode === 'fragment'
1845
+ ? new URLSearchParams(urlObj.hash.slice(1))
1846
+ : urlObj.searchParams;
1847
+
1848
+ return {
1849
+ code: params.get('code'),
1850
+ state: params.get('state'),
1851
+ error: params.get('error'),
1852
+ error_description: params.get('error_description'),
1853
+ };
1854
+ }
1855
+
1856
+ // On native: listen for navigation events in InAppBrowser
1857
+ return new Promise(async (resolve, reject) => {
1858
+ let navigationListener: PluginListenerHandle | null = null;
1859
+ let finishListener: PluginListenerHandle | null = null;
1860
+ let userCancelled = true;
1861
+
1862
+ const cleanupListeners = async () => {
1863
+ if (navigationListener) {
1864
+ await navigationListener.remove();
1865
+ navigationListener = null;
1866
+ }
1867
+ if (finishListener) {
1868
+ await finishListener.remove();
1869
+ finishListener = null;
1870
+ }
1871
+ };
1872
+
1873
+ try {
1874
+ // Listen for page navigation in InAppBrowser
1875
+ navigationListener = await InAppBrowser.addListener(
1876
+ 'browserPageNavigationCompleted',
1877
+ async (event) => {
1878
+ const navigatedUrl = event.url;
1879
+
1880
+ // Check if the navigated URL matches our callback URL
1881
+ if (navigatedUrl && navigatedUrl.startsWith(url)) {
1882
+ try {
1883
+ const urlInstance = new URL(navigatedUrl);
1884
+ const dataString = responseMode === 'query'
1885
+ ? urlInstance.search
1886
+ : urlInstance.hash;
1887
+ const params = Object.fromEntries(
1888
+ new URLSearchParams(dataString.slice(1))
1889
+ );
1890
+
1891
+ userCancelled = false;
1892
+ await InAppBrowser.close();
1893
+ resolve(params);
1894
+ } catch (error) {
1895
+ await InAppBrowser.close();
1896
+ reject(error);
1897
+ }
1898
+ }
1899
+ }
1900
+ );
1901
+
1902
+ // Listen for browser close event
1903
+ finishListener = await InAppBrowser.addListener('browserClosed', async () => {
1904
+ await cleanupListeners();
1905
+
1906
+ if (userCancelled) {
1907
+ reject(new Error('InAppBrowser flow cancelled by user.'));
1908
+ }
1909
+ });
1910
+ } catch (error) {
1911
+ await cleanupListeners();
1912
+ reject(error);
1913
+ }
1914
+ });
1915
+ },
1916
+ });
1917
+ ```
1918
+
1919
+ ---
1920
+
1921
+ ### Caching
1922
+
1923
+ The SDK automatically caches OIDC metadata and JWKS (JSON Web Key Set) to reduce network requests and improve performance.
1924
+
1925
+ #### Metadata caching
1926
+
1927
+ OIDC metadata (authorization endpoint, token endpoint, etc.) is fetched from the `issuer` URL during SDK initialization and cached for **1 hour**.
1928
+
1929
+ The cache is shared across all SDK instances with the same `issuer` URL and stored in memory for the lifetime of the application.
1930
+
1931
+ #### JWKS caching
1932
+
1933
+ The JSON Web Key Set (used for ID token signature verification) is fetched from the `jwks_uri` endpoint and cached for **10 minutes**.
1934
+
1935
+ ```ts
1936
+ // First token verification: fetches JWKS from the IDP
1937
+ await sdk.handleCallback(); // Verifies ID token signature
1938
+
1939
+ // Within 10 minutes: reuses cached JWKS (no network request)
1940
+ await sdk.refresh(); // Verifies new ID token signature using cached keys
1941
+
1942
+ // After 10 minutes: refetches JWKS from the IDP
1943
+ await sdk.refresh(); // Fetches fresh keys if cache expired
1944
+ ```
1945
+
1946
+ The JWKS cache is also shared across all SDK instances with the same `jwks_uri` and stored in memory.
1947
+
1948
+ #### Configuring cache durations
1949
+
1950
+ You can customize the cache durations by passing `discoveryDocumentCacheTTL` and `jwksCacheTTL` options (in seconds) during SDK initialization:
1951
+
1952
+ ```ts
1953
+ const sdk = initFlow({
1954
+ mode: 'redirect',
1955
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
1956
+ clientId: 'YOUR_CLIENT_ID',
1957
+ redirectUri: 'https://your-app.example.com/callback',
1958
+ scopes: ['openid', 'profile', 'email'],
1959
+ discoveryDocumentCacheTTL: 7200, // Cache openID configuration for 2 hours instead of 1 hour
1960
+ jwksCacheTTL: 1800, // Cache JWKS for 30 minutes instead of 10 minutes
1961
+ });
1962
+ ```
1963
+
1964
+ ---
1965
+
1966
+ ## Migration guide
1967
+
1968
+ ### Migrating to v4.0
1969
+
1970
+ v4 replaces the SDK's class-based flow architecture with function-based architecture, and adds first-class support for server-managed (BFF) sessions. `initFlow` itself and all built-in `redirect`/`popup`/`embedded`/`native` flow methods (`login`, `register`, `handleCallback`, `refresh`, `revoke`, `logout`, `subscribeToEvent`, ...) are unchanged - only apps that used `mode: 'custom'` or drove `native` mode through the old `NativeFlowHandler` need to update their code.
1971
+
1972
+ #### Class-based flows replaced by functions
1973
+
1974
+ In v3, flows were classes (`RedirectFlow`, `PopupFlow`, `NativeFlow`, `EmbeddedFlow`), and the only way to customize behavior - for example, to proxy authentication through your own backend - was `mode: 'custom'` with a `customFlow` class that extended one of them and override its methods:
1975
+
1976
+ ```ts
1977
+ // v3
1978
+ import { NativeFlow } from '@strivacity/sdk-core/flows/NativeFlow';
1979
+
1980
+ export class CustomNativeFlow extends NativeFlow {
1981
+ override async refresh(): Promise<void> {
1982
+ // ...
1983
+ }
1984
+ }
1985
+
1986
+ const sdk = initFlow({ mode: 'custom', customFlow: CustomNativeFlow /* ... */ });
1987
+ ```
1988
+
1989
+ v4 removes `mode: 'custom'` and the `customFlow` option entirely, along with the flow classes and deep imports used to extend them. In their place, `createBaseFlow` is a factory function that returns a plain object of methods closing over shared state - build your own flow by composing it, without extending anything:
1990
+
1991
+ ```ts
1992
+ // v4
1993
+ import { createBaseFlow } from '@strivacity/sdk-core/flows/base';
1994
+ import { getDefaultFlowState, getSDKOptions } from '@strivacity/sdk-core/utils';
1995
+
1996
+ export function createCustomFlow(initConfig: SDKInitConfig) {
1997
+ const state = getDefaultFlowState();
1998
+ const options = getSDKOptions(state, initConfig);
1999
+ const base = createBaseFlow(state, options);
2000
+
2001
+ async function refresh(): Promise<void> {
2002
+ // ...
2003
+ }
2004
+
2005
+ return { ...base, refresh };
2006
+ }
2007
+ ```
2008
+
2009
+ See [Custom flow](#custom-flow) above for the full pattern.
2010
+
2011
+ #### Server-managed sessions ([BFF](../../README.md#bff)) are now built in
2012
+
2013
+ In v3, routing authentication through your own backend meant writing a custom flow class like the one above yourself: manually calling `fetch()` against hand-written endpoints, and reimplementing PKCE/state handling, CSRF protection, and server-side token storage on your own.
2014
+
2015
+ v4 replaces v3's approach with a first-class **Server SDK** and a single client-side option. Set `serverSessionUri` on your existing SDK options and the built-in `redirect`/`popup`/`embedded`/`native` flows automatically route login through your server (instead of the identity provider) with tokens kept server-side instead of in client storage:
2016
+
2017
+ ```ts
2018
+ // v4
2019
+ const sdk = initFlow({
2020
+ mode: 'redirect',
2021
+ issuer: 'https://<YOUR_TENANT_DOMAIN>',
2022
+ clientId: 'YOUR_CLIENT_ID',
2023
+ redirectUri: 'https://your-app.example.com/callback',
2024
+ scopes: ['openid', 'profile', 'email'],
2025
+ serverSessionUri: '/api/auth/login', // requests are routed through your server; tokens are never written to client storage
2026
+ });
2027
+ ```
2028
+
2029
+ Pair it with `createBaseServerSDK` from `@strivacity/sdk-core/server` on the backend - PKCE, state, and session storage are all handled by the Server SDK. See [Server-side session management](#server-side-session-management) and [Server SDK](#server-sdk) above, and the [backend example app](../../apps/backend) for a working reference.
2030
+
2031
+ #### Native mode: no more `NativeFlowHandler`
2032
+
2033
+ In v3, `native` mode's `login()`/`register()` returned a separate `NativeFlowHandler` instance, and the flow was driven through that handler:
2034
+
2035
+ ```ts
2036
+ // v3
2037
+ const handler = await sdk.login();
2038
+ const state = await handler.startSession(sessionId);
2039
+ const nextState = await handler.submitForm('formId', { identifier: 'user@example.com' });
2040
+ await handler.finalizeSession(nextState.finalizeUrl);
2041
+ ```
2042
+
2043
+ v4 moves `startSession()`, `submitForm()`, and `finalizeSession()` directly onto the flow instance itself - there's no separate handler object to manage:
2044
+
2045
+ ```ts
2046
+ // v4
2047
+ const state = await sdk.startSession({ sessionId });
2048
+ const nextState = await sdk.submitForm('formId', { identifier: 'user@example.com' });
2049
+ await sdk.finalizeSession(nextState.finalizeUrl);
2050
+ ```
2051
+
2052
+ 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.
2053
+
2054
+ ### Migrating to v3.0
2055
+
2056
+ ##### Entry API Major Changes
2057
+ `sdk.entry()` API now returns a structured object instead of a plain string. Check the example above in the usage section for more details.
2058
+
2059
+ ---
2060
+
2061
+ ## Vulnerability Reporting
2062
+
2063
+ The [Guidelines for responsible disclosure](https://www.strivacity.com/report-a-security-issue) details the procedure for disclosing security issues. Please do not report security vulnerabilities on the public issue tracker.
2064
+
2065
+ ## License
2066
+
2067
+ This package is available under the MIT License. See the [LICENSE](https://github.com/Strivacity/sdk-js/blob/main/LICENSE) file for more info.
2068
+
2069
+ ## Contributing
2070
+
2071
+ Please see our [contributing guide](https://github.com/Strivacity/sdk-js/blob/main/CONTRIBUTING.md).