@redacto.io/consent-sdk-react 10.0.0-beta.11 → 10.0.0-beta.12

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 (259) hide show
  1. package/NOTICE.md +17 -0
  2. package/README.md +67 -3
  3. package/dist/chunk-FEGTW73Q.mjs +182 -0
  4. package/dist/fonts.d.mts +35 -0
  5. package/dist/fonts.d.ts +35 -0
  6. package/dist/fonts.js +183 -0
  7. package/dist/fonts.mjs +10 -0
  8. package/dist/index.d.mts +12 -1
  9. package/dist/index.d.ts +12 -1
  10. package/dist/index.js +613 -249
  11. package/dist/index.mjs +235 -85
  12. package/dist/montserrat.generated-NBTECYRW.mjs +22 -0
  13. package/dist/types-6Aj7faxL.d.mts +57 -0
  14. package/dist/types-6Aj7faxL.d.ts +57 -0
  15. package/package.json +10 -1
  16. package/.turbo/turbo-build.log +0 -22
  17. package/.turbo/turbo-test.log +0 -203
  18. package/.turbo/turbo-verify-pack.log +0 -6
  19. package/CHANGELOG.md +0 -383
  20. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +0 -934
  21. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +0 -2160
  22. package/src/RedactoNoticeAssisted/api/index.ts +0 -275
  23. package/src/RedactoNoticeAssisted/api/types.ts +0 -182
  24. package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
  25. package/src/RedactoNoticeAssisted/constants.ts +0 -51
  26. package/src/RedactoNoticeAssisted/i18n.ts +0 -1408
  27. package/src/RedactoNoticeAssisted/index.ts +0 -1
  28. package/src/RedactoNoticeAssisted/injectStyles.ts +0 -251
  29. package/src/RedactoNoticeAssisted/styles.ts +0 -730
  30. package/src/RedactoNoticeAssisted/types.ts +0 -26
  31. package/src/RedactoNoticeAssisted/useMediaQuery.ts +0 -36
  32. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +0 -1186
  33. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +0 -4299
  34. package/src/RedactoNoticeConsent/api/index.ts +0 -486
  35. package/src/RedactoNoticeConsent/api/sandbox.ts +0 -150
  36. package/src/RedactoNoticeConsent/api/types.ts +0 -505
  37. package/src/RedactoNoticeConsent/assets/redacto-logo.png +0 -0
  38. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +0 -602
  39. package/src/RedactoNoticeConsent/components/index.ts +0 -7
  40. package/src/RedactoNoticeConsent/constants.ts +0 -20
  41. package/src/RedactoNoticeConsent/index.ts +0 -1
  42. package/src/RedactoNoticeConsent/injectStyles.ts +0 -151
  43. package/src/RedactoNoticeConsent/language-codes.ts +0 -95
  44. package/src/RedactoNoticeConsent/services/storage.ts +0 -122
  45. package/src/RedactoNoticeConsent/styles.ts +0 -409
  46. package/src/RedactoNoticeConsent/types.ts +0 -80
  47. package/src/RedactoNoticeConsent/useMediaQuery.ts +0 -36
  48. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.test.tsx +0 -369
  49. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -709
  50. package/src/RedactoNoticeConsentInline/api/index.ts +0 -115
  51. package/src/RedactoNoticeConsentInline/api/notice-read-routing.test.ts +0 -72
  52. package/src/RedactoNoticeConsentInline/api/types.ts +0 -185
  53. package/src/RedactoNoticeConsentInline/assets/redacto-logo.png +0 -0
  54. package/src/RedactoNoticeConsentInline/index.ts +0 -1
  55. package/src/RedactoNoticeConsentInline/injectStyles.ts +0 -40
  56. package/src/RedactoNoticeConsentInline/styles.ts +0 -397
  57. package/src/RedactoNoticeConsentInline/types.ts +0 -51
  58. package/src/RedactoNoticeConsentInline/useMediaQuery.ts +0 -36
  59. package/src/RedactoPrivacyCenter/NotificationsPage.tsx +0 -182
  60. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +0 -19
  61. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +0 -156
  62. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +0 -17
  63. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +0 -79
  64. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +0 -36
  65. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +0 -177
  66. package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +0 -26
  67. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +0 -93
  68. package/src/RedactoPrivacyCenter/api/actions.ts +0 -682
  69. package/src/RedactoPrivacyCenter/api/client.ts +0 -1003
  70. package/src/RedactoPrivacyCenter/api/config.ts +0 -33
  71. package/src/RedactoPrivacyCenter/api/fetcher.ts +0 -275
  72. package/src/RedactoPrivacyCenter/api/orgBranding.ts +0 -16
  73. package/src/RedactoPrivacyCenter/api/upload.ts +0 -93
  74. package/src/RedactoPrivacyCenter/assets/character.svg +0 -31
  75. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +0 -9
  76. package/src/RedactoPrivacyCenter/assets/greenTick.svg +0 -4
  77. package/src/RedactoPrivacyCenter/assets/mail.svg +0 -3
  78. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +0 -9
  79. package/src/RedactoPrivacyCenter/assets/sentImg.svg +0 -9
  80. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +0 -9
  81. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +0 -252
  82. package/src/RedactoPrivacyCenter/components/ActivityTimeline.tsx +0 -239
  83. package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +0 -268
  84. package/src/RedactoPrivacyCenter/components/AppShell/MobileNavDrawer.tsx +0 -218
  85. package/src/RedactoPrivacyCenter/components/AppShell/NavItem.tsx +0 -205
  86. package/src/RedactoPrivacyCenter/components/AppShell/PageHeader.tsx +0 -107
  87. package/src/RedactoPrivacyCenter/components/AppShell/SidebarNav.tsx +0 -100
  88. package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +0 -339
  89. package/src/RedactoPrivacyCenter/components/AppShell/UserMenu.tsx +0 -245
  90. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useIsTruncated.ts +0 -28
  91. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useKeyboardShortcut.ts +0 -40
  92. package/src/RedactoPrivacyCenter/components/AppShell/hooks/useSidebarState.ts +0 -62
  93. package/src/RedactoPrivacyCenter/components/AppShell/usePrivacyCenterExitAction.ts +0 -36
  94. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +0 -38
  95. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +0 -31
  96. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +0 -79
  97. package/src/RedactoPrivacyCenter/components/CaseDetails/FinalizedTooltipWrapper.tsx +0 -25
  98. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +0 -96
  99. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +0 -97
  100. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +0 -156
  101. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +0 -171
  102. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +0 -23
  103. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +0 -116
  104. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +0 -43
  105. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +0 -71
  106. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +0 -469
  107. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +0 -426
  108. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +0 -1199
  109. package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +0 -201
  110. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -138
  111. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +0 -7
  112. package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +0 -179
  113. package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +0 -318
  114. package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +0 -333
  115. package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +0 -102
  116. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +0 -159
  117. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +0 -189
  118. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +0 -118
  119. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +0 -66
  120. package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +0 -222
  121. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +0 -331
  122. package/src/RedactoPrivacyCenter/components/Form.tsx +0 -1821
  123. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +0 -174
  124. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +0 -337
  125. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +0 -334
  126. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +0 -173
  127. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +0 -18
  128. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +0 -54
  129. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +0 -57
  130. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +0 -19
  131. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +0 -92
  132. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +0 -20
  133. package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +0 -58
  134. package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +0 -10
  135. package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +0 -112
  136. package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +0 -27
  137. package/src/RedactoPrivacyCenter/components/Pagination.tsx +0 -140
  138. package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +0 -76
  139. package/src/RedactoPrivacyCenter/components/PrivacyCenterSessionExpired.tsx +0 -97
  140. package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +0 -454
  141. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +0 -270
  142. package/src/RedactoPrivacyCenter/components/SelectUser.tsx +0 -95
  143. package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +0 -195
  144. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +0 -25
  145. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +0 -21
  146. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +0 -159
  147. package/src/RedactoPrivacyCenter/components/constant.ts +0 -52
  148. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +0 -155
  149. package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +0 -203
  150. package/src/RedactoPrivacyCenter/components/types.ts +0 -123
  151. package/src/RedactoPrivacyCenter/components/utils.tsx +0 -10
  152. package/src/RedactoPrivacyCenter/config/routes.ts +0 -9
  153. package/src/RedactoPrivacyCenter/config/table.ts +0 -25
  154. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +0 -252
  155. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +0 -47
  156. package/src/RedactoPrivacyCenter/context/BrandingContext.tsx +0 -52
  157. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.session-expired.test.tsx +0 -92
  158. package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +0 -181
  159. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +0 -11
  160. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +0 -96
  161. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +0 -30
  162. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +0 -80
  163. package/src/RedactoPrivacyCenter/context/ProfileSwitchContext.tsx +0 -15
  164. package/src/RedactoPrivacyCenter/context/types.ts +0 -57
  165. package/src/RedactoPrivacyCenter/lib/api.ts +0 -759
  166. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +0 -67
  167. package/src/RedactoPrivacyCenter/lib/constants.ts +0 -283
  168. package/src/RedactoPrivacyCenter/lib/i18n.ts +0 -81
  169. package/src/RedactoPrivacyCenter/lib/locales.ts +0 -102
  170. package/src/RedactoPrivacyCenter/lib/queries.ts +0 -141
  171. package/src/RedactoPrivacyCenter/lib/sandbox.ts +0 -104
  172. package/src/RedactoPrivacyCenter/lib/types.ts +0 -892
  173. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +0 -37
  174. package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +0 -127
  175. package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +0 -85
  176. package/src/RedactoPrivacyCenter/lib/utils.session-expired.test.ts +0 -109
  177. package/src/RedactoPrivacyCenter/lib/utils.ts +0 -798
  178. package/src/RedactoPrivacyCenter/locales/as/translation.json +0 -340
  179. package/src/RedactoPrivacyCenter/locales/bn/translation.json +0 -340
  180. package/src/RedactoPrivacyCenter/locales/brx/translation.json +0 -340
  181. package/src/RedactoPrivacyCenter/locales/doi/translation.json +0 -340
  182. package/src/RedactoPrivacyCenter/locales/en/translation.json +0 -355
  183. package/src/RedactoPrivacyCenter/locales/gom/translation.json +0 -340
  184. package/src/RedactoPrivacyCenter/locales/gu/translation.json +0 -340
  185. package/src/RedactoPrivacyCenter/locales/hi/translation.json +0 -343
  186. package/src/RedactoPrivacyCenter/locales/kn/translation.json +0 -340
  187. package/src/RedactoPrivacyCenter/locales/ks/translation.json +0 -340
  188. package/src/RedactoPrivacyCenter/locales/mai/translation.json +0 -340
  189. package/src/RedactoPrivacyCenter/locales/ml/translation.json +0 -340
  190. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +0 -340
  191. package/src/RedactoPrivacyCenter/locales/mr/translation.json +0 -340
  192. package/src/RedactoPrivacyCenter/locales/ne/translation.json +0 -340
  193. package/src/RedactoPrivacyCenter/locales/or/translation.json +0 -340
  194. package/src/RedactoPrivacyCenter/locales/pa/translation.json +0 -340
  195. package/src/RedactoPrivacyCenter/locales/sa/translation.json +0 -340
  196. package/src/RedactoPrivacyCenter/locales/sat/translation.json +0 -340
  197. package/src/RedactoPrivacyCenter/locales/sd/translation.json +0 -340
  198. package/src/RedactoPrivacyCenter/locales/ta/translation.json +0 -340
  199. package/src/RedactoPrivacyCenter/locales/te/translation.json +0 -340
  200. package/src/RedactoPrivacyCenter/locales/ur/translation.json +0 -340
  201. package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +0 -66
  202. package/src/RedactoPrivacyCenter/styles/base.css +0 -117
  203. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +0 -3108
  204. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -831
  205. package/src/RedactoPrivacyCenter/styles/responsive.ts +0 -138
  206. package/src/RedactoPrivacyCenter/styles/variables.css +0 -96
  207. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +0 -387
  208. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +0 -468
  209. package/src/RedactoPrivacyCenter/table/index.ts +0 -2
  210. package/src/RedactoPrivacyCenter/types/images.d.ts +0 -29
  211. package/src/RedactoPrivacyCenter/ui/Footer.tsx +0 -69
  212. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +0 -54
  213. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +0 -93
  214. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +0 -47
  215. package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +0 -83
  216. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +0 -93
  217. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +0 -48
  218. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +0 -77
  219. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +0 -55
  220. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +0 -107
  221. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +0 -35
  222. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +0 -132
  223. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +0 -31
  224. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +0 -106
  225. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -206
  226. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +0 -433
  227. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +0 -149
  228. package/src/RedactoPrivacyCenter/ui/PCText.tsx +0 -41
  229. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +0 -43
  230. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +0 -37
  231. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +0 -121
  232. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +0 -25
  233. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -53
  234. package/src/RedactoPrivacyCenter/ui/index.ts +0 -18
  235. package/src/RedactoPrivacyCenter/utils/cn.ts +0 -28
  236. package/src/RedactoPrivacyCenter/utils/date.ts +0 -48
  237. package/src/RedactoPrivacyCenter/utils/status.ts +0 -111
  238. package/src/RedactoPrivacyCenter/utils/toast.ts +0 -130
  239. package/src/index.ts +0 -19
  240. package/src/privacy-center.ts +0 -113
  241. package/src/shared/api-errors.ts +0 -180
  242. package/src/shared/notice-consent-status.ts +0 -190
  243. package/src/shared/sandbox.ts +0 -6
  244. package/src/types/css.d.ts +0 -4
  245. package/src/types/images.d.ts +0 -19
  246. package/src/types/react-jsx.d.ts +0 -26
  247. package/tests/DataAvailabilityContext.test.tsx +0 -123
  248. package/tests/Form.test.tsx +0 -1424
  249. package/tests/ProductSelectionSection.test.tsx +0 -328
  250. package/tests/RequestSubmit.test.tsx +0 -135
  251. package/tests/RevokeWarningMessage.test.tsx +0 -151
  252. package/tests/consentClient.test.ts +0 -217
  253. package/tests/manageConsentServer.test.ts +0 -38
  254. package/tests/mocks.ts +0 -257
  255. package/tests/resolveContactFromJwt.test.ts +0 -44
  256. package/tests/setup.ts +0 -16
  257. package/tsconfig.json +0 -16
  258. package/tsup.config.ts +0 -31
  259. package/vitest.config.ts +0 -16
