@redacto.io/consent-sdk-react 10.3.1 → 11.0.0-beta.2

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.
package/README.md CHANGED
@@ -117,6 +117,67 @@ npm install @tanstack/react-query @tanstack/react-table \
117
117
 
118
118
  If you only use `RedactoNoticeConsent` or `RedactoNoticeConsentInline`, these peers are not needed.
119
119
 
120
+ ### API host (`baseUrl`)
121
+
122
+ Pass the Redacto **API host** as `baseUrl`, for example `https://api.redacto.io`. Every component, `createConsentClient` and `BaseUrlProvider` take it, and the SDK reaches each backend service under it:
123
+
124
+ | Service | URL |
125
+ | --- | --- |
126
+ | Consent server | `{baseUrl}/consent` |
127
+ | Consent ledger | `{baseUrl}/consent-ledger` |
128
+ | User service (Privacy Center branding) | `{baseUrl}/user/api/0` |
129
+
130
+ A host behind a path prefix works the same way: `https://gateway.example.com/redacto` reaches the consent server at `https://gateway.example.com/redacto/consent`, and so on.
131
+
132
+ | Request | Service |
133
+ | --- | --- |
134
+ | `RedactoNoticeConsent` notice read | Ledger with a JWT; consent server in sandbox and in the magic-link (`applicationId`) flow |
135
+ | `RedactoNoticeConsent` notice audio | Ledger, sandbox included |
136
+ | `RedactoNoticeConsentInline` notice read | Ledger with an `accessToken`; consent server without one and in the magic-link (`applicationId`) flow |
137
+ | Consent writes (`submit-consent`) and the already-consented check (`check-consent`) | Ledger |
138
+ | `RedactoNoticeAssisted` notice, audio and OTP; guardian verification (DigiLocker) | Consent server |
139
+ | Privacy Center user consents, product purposes, receipts, manage consent | Ledger |
140
+ | Privacy Center DSAR form, cases, activities, identities, OTP, token exchange, notifications, uploads, products, settings | Consent server |
141
+ | Privacy Center org branding (by organisation UUID) | User service |
142
+
143
+ For custom Privacy Center compositions, `useBaseUrl()` returns the consent server URL as `baseUrl`, the ledger URL as `ledgerBaseUrl` and the host as `apiHost`.
144
+
145
+ #### The consent server URL (deprecated, still supported)
146
+
147
+ Earlier releases documented the **consent server URL** as `baseUrl` (`https://api.redacto.io/consent`), with an optional `ledgerBaseUrl`. Both keep working exactly as before, so upgrading changes nothing until you switch:
148
+
149
+ - With the consent server URL, every request goes where earlier releases sent it. Consent writes, the already-consented check and the Privacy Center's consents and receipts use the ledger only when `ledgerBaseUrl` is set; without it they stay on the consent server's legacy routes. `useBaseUrl()` returns both values as passed, with `apiHost` undefined.
150
+ - Without `baseUrl`, the notice components still use the built-in production default.
151
+ - The SDK tells the two apart by shape: an absolute `http(s)` URL that does not end in `/consent` is the API host; anything else (omitted, ending in `/consent`, or relative) is used as passed. A `baseUrl` ending in `/consent-ledger` resolves to its host the same way, so the ledger URL earlier releases took as `ledgerBaseUrl` also works as `baseUrl`. A consent server that is not mounted at `/consent` is therefore read as a host, so keep passing its URL ending in `/consent` until you switch.
152
+ - Each deprecated form logs one console warning per page load; passing the host clears them.
153
+
154
+ Compared with the consent server URL plus `ledgerBaseUrl`, the host changes two routes: `RedactoNoticeConsentInline` reads its notice from the ledger when it has an `accessToken`, and `RedactoNoticeConsent` plays sandbox audio from the ledger. The user service also keeps any path prefix of the host.
155
+
156
+ #### `ledgerBaseUrl` (deprecated)
157
+
158
+ Read only when `baseUrl` is the consent server URL (`…/consent`) or omitted, as in earlier releases. Ignored when `baseUrl` is the API host: the ledger is then always `{baseUrl}/consent-ledger`, and the SDK logs one console warning asking you to remove `ledgerBaseUrl`.
159
+
160
+ #### Local development
161
+
162
+ Put the services behind one local gateway, the way production does, and pass its address as `baseUrl`. For example, with [Caddy](https://caddyserver.com), the consent server on port 8000 and the ledger on port 8090:
163
+
164
+ ```caddyfile
165
+ :8080 {
166
+ handle /consent-ledger/* {
167
+ reverse_proxy localhost:8090
168
+ }
169
+ handle /consent/* {
170
+ reverse_proxy localhost:8000
171
+ }
172
+ }
173
+ ```
174
+
175
+ Then pass `baseUrl="http://localhost:8080"`. Both services serve their own path prefix (`/consent/…`, `/consent-ledger/…`), so the gateway forwards paths unchanged. Any reverse proxy works the same way (nginx, Traefik, your docker-compose gateway).
176
+
177
+ #### When your backend reuses the value
178
+
179
+ Your backend's token calls go to the consent server. If your frontend sends its `baseUrl` to your backend (as `base_url` in [Backend Server Setup](#backend-server-setup)), the backend appends `/consent` to the host: `{host}/consent/public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/generate`. A backend that still receives the consent server URL keeps appending `/public/...` to it directly.
180
+
120
181
  ## Sandbox Mode (No Backend Required)
121
182
 
122
183
  Sandbox mode lets you build and test a full consent PoC — **notice → Privacy Center → DSAR** — straight from your app, with **no backend and no token minting**. Instead of your server generating a JWT, you paste a **static sandbox token** into the SDK. Everything you create is recorded as test data, isolated from your live records.
@@ -138,6 +199,7 @@ Pass the sandbox `token`, org/workspace, and a test identity (`email`, `mobile`,
138
199
  import { RedactoNoticeConsent } from "@redacto.io/consent-sdk-react";
139
200
 
140
201
  <RedactoNoticeConsent
202
+ baseUrl="https://api.redacto.io"
141
203
  noticeId={NOTICE_UUID}
142
204
  token={SANDBOX_TOKEN}
143
205
  organisationUuid={ORG_UUID}
@@ -156,11 +218,11 @@ Point the Privacy Center at the same sandbox token and test identity. The same t
156
218
  import { RedactoPrivacyCenter } from "@redacto.io/consent-sdk-react/privacy-center";
157
219
 
158
220
  <RedactoPrivacyCenter
221
+ baseUrl="https://api.redacto.io"
159
222
  token={SANDBOX_TOKEN}
160
223
  organisationUuid={ORG_UUID}
161
224
  workspaceUuid={WORKSPACE_UUID}
162
225
  email="test.user@example.com" // or `mobile`, or `ucic`
163
- slug="acme-co"
164
226
  onError={(error) => {
165
227
  console.error(error);
166
228
  return false;
@@ -183,7 +245,7 @@ import { RedactoPrivacyCenter } from "@redacto.io/consent-sdk-react/privacy-cent
183
245
 
184
246
  - The SDK sends the sandbox token as an `X-Consent-Token` header and the test identity inside each request (query params on reads, body on writes) — no JWT decode, no token refresh (a `401` fails fast).
185
247
  - The server records everything under `environment=test`, namespaced so sandbox data never mixes with — or reads — your live records.
186
- - Sandbox always talks to the consent server; it never routes to the Go ledger.
248
+ - Sandbox requests go to the same services as live ones (see [API host](#api-host-baseurl)), except that the notice read stays on the consent server.
187
249
 
188
250
  ### Going live
189
251
 
@@ -216,6 +278,7 @@ app.post(
216
278
  async (req: Request, res: Response): Promise<void> => {
217
279
  try {
218
280
  const { email } = req.body;
281
+ // base_url is the API host the frontend passes as baseUrl, e.g. https://api.redacto.io
219
282
  const { organisation_uuid, workspace_uuid, base_url } = req.query;
220
283
 
221
284
  if (!CMS_API_KEY) {
@@ -224,7 +287,7 @@ app.post(
224
287
 
225
288
  // Call Redacto API to generate access token
226
289
  const response = await axios.post(
227
- `${base_url}/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/generate-access-token`,
290
+ `${base_url}/consent/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/generate`,
228
291
  {
229
292
  email,
230
293
  // expires_in_days: 1, // Optional: set token expiration
@@ -263,7 +326,7 @@ app.post(
263
326
 
264
327
  // Call Redacto API to refresh access token
265
328
  const response = await axios.post(
266
- `${base_url}/consent/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/refresh-token`,
329
+ `${base_url}/consent/public/organisations/${organisation_uuid}/workspaces/${workspace_uuid}/tokens/refresh`,
267
330
  { refresh_token: refresh_token },
268
331
  {
269
332
  headers: {
@@ -287,16 +350,16 @@ app.post(
287
350
 
288
351
  #### Redacto API Endpoints Used
289
352
 
290
- Your backend needs to call the following Redacto API endpoints:
353
+ Your backend needs to call the following Redacto API endpoints on the API host (e.g. `https://api.redacto.io`):
291
354
 
292
355
  1. **Generate Access Token**
293
- - **Endpoint**: `POST /public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/generate-access-token`
356
+ - **Endpoint**: `POST /consent/public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/generate`
294
357
  - **Headers**: `X-CMS-API-Key: {your-cms-api-key}`
295
358
  - **Body**: `{ email, expires_in_days? }`
296
359
  - **Response**: `{ detail: { token, refresh_token, expires_at } }`
297
360
 
298
361
  2. **Refresh Access Token**
299
- - **Endpoint**: `POST /consent/public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/refresh-token`
362
+ - **Endpoint**: `POST /consent/public/organisations/{organisation_uuid}/workspaces/{workspace_uuid}/tokens/refresh`
300
363
  - **Headers**: `X-CMS-API-Key: {your-cms-api-key}`
301
364
  - **Body**: `{ refresh_token }`
302
365
  - **Response**: `{ detail: { token, refresh_token, expires_at } }`
@@ -322,8 +385,8 @@ A modal popup component for consent collection. Displays a full-screen modal wit
322
385
  | `noticeId` | `string` | Yes | - | UUID of the consent notice |
323
386
  | `accessToken` | `string` | Yes | - | Authentication token for API calls |
324
387
  | `refreshToken` | `string` | Yes | - | Refresh token for token renewal |
325
- | `baseUrl` | `string` | No | - | Base URL for the consent API |
326
- | `ledgerBaseUrl` | `string` | No | - | Go ledger base URL. When set, consent writes and prior-consent checks route through the ledger (falls back: ledger → baseUrl → default). Omit to keep the legacy consent-server path. |
388
+ | `baseUrl` | `string` | No | Production API | API host, e.g. `https://api.redacto.io` (see [API host](#api-host-baseurl)). The consent server URL (`…/consent`) still works but is deprecated |
389
+ | `ledgerBaseUrl` | `string` | No | - | **Deprecated.** Read only when `baseUrl` is the consent server URL (`…/consent`) or omitted, as in earlier releases. Ignored when `baseUrl` is the API host. There, it routes consent writes and prior-consent checks through the ledger (falls back: ledger → baseUrl → default) |
327
390
  | `blockUI` | `boolean` | No | `true` | Whether to block UI interaction |
328
391
  | `language` | `string` | No | `"en"` | Language code |
329
392
  | `settings` | `object` | No | - | Optional code overrides; win over the console appearance key by key. See [Appearance](#notice-appearance) |
@@ -409,8 +472,8 @@ The component automatically submits consent when all of the following conditions
409
472
  | `notice_uuid` | `string` | Yes | - | Notice UUID |
410
473
  | `accessToken` | `string` | No | - | Authentication token |
411
474
  | `refreshToken` | `string` | No | - | Refresh token |
412
- | `baseUrl` | `string` | No | - | Base URL for the consent API |
413
- | `ledgerBaseUrl` | `string` | No | - | Go ledger base URL. When set, consent writes and already-consented checks route through the ledger. Omit for legacy behavior. |
475
+ | `baseUrl` | `string` | No | Production API | API host, e.g. `https://api.redacto.io` (see [API host](#api-host-baseurl)). The consent server URL (`…/consent`) still works but is deprecated |
476
+ | `ledgerBaseUrl` | `string` | No | - | **Deprecated.** Read only when `baseUrl` is the consent server URL (`…/consent`) or omitted, as in earlier releases. Ignored when `baseUrl` is the API host. There, it routes consent writes and already-consented checks through the ledger |
414
477
  | `language` | `string` | No | `"en"` | Language code |
415
478
  | `settings` | `object` | No | - | Optional code overrides of the console appearance |
416
479
  | `applicationId` | `string` | No | - | Application-specific UUID |
@@ -440,8 +503,8 @@ A modal, agent-assisted consent widget for collecting consent from an end-user w
440
503
  | `organisationUuid` | `string` | Yes | - | Organisation UUID (path segment of the public notice endpoint) |
441
504
  | `workspaceUuid` | `string` | Yes | - | Workspace UUID (path segment of the public notice endpoint) |
442
505
  | `noticeUuid` | `string` | Yes | - | Notice UUID to fetch and render |
443
- | `baseUrl` | `string` | No | Production consent API | Base URL for the consent API |
444
- | `ledgerBaseUrl` | `string` | No | - | Go ledger base URL. When set, the consent write routes through the ledger (falls back: ledger → baseUrl → default). Notice fetch, OTP, and TTS always stay on `baseUrl`. Omit for legacy behavior. |
506
+ | `baseUrl` | `string` | No | Production API | API host, e.g. `https://api.redacto.io` (see [API host](#api-host-baseurl)). The consent server URL (`…/consent`) still works but is deprecated |
507
+ | `ledgerBaseUrl` | `string` | No | - | **Deprecated.** Read only when `baseUrl` is the consent server URL (`…/consent`) or omitted, as in earlier releases. Ignored when `baseUrl` is the API host. There, it routes the consent write through the ledger (falls back: ledger → baseUrl → default); notice fetch, OTP, and TTS stay on the consent server |
445
508
  | `onDecline` | `() => void` | No | Logs and resets the flow | Called when the agent/customer declines (host should unmount the modal) |
446
509
  | `onComplete` | `() => void` | No | Logs and resets the flow | Called when the agent clicks "Proceed" on the confirmed screen |
447
510
 
@@ -478,6 +541,7 @@ function SignUpPage() {
478
541
 
479
542
  try {
480
543
  // Step 1: Get tokens from your backend
544
+ // BASE_URL is the Redacto API host, e.g. https://api.redacto.io
481
545
  const response = await fetch(
482
546
  `${BACKEND_URL}/api/consent/token?workspace_uuid=${WORKSPACE_UUID}&organisation_uuid=${ORG_UUID}&base_url=${BASE_URL}`,
483
547
  {
@@ -715,7 +779,7 @@ import { RedactoNoticeAssisted } from "@redacto.io/consent-sdk-react";
715
779
  const ORG_UUID = "your-organisation-uuid";
716
780
  const WORKSPACE_UUID = "your-workspace-uuid";
717
781
  const NOTICE_UUID = "your-notice-uuid";
718
- const BASE_URL = "https://api.redacto.io/consent"; // Optional: override the consent API
782
+ const BASE_URL = "https://api.redacto.io"; // The Redacto API host
719
783
 
720
784
  export function AssistedConsent() {
721
785
  const [open, setOpen] = useState(false);
@@ -960,7 +1024,6 @@ import { RedactoPrivacyCenter } from "@redacto.io/consent-sdk-react/privacy-cent
960
1024
 
961
1025
  <RedactoPrivacyCenter
962
1026
  baseUrl={BASE_URL}
963
- slug="acme-co"
964
1027
  accessToken={accessToken}
965
1028
  refreshToken={refreshToken}
966
1029
  settings={{ theme: "glass" }}
@@ -974,23 +1037,121 @@ import { RedactoPrivacyCenter } from "@redacto.io/consent-sdk-react/privacy-cent
974
1037
 
975
1038
  **Props:**
976
1039
 
977
- | Prop | Type | Required | Default | Description |
978
- | -------------- | -------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------- | -------------------------------------------------------- |
979
- | `baseUrl` | `string` | Yes | - | Consent server base URL |
980
- | `ledgerBaseUrl` | `string` | No | - | Go ledger base URL. When set, user consents, group purposes, receipts, and manage-consent route through the ledger, and the DSAR form's product picker is overlaid from ledger-served consents. Omit to use the legacy `/dsar/privacy-center/*` routes. |
981
- | `slug` | `string` | Yes | - | Workspace slug |
982
- | `accessToken` | `string` | Yes | - | JWT access token |
983
- | `refreshToken` | `string` | Yes | - | JWT refresh token |
984
- | `onError` | `(error: Error) => boolean` | Yes | - | Error handler; return `true` if handled |
985
- | `settings` | `{ theme?: "classic" \| "glass" }` | No | - | Presentation options; `theme` picks the visual style (see [Theme](#privacy-center-appearance)). Colours always come from workspace branding |
986
- | `initialPage` | `"consent-manager" \| "form" \| "activity" \| "case-details"` | No | `"consent-manager"` | Page to render first |
987
- | `onBack` | `"back" \| "signout"` | No | - | Behaviour of the navbar back/signout control |
1040
+ | Prop | Type | Required | Default | Description |
1041
+ | ----------------- | ------------------------------------------------- | -------- | ------------------- | ----------- |
1042
+ | `baseUrl` | `string` | Yes | - | API host, e.g. `https://api.redacto.io` (see [API host](#api-host-baseurl)). The consent server URL (`…/consent`) still works but is deprecated |
1043
+ | `ledgerBaseUrl` | `string` | No | - | **Deprecated.** Read only when `baseUrl` is the consent server URL (`…/consent`) or omitted, as in earlier releases. Ignored when `baseUrl` is the API host. There, it routes user consents, group purposes, receipts, and manage-consent through the ledger and overlays the DSAR form's product picker from ledger-served consents; omitted, the legacy `/dsar/privacy-center/*` routes are used |
1044
+ | `slug` | `string` | No | - | **Deprecated** and ignored; will be removed in a future major release. Passing it logs a one-time console warning. The organisation comes from the access token, or from `organisationUuid` and `workspaceUuid` |
1045
+ | `accessToken` | `string` | No | - | JWT access token. Without one the Privacy Center waits for it, or shows its built-in sign-in when `builtInSignIn` is on (see [Sign-in](#privacy-center-sign-in)) |
1046
+ | `refreshToken` | `string` | No | - | JWT refresh token |
1047
+ | `organisationUuid`| `string` | No | - | Organisation UUID for sign-in, settings and branding when there is no token; the built-in sign-in needs it with `workspaceUuid` (also the sandbox org) |
1048
+ | `workspaceUuid` | `string` | No | - | Workspace UUID for sign-in when there is no token; the built-in sign-in needs it with `organisationUuid` (also the sandbox workspace) |
1049
+ | `onError` | `(error: Error) => boolean` | Yes | - | Error handler; return `true` if handled |
1050
+ | `settings` | `PrivacyCenterSettings` | No | - | Overrides for the settings published from the console, merged field by field per group (see [Settings](#privacy-center-settings)). `theme` is kept as an alias for `branding.style` |
1051
+ | `builtInSignIn` | `boolean` | No | `false` | Show the built-in OTP sign-in when there is no token. Off, the Privacy Center keeps its loading state until the host passes a token |
1052
+ | `initialPage` | `"consent-manager" \| "form" \| "activity" \| "case-details" \| "notifications" \| "receipts"` | No | `"consent-manager"` | Page to render first; a section switched off in the settings falls back to the first enabled one |
1053
+ | `onBack` | `"back" \| "signout"` | No | - | Behaviour of the navbar back/signout control |
1054
+ | `onReady` | `(event: { signedIn: boolean }) => void` | No | - | Called once, when the first screen (sign-in or the Privacy Center) is on show |
1055
+ | `onConsentChange` | `(change: PrivacyCenterConsentChange) => void` | No | - | Called after a consent was withdrawn, re-granted or renewed |
1056
+ | `onRequestSubmit` | `(request: PrivacyCenterRequestSubmission) => void` | No | - | Called after a data request (case) was created |
1057
+ | `onSignOut` | `(event: { reason: "user" \| "timeout" \| "session-expired" }) => void` | No | - | Called when the visitor is signed out |
1058
+
1059
+ The callbacks never throw into the Privacy Center: an exception in a handler is logged and ignored. Their payloads:
1060
+
1061
+ ```ts
1062
+ type PrivacyCenterConsentChange = {
1063
+ action: "revoke" | "regrant" | "renew";
1064
+ purposeUuid: string;
1065
+ productUuid: string | null;
1066
+ dataElementUuids: readonly string[]; // empty when the change covered the whole purpose
1067
+ nominatorUuid: string | null; // set when acting on behalf of a nominator
1068
+ };
1069
+
1070
+ type PrivacyCenterRequestSubmission = {
1071
+ caseId: string; // the case id shown to the visitor
1072
+ caseUuid: string;
1073
+ requestType: "access" | "correction" | "erasure" | "nomination" | "grievance";
1074
+ productUuid: string | null; // null for nomination and grievance
1075
+ };
1076
+ ```
1077
+
1078
+ <a id="privacy-center-settings"></a>
1079
+
1080
+ ### Settings
1081
+
1082
+ The Privacy Center reads the settings an admin publishes in the Redacto console (Rights Management › Privacy Center › Settings) from the consent server's public endpoint `GET {consent server}/public/organisations/{organisation_uuid}/privacy-center-settings/?lang={language}` (`{baseUrl}/consent/public/...` with the API host). The organisation comes from `organisationUuid`, the access token, or the session. The settings are fetched again, localised, when the visitor changes language, and cached in memory per organisation and language.
1083
+
1084
+ Rendering never waits for them: the Privacy Center starts with the defaults and swaps the published values in when they arrive. **With nothing published, or if the request fails, it looks and behaves exactly as before settings existed.**
1085
+
1086
+ What the settings control:
1087
+
1088
+ - **Branding**: classic or glass style, the organisation's own colours, font, name and logo instead of the workspace branding, logo height and corner radius.
1089
+ - **Menu**: section order, labels, and which sections are on. A section switched off disappears from the menu and can't be opened; switching off `notifications` hides the bell too.
1090
+ - **Welcome message** at the top of the consent manager.
1091
+ - **Requests**: which request types the form offers, in what order and under what names (narrowed to the server's own list when it sends one), which grievance categories are offered, whether erasure also revokes consent, and the default withdraw warning (a purpose's own warning still wins).
1092
+ - **Languages**: which languages the switcher offers, whether it shows, the default language and whether the browser's language is detected. Once settings are published (or the host overrides them), Urdu, Sindhi and Kashmiri render right to left; with nothing published the text direction is inherited as before.
1093
+ - **Session timeout**: sign the visitor out after 15, 30 or 60 minutes without activity.
1094
+ - **Footer**: links, privacy policy, grievance contact, copyright and "Powered by".
1095
+ - **Sign-in screen**: layout, background, copy, channels and default country code (see below).
1096
+ - **Custom CSS** (see below).
1097
+
1098
+ Copy can use `{brand}`, `{legal}`, `{year}`, `{contact}` and `{name}`; `resolvePlaceholders(text, context)` is exported for the same substitution in your own UI.
1099
+
1100
+ Override any of it in code; each group merges field by field over what was published:
1101
+
1102
+ ```tsx
1103
+ <RedactoPrivacyCenter
1104
+ baseUrl={BASE_URL}
1105
+ accessToken={accessToken}
1106
+ onError={() => false}
1107
+ settings={{
1108
+ branding: { style: "glass" },
1109
+ portal: { welcome_enabled: true, session_timeout_minutes: 30 },
1110
+ languages: { enabled: ["en", "hi"], default: "en" },
1111
+ }}
1112
+ />
1113
+ ```
1114
+
1115
+ The full shape is exported as `ResolvedPrivacyCenterSettings` (with `DEFAULT_PRIVACY_CENTER_SETTINGS`); `PrivacyCenterSettings` is the override type.
1116
+
1117
+ <a id="privacy-center-sign-in"></a>
1118
+
1119
+ ### Sign-in
1120
+
1121
+ Pass `builtInSignIn` and, with no `accessToken`, the Privacy Center shows its own sign-in instead of a loader: the visitor enters a mobile number or email, receives a one-time code (resend after 50 seconds) and is signed straight in with the tokens it issues. Layout (card, split or minimal), background, copy, channels (SMS, email or both), default country code, notice line and support link come from the `sign_in` settings. It is off by default, so a host that passes its token a moment after mounting never flashes a sign-in screen.
1122
+
1123
+ ```tsx
1124
+ <RedactoPrivacyCenter
1125
+ baseUrl={BASE_URL}
1126
+ organisationUuid={ORG_UUID}
1127
+ workspaceUuid={WORKSPACE_UUID}
1128
+ builtInSignIn
1129
+ onError={() => false}
1130
+ />
1131
+ ```
1132
+
1133
+ ### Custom CSS
1134
+
1135
+ `custom_css` from the settings is injected inside an `@scope` for this Privacy Center only, after the same safety checks as a notice's custom CSS, so it never reaches the host page. Target these stable parts:
1136
+
1137
+ | Part | Element |
1138
+ | --- | --- |
1139
+ | `[data-redacto-part="topbar"]` | Top bar |
1140
+ | `[data-redacto-part="nav"]` / `"nav-item"` | Menu and each menu entry |
1141
+ | `[data-redacto-part="welcome"]` | Welcome message |
1142
+ | `[data-redacto-part="consent-card"]` | Consent manager card (and each product card on phones) |
1143
+ | `[data-redacto-part="request-form"]` | Data request form |
1144
+ | `[data-redacto-part="button-primary"]` | Primary buttons |
1145
+ | `[data-redacto-part="footer"]` | Footer |
1146
+ | `[data-redacto-part="signin-card"]` | Sign-in form |
1147
+
1148
+ The `--pc-*` theme variables are scoped the same way: workspace branding and the settings' colours apply to this Privacy Center (and its menus and tooltips) without writing to the host page's `<html>`.
988
1149
 
989
1150
  <a id="privacy-center-appearance"></a>
990
1151
 
991
1152
  ### Theme
992
1153
 
993
- `settings.theme` picks the visual style; colours, logo and brand name still come from the workspace's branding.
1154
+ `settings.theme` (or `settings.branding.style`) picks the visual style; colours, logo and brand name come from the workspace's branding unless the published settings replace them.
994
1155
 
995
1156
  - **`classic`** (default): the standard look.
996
1157
  - **`glass`**: frosted, translucent surfaces over a soft wash of the brand colour, pill-shaped controls and motion. On phones, dialogs and the navigation menu rise as bottom sheets; on wider screens dialogs float as centred cards. Motion respects `prefers-reduced-motion`.
@@ -1016,11 +1177,11 @@ These rely on the same context providers (`AuthProvider`, `BaseUrlProvider`, `Na
1016
1177
 
1017
1178
  For consumers who want their own UI, the headless core exposes the API client, orchestration helpers, hooks, and auth utilities:
1018
1179
 
1019
- - **API client**: `createConsentClient` (returns a typed `BaseRequest`-aware fetcher)
1180
+ - **API client**: `createConsentClient(baseUrl, ledgerBaseUrl?)` (takes the API host, like the components; returns a typed `BaseRequest`-aware fetcher that picks the service per request. The consent server URL and `ledgerBaseUrl` still work as before, deprecated; `ledgerBaseUrl` is ignored with the API host)
1020
1181
  - **Orchestration helpers** (handle auth retry / token refresh internally): `getFormData`, `createCaseRequest`, `fetchProducts`, `fetchUserConsents`, `manageConsent`, `fetchActivities`, `fetchCaseHistory`, `createOtp`, `verifyOtp`, `exchangeToken`
1021
1182
  - **Auth utilities**: `decodeJwtToken`, `isJwtExpired`, `ensureValidToken`, `signOutPrivacyCenter`, `getCookieValue`, `setRefreshTokenCookie`, `checkAndRefreshAuth`, `compareEmails`, `isUserAuthenticated`, `getInitialState`, `getActivityTypeStatus`, `formatActivityType`
1022
1183
  - **React Query hook**: `useCaseHistory`
1023
- - **Context providers**: `AuthProvider` + `useAuth`, `BaseUrlProvider` + `useBaseUrl`, `NavigationProvider` + `useInternalNavigation`
1184
+ - **Context providers**: `AuthProvider` + `useAuth`, `BaseUrlProvider` + `useBaseUrl` (`BaseUrlProvider` takes the API host as `baseUrl`; `useBaseUrl()` returns the consent server URL as `baseUrl`, the ledger URL as `ledgerBaseUrl`, the host as `apiHost` and the client), `NavigationProvider` + `useInternalNavigation`
1024
1185
  - **Types**: `Workspace`, `OrganizationDetail`, `DataElement`, `Purpose`, `UserConsent`, `Activity`, `CaseHistoryDetail`, `RequestTypeEnum`, `GrievanceTypeEnum`, `ConsentStatusEnum`, and more (see `privacy-center.ts` for the full export list)
1025
1186
 
1026
1187
  ### Style Injection
@@ -1073,7 +1234,7 @@ injectPCStyles();
1073
1234
  - Verify authentication setup is correct for your environment
1074
1235
  - Check that API key generation is working
1075
1236
  - Ensure organisation and workspace UUIDs are correct
1076
- - Verify base URL matches your environment (staging vs production)
1237
+ - Verify `base_url` is your environment's API host (staging vs production) and that your backend calls the consent server under it (`{host}/consent/...`)
1077
1238
 
1078
1239
  ### Validation not working
1079
1240
 
@@ -1085,11 +1246,11 @@ injectPCStyles();
1085
1246
 
1086
1247
  ### Redacto CMP API Endpoints
1087
1248
 
1088
- The SDK communicates with Redacto's CMP API. Key endpoints used:
1249
+ The SDK communicates with Redacto's CMP API on the API host (see [API host](#api-host-baseurl)). Key endpoints used:
1089
1250
 
1090
- - **Get Consent Notice**: `GET /public/organisations/{org_uuid}/workspaces/{workspace_uuid}/notices/{notice_uuid}`
1091
- - **Submit Consent**: `POST /public/organisations/{org_uuid}/workspaces/{workspace_uuid}/submit-consent`
1092
- - **Generate Access Token**: `POST /public/organisations/{org_uuid}/workspaces/{workspace_uuid}/tokens/generate-access-token`
1251
+ - **Get Consent Notice** (JWT session): `GET {baseUrl}/consent-ledger/public/organisations/{org_uuid}/workspaces/{workspace_uuid}/notices/{notice_uuid}`
1252
+ - **Submit Consent**: `POST {baseUrl}/consent-ledger/public/organisations/{org_uuid}/workspaces/{workspace_uuid}/submit-consent`
1253
+ - **Generate Access Token** (your backend): `POST {baseUrl}/consent/public/organisations/{org_uuid}/workspaces/{workspace_uuid}/tokens/generate`
1093
1254
 
1094
1255
  For complete API documentation, visit: [Redacto API Documentation](https://staging.api.redacto.io/consent/docs)
1095
1256
 
@@ -218,6 +218,8 @@ var NOTICE_DISALLOWED_CSS_TOKENS = [
218
218
  "expression(",
219
219
  "behavior:"
220
220
  ];
221
+ var CSS_SCOPE_AT_RULE = "@scope";
222
+ var CSS_STRING_SPECIALS_RE = /["\\\n]/g;
221
223
  var CSS_ESCAPE_RE = /\\([0-9a-f]{1,6})(?:\r\n|[ \t\r\n\f])?|\\([\s\S])/gi;
222
224
  var CSS_BRACKET_PAIRS = {
223
225
  "{": "}",
@@ -255,6 +257,63 @@ var buildTtsCssVariables = (colors) => Object.fromEntries(
255
257
  );
256
258
  var buildTtsRootAttributes = (colors) => colors.tts_highlight_text ? { [TTS_TEXT_ATTRIBUTE]: "" } : {};
257
259
 
260
+ // src/shared/appearance/customCss.ts
261
+ var decodeCssEscapes = (value) => value.replace(
262
+ CSS_ESCAPE_RE,
263
+ (_match, hex, literal) => {
264
+ if (hex === void 0) {
265
+ return literal;
266
+ }
267
+ const codePoint = Number.parseInt(hex, HEX_RADIX);
268
+ if (codePoint === 0 || codePoint > MAX_CODE_POINT || codePoint >= SURROGATE_MIN && codePoint <= SURROGATE_MAX) {
269
+ return CSS_REPLACEMENT_CHARACTER;
270
+ }
271
+ return String.fromCodePoint(codePoint);
272
+ }
273
+ );
274
+ var hasBalancedBlocks = (css) => {
275
+ const open = [];
276
+ let quote;
277
+ let inComment = false;
278
+ for (let index = 0; index < css.length; index += 1) {
279
+ const char = css[index];
280
+ if (inComment) {
281
+ if (char === "*" && css[index + 1] === "/") {
282
+ inComment = false;
283
+ index += 1;
284
+ }
285
+ } else if (char === "\\") {
286
+ index += 1;
287
+ } else if (quote) {
288
+ if (char === quote) {
289
+ quote = void 0;
290
+ } else if (CSS_NEWLINES.includes(char)) {
291
+ return false;
292
+ }
293
+ } else if (char === "/" && css[index + 1] === "*") {
294
+ inComment = true;
295
+ index += 1;
296
+ } else if (CSS_QUOTES.includes(char)) {
297
+ quote = char;
298
+ } else if (char in CSS_BRACKET_PAIRS) {
299
+ open.push(CSS_BRACKET_PAIRS[char]);
300
+ } else if (CSS_CLOSING_BRACKETS.includes(char) && open.pop() !== char) {
301
+ return false;
302
+ }
303
+ }
304
+ return open.length === 0 && !quote && !inComment;
305
+ };
306
+ var isSafeCustomCss = (css) => {
307
+ if (css.length > NOTICE_CUSTOM_CSS_MAX_LENGTH || !hasBalancedBlocks(css)) {
308
+ return false;
309
+ }
310
+ const lowered = css.toLowerCase();
311
+ const decoded = decodeCssEscapes(css).toLowerCase();
312
+ return NOTICE_DISALLOWED_CSS_TOKENS.every(
313
+ (token) => !lowered.includes(token) && !decoded.includes(token)
314
+ );
315
+ };
316
+
258
317
  // src/shared/appearance/registry.ts
259
318
  var DEFAULT_APPEARANCE = "classic";
260
319
  var APPEARANCE_ATTRIBUTE = "data-redacto-appearance";
@@ -325,63 +384,6 @@ var prefersReducedMotion = () => {
325
384
  return typeof window !== "undefined" && ((_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, "(prefers-reduced-motion: reduce)")) == null ? void 0 : _b.matches) === true;
326
385
  };
327
386
 
328
- // src/shared/appearance/customCss.ts
329
- var decodeCssEscapes = (value) => value.replace(
330
- CSS_ESCAPE_RE,
331
- (_match, hex, literal) => {
332
- if (hex === void 0) {
333
- return literal;
334
- }
335
- const codePoint = Number.parseInt(hex, HEX_RADIX);
336
- if (codePoint === 0 || codePoint > MAX_CODE_POINT || codePoint >= SURROGATE_MIN && codePoint <= SURROGATE_MAX) {
337
- return CSS_REPLACEMENT_CHARACTER;
338
- }
339
- return String.fromCodePoint(codePoint);
340
- }
341
- );
342
- var hasBalancedBlocks = (css) => {
343
- const open = [];
344
- let quote;
345
- let inComment = false;
346
- for (let index = 0; index < css.length; index += 1) {
347
- const char = css[index];
348
- if (inComment) {
349
- if (char === "*" && css[index + 1] === "/") {
350
- inComment = false;
351
- index += 1;
352
- }
353
- } else if (char === "\\") {
354
- index += 1;
355
- } else if (quote) {
356
- if (char === quote) {
357
- quote = void 0;
358
- } else if (CSS_NEWLINES.includes(char)) {
359
- return false;
360
- }
361
- } else if (char === "/" && css[index + 1] === "*") {
362
- inComment = true;
363
- index += 1;
364
- } else if (CSS_QUOTES.includes(char)) {
365
- quote = char;
366
- } else if (char in CSS_BRACKET_PAIRS) {
367
- open.push(CSS_BRACKET_PAIRS[char]);
368
- } else if (CSS_CLOSING_BRACKETS.includes(char) && open.pop() !== char) {
369
- return false;
370
- }
371
- }
372
- return open.length === 0 && !quote && !inComment;
373
- };
374
- var isSafeCustomCss = (css) => {
375
- if (css.length > NOTICE_CUSTOM_CSS_MAX_LENGTH || !hasBalancedBlocks(css)) {
376
- return false;
377
- }
378
- const lowered = css.toLowerCase();
379
- const decoded = decodeCssEscapes(css).toLowerCase();
380
- return NOTICE_DISALLOWED_CSS_TOKENS.every(
381
- (token) => !lowered.includes(token) && !decoded.includes(token)
382
- );
383
- };
384
-
385
387
  // src/shared/appearance/resolve.ts
386
388
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
387
389
  var pickColors = (raw) => {
@@ -680,6 +682,66 @@ var useMediaQuery = (query) => {
680
682
  return matches;
681
683
  };
682
684
 
685
+ // src/shared/endpoints.ts
686
+ var CONSENT_PATH = "/consent";
687
+ var LEDGER_PATH = "/consent-ledger";
688
+ var ABSOLUTE_HTTP_URL = /^https?:\/\/(?:\[[0-9a-f:.]+\]|[^\s/?#@:[\]]+)(?::\d+)?(?:\/[^\s?#]*)?$/i;
689
+ var trimUrl = (value) => (value != null ? value : "").trim().replace(/\/+$/, "");
690
+ var isConsentServerUrl = (trimmed) => ABSOLUTE_HTTP_URL.test(trimmed) && trimmed.endsWith(CONSENT_PATH);
691
+ var isHostForm = (trimmed) => ABSOLUTE_HTTP_URL.test(trimmed) && !trimmed.endsWith(CONSENT_PATH);
692
+ function resolveEndpoints(baseUrl, ledgerBaseUrl) {
693
+ const trimmed = trimUrl(baseUrl);
694
+ if (!isHostForm(trimmed)) {
695
+ return { mode: "legacy", apiHost: void 0, baseUrl, ledgerBaseUrl };
696
+ }
697
+ const apiHost = trimmed.endsWith(LEDGER_PATH) ? trimmed.slice(0, -LEDGER_PATH.length) : trimmed;
698
+ return {
699
+ mode: "host",
700
+ apiHost,
701
+ baseUrl: `${apiHost}${CONSENT_PATH}`,
702
+ ledgerBaseUrl: `${apiHost}${LEDGER_PATH}`
703
+ };
704
+ }
705
+ var ENDPOINT_WARNINGS = {
706
+ missingBaseUrl: "[Redacto] baseUrl is not set, so the SDK's built-in production default is used. Pass baseUrl: your API host.",
707
+ consentServerBaseUrl: "[Redacto] baseUrl is the consent server URL (ending in /consent), which is deprecated. Pass the API host instead; the SDK derives the consent server and ledger URLs from it.",
708
+ ledgerBaseUrl: "[Redacto] ledgerBaseUrl is deprecated. Pass the Redacto API host as baseUrl instead; the SDK then reaches the consent server and the ledger under it.",
709
+ ledgerBaseUrlIgnored: "[Redacto] ledgerBaseUrl is ignored because baseUrl is the API host; the SDK reaches the ledger under that host. Remove ledgerBaseUrl.",
710
+ slugIgnored: "[Redacto] slug is deprecated and ignored. The Privacy Center reads the organisation from the access token, or from organisationUuid and workspaceUuid. Remove slug."
711
+ };
712
+ var issuedWarnings = /* @__PURE__ */ new Set();
713
+ var warnOnce = (warning) => {
714
+ if (issuedWarnings.has(warning)) {
715
+ return;
716
+ }
717
+ issuedWarnings.add(warning);
718
+ console.warn(ENDPOINT_WARNINGS[warning]);
719
+ };
720
+ var warnEndpointUsage = (baseUrl, ledgerBaseUrl, { hasBuiltInDefault }) => {
721
+ if (!baseUrl) {
722
+ if (hasBuiltInDefault) {
723
+ warnOnce("missingBaseUrl");
724
+ }
725
+ } else if (isConsentServerUrl(trimUrl(baseUrl))) {
726
+ warnOnce("consentServerBaseUrl");
727
+ }
728
+ if (trimUrl(ledgerBaseUrl)) {
729
+ warnOnce(isHostForm(trimUrl(baseUrl)) ? "ledgerBaseUrlIgnored" : "ledgerBaseUrl");
730
+ }
731
+ };
732
+ var warnSlugDeprecated = () => {
733
+ warnOnce("slugIgnored");
734
+ };
735
+
736
+ // src/shared/useResolvedEndpoints.ts
737
+ import { useMemo } from "react";
738
+ var useResolvedEndpoints = (baseUrl, ledgerBaseUrl, { warn = true, hasBuiltInDefault = true } = {}) => useMemo(() => {
739
+ if (warn) {
740
+ warnEndpointUsage(baseUrl, ledgerBaseUrl, { hasBuiltInDefault });
741
+ }
742
+ return resolveEndpoints(baseUrl, ledgerBaseUrl);
743
+ }, [baseUrl, ledgerBaseUrl, warn, hasBuiltInDefault]);
744
+
683
745
  // src/shared/sandbox.ts
684
746
  var SANDBOX_TOKEN_HEADER = "X-Consent-Token";
685
747
 
@@ -687,6 +749,36 @@ var SANDBOX_TOKEN_HEADER = "X-Consent-Token";
687
749
  var DEFAULT_CONSENT_API_BASE_URL = "https://api.redacto.io/consent";
688
750
  var USER_SERVICE_PATH = "/user/api/0";
689
751
 
752
+ // src/shared/appearance/scopedCss.ts
753
+ var escapesScope = (style) => {
754
+ var _a, _b;
755
+ return Array.from((_b = (_a = style.sheet) == null ? void 0 : _a.cssRules) != null ? _b : []).some(
756
+ (rule) => !rule.cssText.startsWith(CSS_SCOPE_AT_RULE)
757
+ );
758
+ };
759
+ var applyScopedCustomCss = ({
760
+ scopeAttribute,
761
+ scopeId,
762
+ markerAttribute,
763
+ css
764
+ }) => {
765
+ if (typeof document === "undefined" || !css.trim() || !isSafeCustomCss(css)) {
766
+ return () => void 0;
767
+ }
768
+ const id = scopeId.replace(CSS_STRING_SPECIALS_RE, "\\$&");
769
+ const style = document.createElement("style");
770
+ style.setAttribute(markerAttribute, scopeId);
771
+ style.textContent = `${CSS_SCOPE_AT_RULE} ([${scopeAttribute}="${id}"]) {
772
+ ${css}
773
+ }`;
774
+ document.head.appendChild(style);
775
+ if (escapesScope(style)) {
776
+ style.remove();
777
+ return () => void 0;
778
+ }
779
+ return () => style.remove();
780
+ };
781
+
690
782
  export {
691
783
  SANDBOX_TOKEN_HEADER,
692
784
  withIdempotencyKey,
@@ -724,5 +816,10 @@ export {
724
816
  Collapse,
725
817
  configureNoticeFonts,
726
818
  loadNoticeWebfont,
727
- useMediaQuery
819
+ useMediaQuery,
820
+ applyScopedCustomCss,
821
+ resolveEndpoints,
822
+ warnEndpointUsage,
823
+ warnSlugDeprecated,
824
+ useResolvedEndpoints
728
825
  };