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