@@ -1,798 +0,0 @@
1
- import { refreshOtpTokenServer } from './api';
2
- import { isTerminalRefreshError } from '../api/fetcher';
3
- import { getSandboxConfig } from './sandbox';
4
- import {
5
- MIN_REFRESH_BACKOFF_MS,
6
- MAX_REFRESH_BACKOFF_MS,
7
- REFRESH_TOKEN_STORAGE_KEY,
8
- LEGACY_REFRESH_TOKEN_COOKIE,
9
- } from './constants';
10
- import type { BaseUrlContextValue } from '../context/BaseUrlContext';
11
-
12
- // Token refresh lock to prevent concurrent refresh attempts
13
- let isRefreshing = false;
14
- let refreshPromise: Promise<string | null> | null = null;
15
-
16
- // ─── OTP refresh circuit breaker (incident 2026-06-22) ──────────────────────
17
- // ensureValidToken is called from 3 independent 30s pollers, before every
18
- // authenticated API call, and from render-driven effects. Without a memory of
19
- // failure, a single dead refresh token gets re-submitted on every one of those
20
- // triggers, hammering POST /otp/refresh until the tab closes. These guards make a
21
- // terminally-failed token cost exactly one request, and apply backoff to transient
22
- // failures.
23
- //
24
- // `deadRefreshToken` holds the *value* of a refresh token that terminally failed.
25
- // It auto-clears the moment a different token value appears (i.e. the user
26
- // re-authenticated), so the breaker never permanently bricks a session.
27
- let deadRefreshToken: string | null = null;
28
- // Transient-failure backoff window (epoch ms) and the current backoff duration.
29
- let transientRefreshFailureUntil = 0;
30
- let transientRefreshBackoffMs = 0;
31
-
32
- // ─── Terminal "session expired" signal ──────────────────────────────────────
33
- // The circuit breaker above stops the *network* (a dead refresh token costs one
34
- // request). This listener lets the React tree learn that the session is
35
- // terminally over so it can show a dedicated "session expired" screen and gate
36
- // every further fetch — instead of silently rendering empty/stale content or
37
- // hard-navigating away. A single module-level listener, consistent with the
38
- // single-instance breaker singleton above (one Privacy Center per page).
39
- let sessionExpiredListener: (() => void) | null = null;
40
-
41
- /**
42
- * Register (or clear, with `null`) the callback fired when a refresh terminally
43
- * fails — i.e. the refresh token is expired/invalid and re-authentication is
44
- * required. AuthProvider registers its `markSessionExpired` here. Exported for
45
- * that wiring and for tests; not part of the package's public API.
46
- */
47
- export const setSessionExpiredListener = (fn: (() => void) | null): void => {
48
- sessionExpiredListener = fn;
49
- };
50
-
51
- /** Notify the React tree that the session has terminally expired. */
52
- const notifySessionExpired = (): void => {
53
- sessionExpiredListener?.();
54
- };
55
-
56
- // Buffer time in seconds before token expiration to proactively refresh
57
- // Using 30 seconds to ensure tokens are refreshed before they expire
58
- const TOKEN_EXPIRY_BUFFER_SECONDS = 30;
59
-
60
- export const getCookieValue = (name: string) => {
61
- const cookies = document.cookie.split('; ');
62
- for (let cookie of cookies) {
63
- const [key, value] = cookie.split('=');
64
- if (key === name) {
65
- return value;
66
- }
67
- }
68
- return null;
69
- };
70
-
71
- // ─── Refresh token persistence ──────────────────────────────────────────────
72
- // The refresh token lives in localStorage (key REFRESH_TOKEN_STORAGE_KEY), not a
73
- // cookie. A path-scoped cookie is not readable when the host mounts the privacy
74
- // center under a basePath (e.g. consent-app at /consent → /consent/privacy-center/...),
75
- // so a rotated token can't be read back and the stale token gets re-presented,
76
- // causing a 401 "Refresh token not found or already used" storm. localStorage is
77
- // origin-scoped (no path matching), so it is readable under any basePath.
78
- //
79
- // localStorage access throws when storage is disabled or over quota (private
80
- // modes, embedded webviews). Guard every access so the SDK degrades to "refresh
81
- // fails -> re-auth" instead of crashing the render tree (setRefreshTokenCookie
82
- // runs during AuthProvider's render). Mirrors components/AppShell/hooks/useSidebarState.ts.
83
- const readStorage = (key: string): string | null => {
84
- try {
85
- return window.localStorage.getItem(key);
86
- } catch {
87
- return null;
88
- }
89
- };
90
-
91
- const writeStorage = (key: string, value: string): void => {
92
- try {
93
- window.localStorage.setItem(key, value);
94
- } catch {
95
- // localStorage unavailable / quota exceeded — ignore
96
- }
97
- };
98
-
99
- const removeStorage = (key: string): void => {
100
- try {
101
- window.localStorage.removeItem(key);
102
- } catch {
103
- // localStorage unavailable — ignore
104
- }
105
- };
106
-
107
- /**
108
- * Expire the SDK's own legacy refresh_token cookie (older versions wrote it at
109
- * path=/privacy-center). Scoped to that path only on purpose: a refresh_token
110
- * cookie at path=/ belongs to the host app, and clearing it would sign the host
111
- * out.
112
- */
113
- const clearLegacyRefreshTokenCookie = (): void => {
114
- if (typeof window === 'undefined') {
115
- return;
116
- }
117
- document.cookie = `${LEGACY_REFRESH_TOKEN_COOKIE}=; path=/privacy-center; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
118
- };
119
-
120
- /**
121
- * Persist the refresh token. Named `setRefreshTokenCookie` for backward
122
- * compatibility (it is part of the package's exported surface); it now writes
123
- * to localStorage rather than a cookie, and clears any cookie an older SDK
124
- * version left at path=/privacy-center.
125
- * @param refreshToken - The refresh token value to set
126
- */
127
- export const setRefreshTokenCookie = (refreshToken: string): void => {
128
- if (typeof window === 'undefined') {
129
- return;
130
- }
131
- writeStorage(REFRESH_TOKEN_STORAGE_KEY, refreshToken);
132
- clearLegacyRefreshTokenCookie();
133
- };
134
-
135
- /**
136
- * Read the persisted refresh token from localStorage.
137
- *
138
- * Intentionally does NOT fall back to reading a `refresh_token` cookie: a cookie
139
- * read is name-only (the path isn't visible to JS), so it cannot tell the SDK's
140
- * own legacy cookie from a host app's unrelated `refresh_token` cookie — reading
141
- * the latter would leak a host credential to the OTP refresh endpoint. Existing
142
- * sessions are preserved by AuthProvider re-seeding storage from the token prop.
143
- */
144
- export const getStoredRefreshToken = (): string | null => {
145
- if (typeof window === 'undefined') {
146
- return null;
147
- }
148
- return readStorage(REFRESH_TOKEN_STORAGE_KEY);
149
- };
150
-
151
- /**
152
- * Resolve the refresh token currently in play: from auth context (props mode) or
153
- * persisted storage (legacy mode). Centralised so the circuit breaker and the
154
- * refresh routine always agree on exactly which token value is being used.
155
- */
156
- const getActiveRefreshToken = (authContext?: { refreshToken?: string }): string | null => {
157
- if (authContext) {
158
- return authContext.refreshToken && authContext.refreshToken.trim() !== ''
159
- ? authContext.refreshToken
160
- : null;
161
- }
162
- if (typeof window === 'undefined') return null;
163
- return getStoredRefreshToken();
164
- };
165
-
166
- /**
167
- * Clear all OTP-refresh circuit-breaker state. Called after a successful refresh
168
- * and exposed for tests. Deliberately NOT called from signOutPrivacyCenter: in
169
- * props mode the refresh token lives in a prop that sign-out cannot clear, so
170
- * clearing the breaker there would immediately re-arm the retry storm. Recovery
171
- * instead happens automatically when a *different* token value appears.
172
- */
173
- export const resetRefreshCircuit = (): void => {
174
- deadRefreshToken = null;
175
- transientRefreshFailureUntil = 0;
176
- transientRefreshBackoffMs = 0;
177
- };
178
-
179
- /**
180
- * True when attempting a refresh right now would be pointless — the active refresh
181
- * token has already terminally failed, or we're inside the transient backoff
182
- * window. Pollers consult this to avoid scheduling doomed work.
183
- */
184
- export const isRefreshBlocked = (authContext?: { refreshToken?: string }): boolean => {
185
- const active = getActiveRefreshToken(authContext);
186
- if (active !== null && active === deadRefreshToken) return true;
187
- if (Date.now() < transientRefreshFailureUntil) return true;
188
- return false;
189
- };
190
-
191
- /** Record a transient refresh failure (network / 5xx) and grow the backoff window. */
192
- const noteTransientRefreshFailure = (): void => {
193
- transientRefreshBackoffMs = Math.min(
194
- Math.max(transientRefreshBackoffMs * 2, MIN_REFRESH_BACKOFF_MS),
195
- MAX_REFRESH_BACKOFF_MS,
196
- );
197
- transientRefreshFailureUntil = Date.now() + transientRefreshBackoffMs;
198
- };
199
-
200
- /** Trip the circuit breaker for a refresh token value that terminally failed. */
201
- const markRefreshTokenDead = (token: string | null): void => {
202
- if (token) {
203
- deadRefreshToken = token;
204
- }
205
- };
206
-
207
- /**
208
- * Decode JWT token and extract payload
209
- * @param token - JWT token string
210
- * @returns Decoded JWT payload or null if decoding fails
211
- */
212
- export const decodeJwtToken = (token: string): { organisation_uuid?: string; organisation_name?: string; workspace_uuid?: string; [key: string]: any } | null => {
213
- if (!token || typeof token !== 'string') {
214
- return null;
215
- }
216
-
217
- try {
218
- // JWT format: header.payload.signature
219
- const parts = token.split('.');
220
- if (parts.length !== 3) {
221
- // Malformed JWT
222
- return null;
223
- }
224
-
225
- // Decode payload (second part)
226
- const payload = parts[1];
227
-
228
- // Base64URL decode
229
- // Replace URL-safe characters and add padding if needed
230
- let base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
231
- const padding = base64.length % 4;
232
- if (padding) {
233
- base64 += '='.repeat(4 - padding);
234
- }
235
-
236
- // Decode base64
237
- const decodedPayload = atob(base64);
238
- const payloadObj = JSON.parse(decodedPayload) as { organisation_uuid?: string; organisation_name?: string; workspace_uuid?: string; [key: string]: any };
239
-
240
- return payloadObj;
241
- } catch (error) {
242
- // If decoding fails, return null
243
- console.error('Error decoding JWT:', error);
244
- return null;
245
- }
246
- };
247
-
248
- /**
249
- * Resolve the user's contact identifier from a decoded JWT.
250
- *
251
- * Identifiers live under `user_data` (primary_email / primary_mobile /
252
- * org_user_id); some token shapes also expose `contact`/`primary_email` at the
253
- * top level. Email is preferred, but a mobile-only login must still resolve to
254
- * the phone (or org_user_id) so case/grievance and consent-management actions —
255
- * which key off this contact — work without an email. The backend matches
256
- * identifiers type-aware (phone-to-phone), so storing the mobile here is safe.
257
- */
258
- export const resolveContactFromJwt = (
259
- decoded: { [key: string]: any } | null,
260
- ): string | null => {
261
- if (!decoded) {
262
- return null;
263
- }
264
- const userData = (decoded.user_data as Record<string, unknown>) || {};
265
- const candidate =
266
- decoded.contact ||
267
- decoded.primary_email ||
268
- userData.primary_email ||
269
- userData.primary_mobile ||
270
- userData.org_user_id;
271
- return typeof candidate === 'string' && candidate.trim() !== ''
272
- ? candidate
273
- : null;
274
- };
275
-
276
- /**
277
- * Decode JWT payload and check if token is expired or will expire soon
278
- * @param token - JWT token string
279
- * @param bufferSeconds - Buffer time in seconds before expiration to consider token expired (default: 5 minutes)
280
- * @returns true if token is expired, invalid, or will expire within buffer time; false if valid
281
- */
282
- export const isJwtExpired = (token: string, bufferSeconds: number = 5 * 60): boolean => {
283
- if (!token || typeof token !== 'string') {
284
- return true;
285
- }
286
-
287
- try {
288
- // JWT format: header.payload.signature
289
- const parts = token.split('.');
290
- if (parts.length !== 3) {
291
- // Malformed JWT
292
- return true;
293
- }
294
-
295
- // Decode payload (second part)
296
- const payload = parts[1];
297
-
298
- // Base64URL decode
299
- // Replace URL-safe characters and add padding if needed
300
- let base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
301
- const padding = base64.length % 4;
302
- if (padding) {
303
- base64 += '='.repeat(4 - padding);
304
- }
305
-
306
- // Decode base64
307
- const decodedPayload = atob(base64);
308
- const payloadObj: { exp?: number } = JSON.parse(decodedPayload) as { exp?: number };
309
-
310
- // Check if exp claim exists
311
- if (typeof payloadObj.exp !== 'number') {
312
- // Missing exp claim - treat as invalid
313
- return true;
314
- }
315
-
316
- // Get current time in seconds
317
- const now = Math.floor(Date.now() / 1000);
318
-
319
- // Check if token is expired or will expire within buffer time
320
- const isExpired = payloadObj.exp <= (now + bufferSeconds);
321
-
322
- // exp is in seconds since epoch
323
- return isExpired;
324
- } catch (error) {
325
- // If decoding fails, treat as expired/invalid
326
- console.error('Error decoding JWT:', error);
327
- return true;
328
- }
329
- };
330
-
331
- /**
332
- * Check if user is authenticated in privacy center
333
- * User is considered authenticated if both access_token and email exist in sessionStorage
334
- */
335
- export const isUserAuthenticated = (expectedSlug: string): boolean => {
336
- if (typeof window === 'undefined') return false;
337
- const accessToken = sessionStorage.getItem('access_token');
338
- const email = sessionStorage.getItem('email');
339
- const orgSlug = sessionStorage.getItem('pc_org_slug');
340
-
341
- // Require that tokens exist and the stored organization slug matches
342
- if (!accessToken || !email || !orgSlug) {
343
- return false;
344
- }
345
-
346
- return orgSlug === expectedSlug;
347
- };
348
-
349
- /**
350
- * Get initial state for sign-in form visibility
351
- * Returns true if window is undefined (SSR) or user is not authenticated
352
- * @param slug - The organization slug to check authentication against
353
- * @returns true if sign-in form should be shown, false otherwise
354
- */
355
- export const getInitialState = (slug: string): boolean => {
356
- if (typeof window === 'undefined') return true;
357
- return !isUserAuthenticated(slug);
358
- };
359
-
360
- /**
361
- * Check authentication status and attempt token refresh if needed
362
- * This function checks if the user is authenticated, validates JWT expiration,
363
- * and attempts to refresh using the refresh token cookie if the token is expired or missing.
364
- * @param expectedSlug - The organization slug to verify against
365
- * @param organizationId - Optional organization ID (will be read from sessionStorage if not provided)
366
- * @param workspaceId - Optional workspace ID (will be read from sessionStorage if not provided)
367
- * @returns Promise<boolean> - true if authenticated with valid non-expired token, false otherwise
368
- */
369
- export const checkAndRefreshAuth = async (
370
- expectedSlug: string,
371
- organizationId?: string,
372
- workspaceId?: string,
373
- consentClient?: BaseUrlContextValue['consentClient']
374
- ): Promise<boolean> => {
375
- if (typeof window === 'undefined') return false;
376
-
377
- // Sandbox PoC: the static token is always "valid" and there is nothing to
378
- // refresh, so authentication is satisfied unconditionally.
379
- if (getSandboxConfig()) {
380
- return true;
381
- }
382
-
383
- const accessToken = sessionStorage.getItem('access_token');
384
- const email = sessionStorage.getItem('email');
385
- const orgSlug = sessionStorage.getItem('pc_org_slug');
386
-
387
- // If slug doesn't match, user is not authenticated for this organization
388
- if (orgSlug && orgSlug !== expectedSlug) {
389
- return false;
390
- }
391
-
392
- // Check if we need to refresh (token missing or expired)
393
- const needsRefresh = !accessToken || (accessToken && isJwtExpired(accessToken, TOKEN_EXPIRY_BUFFER_SECONDS));
394
-
395
- // If we have a valid token and all required data, user is authenticated
396
- if (!needsRefresh && accessToken && email && orgSlug && orgSlug === expectedSlug) {
397
- return true;
398
- }
399
-
400
- // If token is missing or expired, and we have email and orgSlug, try to refresh
401
- if (needsRefresh && email && orgSlug === expectedSlug) {
402
- // Get organizationId and workspaceId from sessionStorage if not provided
403
- const orgId = organizationId || sessionStorage.getItem('pc_org_id') || '';
404
- const wsId = workspaceId || sessionStorage.getItem('pc_workspace_id') || '';
405
-
406
- if (!orgId || !wsId) {
407
- return false;
408
- }
409
-
410
- const refreshToken = getStoredRefreshToken();
411
- if (!refreshToken) {
412
- // No refresh token available - terminal: surface the session-expired screen.
413
- signOutPrivacyCenter();
414
- notifySessionExpired();
415
- return false;
416
- }
417
-
418
- // Circuit breaker (incident 2026-06-22): a new token re-arms; a known-dead
419
- // token or an open backoff window short-circuits without hitting the network.
420
- if (deadRefreshToken !== null && refreshToken !== deadRefreshToken) {
421
- resetRefreshCircuit();
422
- }
423
- if (refreshToken === deadRefreshToken || Date.now() < transientRefreshFailureUntil) {
424
- return false;
425
- }
426
-
427
- try {
428
- const result = await refreshOtpTokenServer(orgId, wsId, refreshToken, consentClient);
429
- if (result && result.success !== false) {
430
- const newAccessToken = (result as { access_token?: string })?.access_token;
431
- const newRefreshToken = (result as { refresh_token?: string })?.refresh_token;
432
-
433
- if (newAccessToken) {
434
- // Validate the new token is not expired before storing
435
- if (isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS)) {
436
- console.error('Refreshed access token is already expired - clearing auth state');
437
- markRefreshTokenDead(refreshToken);
438
- signOutPrivacyCenter();
439
- notifySessionExpired();
440
- return false;
441
- }
442
- sessionStorage.setItem('access_token', newAccessToken);
443
- // Success — re-arm the circuit breaker.
444
- resetRefreshCircuit();
445
- } else {
446
- // No access token in response - refresh failed
447
- console.error('Refresh response missing access token - clearing auth state');
448
- markRefreshTokenDead(refreshToken);
449
- signOutPrivacyCenter();
450
- notifySessionExpired();
451
- return false;
452
- }
453
-
454
- if (newRefreshToken) {
455
- setRefreshTokenCookie(newRefreshToken);
456
- }
457
-
458
- // Verify we now have a valid, non-expired access token
459
- const isValid = !!newAccessToken && !isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS);
460
- if (!isValid) {
461
- console.error('Refreshed token validation failed - clearing auth state');
462
- signOutPrivacyCenter();
463
- }
464
- return isValid;
465
- } else {
466
- // Refresh token expired/revoked or server returned failure - terminal.
467
- markRefreshTokenDead(refreshToken);
468
- signOutPrivacyCenter();
469
- notifySessionExpired();
470
- return false;
471
- }
472
- } catch (error: any) {
473
- if (isTerminalRefreshError(error)) {
474
- // Refresh token is expired / not found / already used - trip the breaker.
475
- markRefreshTokenDead(refreshToken);
476
- signOutPrivacyCenter();
477
- notifySessionExpired();
478
- return false;
479
- }
480
-
481
- // Transient (network / 5xx): keep auth state, back off, retry on a later call.
482
- noteTransientRefreshFailure();
483
- console.error('[Token Refresh] Unexpected error refreshing token:', error);
484
- return false;
485
- }
486
- }
487
-
488
- return false;
489
- };
490
-
491
- /**
492
- * Compare two email addresses (case-insensitive, handles URL encoding)
493
- * @param email1 - First email (can be URL-encoded)
494
- * @param email2 - Second email
495
- * @returns true if emails match (case-insensitive), false otherwise
496
- */
497
- export const compareEmails = (email1: string | null, email2: string | null): boolean => {
498
- if (!email1 || !email2) {
499
- return false;
500
- }
501
-
502
- // Decode URL-encoded email (handles %40 for @, %2B for +, etc.)
503
- const decodedEmail1 = decodeURIComponent(email1.trim());
504
- const normalizedEmail1 = decodedEmail1.toLowerCase();
505
- const normalizedEmail2 = email2.trim().toLowerCase();
506
-
507
- return normalizedEmail1 === normalizedEmail2;
508
- };
509
-
510
- /**
511
- * Sign out from privacy center only
512
- * Clears privacy-center specific storage and cookies without touching Supabase auth
513
- */
514
- export const signOutPrivacyCenter = (): void => {
515
- if (typeof window === 'undefined') return;
516
-
517
- const privacyCenterKeys = [
518
- 'access_token',
519
- 'email',
520
- 'i18nextLng',
521
- 'lang',
522
- 'pc_org_slug',
523
- 'pc_org_id',
524
- 'pc_workspace_id',
525
- 'pc_current_page',
526
- 'pc_grievance_view',
527
- 'pc_selected_case_uuid',
528
- ];
529
- privacyCenterKeys.forEach((key) => {
530
- sessionStorage.removeItem(key);
531
- });
532
-
533
- // Clear the persisted refresh token (and the SDK's own legacy cookie from
534
- // older versions). The host app's path=/ cookie is intentionally left alone.
535
- removeStorage(REFRESH_TOKEN_STORAGE_KEY);
536
- clearLegacyRefreshTokenCookie();
537
- };
538
-
539
- /**
540
- * Ensure a valid access token is available before making API calls.
541
- * This function checks if the current token is valid (not expired).
542
- * If expired or missing, it attempts to refresh using the refresh token.
543
- * If refresh fails (refresh token expired), it signs out and redirects to sign-in.
544
- *
545
- * @param organizationId - The organization UUID
546
- * @param workspaceId - The workspace UUID
547
- * @param slug - The organization slug for redirect
548
- * @param authContext - Optional auth context values (accessToken, refreshToken, onError, updateTokens)
549
- * @returns Promise<string | null> - Valid access token or null if auth failed
550
- */
551
- export const ensureValidToken = async (
552
- organizationId?: string,
553
- workspaceId?: string,
554
- slug?: string,
555
- authContext?: {
556
- accessToken?: string;
557
- refreshToken?: string;
558
- onError?: (error: Error) => boolean;
559
- updateTokens?: (accessToken: string, refreshToken?: string) => void;
560
- },
561
- consentClient?: BaseUrlContextValue['consentClient']
562
- ): Promise<string | null> => {
563
- if (typeof window === 'undefined') return null;
564
-
565
- // Sandbox PoC: no JWT, no expiry, no refresh — hand back the static token so
566
- // callers proceed straight to the request (which carries the X-Consent-Token
567
- // header instead of Authorization, and the acting identity in its payload).
568
- const sandbox = getSandboxConfig();
569
- if (sandbox) {
570
- return sandbox.token;
571
- }
572
-
573
- // Use context accessToken if provided, otherwise fall back to sessionStorage
574
- const accessToken = authContext?.accessToken || sessionStorage.getItem('access_token');
575
-
576
- // Check if current token is valid (exists, is a string, and not expired)
577
- // Use a buffer to proactively refresh before actual expiry
578
- if (
579
- accessToken &&
580
- typeof accessToken === 'string' &&
581
- accessToken.trim() !== '' &&
582
- !isJwtExpired(accessToken, TOKEN_EXPIRY_BUFFER_SECONDS)
583
- ) {
584
- return accessToken;
585
- }
586
-
587
- // Circuit breaker: before any network work, consult the failure memory.
588
- // (Incident 2026-06-22 — stops the /otp/refresh retry storm at its source.)
589
- const activeRefreshToken = getActiveRefreshToken(authContext);
590
- // A new refresh token value means the user re-authenticated — re-arm the breaker.
591
- if (deadRefreshToken !== null && activeRefreshToken !== deadRefreshToken) {
592
- resetRefreshCircuit();
593
- }
594
- // Known-dead token: do not touch the network until a different token appears.
595
- if (activeRefreshToken !== null && activeRefreshToken === deadRefreshToken) {
596
- return null;
597
- }
598
- // Transient-failure backoff window still open: skip the network.
599
- if (Date.now() < transientRefreshFailureUntil) {
600
- return null;
601
- }
602
-
603
- // Token is missing or expired, need to refresh
604
- // Use lock to prevent concurrent refresh attempts
605
- if (isRefreshing && refreshPromise) {
606
- // Wait for ongoing refresh to complete
607
- const newToken = await refreshPromise;
608
- // Return the new token directly (it's already stored in sessionStorage/context)
609
- return newToken;
610
- }
611
-
612
- // Start refresh process
613
- isRefreshing = true;
614
- refreshPromise = performTokenRefresh(organizationId, workspaceId, slug, authContext, consentClient);
615
-
616
- try {
617
- const newToken = await refreshPromise;
618
- // Return the new token directly (it's already stored in sessionStorage/context)
619
- return newToken;
620
- } finally {
621
- isRefreshing = false;
622
- refreshPromise = null;
623
- }
624
- };
625
-
626
- /**
627
- * Internal function to perform token refresh
628
- * @returns Promise<string | null> - The new access token on success, null on failure
629
- */
630
- const performTokenRefresh = async (
631
- organizationId?: string,
632
- workspaceId?: string,
633
- // Retained for call-site compatibility; redirect-by-slug was removed in favor
634
- // of the terminal "session expired" screen, so the value is intentionally unused.
635
- _slug?: string,
636
- authContext?: {
637
- accessToken?: string;
638
- refreshToken?: string;
639
- onError?: (error: Error) => boolean;
640
- updateTokens?: (accessToken: string, refreshToken?: string) => void;
641
- },
642
- consentClient?: BaseUrlContextValue['consentClient']
643
- ): Promise<string | null> => {
644
- // Get organizationId and workspaceId from sessionStorage if not provided
645
- const orgId = organizationId || sessionStorage.getItem('pc_org_id') || '';
646
- const wsId = workspaceId || sessionStorage.getItem('pc_workspace_id') || '';
647
-
648
- if (!orgId || !wsId) {
649
- console.error('[Token Refresh] Missing organizationId or workspaceId');
650
- const error = new Error('Missing organizationId or workspaceId');
651
- // Inform the host (informational only — we no longer auto-navigate; the
652
- // component surfaces a terminal "session expired" screen instead).
653
- authContext?.onError?.(error);
654
- signOutPrivacyCenter();
655
- notifySessionExpired();
656
- return null;
657
- }
658
-
659
- // Resolve the refresh token (props mode → context, legacy mode → storage) via
660
- // the shared helper so the value matches what the circuit breaker keyed on.
661
- const refreshTokenValue: string | null = getActiveRefreshToken(authContext);
662
-
663
- // Validate refreshToken is a non-empty string
664
- if (!refreshTokenValue || typeof refreshTokenValue !== 'string' || refreshTokenValue.trim() === '') {
665
- // No refresh token available - user needs to sign in again
666
- // Log presence only — never the raw token, which would leak a credential to
667
- // browser log collectors.
668
- console.warn('[Token Refresh] No valid refresh token available', {
669
- hasAuthContext: !!authContext,
670
- hasRefreshTokenInContext: !!authContext?.refreshToken?.trim(),
671
- hasRefreshTokenInStorage: !!getStoredRefreshToken(),
672
- });
673
- const error = new Error('No refresh token available');
674
- // Inform the host (informational only — we no longer auto-navigate).
675
- authContext?.onError?.(error);
676
- signOutPrivacyCenter();
677
- notifySessionExpired();
678
- return null;
679
- }
680
-
681
- // At this point, refreshTokenValue is guaranteed to be a non-empty string
682
- const refreshToken: string = refreshTokenValue;
683
-
684
- try {
685
- const result = await refreshOtpTokenServer(orgId, wsId, refreshToken, consentClient);
686
-
687
- if (result && result.success !== false) {
688
- const newAccessToken = (result as { access_token?: string })?.access_token;
689
- const newRefreshToken = (result as { refresh_token?: string })?.refresh_token;
690
-
691
- if (newAccessToken) {
692
- // Validate the new token is not expired before storing
693
- if (isJwtExpired(newAccessToken, TOKEN_EXPIRY_BUFFER_SECONDS)) {
694
- console.error('[Token Refresh] New access token is already expired');
695
- markRefreshTokenDead(refreshToken);
696
- const error = new Error('New access token is already expired');
697
- // Inform the host (informational only — we no longer auto-navigate).
698
- authContext?.onError?.(error);
699
- signOutPrivacyCenter();
700
- notifySessionExpired();
701
- return null;
702
- }
703
-
704
- // Update tokens via context if available, otherwise use storage
705
- if (authContext?.updateTokens) {
706
- authContext.updateTokens(newAccessToken, newRefreshToken);
707
- } else {
708
- sessionStorage.setItem('access_token', newAccessToken);
709
- if (newRefreshToken) {
710
- setRefreshTokenCookie(newRefreshToken);
711
- }
712
- }
713
-
714
- // Success — re-arm the circuit breaker so future failures are tracked afresh.
715
- resetRefreshCircuit();
716
- // Return the new access token directly (already stored in sessionStorage/context)
717
- return newAccessToken;
718
- } else {
719
- // No access token in response - refresh failed
720
- console.error('[Token Refresh] Response missing access token');
721
- markRefreshTokenDead(refreshToken);
722
- const error = new Error('Response missing access token');
723
- // Inform the host (informational only — we no longer auto-navigate).
724
- authContext?.onError?.(error);
725
- signOutPrivacyCenter();
726
- notifySessionExpired();
727
- return null;
728
- }
729
- } else {
730
- // Refresh token expired/revoked or server returned failure - terminal.
731
- markRefreshTokenDead(refreshToken);
732
- const error = new Error('Refresh token expired or revoked');
733
- // Inform the host (informational only — we no longer auto-navigate).
734
- authContext?.onError?.(error);
735
- signOutPrivacyCenter();
736
- notifySessionExpired();
737
- return null;
738
- }
739
- } catch (error: any) {
740
- if (isTerminalRefreshError(error)) {
741
- // Terminal: refresh token expired / not found / already used / 4xx. Trip the
742
- // breaker so this dead token is never re-submitted, then surface the
743
- // session-expired screen (onError is informational; we no longer navigate).
744
- markRefreshTokenDead(refreshToken);
745
- const err = new Error('Refresh token expired or revoked');
746
- authContext?.onError?.(err);
747
- signOutPrivacyCenter();
748
- notifySessionExpired();
749
- return null;
750
- }
751
-
752
- // Transient (network / 5xx): keep auth state, back off, and let a later call retry.
753
- noteTransientRefreshFailure();
754
- console.error('[Token Refresh] Unexpected error:', error);
755
- // Still call onError for unexpected errors
756
- if (authContext?.onError) {
757
- const err = error instanceof Error ? error : new Error(String(error));
758
- authContext.onError(err);
759
- }
760
- return null;
761
- }
762
- };
763
-
764
- // Activity helper functions
765
-
766
- export type ActivityBadgeStatus =
767
- | 'success'
768
- | 'warning'
769
- | 'error'
770
- | 'info'
771
- | 'pending';
772
-
773
- /**
774
- * Get badge status based on activity type
775
- */
776
- export const getActivityTypeStatus = (
777
- activityType: string
778
- ): ActivityBadgeStatus => {
779
- const type = activityType.toLowerCase();
780
- if (type.includes('consent') || type.includes('grant')) return 'success';
781
- if (type.includes('revoke') || type.includes('withdraw')) return 'error';
782
- if (type.includes('update') || type.includes('modify')) return 'warning';
783
- if (type.includes('view') || type.includes('access')) return 'info';
784
- return 'pending';
785
- };
786
-
787
- /**
788
- * Format activity type for display
789
- */
790
- export const formatActivityType = (activityType: string): string => {
791
- return activityType
792
- .split('_')
793
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
794
- .join(' ');
795
- };
796
-
797
-
798
- export const getVar = (varName: string): string => `var(${varName})`;