@redacto.io/consent-sdk-react 10.0.0-beta.9 → 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 (264) 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/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 +724 -365
  11. package/dist/index.mjs +346 -201
  12. package/dist/montserrat.generated-NBTECYRW.mjs +22 -0
  13. package/dist/privacy-center.d.mts +0 -1
  14. package/dist/privacy-center.d.ts +0 -1
  15. package/dist/privacy-center.js +2 -66
  16. package/dist/privacy-center.mjs +2 -66
  17. package/dist/types-6Aj7faxL.d.mts +57 -0
  18. package/dist/types-6Aj7faxL.d.ts +57 -0
  19. package/package.json +10 -1
  20. package/.turbo/turbo-build.log +0 -22
  21. package/.turbo/turbo-test.log +0 -203
  22. package/.turbo/turbo-verify-pack.log +0 -6
  23. package/CHANGELOG.md +0 -353
  24. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.test.tsx +0 -934
  25. package/src/RedactoNoticeAssisted/RedactoNoticeAssisted.tsx +0 -2160
  26. package/src/RedactoNoticeAssisted/api/index.ts +0 -275
  27. package/src/RedactoNoticeAssisted/api/types.ts +0 -182
  28. package/src/RedactoNoticeAssisted/assets/redacto-logo.png +0 -0
  29. package/src/RedactoNoticeAssisted/constants.ts +0 -51
  30. package/src/RedactoNoticeAssisted/i18n.ts +0 -1408
  31. package/src/RedactoNoticeAssisted/index.ts +0 -1
  32. package/src/RedactoNoticeAssisted/injectStyles.ts +0 -251
  33. package/src/RedactoNoticeAssisted/styles.ts +0 -730
  34. package/src/RedactoNoticeAssisted/types.ts +0 -26
  35. package/src/RedactoNoticeAssisted/useMediaQuery.ts +0 -36
  36. package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +0 -1152
  37. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +0 -4308
  38. package/src/RedactoNoticeConsent/api/index.ts +0 -542
  39. package/src/RedactoNoticeConsent/api/sandbox.ts +0 -150
  40. package/src/RedactoNoticeConsent/api/types.ts +0 -515
  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 -80
  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 -93
  71. package/src/RedactoPrivacyCenter/api/actions.ts +0 -695
  72. package/src/RedactoPrivacyCenter/api/client.ts +0 -1003
  73. package/src/RedactoPrivacyCenter/api/config.ts +0 -33
  74. package/src/RedactoPrivacyCenter/api/fetcher.ts +0 -275
  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 -1821
  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 -195
  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 -155
  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 -252
  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 -57
  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 -283
  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/sandbox.ts +0 -104
  175. package/src/RedactoPrivacyCenter/lib/types.ts +0 -896
  176. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +0 -37
  177. package/src/RedactoPrivacyCenter/lib/utils.refresh-circuit.test.ts +0 -127
  178. package/src/RedactoPrivacyCenter/lib/utils.refresh-storage.test.ts +0 -85
  179. package/src/RedactoPrivacyCenter/lib/utils.session-expired.test.ts +0 -109
  180. package/src/RedactoPrivacyCenter/lib/utils.ts +0 -798
  181. package/src/RedactoPrivacyCenter/locales/as/translation.json +0 -342
  182. package/src/RedactoPrivacyCenter/locales/bn/translation.json +0 -342
  183. package/src/RedactoPrivacyCenter/locales/brx/translation.json +0 -342
  184. package/src/RedactoPrivacyCenter/locales/doi/translation.json +0 -342
  185. package/src/RedactoPrivacyCenter/locales/en/translation.json +0 -357
  186. package/src/RedactoPrivacyCenter/locales/gom/translation.json +0 -342
  187. package/src/RedactoPrivacyCenter/locales/gu/translation.json +0 -342
  188. package/src/RedactoPrivacyCenter/locales/hi/translation.json +0 -345
  189. package/src/RedactoPrivacyCenter/locales/kn/translation.json +0 -342
  190. package/src/RedactoPrivacyCenter/locales/ks/translation.json +0 -342
  191. package/src/RedactoPrivacyCenter/locales/mai/translation.json +0 -342
  192. package/src/RedactoPrivacyCenter/locales/ml/translation.json +0 -342
  193. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +0 -342
  194. package/src/RedactoPrivacyCenter/locales/mr/translation.json +0 -342
  195. package/src/RedactoPrivacyCenter/locales/ne/translation.json +0 -342
  196. package/src/RedactoPrivacyCenter/locales/or/translation.json +0 -342
  197. package/src/RedactoPrivacyCenter/locales/pa/translation.json +0 -342
  198. package/src/RedactoPrivacyCenter/locales/sa/translation.json +0 -342
  199. package/src/RedactoPrivacyCenter/locales/sat/translation.json +0 -342
  200. package/src/RedactoPrivacyCenter/locales/sd/translation.json +0 -342
  201. package/src/RedactoPrivacyCenter/locales/ta/translation.json +0 -342
  202. package/src/RedactoPrivacyCenter/locales/te/translation.json +0 -342
  203. package/src/RedactoPrivacyCenter/locales/ur/translation.json +0 -342
  204. package/src/RedactoPrivacyCenter/styles/applyBrandingTheme.ts +0 -66
  205. package/src/RedactoPrivacyCenter/styles/base.css +0 -117
  206. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +0 -3108
  207. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -831
  208. package/src/RedactoPrivacyCenter/styles/responsive.ts +0 -138
  209. package/src/RedactoPrivacyCenter/styles/variables.css +0 -96
  210. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +0 -387
  211. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +0 -468
  212. package/src/RedactoPrivacyCenter/table/index.ts +0 -2
  213. package/src/RedactoPrivacyCenter/types/images.d.ts +0 -29
  214. package/src/RedactoPrivacyCenter/ui/Footer.tsx +0 -69
  215. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +0 -54
  216. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +0 -93
  217. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +0 -47
  218. package/src/RedactoPrivacyCenter/ui/PCAvatar.test.tsx +0 -83
  219. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +0 -93
  220. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +0 -48
  221. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +0 -77
  222. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +0 -55
  223. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +0 -107
  224. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +0 -35
  225. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +0 -132
  226. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +0 -31
  227. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +0 -106
  228. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -206
  229. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +0 -433
  230. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +0 -149
  231. package/src/RedactoPrivacyCenter/ui/PCText.tsx +0 -41
  232. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +0 -43
  233. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +0 -37
  234. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +0 -121
  235. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +0 -25
  236. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -53
  237. package/src/RedactoPrivacyCenter/ui/index.ts +0 -18
  238. package/src/RedactoPrivacyCenter/utils/cn.ts +0 -28
  239. package/src/RedactoPrivacyCenter/utils/date.ts +0 -48
  240. package/src/RedactoPrivacyCenter/utils/status.ts +0 -111
  241. package/src/RedactoPrivacyCenter/utils/toast.ts +0 -130
  242. package/src/index.ts +0 -19
  243. package/src/privacy-center.ts +0 -113
  244. package/src/shared/api-errors.ts +0 -180
  245. package/src/shared/notice-consent-status.ts +0 -190
  246. package/src/shared/sandbox.ts +0 -6
  247. package/src/types/css.d.ts +0 -4
  248. package/src/types/images.d.ts +0 -19
  249. package/src/types/react-jsx.d.ts +0 -26
  250. package/tests/DataAvailabilityContext.test.tsx +0 -123
  251. package/tests/Form.test.tsx +0 -1424
  252. package/tests/ManageConsentErrorMapping.test.tsx +0 -85
  253. package/tests/ProductSelectionSection.test.tsx +0 -328
  254. package/tests/RequestSubmit.test.tsx +0 -135
  255. package/tests/RevocablePurpose.test.tsx +0 -146
  256. package/tests/RevokeWarningMessage.test.tsx +0 -151
  257. package/tests/consentClient.test.ts +0 -217
  258. package/tests/manageConsentServer.test.ts +0 -38
  259. package/tests/mocks.ts +0 -257
  260. package/tests/resolveContactFromJwt.test.ts +0 -44
  261. package/tests/setup.ts +0 -16
  262. package/tsconfig.json +0 -16
  263. package/tsup.config.ts +0 -31
  264. 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})`;