@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
@@ -0,0 +1,487 @@
1
+ import { beforeEach, describe, test, expect, vi } from 'vitest';
2
+ import { http, HttpResponse } from 'msw';
3
+ import { createOptions, getMockStorage } from '@strivacity/testing/mocks/sdk';
4
+ import { worker } from '@strivacity/testing/mocks/msw';
5
+ import { createNativeFlow } from '../../../src/flows/native';
6
+ import { FallbackError, NetworkError, OidcError, ProtocolError, ServerError, SessionExpiredError } from '../../../src/utils';
7
+
8
+ describe('createNativeFlow', () => {
9
+ let options: ReturnType<typeof createOptions>;
10
+ let flow: ReturnType<typeof createNativeFlow>;
11
+
12
+ beforeEach(() => {
13
+ globalThis.location.href = 'https://brandtegrity.io';
14
+
15
+ options = createOptions();
16
+ flow = createNativeFlow(options);
17
+ });
18
+
19
+ describe('init', () => {
20
+ test('lazyLoad: true', async () => {
21
+ flow = createNativeFlow(createOptions({ lazyLoad: true }));
22
+
23
+ // We need to wait for the next tick to ensure that any asynchronous initialization logic has a chance to run before we check the initialized state.
24
+ await Promise.resolve();
25
+
26
+ expect(flow.initialized).toBe(false);
27
+
28
+ await flow.init();
29
+
30
+ expect(flow.initialized).toBe(true);
31
+ });
32
+
33
+ test('lazyLoad: false', async () => {
34
+ flow = createNativeFlow(createOptions({ lazyLoad: false }));
35
+
36
+ await vi.waitFor(() => expect(flow.initialized).toBe(true));
37
+ });
38
+ });
39
+
40
+ describe('startSession', () => {
41
+ test('should submit the init form and collect the session identifiers when the response contains a session_id', async () => {
42
+ worker.use(
43
+ http.get('https://brandtegrity.io/oauth2/auth', () =>
44
+ HttpResponse.text('https://brandtegrity.io/login?session_id=abc123&short_app_id=app&language=hu-HU'),
45
+ ),
46
+ http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })),
47
+ );
48
+
49
+ const data = await flow.startSession();
50
+
51
+ expect(flow.sessionId).toBe('abc123');
52
+ expect(flow.shortAppId).toBe('app');
53
+ expect(flow.language).toBe('hu-HU');
54
+ expect(data).toEqual({ screen: 'identification' });
55
+ });
56
+
57
+ test('should continue an existing session without contacting the authorization endpoint when a sessionId is provided', async () => {
58
+ worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })));
59
+
60
+ const data = await flow.startSession({ sessionId: 'existing-session' });
61
+
62
+ expect(flow.sessionId).toBe('existing-session');
63
+ expect(data).toEqual({ screen: 'identification' });
64
+ });
65
+
66
+ describe('should exchange the code for tokens', () => {
67
+ test('w/o serverSessionUri', async () => {
68
+ worker.use(
69
+ http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
70
+ const state = new URL(request.url).searchParams.get('state');
71
+
72
+ return HttpResponse.text(`https://brandtegrity.io/callback?code=abc123&state=${state}`);
73
+ }),
74
+ http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
75
+ );
76
+
77
+ await flow.startSession();
78
+
79
+ expect(globalThis.location.href).toBe('https://brandtegrity.io/');
80
+ expect(flow.accessToken).toBe('access-token');
81
+ });
82
+
83
+ test('w/ serverSessionUri', async () => {
84
+ worker.use(http.get('https://app.example.com/login', () => HttpResponse.text('https://brandtegrity.io/callback?code=abc123')));
85
+
86
+ options = createOptions({ serverSessionUri: 'https://app.example.com/login' });
87
+ flow = createNativeFlow(options);
88
+
89
+ await flow.startSession();
90
+
91
+ expect(globalThis.location.href).toBe('https://brandtegrity.io/callback?code=abc123');
92
+ expect(flow.accessToken).toBeNull();
93
+ });
94
+ });
95
+ });
96
+
97
+ describe('submitForm', () => {
98
+ test('should throw an error when the session ID is missing', async () => {
99
+ await expect(flow.submitForm()).rejects.toThrow('Session ID is missing. Cannot submit form.');
100
+ });
101
+
102
+ test('should submit to the init endpoint when no formId is provided', async () => {
103
+ worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })));
104
+
105
+ flow.sessionId = 'abc123';
106
+
107
+ await expect(flow.submitForm()).resolves.toEqual({ screen: 'identification' });
108
+ });
109
+
110
+ test('should submit to the form-specific endpoint when a formId is provided', async () => {
111
+ worker.use(http.post('https://brandtegrity.io/flow/api/v1/form/identifier', () => HttpResponse.json({ screen: 'password' })));
112
+
113
+ flow.sessionId = 'abc123';
114
+
115
+ await expect(flow.submitForm('identifier', { username: 'user' })).resolves.toEqual({ screen: 'password' });
116
+ });
117
+
118
+ test('should finalize the session and exchange the code for tokens when the response contains a finalizeUrl', async () => {
119
+ const stateData = await getMockStorage(options.stateStorage).generateState();
120
+
121
+ worker.use(
122
+ http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ finalizeUrl: 'https://brandtegrity.io/finalize' })),
123
+ http.get('https://brandtegrity.io/finalize', () => HttpResponse.text(`https://brandtegrity.io/callback?code=abc123&state=${stateData.id}`)),
124
+ http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
125
+ );
126
+
127
+ flow.sessionId = 'abc123';
128
+
129
+ await flow.submitForm();
130
+
131
+ expect(flow.accessToken).toBe('access-token');
132
+ });
133
+
134
+ test('should reject with a SessionExpiredError on HTTP 403', async () => {
135
+ worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => new HttpResponse(null, { status: 403 })));
136
+
137
+ flow.sessionId = 'abc123';
138
+
139
+ await expect(flow.submitForm()).rejects.toThrow(SessionExpiredError);
140
+ });
141
+
142
+ test('should reject with a FallbackError when the response only contains a hostedUrl', async () => {
143
+ worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ hostedUrl: 'https://brandtegrity.io/hosted' })));
144
+
145
+ flow.sessionId = 'abc123';
146
+
147
+ await expect(flow.submitForm()).rejects.toThrow(FallbackError);
148
+ });
149
+
150
+ test('the SDK never opens a browser/popup itself on a Fallback', async () => {
151
+ const urlHandler = vi.fn();
152
+ options = createOptions({ urlHandler });
153
+ flow = createNativeFlow(options);
154
+ flow.sessionId = 'abc123';
155
+ worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ hostedUrl: 'https://brandtegrity.io/hosted' })));
156
+
157
+ await expect(flow.submitForm()).rejects.toThrow(FallbackError);
158
+
159
+ expect(urlHandler).not.toHaveBeenCalled();
160
+ });
161
+
162
+ test('a chained journey of rejections returns each screen independently, never bleeding a stale message into a later one', async () => {
163
+ flow.sessionId = 'abc123';
164
+
165
+ worker.use(
166
+ http.post('https://brandtegrity.io/flow/api/v1/form/password', () =>
167
+ HttpResponse.json({ screen: 'password', messages: { global: { type: 'error', text: 'Wrong password' } } }),
168
+ ),
169
+ );
170
+ const first = await flow.submitForm('password', { password: 'wrong-1' });
171
+ expect(first.messages?.global?.text).toBe('Wrong password');
172
+
173
+ worker.use(
174
+ http.post('https://brandtegrity.io/flow/api/v1/form/password', () =>
175
+ HttpResponse.json({ screen: 'password', messages: { global: { type: 'error', text: 'Account locked' } } }),
176
+ ),
177
+ );
178
+ const second = await flow.submitForm('password', { password: 'wrong-2' });
179
+ expect(second.messages?.global?.text).toBe('Account locked');
180
+ expect(second.messages?.global?.text).not.toBe(first.messages?.global?.text);
181
+
182
+ worker.use(http.post('https://brandtegrity.io/flow/api/v1/form/password', () => HttpResponse.json({ screen: 'passcode' })));
183
+ const third = await flow.submitForm('password', { password: 'correct' });
184
+ expect(third.messages).toBeUndefined();
185
+ expect(third.screen).toBe('passcode');
186
+ });
187
+ });
188
+
189
+ describe('finalizeSession', () => {
190
+ test('should throw when the session ID is missing', async () => {
191
+ await expect(flow.finalizeSession('https://brandtegrity.io/finalize')).rejects.toThrow('Session ID is missing. Cannot finalize login session.');
192
+ });
193
+
194
+ describe('should exchange the code for tokens', () => {
195
+ test('w/o serverSessionUri', async () => {
196
+ const stateData = await getMockStorage(options.stateStorage).generateState();
197
+
198
+ worker.use(
199
+ http.get('https://brandtegrity.io/finalize', () => HttpResponse.text(`https://brandtegrity.io/callback?code=abc123&state=${stateData.id}`)),
200
+ http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
201
+ );
202
+
203
+ flow.sessionId = 'abc123';
204
+
205
+ await flow.finalizeSession('https://brandtegrity.io/finalize');
206
+
207
+ expect(flow.accessToken).toBe('access-token');
208
+ });
209
+
210
+ test('w/ serverSessionUri', async () => {
211
+ worker.use(http.get('https://brandtegrity.io/finalize', () => HttpResponse.text('https://brandtegrity.io/callback?code=abc123')));
212
+
213
+ options = createOptions({ serverSessionUri: 'https://app.example.com/login' });
214
+ flow = createNativeFlow(options);
215
+
216
+ flow.sessionId = 'abc123';
217
+
218
+ await flow.finalizeSession('https://brandtegrity.io/finalize');
219
+
220
+ expect(globalThis.location.href).toBe('https://brandtegrity.io/callback?code=abc123');
221
+ expect(flow.accessToken).toBeNull();
222
+ });
223
+ });
224
+
225
+ test('should throw a ProtocolError when the redirect URI in the response is invalid', async () => {
226
+ worker.use(http.get('https://brandtegrity.io/finalize', () => HttpResponse.text('not-a-valid-url')));
227
+
228
+ flow.sessionId = 'abc123';
229
+
230
+ await expect(flow.finalizeSession('https://brandtegrity.io/finalize')).rejects.toThrow(new ProtocolError('Invalid redirect URI'));
231
+ expect(flow.options.logging?.error).toHaveBeenCalledWith('Finalize login session error', expect.any(ProtocolError));
232
+ });
233
+ });
234
+
235
+ describe('login', () => {
236
+ test('backward compatibility', async () => {
237
+ worker.use(
238
+ http.get('https://brandtegrity.io/oauth2/auth', () =>
239
+ HttpResponse.text('https://brandtegrity.io/login?session_id=xyz789&short_app_id=app2&language=en-US'),
240
+ ),
241
+ http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })),
242
+ );
243
+
244
+ const loginFlow = flow.login();
245
+
246
+ expect(loginFlow.sessionId).toBeNull();
247
+ expect(loginFlow.shortAppId).toBeNull();
248
+ expect(loginFlow.language).toBe('en-US');
249
+
250
+ loginFlow.sessionId = 'abc123';
251
+ loginFlow.shortAppId = 'app';
252
+ loginFlow.language = 'hu-HU';
253
+
254
+ expect(flow.sessionId).toBe('abc123');
255
+ expect(flow.shortAppId).toBe('app');
256
+ expect(flow.language).toBe('hu-HU');
257
+
258
+ await loginFlow.startSession();
259
+
260
+ expect(flow.sessionId).toBe('xyz789');
261
+ expect(flow.shortAppId).toBe('app2');
262
+ expect(flow.language).toBe('en-US');
263
+
264
+ const stateData = await getMockStorage(options.stateStorage).generateState();
265
+
266
+ worker.use(
267
+ http.get('https://brandtegrity.io/finalize', () => HttpResponse.text(`https://brandtegrity.io/callback?code=abc123&state=${stateData.id}`)),
268
+ http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
269
+ );
270
+
271
+ await loginFlow.finalizeSession('https://brandtegrity.io/finalize');
272
+
273
+ expect(flow.accessToken).toBe('access-token');
274
+ });
275
+
276
+ test('should forward login params to startSession', async () => {
277
+ let capturedUrl: URL | undefined;
278
+
279
+ worker.use(
280
+ http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
281
+ capturedUrl = new URL(request.url);
282
+
283
+ return HttpResponse.text('https://brandtegrity.io/login?session_id=abc123&short_app_id=app&language=en-US');
284
+ }),
285
+ http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })),
286
+ );
287
+
288
+ await flow.login({ prompt: 'login', loginHint: 'test@example.com' }).startSession();
289
+
290
+ expect(capturedUrl?.searchParams.get('prompt')).toBe('login');
291
+ expect(capturedUrl?.searchParams.get('login_hint')).toBe('test@example.com');
292
+ expect(flow.sessionId).toBe('abc123');
293
+ expect(flow.shortAppId).toBe('app');
294
+ expect(flow.language).toBe('en-US');
295
+ });
296
+
297
+ test('should submit forms through the login flow instance', async () => {
298
+ worker.use(http.post('https://brandtegrity.io/flow/api/v1/form/identifier', () => HttpResponse.json({ screen: 'password' })));
299
+
300
+ const loginFlow = flow.login();
301
+ loginFlow.sessionId = 'abc123';
302
+
303
+ await expect(loginFlow.submitForm('identifier', { username: 'user' })).resolves.toEqual({ screen: 'password' });
304
+ });
305
+ });
306
+
307
+ describe('register', () => {
308
+ test('should force the "create" prompt', async () => {
309
+ let capturedUrl: URL | undefined;
310
+
311
+ worker.use(
312
+ http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
313
+ capturedUrl = new URL(request.url);
314
+
315
+ return HttpResponse.text('https://brandtegrity.io/login?session_id=abc123&short_app_id=app&language=en-US');
316
+ }),
317
+ http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })),
318
+ );
319
+
320
+ await flow.register().startSession();
321
+
322
+ expect(capturedUrl?.searchParams.get('prompt')).toBe('create');
323
+ expect(flow.sessionId).toBe('abc123');
324
+ expect(flow.shortAppId).toBe('app');
325
+ expect(flow.language).toBe('en-US');
326
+ });
327
+ });
328
+
329
+ describe('entry', () => {
330
+ test('should fetch the entry data preserving query parameters', async () => {
331
+ let capturedUrl: URL | undefined;
332
+
333
+ worker.use(
334
+ http.get('https://brandtegrity.io/provider/flow/entry', ({ request }) => {
335
+ capturedUrl = new URL(request.url);
336
+
337
+ return HttpResponse.text('https://brandtegrity.io/entry?session_id=abc123&short_app_id=app&language=hu-HU');
338
+ }),
339
+ );
340
+
341
+ const data = await flow.entry('https://brandtegrity.io/entry?challenge=abc123&foo=bar');
342
+
343
+ expect(capturedUrl?.searchParams.get('challenge')).toBe('abc123');
344
+ expect(capturedUrl?.searchParams.get('foo')).toBe('bar');
345
+ expect(capturedUrl?.searchParams.get('sdk')).toBe('web');
346
+ expect(capturedUrl?.searchParams.get('client_id')).toBe('client-id');
347
+ expect(capturedUrl?.searchParams.get('redirect_uri')).toBe('https://brandtegrity.io/callback');
348
+ expect(data).toEqual({ session_id: 'abc123', short_app_id: 'app', language: 'hu-HU' });
349
+ expect(flow.session).toBeNull();
350
+ await expect(flow.isAuthenticated).resolves.toBe(false);
351
+ });
352
+
353
+ test('should default to the current window location when no url is provided', async () => {
354
+ window.location.search = '?challenge=abc123&foo=bar';
355
+
356
+ let capturedUrl: URL | undefined;
357
+
358
+ worker.use(
359
+ http.get('https://brandtegrity.io/provider/flow/entry', ({ request }) => {
360
+ capturedUrl = new URL(request.url);
361
+
362
+ return HttpResponse.text('https://brandtegrity.io/entry?session_id=abc123&short_app_id=app&language=hu-HU');
363
+ }),
364
+ );
365
+
366
+ const data = await flow.entry();
367
+
368
+ expect(capturedUrl?.searchParams.get('challenge')).toBe('abc123');
369
+ expect(capturedUrl?.searchParams.get('foo')).toBe('bar');
370
+ expect(data.session_id).toBe('abc123');
371
+ });
372
+
373
+ describe('should reject with an error when the HTTP request fails', () => {
374
+ test('HTTP 400', async () => {
375
+ worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => new HttpResponse('{}', { status: 400 })));
376
+
377
+ await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new ProtocolError('Entry request failed'));
378
+ expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ProtocolError));
379
+ });
380
+
381
+ test('HTTP 400 with an OIDC error body', async () => {
382
+ worker.use(
383
+ http.get('https://brandtegrity.io/provider/flow/entry', () =>
384
+ HttpResponse.json({ error: 'access_denied', error_description: 'User denied access' }, { status: 400 }),
385
+ ),
386
+ );
387
+
388
+ await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new OidcError('access_denied', 'User denied access'));
389
+ expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(OidcError));
390
+ });
391
+
392
+ test('HTTP 400 with an errorKey body', async () => {
393
+ worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => HttpResponse.json({ errorKey: 'invalid_request' }, { status: 400 })));
394
+
395
+ await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new ProtocolError('invalid_request'));
396
+ expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ProtocolError));
397
+ });
398
+
399
+ test('HTTP 429', async () => {
400
+ worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => new HttpResponse(null, { status: 429 })));
401
+
402
+ await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new ServerError('Entry request failed', 429));
403
+ expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ServerError));
404
+ });
405
+
406
+ test('HTTP 500', async () => {
407
+ worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => new HttpResponse(null, { status: 500 })));
408
+
409
+ await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new ServerError('Entry request failed', 500));
410
+ expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ServerError));
411
+ });
412
+
413
+ test('HTTP 503', async () => {
414
+ worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => HttpResponse.error()));
415
+
416
+ await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(
417
+ new NetworkError('Network request failed: https://brandtegrity.io/provider/flow/entry'),
418
+ );
419
+ expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(NetworkError));
420
+ });
421
+
422
+ test('should throw a ProtocolError when "session_id" is missing from the response', async () => {
423
+ worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => HttpResponse.text('https://brandtegrity.io/entry?short_app_id=app')));
424
+
425
+ await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(
426
+ new ProtocolError('"session_id" is missing from the response'),
427
+ );
428
+ expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ProtocolError));
429
+ });
430
+ });
431
+
432
+ test('should fall back to the response URL when the response body is not a valid URL', async () => {
433
+ worker.use(
434
+ http.get('https://brandtegrity.io/provider/flow/entry', () =>
435
+ HttpResponse.redirect('https://brandtegrity.io/entry-redirected?session_id=abc123&short_app_id=app&language=hu-HU'),
436
+ ),
437
+ http.get('https://brandtegrity.io/entry-redirected', () => HttpResponse.text('<html>not a url</html>')),
438
+ );
439
+
440
+ const data = await flow.entry('https://brandtegrity.io/entry?challenge=abc123');
441
+
442
+ expect(data).toEqual({ session_id: 'abc123', short_app_id: 'app', language: 'hu-HU' });
443
+ });
444
+ });
445
+
446
+ test('flow state accessors', async () => {
447
+ options = createOptions({ lazyLoad: true });
448
+ flow = createNativeFlow(options);
449
+
450
+ expect(flow.options).toBe(options);
451
+ expect(flow.initialized).toBeFalsy();
452
+ expect(flow.storage).toBe(options.storage);
453
+ expect(flow.httpClient).toBe(options.httpClient);
454
+ expect(flow.logging).toBe(options.logging);
455
+ expect(flow.sessionId).toBeNull();
456
+ expect(flow.shortAppId).toBeNull();
457
+ expect(flow.language).toBe('en-US');
458
+ await expect(flow.isAuthenticated).resolves.toBeFalsy();
459
+ expect(flow.isAuthenticatedSync).toBeFalsy();
460
+ expect(flow.accessToken).toBeNull();
461
+ expect(flow.accessTokenExpired).toBeTruthy();
462
+ expect(flow.accessTokenExpirationDate).toBeNull();
463
+ expect(flow.refreshToken).toBeNull();
464
+ expect(flow.idToken).toBeNull();
465
+ expect(flow.idTokenClaims).toBeNull();
466
+
467
+ await flow.init();
468
+
469
+ flow.sessionId = 'abc123';
470
+ flow.shortAppId = 'app';
471
+ flow.language = 'hu-HU';
472
+ flow.session = getMockStorage(options.storage).generateSession();
473
+
474
+ expect(flow.initialized).toBeTruthy();
475
+ expect(flow.sessionId).toBe('abc123');
476
+ expect(flow.shortAppId).toBe('app');
477
+ expect(flow.language).toBe('hu-HU');
478
+ await expect(flow.isAuthenticated).resolves.toBeTruthy();
479
+ expect(flow.isAuthenticatedSync).toBeTruthy();
480
+ expect(flow.accessToken).toBe(flow.session.access_token);
481
+ expect(flow.accessTokenExpired).toBeFalsy();
482
+ expect(flow.accessTokenExpirationDate).toBe(flow.session.expires_at);
483
+ expect(flow.refreshToken).toBe(flow.session.refresh_token);
484
+ expect(flow.idToken).toBe(flow.session.id_token);
485
+ expect(flow.idTokenClaims).toBe(flow.session?.claims);
486
+ });
487
+ });