@strivacity/sdk-core 3.0.2 → 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 (328) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1873 -340
  3. package/dist/assets/oidc.cjs +2 -0
  4. package/dist/assets/oidc.cjs.map +1 -0
  5. package/dist/assets/oidc.mjs +2 -0
  6. package/dist/assets/oidc.mjs.map +1 -0
  7. package/dist/assets/server.cjs +2 -0
  8. package/dist/assets/server.cjs.map +1 -0
  9. package/dist/assets/server.mjs +2 -0
  10. package/dist/assets/server.mjs.map +1 -0
  11. package/dist/flows/base.cjs +2 -0
  12. package/dist/flows/base.cjs.map +1 -0
  13. package/dist/flows/base.d.ts +26 -0
  14. package/dist/flows/base.mjs +2 -0
  15. package/dist/flows/base.mjs.map +1 -0
  16. package/dist/flows/embedded.cjs +2 -0
  17. package/dist/flows/embedded.cjs.map +1 -0
  18. package/dist/flows/embedded.d.ts +9 -0
  19. package/dist/flows/embedded.mjs +2 -0
  20. package/dist/flows/embedded.mjs.map +1 -0
  21. package/dist/flows/index.cjs +1 -0
  22. package/dist/flows/index.d.ts +5 -0
  23. package/dist/flows/index.mjs +1 -0
  24. package/dist/flows/native.cjs +2 -0
  25. package/dist/flows/native.cjs.map +1 -0
  26. package/dist/flows/native.d.ts +9 -0
  27. package/dist/flows/native.mjs +2 -0
  28. package/dist/flows/native.mjs.map +1 -0
  29. package/dist/flows/popup.cjs +2 -0
  30. package/dist/flows/popup.cjs.map +1 -0
  31. package/dist/flows/popup.d.ts +9 -0
  32. package/dist/flows/popup.mjs +2 -0
  33. package/dist/flows/popup.mjs.map +1 -0
  34. package/dist/flows/redirect.cjs +2 -0
  35. package/dist/flows/redirect.cjs.map +1 -0
  36. package/dist/flows/redirect.d.ts +9 -0
  37. package/dist/flows/redirect.mjs +2 -0
  38. package/dist/flows/redirect.mjs.map +1 -0
  39. package/dist/handlers/embedded.cjs +2 -0
  40. package/dist/handlers/embedded.cjs.map +1 -0
  41. package/dist/handlers/embedded.d.ts +37 -0
  42. package/dist/handlers/embedded.mjs +2 -0
  43. package/dist/handlers/embedded.mjs.map +1 -0
  44. package/dist/handlers/index.cjs +1 -0
  45. package/dist/handlers/index.d.ts +2 -0
  46. package/dist/handlers/index.mjs +1 -0
  47. package/dist/handlers/native.cjs +2 -0
  48. package/dist/handlers/native.cjs.map +1 -0
  49. package/dist/handlers/native.d.ts +56 -0
  50. package/dist/handlers/native.mjs +2 -0
  51. package/dist/handlers/native.mjs.map +1 -0
  52. package/dist/handlers/popup.cjs +2 -0
  53. package/dist/handlers/popup.cjs.map +1 -0
  54. package/dist/handlers/popup.d.ts +21 -0
  55. package/dist/handlers/popup.mjs +2 -0
  56. package/dist/handlers/popup.mjs.map +1 -0
  57. package/dist/handlers/redirect.cjs +2 -0
  58. package/dist/handlers/redirect.cjs.map +1 -0
  59. package/dist/handlers/redirect.d.ts +18 -0
  60. package/dist/handlers/redirect.mjs +2 -0
  61. package/dist/handlers/redirect.mjs.map +1 -0
  62. package/dist/index.cjs +2 -2
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.ts +16 -21
  65. package/dist/index.mjs +2 -2
  66. package/dist/index.mjs.map +1 -1
  67. package/dist/server/base.cjs +2 -0
  68. package/dist/server/base.cjs.map +1 -0
  69. package/dist/server/base.d.ts +15 -0
  70. package/dist/server/base.mjs +2 -0
  71. package/dist/server/base.mjs.map +1 -0
  72. package/dist/server/index.cjs +1 -0
  73. package/dist/server/index.d.ts +1 -0
  74. package/dist/server/index.mjs +1 -0
  75. package/dist/storages/cache.cjs +2 -0
  76. package/dist/storages/cache.cjs.map +1 -0
  77. package/dist/storages/cache.d.ts +11 -0
  78. package/dist/storages/cache.mjs +2 -0
  79. package/dist/storages/cache.mjs.map +1 -0
  80. package/dist/storages/index.cjs +1 -0
  81. package/dist/storages/index.d.ts +7 -0
  82. package/dist/storages/index.mjs +1 -0
  83. package/dist/storages/indexeddb.cjs +2 -0
  84. package/dist/storages/indexeddb.cjs.map +1 -0
  85. package/dist/storages/indexeddb.d.ts +10 -0
  86. package/dist/storages/indexeddb.mjs +2 -0
  87. package/dist/storages/indexeddb.mjs.map +1 -0
  88. package/dist/storages/local.cjs +2 -0
  89. package/dist/storages/local.cjs.map +1 -0
  90. package/dist/storages/local.d.ts +7 -0
  91. package/dist/storages/local.mjs +2 -0
  92. package/dist/storages/local.mjs.map +1 -0
  93. package/dist/storages/memory.cjs +2 -0
  94. package/dist/storages/memory.cjs.map +1 -0
  95. package/dist/storages/memory.d.ts +8 -0
  96. package/dist/storages/memory.mjs +2 -0
  97. package/dist/storages/memory.mjs.map +1 -0
  98. package/dist/storages/server.cjs +1 -0
  99. package/dist/storages/server.d.ts +39 -0
  100. package/dist/storages/server.mjs +1 -0
  101. package/dist/storages/session.cjs +2 -0
  102. package/dist/storages/session.cjs.map +1 -0
  103. package/dist/storages/session.d.ts +7 -0
  104. package/dist/storages/session.mjs +2 -0
  105. package/dist/storages/session.mjs.map +1 -0
  106. package/dist/storages/worker.cjs +2 -0
  107. package/dist/storages/worker.cjs.map +1 -0
  108. package/dist/storages/worker.d.ts +46 -0
  109. package/dist/storages/worker.mjs +2 -0
  110. package/dist/storages/worker.mjs.map +1 -0
  111. package/dist/types/common.cjs +0 -0
  112. package/dist/types/common.d.ts +145 -0
  113. package/dist/types/common.mjs +0 -0
  114. package/dist/types/embedded.cjs +0 -0
  115. package/dist/types/embedded.d.ts +103 -0
  116. package/dist/types/embedded.mjs +0 -0
  117. package/dist/types/index.cjs +1 -0
  118. package/dist/types/index.d.ts +7 -0
  119. package/dist/types/index.mjs +1 -0
  120. package/dist/types/native.cjs +0 -0
  121. package/dist/types/native.d.ts +385 -0
  122. package/dist/types/native.mjs +0 -0
  123. package/dist/types/oidc.cjs +2 -0
  124. package/dist/types/oidc.cjs.map +1 -0
  125. package/dist/types/oidc.d.ts +768 -0
  126. package/dist/types/oidc.mjs +2 -0
  127. package/dist/types/oidc.mjs.map +1 -0
  128. package/dist/types/popup.cjs +0 -0
  129. package/dist/types/popup.d.ts +69 -0
  130. package/dist/types/popup.mjs +0 -0
  131. package/dist/types/redirect.cjs +0 -0
  132. package/dist/types/redirect.d.ts +21 -0
  133. package/dist/types/redirect.mjs +0 -0
  134. package/dist/types/server.cjs +0 -0
  135. package/dist/types/server.d.ts +200 -0
  136. package/dist/types/server.mjs +0 -0
  137. package/dist/utils/base64url.cjs +2 -0
  138. package/dist/utils/base64url.cjs.map +1 -0
  139. package/dist/utils/base64url.d.ts +14 -0
  140. package/dist/utils/base64url.mjs +2 -0
  141. package/dist/utils/base64url.mjs.map +1 -0
  142. package/dist/utils/common.cjs +2 -0
  143. package/dist/utils/common.cjs.map +1 -0
  144. package/dist/utils/common.d.ts +23 -0
  145. package/dist/utils/common.mjs +2 -0
  146. package/dist/utils/common.mjs.map +1 -0
  147. package/dist/utils/credentials.cjs +2 -2
  148. package/dist/utils/credentials.cjs.map +1 -1
  149. package/dist/utils/credentials.d.ts +19 -3
  150. package/dist/utils/credentials.mjs +2 -2
  151. package/dist/utils/credentials.mjs.map +1 -1
  152. package/dist/utils/crypto.cjs +2 -2
  153. package/dist/utils/crypto.cjs.map +1 -1
  154. package/dist/utils/crypto.d.ts +16 -26
  155. package/dist/utils/crypto.mjs +2 -2
  156. package/dist/utils/crypto.mjs.map +1 -1
  157. package/dist/utils/dom.cjs +2 -0
  158. package/dist/utils/dom.cjs.map +1 -0
  159. package/dist/utils/dom.d.ts +7 -0
  160. package/dist/utils/dom.mjs +2 -0
  161. package/dist/utils/dom.mjs.map +1 -0
  162. package/dist/utils/errors.cjs +2 -2
  163. package/dist/utils/errors.cjs.map +1 -1
  164. package/dist/utils/errors.d.ts +62 -1
  165. package/dist/utils/errors.mjs +2 -2
  166. package/dist/utils/errors.mjs.map +1 -1
  167. package/dist/utils/httpClient.cjs +2 -0
  168. package/dist/utils/httpClient.cjs.map +1 -0
  169. package/dist/utils/httpClient.d.ts +2 -0
  170. package/dist/utils/httpClient.mjs +2 -0
  171. package/dist/utils/httpClient.mjs.map +1 -0
  172. package/dist/utils/index.cjs +1 -0
  173. package/dist/utils/index.d.ts +12 -0
  174. package/dist/utils/index.mjs +1 -0
  175. package/dist/utils/logging.cjs +2 -0
  176. package/dist/utils/logging.cjs.map +1 -0
  177. package/dist/utils/logging.d.ts +2 -0
  178. package/dist/utils/logging.mjs +2 -0
  179. package/dist/utils/logging.mjs.map +1 -0
  180. package/dist/utils/oidc.cjs +1 -0
  181. package/dist/utils/oidc.d.ts +264 -0
  182. package/dist/utils/oidc.mjs +1 -0
  183. package/dist/utils/server.cjs +1 -0
  184. package/dist/utils/server.d.ts +72 -0
  185. package/dist/utils/server.mjs +1 -0
  186. package/dist/utils/session.cjs +2 -0
  187. package/dist/utils/session.cjs.map +1 -0
  188. package/dist/utils/session.d.ts +29 -0
  189. package/dist/utils/session.mjs +2 -0
  190. package/dist/utils/session.mjs.map +1 -0
  191. package/dist/utils/state.cjs +1 -0
  192. package/dist/utils/state.d.ts +23 -0
  193. package/dist/utils/state.mjs +1 -0
  194. package/package.json +53 -1
  195. package/testing/tests/flows/base.spec.ts +1030 -0
  196. package/testing/tests/flows/embedded.spec.ts +443 -0
  197. package/testing/tests/flows/index.spec.ts +8 -0
  198. package/testing/tests/flows/native.spec.ts +487 -0
  199. package/testing/tests/flows/popup.spec.ts +400 -0
  200. package/testing/tests/flows/redirect.spec.ts +369 -0
  201. package/testing/tests/handlers/embedded.spec.ts +199 -0
  202. package/testing/tests/handlers/index.spec.ts +8 -0
  203. package/testing/tests/handlers/native.spec.ts +283 -0
  204. package/testing/tests/handlers/popup.spec.ts +206 -0
  205. package/testing/tests/handlers/redirect.spec.ts +70 -0
  206. package/testing/tests/index.spec.ts +78 -0
  207. package/testing/tests/server/base.spec.ts +846 -0
  208. package/testing/tests/server/index.spec.ts +15 -0
  209. package/testing/tests/storages/cache.spec.ts +107 -0
  210. package/testing/tests/storages/index.spec.ts +22 -0
  211. package/testing/tests/storages/indexeddb.spec.ts +113 -0
  212. package/testing/tests/storages/local.spec.ts +46 -0
  213. package/testing/tests/storages/memory.spec.ts +51 -0
  214. package/testing/tests/storages/server.spec.ts +435 -0
  215. package/testing/tests/storages/session.spec.ts +46 -0
  216. package/testing/tests/storages/worker.spec.ts +106 -0
  217. package/testing/tests/utils/common.spec.ts +77 -0
  218. package/testing/tests/utils/credentials.spec.ts +195 -0
  219. package/testing/tests/utils/dom.spec.ts +37 -0
  220. package/testing/tests/utils/index.spec.ts +71 -0
  221. package/testing/tests/utils/logging.spec.ts +82 -0
  222. package/testing/tests/utils/oidc.spec.ts +744 -0
  223. package/testing/tests/utils/server.spec.ts +235 -0
  224. package/dist/flows/BaseFlow.cjs +0 -2
  225. package/dist/flows/BaseFlow.cjs.map +0 -1
  226. package/dist/flows/BaseFlow.d.ts +0 -191
  227. package/dist/flows/BaseFlow.mjs +0 -2
  228. package/dist/flows/BaseFlow.mjs.map +0 -1
  229. package/dist/flows/EmbeddedFlow.cjs +0 -2
  230. package/dist/flows/EmbeddedFlow.cjs.map +0 -1
  231. package/dist/flows/EmbeddedFlow.d.ts +0 -26
  232. package/dist/flows/EmbeddedFlow.mjs +0 -2
  233. package/dist/flows/EmbeddedFlow.mjs.map +0 -1
  234. package/dist/flows/NativeFlow.cjs +0 -2
  235. package/dist/flows/NativeFlow.cjs.map +0 -1
  236. package/dist/flows/NativeFlow.d.ts +0 -34
  237. package/dist/flows/NativeFlow.mjs +0 -2
  238. package/dist/flows/NativeFlow.mjs.map +0 -1
  239. package/dist/flows/PopupFlow.cjs +0 -2
  240. package/dist/flows/PopupFlow.cjs.map +0 -1
  241. package/dist/flows/PopupFlow.d.ts +0 -36
  242. package/dist/flows/PopupFlow.mjs +0 -2
  243. package/dist/flows/PopupFlow.mjs.map +0 -1
  244. package/dist/flows/RedirectFlow.cjs +0 -2
  245. package/dist/flows/RedirectFlow.cjs.map +0 -1
  246. package/dist/flows/RedirectFlow.d.ts +0 -38
  247. package/dist/flows/RedirectFlow.mjs +0 -2
  248. package/dist/flows/RedirectFlow.mjs.map +0 -1
  249. package/dist/handlers/BaseFlowHandler.cjs +0 -2
  250. package/dist/handlers/BaseFlowHandler.cjs.map +0 -1
  251. package/dist/handlers/BaseFlowHandler.d.ts +0 -61
  252. package/dist/handlers/BaseFlowHandler.mjs +0 -2
  253. package/dist/handlers/BaseFlowHandler.mjs.map +0 -1
  254. package/dist/handlers/EmbeddedFlowHandler.cjs +0 -2
  255. package/dist/handlers/EmbeddedFlowHandler.cjs.map +0 -1
  256. package/dist/handlers/EmbeddedFlowHandler.d.ts +0 -28
  257. package/dist/handlers/EmbeddedFlowHandler.mjs +0 -2
  258. package/dist/handlers/EmbeddedFlowHandler.mjs.map +0 -1
  259. package/dist/handlers/NativeFlowHandler.cjs +0 -2
  260. package/dist/handlers/NativeFlowHandler.cjs.map +0 -1
  261. package/dist/handlers/NativeFlowHandler.d.ts +0 -31
  262. package/dist/handlers/NativeFlowHandler.mjs +0 -2
  263. package/dist/handlers/NativeFlowHandler.mjs.map +0 -1
  264. package/dist/storages/LocalStorage.cjs +0 -2
  265. package/dist/storages/LocalStorage.cjs.map +0 -1
  266. package/dist/storages/LocalStorage.d.ts +0 -23
  267. package/dist/storages/LocalStorage.mjs +0 -2
  268. package/dist/storages/LocalStorage.mjs.map +0 -1
  269. package/dist/storages/SessionStorage.cjs +0 -2
  270. package/dist/storages/SessionStorage.cjs.map +0 -1
  271. package/dist/storages/SessionStorage.d.ts +0 -23
  272. package/dist/storages/SessionStorage.mjs +0 -2
  273. package/dist/storages/SessionStorage.mjs.map +0 -1
  274. package/dist/types.cjs +0 -2
  275. package/dist/types.cjs.map +0 -1
  276. package/dist/types.d.ts +0 -1263
  277. package/dist/types.mjs +0 -2
  278. package/dist/types.mjs.map +0 -1
  279. package/dist/utils/HttpClient.cjs +0 -2
  280. package/dist/utils/HttpClient.cjs.map +0 -1
  281. package/dist/utils/HttpClient.d.ts +0 -4
  282. package/dist/utils/HttpClient.mjs +0 -2
  283. package/dist/utils/HttpClient.mjs.map +0 -1
  284. package/dist/utils/Logging.cjs +0 -2
  285. package/dist/utils/Logging.cjs.map +0 -1
  286. package/dist/utils/Logging.d.ts +0 -8
  287. package/dist/utils/Logging.mjs +0 -2
  288. package/dist/utils/Logging.mjs.map +0 -1
  289. package/dist/utils/Metadata.cjs +0 -2
  290. package/dist/utils/Metadata.cjs.map +0 -1
  291. package/dist/utils/Metadata.d.ts +0 -98
  292. package/dist/utils/Metadata.mjs +0 -2
  293. package/dist/utils/Metadata.mjs.map +0 -1
  294. package/dist/utils/Session.cjs +0 -2
  295. package/dist/utils/Session.cjs.map +0 -1
  296. package/dist/utils/Session.d.ts +0 -82
  297. package/dist/utils/Session.mjs +0 -2
  298. package/dist/utils/Session.mjs.map +0 -1
  299. package/dist/utils/State.cjs +0 -2
  300. package/dist/utils/State.cjs.map +0 -1
  301. package/dist/utils/State.d.ts +0 -43
  302. package/dist/utils/State.mjs +0 -2
  303. package/dist/utils/State.mjs.map +0 -1
  304. package/dist/utils/base64Url.cjs +0 -2
  305. package/dist/utils/base64Url.cjs.map +0 -1
  306. package/dist/utils/base64Url.d.ts +0 -27
  307. package/dist/utils/base64Url.mjs +0 -2
  308. package/dist/utils/base64Url.mjs.map +0 -1
  309. package/dist/utils/date.cjs +0 -2
  310. package/dist/utils/date.cjs.map +0 -1
  311. package/dist/utils/date.d.ts +0 -6
  312. package/dist/utils/date.mjs +0 -2
  313. package/dist/utils/date.mjs.map +0 -1
  314. package/dist/utils/handlers.cjs +0 -2
  315. package/dist/utils/handlers.cjs.map +0 -1
  316. package/dist/utils/handlers.d.ts +0 -35
  317. package/dist/utils/handlers.mjs +0 -2
  318. package/dist/utils/handlers.mjs.map +0 -1
  319. package/dist/utils/jwt.cjs +0 -2
  320. package/dist/utils/jwt.cjs.map +0 -1
  321. package/dist/utils/jwt.d.ts +0 -34
  322. package/dist/utils/jwt.mjs +0 -2
  323. package/dist/utils/jwt.mjs.map +0 -1
  324. package/dist/utils/object.cjs +0 -2
  325. package/dist/utils/object.cjs.map +0 -1
  326. package/dist/utils/object.d.ts +0 -1
  327. package/dist/utils/object.mjs +0 -2
  328. package/dist/utils/object.mjs.map +0 -1
@@ -0,0 +1,846 @@
1
+ import { describe, test, expect, beforeAll, vi } from 'vitest';
2
+ import { http, HttpResponse } from 'msw';
3
+ import {
4
+ createSessionData,
5
+ createMockEvent,
6
+ createMockStorage,
7
+ createServerOptions,
8
+ createServerSDK,
9
+ createServerMockStorage,
10
+ getPostLogoutTokenRequest,
11
+ seedState,
12
+ serverAdapter,
13
+ type MockEvent,
14
+ } from '@strivacity/testing/mocks/sdk';
15
+ import { worker } from '@strivacity/testing/mocks/msw';
16
+ import { buildLogoutTokenClaims, exportSigningKey, generateRSAKeyPair, signLogoutToken } from '@strivacity/testing/mocks/oidc';
17
+ import type { ServerAdapter } from '../../../src/types';
18
+ import { BACKCHANNEL_LOGOUT_EVENT, timestamp, ConfigurationError, ProtocolError, serializeSession } from '../../../src/utils';
19
+ import { createBaseServerSDK } from '../../../src/server/base';
20
+
21
+ describe('createBaseServerSDK', () => {
22
+ describe('init', () => {
23
+ test.each([
24
+ ['issuer', { issuer: '' }, 'Missing option: issuer'],
25
+ ['clientId', { clientId: '' }, 'Missing option: clientId'],
26
+ [
27
+ 'redirectUri',
28
+ { redirectUri: '', postLoginRedirectUri: 'https://brandtegrity.io', postLogoutRedirectUri: 'https://brandtegrity.io' },
29
+ 'Missing option: redirectUri',
30
+ ],
31
+ ])('should throw a ConfigurationError when %s is missing', (_name, overrides, message) => {
32
+ const options = createServerOptions(overrides);
33
+
34
+ expect(() => createBaseServerSDK(serverAdapter, options)).toThrow(new ConfigurationError(message));
35
+ expect(options.logging?.error).toHaveBeenCalledWith('Required option missing', expect.any(ConfigurationError));
36
+ });
37
+
38
+ test('should throw a ConfigurationError when scopes is not an array', () => {
39
+ const options = createServerOptions({ scopes: 'openid' as unknown as Array<string> });
40
+
41
+ expect(() => createBaseServerSDK(serverAdapter, options)).toThrow(new ConfigurationError('Invalid option: scopes'));
42
+ expect(options.logging?.error).toHaveBeenCalledWith('Invalid option provided', expect.any(ConfigurationError));
43
+ });
44
+ });
45
+
46
+ describe('getSession', () => {
47
+ test('should return null when there is no session', async () => {
48
+ const { sdk } = createServerSDK();
49
+
50
+ await expect(sdk.getSession(createMockEvent())).resolves.toBeNull();
51
+ });
52
+
53
+ test('should return the persisted session', async () => {
54
+ const { sdk, storage, options } = createServerSDK();
55
+ const session = createSessionData();
56
+ await storage.set(options.storageTokenName, serializeSession(session));
57
+
58
+ await expect(sdk.getSession(createMockEvent())).resolves.toEqual(session);
59
+ });
60
+ });
61
+
62
+ describe('updateSession', () => {
63
+ test('should persist the session to storage', async () => {
64
+ const { sdk } = createServerSDK();
65
+ const session = createSessionData();
66
+
67
+ await sdk.updateSession(session, createMockEvent());
68
+
69
+ await expect(sdk.getSession(createMockEvent())).resolves.toEqual(session);
70
+ });
71
+ });
72
+
73
+ describe('refreshSession', () => {
74
+ test('should refresh the session and persist the new tokens', async () => {
75
+ const { sdk, storage, options } = createServerSDK();
76
+ await storage.set(options.storageTokenName, serializeSession(createSessionData({ refresh_token: 'old-refresh-token' })));
77
+
78
+ worker.use(
79
+ http.post('https://brandtegrity.io/oauth2/token', () =>
80
+ HttpResponse.json({ access_token: 'new-access-token', refresh_token: 'new-refresh-token', token_type: 'Bearer', expires_in: 3600 }),
81
+ ),
82
+ );
83
+
84
+ const refreshed = await sdk.refreshSession(createMockEvent());
85
+
86
+ expect(refreshed.access_token).toBe('new-access-token');
87
+ await expect(sdk.getSession(createMockEvent())).resolves.toEqual(refreshed);
88
+ });
89
+
90
+ test('should throw a ProtocolError when there is no session', async () => {
91
+ const { sdk } = createServerSDK();
92
+
93
+ await expect(sdk.refreshSession(createMockEvent())).rejects.toThrow(new ProtocolError('No session available to refresh'));
94
+ });
95
+
96
+ test('should throw a ConfigurationError when there is no refresh token', async () => {
97
+ const { sdk, storage, options } = createServerSDK();
98
+ await storage.set(options.storageTokenName, serializeSession(createSessionData({ refresh_token: null })));
99
+
100
+ await expect(sdk.refreshSession(createMockEvent())).rejects.toThrow(new ConfigurationError('No refresh token available'));
101
+ });
102
+
103
+ test('should clean up the session on failure', async () => {
104
+ const { sdk, storage, options } = createServerSDK();
105
+ await storage.set(options.storageTokenName, serializeSession(createSessionData()));
106
+
107
+ worker.use(http.post('https://brandtegrity.io/oauth2/token', () => new HttpResponse(null, { status: 400 })));
108
+
109
+ await expect(sdk.refreshSession(createMockEvent())).rejects.toThrow();
110
+ await expect(sdk.getSession(createMockEvent())).resolves.toBeNull();
111
+ });
112
+ });
113
+
114
+ describe('revokeSession', () => {
115
+ test('should revoke the refresh token and clear the session', async () => {
116
+ const { sdk, storage, options } = createServerSDK();
117
+ const session = createSessionData();
118
+ await storage.set(options.storageTokenName, serializeSession(session));
119
+ let revokedTokenTypeHint: string | null = null;
120
+
121
+ worker.use(
122
+ http.post('https://brandtegrity.io/oauth2/revoke', async ({ request }) => {
123
+ revokedTokenTypeHint = new URLSearchParams(await request.text()).get('token_type_hint');
124
+ return HttpResponse.json({});
125
+ }),
126
+ );
127
+
128
+ await sdk.revokeSession(createMockEvent());
129
+
130
+ expect(revokedTokenTypeHint).toBe('refresh_token');
131
+ await expect(sdk.getSession(createMockEvent())).resolves.toBeNull();
132
+ });
133
+
134
+ test('should revoke the access token when there is no refresh token', async () => {
135
+ const { sdk, storage, options } = createServerSDK();
136
+ await storage.set(options.storageTokenName, serializeSession(createSessionData({ refresh_token: null })));
137
+ let revokedTokenTypeHint: string | null = null;
138
+
139
+ worker.use(
140
+ http.post('https://brandtegrity.io/oauth2/revoke', async ({ request }) => {
141
+ revokedTokenTypeHint = new URLSearchParams(await request.text()).get('token_type_hint');
142
+ return HttpResponse.json({});
143
+ }),
144
+ );
145
+
146
+ await sdk.revokeSession(createMockEvent());
147
+
148
+ expect(revokedTokenTypeHint).toBe('access_token');
149
+ });
150
+
151
+ test('should be a no-op when there is no session', async () => {
152
+ const { sdk } = createServerSDK();
153
+
154
+ await expect(sdk.revokeSession(createMockEvent())).resolves.toBeUndefined();
155
+ });
156
+
157
+ test('should rethrow on failure', async () => {
158
+ const { sdk, storage, options } = createServerSDK();
159
+ await storage.set(options.storageTokenName, serializeSession(createSessionData()));
160
+
161
+ worker.use(http.post('https://brandtegrity.io/oauth2/revoke', () => new HttpResponse(null, { status: 400 })));
162
+
163
+ await expect(sdk.revokeSession(createMockEvent())).rejects.toThrow();
164
+ });
165
+ });
166
+
167
+ describe('getEntrySession', () => {
168
+ test('should fetch the entry data', async () => {
169
+ const { sdk } = createServerSDK();
170
+
171
+ worker.use(
172
+ http.get('https://brandtegrity.io/provider/flow/entry', () =>
173
+ HttpResponse.text('https://brandtegrity.io/entry?session_id=abc123&short_app_id=app&language=hu-HU'),
174
+ ),
175
+ );
176
+
177
+ await expect(sdk.getEntrySession('https://brandtegrity.io/entry?challenge=abc123')).resolves.toEqual({
178
+ session_id: 'abc123',
179
+ short_app_id: 'app',
180
+ language: 'hu-HU',
181
+ });
182
+ });
183
+
184
+ test('should use web-embedded as the sdk mode for embedded mode', async () => {
185
+ const { sdk } = createServerSDK({ mode: 'embedded' });
186
+ let capturedUrl: URL | undefined;
187
+
188
+ worker.use(
189
+ http.get('https://brandtegrity.io/provider/flow/entry', ({ request }) => {
190
+ capturedUrl = new URL(request.url);
191
+ return HttpResponse.text('https://brandtegrity.io/entry?session_id=abc123&short_app_id=app&language=hu-HU');
192
+ }),
193
+ );
194
+
195
+ await sdk.getEntrySession('https://brandtegrity.io/entry');
196
+
197
+ expect(capturedUrl?.searchParams.get('sdk')).toBe('web-embedded');
198
+ });
199
+ });
200
+
201
+ describe('completeLogin', () => {
202
+ test('should exchange the code for tokens and persist the session', async () => {
203
+ const { sdk, stateStorage } = createServerSDK();
204
+ const stateData = await seedState(stateStorage);
205
+
206
+ worker.use(
207
+ http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
208
+ );
209
+
210
+ const event = createMockEvent();
211
+ const session = await sdk.completeLogin({ code: 'abc123', state: stateData.id }, event);
212
+
213
+ expect(session.access_token).toBe('access-token');
214
+ await expect(sdk.getSession(event)).resolves.toEqual(session);
215
+ });
216
+
217
+ test('should reject when the response is missing an access_token', async () => {
218
+ const { sdk, stateStorage } = createServerSDK();
219
+ const stateData = await seedState(stateStorage);
220
+
221
+ worker.use(http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ token_type: 'Bearer', expires_in: 3600 })));
222
+
223
+ await expect(sdk.completeLogin({ code: 'abc123', state: stateData.id })).rejects.toThrow(new ProtocolError('Missing access_token'));
224
+ });
225
+ });
226
+
227
+ describe('logout', () => {
228
+ test('should clear the session and return the end-session url', async () => {
229
+ const { sdk, storage, options } = createServerSDK();
230
+ await storage.set(options.storageTokenName, serializeSession(createSessionData({ id_token: 'id-token' })));
231
+
232
+ const url = await sdk.logout('https://brandtegrity.io/bye', createMockEvent());
233
+
234
+ expect(`${url.origin}${url.pathname}`).toBe('https://brandtegrity.io/oauth2/sessions/logout');
235
+ expect(url.searchParams.get('id_token_hint')).toBe('id-token');
236
+ expect(url.searchParams.get('post_logout_redirect_uri')).toBe('https://brandtegrity.io/bye');
237
+ await expect(sdk.getSession(createMockEvent())).resolves.toBeNull();
238
+ });
239
+
240
+ test('should return the postLogoutRedirectUri unchanged when there is no id token', async () => {
241
+ const { sdk, storage, options } = createServerSDK();
242
+ await storage.set(options.storageTokenName, serializeSession(createSessionData({ id_token: null })));
243
+
244
+ const url = await sdk.logout('https://brandtegrity.io/bye', createMockEvent());
245
+
246
+ expect(url.toString()).toBe('https://brandtegrity.io/bye');
247
+ await expect(sdk.getSession(createMockEvent())).resolves.toBeNull();
248
+ });
249
+ });
250
+
251
+ describe('redirect', () => {
252
+ test('uses the adapter-provided redirect override when available', async () => {
253
+ const customRedirect = vi.fn(
254
+ (url: string | URL, status: number) => new Response(null, { status, headers: { 'x-custom-redirect': 'true', location: url.toString() } }),
255
+ );
256
+ const customServerAdapter: ServerAdapter<MockEvent> = { toRequest: (event) => event.request, redirect: customRedirect };
257
+ const storage = createMockStorage();
258
+ const initOptions = createServerOptions({ storage, stateStorage: createMockStorage() });
259
+ const sdk = createBaseServerSDK(customServerAdapter, initOptions);
260
+ await storage.set(sdk.options.storageTokenName, serializeSession(createSessionData()));
261
+
262
+ worker.use(http.post('https://brandtegrity.io/oauth2/revoke', () => HttpResponse.json({})));
263
+
264
+ const response = await sdk.handleRevoke(createMockEvent());
265
+
266
+ expect(customRedirect).toHaveBeenCalledWith(expect.any(URL), 302, expect.anything());
267
+ expect(response.headers.get('x-custom-redirect')).toBe('true');
268
+ });
269
+
270
+ test('falls back to a plain Response with a Location header when the adapter has no override', async () => {
271
+ const { sdk, storage, options } = createServerSDK();
272
+ await storage.set(options.storageTokenName, serializeSession(createSessionData()));
273
+
274
+ worker.use(http.post('https://brandtegrity.io/oauth2/revoke', () => HttpResponse.json({})));
275
+
276
+ const response = await sdk.handleRevoke(createMockEvent());
277
+
278
+ expect(response.status).toBe(302);
279
+ expect(response.headers.get('location')).toBe(new URL(options.postLogoutRedirectUri).toString());
280
+ });
281
+ });
282
+
283
+ describe('handleLogin', () => {
284
+ test('should build the authorization URL, proxy the IDP redirect response, and set the returnTo cookie', async () => {
285
+ const { sdk } = createServerSDK();
286
+ let capturedUrl: URL | undefined;
287
+
288
+ worker.use(
289
+ http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
290
+ capturedUrl = new URL(request.url);
291
+ return new HttpResponse(null, { status: 302, headers: { location: 'https://brandtegrity.io/hosted-login' } });
292
+ }),
293
+ );
294
+
295
+ const response = await sdk.handleLogin(createMockEvent('https://brandtegrity.io/auth/login?returnTo=/dashboard&loginHint=user@example.com'));
296
+
297
+ expect(capturedUrl?.searchParams.get('login_hint')).toBe('user@example.com');
298
+ expect(response.status).toBe(302);
299
+ expect(response.headers.get('location')).toBe('https://brandtegrity.io/hosted-login');
300
+
301
+ const returnToCookie = response.headers.getSetCookie().find((cookie) => cookie.startsWith('sty.returnTo='));
302
+
303
+ expect(decodeURIComponent(returnToCookie!.split(';')[0].split('=')[1])).toBe('https://brandtegrity.io/dashboard');
304
+ });
305
+
306
+ test('should forward the sdk query param to the IDP request in embedded mode', async () => {
307
+ const { sdk } = createServerSDK({ mode: 'embedded' });
308
+ let capturedUrl: URL | undefined;
309
+
310
+ worker.use(
311
+ http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
312
+ capturedUrl = new URL(request.url);
313
+ return new HttpResponse(null, { status: 302, headers: { location: 'https://brandtegrity.io/hosted-login' } });
314
+ }),
315
+ );
316
+
317
+ await sdk.handleLogin(createMockEvent('https://brandtegrity.io/auth/login?sdk=web-embedded'));
318
+
319
+ expect(capturedUrl?.searchParams.get('sdk')).toBe('web-embedded');
320
+ });
321
+
322
+ test('should reject (not redirect to /error) when returnTo points to a different origin', async () => {
323
+ const { sdk } = createServerSDK();
324
+
325
+ await expect(sdk.handleLogin(createMockEvent('https://brandtegrity.io/auth/login?returnTo=https://evil.example.com'))).rejects.toThrow(
326
+ /Invalid returnTo URL/,
327
+ );
328
+ });
329
+ });
330
+
331
+ describe('handleRegister', () => {
332
+ test('should force the create prompt', async () => {
333
+ const { sdk } = createServerSDK();
334
+ let capturedUrl: URL | undefined;
335
+
336
+ worker.use(
337
+ http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
338
+ capturedUrl = new URL(request.url);
339
+ return new HttpResponse(null, { status: 302, headers: { location: 'https://brandtegrity.io/hosted-login' } });
340
+ }),
341
+ );
342
+
343
+ await sdk.handleRegister(createMockEvent('https://brandtegrity.io/auth/register'));
344
+
345
+ expect(capturedUrl?.searchParams.get('prompt')).toBe('create');
346
+ });
347
+ });
348
+
349
+ describe('handleCallback', () => {
350
+ test('should exchange the code, clear the returnTo cookie, and redirect to it', async () => {
351
+ const { sdk, stateStorage } = createServerSDK();
352
+ const stateData = await seedState(stateStorage);
353
+
354
+ worker.use(
355
+ http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
356
+ );
357
+
358
+ const response = await sdk.handleCallback({
359
+ request: (() => {
360
+ const request = new Request(`https://brandtegrity.io/auth/callback?code=abc123&state=${stateData.id}`);
361
+ // NOTE: `Request`'s constructor strips forbidden headers like `cookie`, so it must be set directly on the built request instead.
362
+ request.headers.set('cookie', `sty.returnTo=${encodeURIComponent('https://brandtegrity.io/dashboard')}`);
363
+ return request;
364
+ })(),
365
+ });
366
+
367
+ expect(response.status).toBe(302);
368
+ expect(response.headers.get('location')).toBe('https://brandtegrity.io/dashboard');
369
+
370
+ const clearedCookie = response.headers.getSetCookie().find((cookie) => cookie.startsWith('sty.returnTo='));
371
+
372
+ expect(clearedCookie).toContain('Expires=Thu, 01 Jan 1970');
373
+ });
374
+
375
+ test('should redirect to postLoginRedirectUri when there is no returnTo cookie', async () => {
376
+ const { sdk, stateStorage, options } = createServerSDK();
377
+ const stateData = await seedState(stateStorage);
378
+
379
+ worker.use(
380
+ http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
381
+ );
382
+
383
+ const response = await sdk.handleCallback(createMockEvent(`https://brandtegrity.io/auth/callback?code=abc123&state=${stateData.id}`));
384
+
385
+ expect(response.headers.get('location')).toBe(new URL(options.postLoginRedirectUri).toString());
386
+ });
387
+
388
+ test('should return a popup-closing HTML response when the login was initiated from a popup', async () => {
389
+ const { sdk, stateStorage } = createServerSDK();
390
+ const stateData = await seedState(stateStorage, { metadata: { display: 'popup' } });
391
+
392
+ worker.use(
393
+ http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
394
+ );
395
+
396
+ const response = await sdk.handleCallback(createMockEvent(`https://brandtegrity.io/auth/callback?code=abc123&state=${stateData.id}`));
397
+
398
+ expect(response.headers.get('content-type')).toContain('text/html');
399
+ await expect(response.text()).resolves.toContain('window.close()');
400
+ });
401
+
402
+ test('should redirect to /error when the state is missing', async () => {
403
+ const { sdk } = createServerSDK();
404
+
405
+ const response = await sdk.handleCallback(createMockEvent('https://brandtegrity.io/auth/callback?code=abc123&state=missing'));
406
+
407
+ expect(response.status).toBe(302);
408
+ const location = new URL(response.headers.get('location') as string);
409
+
410
+ expect(`${location.origin}${location.pathname}`).toBe('https://brandtegrity.io/error');
411
+ expect(location.searchParams.get('type')).toBe('protocol');
412
+ expect(location.searchParams.get('error')).toBe('ProtocolError');
413
+ expect(location.searchParams.get('error_description')).toBe('State not found or expired. Please try logging in again.');
414
+ });
415
+
416
+ test('should redirect to /error with the OIDC error when the token exchange fails', async () => {
417
+ const { sdk, stateStorage } = createServerSDK();
418
+ const stateData = await seedState(stateStorage);
419
+
420
+ worker.use(
421
+ http.post('https://brandtegrity.io/oauth2/token', () =>
422
+ HttpResponse.json({ error: 'invalid_grant', error_description: 'Code expired' }, { status: 400 }),
423
+ ),
424
+ );
425
+
426
+ const response = await sdk.handleCallback(createMockEvent(`https://brandtegrity.io/auth/callback?code=abc123&state=${stateData.id}`));
427
+
428
+ const location = new URL(response.headers.get('location') as string);
429
+
430
+ expect(location.searchParams.get('type')).toBe('oidc');
431
+ expect(location.searchParams.get('error')).toBe('invalid_grant');
432
+ expect(location.searchParams.get('error_description')).toBe('Code expired');
433
+ });
434
+ });
435
+
436
+ describe('handleRefresh', () => {
437
+ test('should refresh the session and return 204 when there is no returnTo', async () => {
438
+ const { sdk, storage, options } = createServerSDK();
439
+ await storage.set(options.storageTokenName, serializeSession(createSessionData()));
440
+
441
+ worker.use(
442
+ http.post('https://brandtegrity.io/oauth2/token', () =>
443
+ HttpResponse.json({ access_token: 'new-access-token', token_type: 'Bearer', expires_in: 3600 }),
444
+ ),
445
+ );
446
+
447
+ const response = await sdk.handleRefresh(createMockEvent('https://brandtegrity.io/auth/refresh'));
448
+
449
+ expect(response.status).toBe(204);
450
+ });
451
+
452
+ test('should redirect to a safe returnTo after refreshing', async () => {
453
+ const { sdk, storage, options } = createServerSDK();
454
+ await storage.set(options.storageTokenName, serializeSession(createSessionData()));
455
+ worker.use(
456
+ http.post('https://brandtegrity.io/oauth2/token', () =>
457
+ HttpResponse.json({ access_token: 'new-access-token', token_type: 'Bearer', expires_in: 3600 }),
458
+ ),
459
+ );
460
+
461
+ const response = await sdk.handleRefresh(createMockEvent('https://brandtegrity.io/auth/refresh?returnTo=/dashboard'));
462
+
463
+ expect(response.status).toBe(302);
464
+ expect(response.headers.get('location')).toBe('https://brandtegrity.io/dashboard');
465
+ });
466
+
467
+ test('should reject (not redirect to /error) when returnTo points to a different origin', async () => {
468
+ const { sdk } = createServerSDK();
469
+
470
+ await expect(sdk.handleRefresh(createMockEvent('https://brandtegrity.io/auth/refresh?returnTo=https://evil.example.com'))).rejects.toThrow(
471
+ /Invalid returnTo URL/,
472
+ );
473
+ });
474
+
475
+ test('should redirect to /error when there is no session to refresh', async () => {
476
+ const { sdk } = createServerSDK();
477
+
478
+ const response = await sdk.handleRefresh(createMockEvent('https://brandtegrity.io/auth/refresh'));
479
+
480
+ const location = new URL(response.headers.get('location') as string);
481
+
482
+ expect(`${location.origin}${location.pathname}`).toBe('https://brandtegrity.io/error');
483
+ expect(location.searchParams.get('error_description')).toBe('No session available to refresh');
484
+ });
485
+ });
486
+
487
+ describe('handleRevoke', () => {
488
+ test('should revoke the session and redirect to postLogoutRedirectUri', async () => {
489
+ const { sdk, storage, options } = createServerSDK();
490
+ await storage.set(options.storageTokenName, serializeSession(createSessionData()));
491
+
492
+ worker.use(http.post('https://brandtegrity.io/oauth2/revoke', () => HttpResponse.json({})));
493
+
494
+ const response = await sdk.handleRevoke(createMockEvent('https://brandtegrity.io/auth/revoke'));
495
+
496
+ expect(response.status).toBe(302);
497
+ expect(response.headers.get('location')).toBe(new URL(options.postLogoutRedirectUri).toString());
498
+ });
499
+
500
+ test('should redirect to /error with an "unknown" shape when a non-Error value is thrown', async () => {
501
+ const { sdk, storage } = createServerSDK();
502
+ vi.spyOn(storage, 'get').mockRejectedValueOnce('boom');
503
+
504
+ const response = await sdk.handleRevoke(createMockEvent('https://brandtegrity.io/auth/revoke'));
505
+
506
+ const location = new URL(response.headers.get('location') as string);
507
+
508
+ expect(`${location.origin}${location.pathname}`).toBe('https://brandtegrity.io/error');
509
+ expect(location.searchParams.get('type')).toBe('unknown');
510
+ expect(location.searchParams.get('error')).toBe('unknown_error');
511
+ expect(location.searchParams.get('error_description')).toBe('Unknown error');
512
+ });
513
+ });
514
+
515
+ describe('handleEntry', () => {
516
+ test('should redirect to the login page with the entry session data', async () => {
517
+ const { sdk, options } = createServerSDK();
518
+
519
+ worker.use(
520
+ http.get('https://brandtegrity.io/provider/flow/entry', () =>
521
+ HttpResponse.text('https://brandtegrity.io/entry?session_id=abc123&short_app_id=app&language=hu-HU'),
522
+ ),
523
+ );
524
+
525
+ const response = await sdk.handleEntry(createMockEvent('https://brandtegrity.io/auth/entry?challenge=abc123'));
526
+
527
+ expect(response.status).toBe(302);
528
+ const location = new URL(response.headers.get('location') as string);
529
+
530
+ expect(`${location.origin}${location.pathname}`).toBe(`https://brandtegrity.io${options.loginUri}`);
531
+ expect(location.searchParams.get('session_id')).toBe('abc123');
532
+ expect(location.searchParams.get('short_app_id')).toBe('app');
533
+ expect(location.searchParams.get('language')).toBe('hu-HU');
534
+ });
535
+
536
+ test('should redirect to /error when the entry request fails', async () => {
537
+ const { sdk } = createServerSDK();
538
+
539
+ worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => new HttpResponse('{}', { status: 400 })));
540
+
541
+ const response = await sdk.handleEntry(createMockEvent('https://brandtegrity.io/auth/entry?challenge=abc123'));
542
+
543
+ const location = new URL(response.headers.get('location') as string);
544
+
545
+ expect(`${location.origin}${location.pathname}`).toBe('https://brandtegrity.io/error');
546
+ });
547
+ });
548
+
549
+ describe('handleLogout', () => {
550
+ test('should redirect to the end-session URL', async () => {
551
+ const { sdk, storage, options } = createServerSDK();
552
+ await storage.set(options.storageTokenName, serializeSession(createSessionData({ id_token: 'id-token' })));
553
+
554
+ const response = await sdk.handleLogout(createMockEvent('https://brandtegrity.io/auth/logout'));
555
+
556
+ expect(response.status).toBe(302);
557
+ const location = new URL(response.headers.get('location') as string);
558
+
559
+ expect(`${location.origin}${location.pathname}`).toBe('https://brandtegrity.io/oauth2/sessions/logout');
560
+ expect(location.searchParams.get('id_token_hint')).toBe('id-token');
561
+ });
562
+
563
+ test('should redirect to /error with an "Error without category" shape when cleanup fails', async () => {
564
+ const { sdk, storage, options } = createServerSDK();
565
+ await storage.set(options.storageTokenName, serializeSession(createSessionData({ id_token: 'id-token' })));
566
+ vi.spyOn(storage, 'delete').mockRejectedValueOnce(new Error('storage failure'));
567
+
568
+ const response = await sdk.handleLogout(createMockEvent('https://brandtegrity.io/auth/logout'));
569
+
570
+ const location = new URL(response.headers.get('location') as string);
571
+
572
+ expect(`${location.origin}${location.pathname}`).toBe('https://brandtegrity.io/error');
573
+ expect(location.searchParams.get('type')).toBe('unknown');
574
+ expect(location.searchParams.get('error')).toBe('unknown_error');
575
+ expect(location.searchParams.get('error_description')).toBe('storage failure');
576
+ });
577
+ });
578
+
579
+ describe('handleBackChannelLogout', () => {
580
+ let keyPair: CryptoKeyPair;
581
+
582
+ function mockJwks(signingKey: unknown): void {
583
+ worker.use(http.get('https://brandtegrity.io/.well-known/jwks.json', () => HttpResponse.json({ keys: [signingKey] })));
584
+ }
585
+
586
+ beforeAll(async () => {
587
+ keyPair = await generateRSAKeyPair();
588
+ });
589
+
590
+ test('should return 501 when the storage does not support deleteByLogoutToken', async () => {
591
+ const { sdk } = createServerSDK();
592
+
593
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest('token'));
594
+
595
+ expect(response.status).toBe(501);
596
+ });
597
+
598
+ test('should return 400 for a request that is not form-urlencoded', async () => {
599
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
600
+
601
+ const response = await sdk.handleBackChannelLogout({ request: new Request('https://brandtegrity.io/auth/backchannel-logout', { method: 'POST' }) });
602
+
603
+ expect(response.status).toBe(400);
604
+ await expect(response.text()).resolves.toContain('Invalid content type');
605
+ });
606
+
607
+ test('should return 400 when logout_token is missing', async () => {
608
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
609
+
610
+ const response = await sdk.handleBackChannelLogout({
611
+ request: new Request('https://brandtegrity.io/auth/backchannel-logout', {
612
+ method: 'POST',
613
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
614
+ body: '',
615
+ }),
616
+ });
617
+
618
+ expect(response.status).toBe(400);
619
+ await expect(response.text()).resolves.toContain('Missing logout_token');
620
+ });
621
+
622
+ test('should return 400 for an unverifiable logout_token', async () => {
623
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
624
+
625
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest('not-a-jwt'));
626
+
627
+ expect(response.status).toBe(400);
628
+ await expect(response.text()).resolves.toContain('Invalid logout_token');
629
+ });
630
+
631
+ test('should return 400 when the iss claim does not match', async () => {
632
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
633
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-iss'));
634
+ const token = await signLogoutToken(buildLogoutTokenClaims({ iss: 'https://impostor.example.com/' }), keyPair.privateKey, 'kid-iss');
635
+
636
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
637
+
638
+ expect(response.status).toBe(400);
639
+ await expect(response.text()).resolves.toContain('invalid iss claim');
640
+ });
641
+
642
+ test('should return 400 when the aud claim does not match', async () => {
643
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
644
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-aud'));
645
+ const token = await signLogoutToken(buildLogoutTokenClaims({ aud: 'other-client' }), keyPair.privateKey, 'kid-aud');
646
+
647
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
648
+
649
+ expect(response.status).toBe(400);
650
+ await expect(response.text()).resolves.toContain('invalid aud claim');
651
+ });
652
+
653
+ test('should return 400 when the iat claim is missing or stale', async () => {
654
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
655
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-iat'));
656
+ const token = await signLogoutToken(buildLogoutTokenClaims({ iat: timestamp() - 1000 }), keyPair.privateKey, 'kid-iat');
657
+
658
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
659
+
660
+ expect(response.status).toBe(400);
661
+ await expect(response.text()).resolves.toContain('invalid or stale iat claim');
662
+ });
663
+
664
+ test('should return 400 when the jti claim is missing', async () => {
665
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
666
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-jti'));
667
+ const claims = buildLogoutTokenClaims();
668
+ delete claims.jti;
669
+ const token = await signLogoutToken(claims, keyPair.privateKey, 'kid-jti');
670
+
671
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
672
+
673
+ expect(response.status).toBe(400);
674
+ await expect(response.text()).resolves.toContain('missing jti claim');
675
+ });
676
+
677
+ test('should return 400 when the nonce claim is present', async () => {
678
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
679
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-nonce'));
680
+ const token = await signLogoutToken(buildLogoutTokenClaims({ nonce: 'should-not-be-here' }), keyPair.privateKey, 'kid-nonce');
681
+
682
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
683
+
684
+ expect(response.status).toBe(400);
685
+ await expect(response.text()).resolves.toContain('nonce claim must not be present');
686
+ });
687
+
688
+ test('should return 400 when the backchannel-logout event is missing', async () => {
689
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
690
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-events'));
691
+ const token = await signLogoutToken(buildLogoutTokenClaims({ events: {} }), keyPair.privateKey, 'kid-events');
692
+
693
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
694
+
695
+ expect(response.status).toBe(400);
696
+ await expect(response.text()).resolves.toContain(`missing ${BACKCHANNEL_LOGOUT_EVENT} in events`);
697
+ });
698
+
699
+ test('should return 400 when neither sid nor sub is present', async () => {
700
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
701
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-sub-sid'));
702
+ const claims = buildLogoutTokenClaims();
703
+ delete claims.sid;
704
+ const token = await signLogoutToken(claims, keyPair.privateKey, 'kid-sub-sid');
705
+
706
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
707
+
708
+ expect(response.status).toBe(400);
709
+ await expect(response.text()).resolves.toContain('must contain sid or sub claim');
710
+ });
711
+
712
+ test('should delete the session and return 200 for a valid logout_token', async () => {
713
+ const storage = createServerMockStorage();
714
+ const { sdk } = createServerSDK({ storage });
715
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-valid'));
716
+ const token = await signLogoutToken(buildLogoutTokenClaims(), keyPair.privateKey, 'kid-valid');
717
+
718
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
719
+
720
+ expect(response.status).toBe(200);
721
+ expect(storage.deleteByLogoutToken).toHaveBeenCalledWith({ sid: 'session-id' });
722
+ });
723
+
724
+ test('should reject a replayed logout_token', async () => {
725
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
726
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-replay'));
727
+ const token = await signLogoutToken(buildLogoutTokenClaims({ jti: 'replay-jti' }), keyPair.privateKey, 'kid-replay');
728
+
729
+ await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
730
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
731
+
732
+ expect(response.status).toBe(400);
733
+ await expect(response.text()).resolves.toContain('token already used');
734
+ });
735
+
736
+ test('should return 500 when deleting the session fails', async () => {
737
+ const storage = createServerMockStorage();
738
+
739
+ vi.mocked(storage.deleteByLogoutToken).mockRejectedValueOnce(new Error('db down'));
740
+ const { sdk } = createServerSDK({ storage });
741
+ mockJwks(await exportSigningKey(keyPair.publicKey, 'kid-fail'));
742
+ const token = await signLogoutToken(buildLogoutTokenClaims(), keyPair.privateKey, 'kid-fail');
743
+
744
+ const response = await sdk.handleBackChannelLogout(getPostLogoutTokenRequest(token));
745
+
746
+ expect(response.status).toBe(500);
747
+ await expect(response.text()).resolves.toContain('Failed to delete session: db down');
748
+ });
749
+ });
750
+
751
+ describe('handler', () => {
752
+ test('GET /auth/login', async () => {
753
+ const { sdk } = createServerSDK();
754
+ worker.use(
755
+ http.get(
756
+ 'https://brandtegrity.io/oauth2/auth',
757
+ () => new HttpResponse(null, { status: 302, headers: { location: 'https://brandtegrity.io/hosted-login' } }),
758
+ ),
759
+ );
760
+
761
+ const response = await sdk.handler(createMockEvent('https://brandtegrity.io/auth/login'));
762
+
763
+ expect(response?.headers.get('location')).toBe('https://brandtegrity.io/hosted-login');
764
+ });
765
+
766
+ test('GET /auth/register', async () => {
767
+ const { sdk } = createServerSDK();
768
+ let capturedUrl: URL | undefined;
769
+ worker.use(
770
+ http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
771
+ capturedUrl = new URL(request.url);
772
+ return new HttpResponse(null, { status: 302, headers: { location: 'https://brandtegrity.io/hosted-login' } });
773
+ }),
774
+ );
775
+
776
+ await sdk.handler(createMockEvent('https://brandtegrity.io/auth/register'));
777
+
778
+ expect(capturedUrl?.searchParams.get('prompt')).toBe('create');
779
+ });
780
+
781
+ test('GET /auth/callback', async () => {
782
+ const { sdk } = createServerSDK();
783
+
784
+ const response = await sdk.handler(createMockEvent('https://brandtegrity.io/auth/callback?code=abc123&state=missing'));
785
+
786
+ expect(response?.headers.get('location')).toContain('/error');
787
+ });
788
+
789
+ test('GET /auth/refresh', async () => {
790
+ const { sdk } = createServerSDK();
791
+
792
+ const response = await sdk.handler(createMockEvent('https://brandtegrity.io/auth/refresh'));
793
+
794
+ expect(response?.headers.get('location')).toContain('/error');
795
+ });
796
+
797
+ test('GET /auth/revoke', async () => {
798
+ const { sdk } = createServerSDK();
799
+
800
+ const response = await sdk.handler(createMockEvent('https://brandtegrity.io/auth/revoke'));
801
+
802
+ expect(response?.status).toBe(302);
803
+ });
804
+
805
+ test('GET /auth/entry', async () => {
806
+ const { sdk } = createServerSDK();
807
+ worker.use(
808
+ http.get('https://brandtegrity.io/provider/flow/entry', () =>
809
+ HttpResponse.text('https://brandtegrity.io/entry?session_id=abc123&short_app_id=app&language=hu-HU'),
810
+ ),
811
+ );
812
+
813
+ const response = await sdk.handler(createMockEvent('https://brandtegrity.io/auth/entry'));
814
+
815
+ expect(response?.headers.get('location')).toContain(sdk.options.loginUri);
816
+ });
817
+
818
+ test('GET /auth/logout', async () => {
819
+ const { sdk } = createServerSDK();
820
+
821
+ const response = await sdk.handler(createMockEvent('https://brandtegrity.io/auth/logout'));
822
+
823
+ expect(response?.status).toBe(302);
824
+ });
825
+
826
+ test('POST /auth/backchannel-logout', async () => {
827
+ const { sdk } = createServerSDK({ storage: createServerMockStorage() });
828
+
829
+ const response = await sdk.handler({ request: new Request('https://brandtegrity.io/auth/backchannel-logout', { method: 'POST' }) });
830
+
831
+ expect(response?.status).toBe(400);
832
+ });
833
+
834
+ test('returns null for unmatched routes', async () => {
835
+ const { sdk } = createServerSDK();
836
+
837
+ await expect(sdk.handler(createMockEvent('https://brandtegrity.io/not-a-route'))).resolves.toBeNull();
838
+ });
839
+
840
+ test('returns null when the method does not match', async () => {
841
+ const { sdk } = createServerSDK();
842
+
843
+ await expect(sdk.handler({ request: new Request('https://brandtegrity.io/auth/login', { method: 'POST' }) })).resolves.toBeNull();
844
+ });
845
+ });
846
+ });