@redacto.io/consent-sdk-react 10.0.0-beta.8 → 10.0.0-beta.9
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/CHANGELOG.md +10 -0
- package/dist/{chunk-6A665OPR.mjs → chunk-OGL6OC3A.mjs} +4 -0
- package/dist/index.d.mts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +162 -56
- package/dist/index.mjs +161 -57
- package/dist/privacy-center.d.mts +51 -10
- package/dist/privacy-center.d.ts +51 -10
- package/dist/privacy-center.js +250 -70
- package/dist/privacy-center.mjs +249 -71
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +55 -5
- package/src/RedactoNoticeConsent/api/index.ts +78 -48
- package/src/RedactoNoticeConsent/api/sandbox.ts +150 -0
- package/src/RedactoNoticeConsent/api/types.ts +57 -11
- package/src/RedactoNoticeConsent/types.ts +28 -2
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +37 -2
- package/src/RedactoPrivacyCenter/api/client.ts +116 -24
- package/src/RedactoPrivacyCenter/api/fetcher.ts +24 -9
- package/src/RedactoPrivacyCenter/components/Form.tsx +55 -30
- package/src/RedactoPrivacyCenter/components/SelectUserGate.tsx +8 -0
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +42 -6
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +131 -39
- package/src/RedactoPrivacyCenter/context/types.ts +38 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +12 -5
- package/src/RedactoPrivacyCenter/lib/sandbox.ts +104 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +20 -0
- package/src/RedactoPrivacyCenter/lib/utils.ts +15 -0
- package/src/shared/sandbox.ts +6 -0
- package/tests/Form.test.tsx +142 -5
|
@@ -1297,6 +1297,12 @@ export const RedactoNoticeConsent = ({
|
|
|
1297
1297
|
noticeId,
|
|
1298
1298
|
accessToken,
|
|
1299
1299
|
refreshToken,
|
|
1300
|
+
token,
|
|
1301
|
+
email,
|
|
1302
|
+
mobile,
|
|
1303
|
+
ucic,
|
|
1304
|
+
organisationUuid,
|
|
1305
|
+
workspaceUuid,
|
|
1300
1306
|
baseUrl,
|
|
1301
1307
|
ledgerBaseUrl,
|
|
1302
1308
|
language = "en",
|
|
@@ -1312,6 +1318,25 @@ export const RedactoNoticeConsent = ({
|
|
|
1312
1318
|
includeFullyConsentedData = false,
|
|
1313
1319
|
reviewModeButtonText = "Continue",
|
|
1314
1320
|
}: Props) => {
|
|
1321
|
+
// =============================================================================
|
|
1322
|
+
// SANDBOX MODE
|
|
1323
|
+
// =============================================================================
|
|
1324
|
+
|
|
1325
|
+
// Sandbox PoC: when `token` is set, every API call bypasses the JWT and sends
|
|
1326
|
+
// the single X-Consent-Token header, carrying the acting identity (subject) in
|
|
1327
|
+
// each request's own payload. Org/workspace/subject are taken straight from
|
|
1328
|
+
// props. Passed through to the API layer alongside `accessToken`.
|
|
1329
|
+
const sandboxParams = useMemo(
|
|
1330
|
+
() => ({
|
|
1331
|
+
token,
|
|
1332
|
+
sandboxUcic: ucic,
|
|
1333
|
+
sandboxSubject: email || mobile,
|
|
1334
|
+
organisationUuid,
|
|
1335
|
+
workspaceUuid,
|
|
1336
|
+
}),
|
|
1337
|
+
[token, ucic, email, mobile, organisationUuid, workspaceUuid]
|
|
1338
|
+
);
|
|
1339
|
+
|
|
1315
1340
|
// =============================================================================
|
|
1316
1341
|
// PROP VALIDATION
|
|
1317
1342
|
// =============================================================================
|
|
@@ -1321,11 +1346,31 @@ export const RedactoNoticeConsent = ({
|
|
|
1321
1346
|
if (!noticeId?.trim()) {
|
|
1322
1347
|
return "RedactoNoticeConsent: 'noticeId' prop is required and cannot be empty";
|
|
1323
1348
|
}
|
|
1349
|
+
// Sandbox mode: require a static token, org/workspace, and a subject instead
|
|
1350
|
+
// of a JWT access token.
|
|
1351
|
+
if (token?.trim()) {
|
|
1352
|
+
if (!organisationUuid?.trim() || !workspaceUuid?.trim()) {
|
|
1353
|
+
return "RedactoNoticeConsent: sandbox mode requires 'organisationUuid' and 'workspaceUuid'";
|
|
1354
|
+
}
|
|
1355
|
+
if (!email?.trim() && !mobile?.trim() && !ucic?.trim()) {
|
|
1356
|
+
return "RedactoNoticeConsent: sandbox mode requires 'email', 'mobile', or 'ucic'";
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1324
1360
|
if (!accessToken?.trim()) {
|
|
1325
1361
|
return "RedactoNoticeConsent: 'accessToken' prop is required and cannot be empty";
|
|
1326
1362
|
}
|
|
1327
1363
|
return null;
|
|
1328
|
-
}, [
|
|
1364
|
+
}, [
|
|
1365
|
+
noticeId,
|
|
1366
|
+
accessToken,
|
|
1367
|
+
token,
|
|
1368
|
+
organisationUuid,
|
|
1369
|
+
workspaceUuid,
|
|
1370
|
+
email,
|
|
1371
|
+
mobile,
|
|
1372
|
+
ucic,
|
|
1373
|
+
]);
|
|
1329
1374
|
|
|
1330
1375
|
// Show error UI if props are invalid
|
|
1331
1376
|
if (propValidationError) {
|
|
@@ -1822,6 +1867,7 @@ export const RedactoNoticeConsent = ({
|
|
|
1822
1867
|
validate_against: validateAgainst,
|
|
1823
1868
|
include_fully_consented_data: includeFullyConsentedData,
|
|
1824
1869
|
signal: abortControllerRef.current?.signal,
|
|
1870
|
+
...sandboxParams,
|
|
1825
1871
|
});
|
|
1826
1872
|
const activeConfig = consentContentData.detail.active_config;
|
|
1827
1873
|
setContent(activeConfig);
|
|
@@ -1856,6 +1902,7 @@ export const RedactoNoticeConsent = ({
|
|
|
1856
1902
|
ledgerBaseUrl,
|
|
1857
1903
|
noticeUuid: noticeId,
|
|
1858
1904
|
signal: abortControllerRef.current?.signal,
|
|
1905
|
+
...sandboxParams,
|
|
1859
1906
|
});
|
|
1860
1907
|
if (ledgerStatus) {
|
|
1861
1908
|
const overlay = buildLedgerConsentOverlay(
|
|
@@ -2033,6 +2080,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2033
2080
|
noticeId,
|
|
2034
2081
|
accessToken,
|
|
2035
2082
|
refreshToken,
|
|
2083
|
+
sandboxParams,
|
|
2036
2084
|
language,
|
|
2037
2085
|
applicationId,
|
|
2038
2086
|
validateAgainst,
|
|
@@ -2194,6 +2242,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2194
2242
|
guardianVerificationReference: verificationReference || undefined,
|
|
2195
2243
|
selfDeclaredAdult: selfDeclaredAdult || undefined,
|
|
2196
2244
|
signal: submitController.signal,
|
|
2245
|
+
...sandboxParams,
|
|
2197
2246
|
});
|
|
2198
2247
|
}
|
|
2199
2248
|
onAccept?.();
|
|
@@ -2437,7 +2486,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2437
2486
|
} finally {
|
|
2438
2487
|
setIsSubmittingGuardian(false);
|
|
2439
2488
|
}
|
|
2440
|
-
}, [accessToken, baseUrl, guardianFormData, digilockerMode, digilockerCallbackUrl, selectedPurposes, selectedDataElements, noticeId, onError]);
|
|
2489
|
+
}, [accessToken, sandboxParams, baseUrl, guardianFormData, digilockerMode, digilockerCallbackUrl, selectedPurposes, selectedDataElements, noticeId, onError]);
|
|
2441
2490
|
|
|
2442
2491
|
// Age verification handlers
|
|
2443
2492
|
const handleAgeVerificationYes = useCallback(() => {
|
|
@@ -2516,7 +2565,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2516
2565
|
setIsPollingStatus(false);
|
|
2517
2566
|
isPollingStatusRef.current = false;
|
|
2518
2567
|
setIsInitiatingVerification(false);
|
|
2519
|
-
}, [accessToken, baseUrl]);
|
|
2568
|
+
}, [accessToken, sandboxParams, baseUrl]);
|
|
2520
2569
|
|
|
2521
2570
|
const startPopupMonitoring = useCallback((popup: Window) => {
|
|
2522
2571
|
// Check every 500ms if popup is closed
|
|
@@ -2697,7 +2746,7 @@ export const RedactoNoticeConsent = ({
|
|
|
2697
2746
|
// First poll after 3s
|
|
2698
2747
|
statusPollIntervalRef.current = window.setTimeout(poll, POLL_INTERVAL);
|
|
2699
2748
|
},
|
|
2700
|
-
[accessToken, baseUrl, getVerificationErrorMessage]
|
|
2749
|
+
[accessToken, sandboxParams, baseUrl, getVerificationErrorMessage]
|
|
2701
2750
|
);
|
|
2702
2751
|
|
|
2703
2752
|
|
|
@@ -3232,6 +3281,7 @@ export const RedactoNoticeConsent = ({
|
|
|
3232
3281
|
noticeUuid: content.notice_uuid,
|
|
3233
3282
|
language: languageCode,
|
|
3234
3283
|
signal: ttsAbortController.signal,
|
|
3284
|
+
...sandboxParams,
|
|
3235
3285
|
})
|
|
3236
3286
|
.then((response) => {
|
|
3237
3287
|
// Store the response and mark TTS as available
|
|
@@ -3250,7 +3300,7 @@ export const RedactoNoticeConsent = ({
|
|
|
3250
3300
|
return () => {
|
|
3251
3301
|
ttsAbortController.abort();
|
|
3252
3302
|
};
|
|
3253
|
-
}, [content, selectedLanguage, accessToken, baseUrl, getLanguageCode]);
|
|
3303
|
+
}, [content, selectedLanguage, accessToken, sandboxParams, baseUrl, getLanguageCode]);
|
|
3254
3304
|
|
|
3255
3305
|
const modalStyle = {
|
|
3256
3306
|
borderRadius: settings?.borderRadius || "8px",
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { jwtDecode } from "jwt-decode";
|
|
2
2
|
import { parseApiError } from "../../shared/api-errors";
|
|
3
|
+
import {
|
|
4
|
+
buildAuthHeaders,
|
|
5
|
+
resolveRequestAuth,
|
|
6
|
+
sandboxBodyIdentity,
|
|
7
|
+
sandboxQueryIdentity,
|
|
8
|
+
} from "./sandbox";
|
|
3
9
|
import type {
|
|
4
10
|
ConsentContent,
|
|
5
11
|
ConsentEventPayload,
|
|
@@ -69,25 +75,28 @@ export const fetchConsentContent = async ({
|
|
|
69
75
|
validate_against = "all",
|
|
70
76
|
include_fully_consented_data = false,
|
|
71
77
|
signal,
|
|
78
|
+
...sandboxParams
|
|
72
79
|
}: FetchConsentContentParams & { signal?: AbortSignal }): Promise<ConsentContent> => {
|
|
73
|
-
if (!noticeId || !
|
|
74
|
-
throw new Error("noticeId
|
|
80
|
+
if (!noticeId || !validate_against) {
|
|
81
|
+
throw new Error("noticeId is required");
|
|
75
82
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!decodedToken) {
|
|
79
|
-
throw new Error("Invalid access token");
|
|
83
|
+
if (!accessToken && !sandboxParams.token) {
|
|
84
|
+
throw new Error("accessToken or token is required");
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
const {
|
|
83
|
-
|
|
84
|
-
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
85
|
-
}
|
|
87
|
+
const { organisationUuid: ORGANISATION_UUID, workspaceUuid: WORKSPACE_UUID, sandbox } =
|
|
88
|
+
resolveRequestAuth(accessToken, sandboxParams, decodeTokenSafely);
|
|
86
89
|
|
|
87
90
|
const apiBaseUrl = baseUrl || BASE_URL;
|
|
88
91
|
|
|
89
|
-
// Create cache key for this request - include
|
|
90
|
-
|
|
92
|
+
// Create cache key for this request - include the auth identity (token or
|
|
93
|
+
// sandbox org/workspace/subject) so different users get different cache
|
|
94
|
+
// entries. Org + workspace are part of the sandbox identity so two sessions
|
|
95
|
+
// sharing a subject but scoped to different orgs never collide.
|
|
96
|
+
const cacheIdentity = sandbox
|
|
97
|
+
? `sandbox:${sandbox.organisationUuid}:${sandbox.workspaceUuid}:${sandbox.ucic || sandbox.subject}`
|
|
98
|
+
: accessToken;
|
|
99
|
+
const cacheKey = `${cacheIdentity}-${noticeId}-${validate_against}-${language}-${specific_uuid || ''}-${include_fully_consented_data}`;
|
|
91
100
|
|
|
92
101
|
if (validate_against === "all" && !specific_uuid) {
|
|
93
102
|
const cachedData = getCachedData(cacheKey);
|
|
@@ -107,11 +116,18 @@ export const fetchConsentContent = async ({
|
|
|
107
116
|
if (include_fully_consented_data) {
|
|
108
117
|
url.searchParams.append("include_fully_consented_data", "true");
|
|
109
118
|
}
|
|
119
|
+
// Sandbox mode: the acting identity travels as query params
|
|
120
|
+
// (primary_email / primary_mobile); the credential is the X-Consent-Token header.
|
|
121
|
+
if (sandbox) {
|
|
122
|
+
for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
|
|
123
|
+
url.searchParams.append(key, value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
110
126
|
|
|
111
127
|
const response = await fetch(url.toString(), {
|
|
112
128
|
method: "GET",
|
|
113
129
|
headers: {
|
|
114
|
-
|
|
130
|
+
...buildAuthHeaders(accessToken, sandbox),
|
|
115
131
|
"Accept-Language": language,
|
|
116
132
|
},
|
|
117
133
|
signal,
|
|
@@ -145,24 +161,23 @@ export const submitConsentEvent = async ({
|
|
|
145
161
|
guardianVerificationReference,
|
|
146
162
|
selfDeclaredAdult,
|
|
147
163
|
signal,
|
|
164
|
+
...sandboxParams
|
|
148
165
|
}: SubmitConsentEventParams & { signal?: AbortSignal }): Promise<void> => {
|
|
149
|
-
if (!noticeUuid || !
|
|
150
|
-
throw new Error("noticeUuid
|
|
166
|
+
if (!noticeUuid || !Array.isArray(purposes)) {
|
|
167
|
+
throw new Error("noticeUuid and purposes array are required");
|
|
151
168
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!decodedToken) {
|
|
155
|
-
throw new Error("Invalid access token");
|
|
169
|
+
if (!accessToken && !sandboxParams.token) {
|
|
170
|
+
throw new Error("accessToken or token is required");
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
const {
|
|
159
|
-
|
|
160
|
-
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
161
|
-
}
|
|
173
|
+
const { organisationUuid: ORGANISATION_UUID, workspaceUuid: WORKSPACE_UUID, sandbox } =
|
|
174
|
+
resolveRequestAuth(accessToken, sandboxParams, decodeTokenSafely);
|
|
162
175
|
|
|
163
176
|
// Fall back to the consent server when no ledger is configured, so existing
|
|
164
|
-
// integrations that only pass baseUrl remain backwards compatible.
|
|
165
|
-
|
|
177
|
+
// integrations that only pass baseUrl remain backwards compatible. Sandbox
|
|
178
|
+
// never routes to the Go ledger (no X-Consent-Token support), so a sandbox
|
|
179
|
+
// session forces the consent-server path even if a ledgerBaseUrl is set.
|
|
180
|
+
const apiBaseUrl = (sandbox ? null : ledgerBaseUrl) || baseUrl || BASE_URL;
|
|
166
181
|
|
|
167
182
|
const validatedPurposes = purposes.map((purpose) => {
|
|
168
183
|
if (!purpose.uuid) {
|
|
@@ -196,15 +211,19 @@ export const submitConsentEvent = async ({
|
|
|
196
211
|
self_declared_adult: selfDeclaredAdult,
|
|
197
212
|
};
|
|
198
213
|
|
|
214
|
+
// Sandbox mode: merge the acting test identity into the body (the server has
|
|
215
|
+
// no subject header; it namespaces this identity `test::`).
|
|
216
|
+
const requestBody = sandbox ? { ...payload, ...sandboxBodyIdentity(sandbox) } : payload;
|
|
217
|
+
|
|
199
218
|
const response = await fetch(
|
|
200
219
|
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/submit-consent`,
|
|
201
220
|
{
|
|
202
221
|
method: "POST",
|
|
203
222
|
headers: {
|
|
204
|
-
|
|
223
|
+
...buildAuthHeaders(accessToken, sandbox),
|
|
205
224
|
"Content-Type": "application/json",
|
|
206
225
|
},
|
|
207
|
-
body: JSON.stringify(
|
|
226
|
+
body: JSON.stringify(requestBody),
|
|
208
227
|
signal,
|
|
209
228
|
}
|
|
210
229
|
);
|
|
@@ -283,29 +302,32 @@ export const fetchTTSAudioUrls = async ({
|
|
|
283
302
|
noticeUuid,
|
|
284
303
|
language,
|
|
285
304
|
signal,
|
|
305
|
+
...sandboxParams
|
|
286
306
|
}: FetchTTSAudioUrlsParams & { signal?: AbortSignal }): Promise<TTSAudioUrlsResponse> => {
|
|
287
|
-
if (!accessToken || !noticeUuid || !language) {
|
|
288
|
-
throw new Error("accessToken, noticeUuid, and language are required");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const decodedToken = decodeTokenSafely(accessToken);
|
|
292
|
-
if (!decodedToken) {
|
|
293
|
-
throw new Error("Invalid access token");
|
|
307
|
+
if ((!accessToken && !sandboxParams.token) || !noticeUuid || !language) {
|
|
308
|
+
throw new Error("accessToken (or token), noticeUuid, and language are required");
|
|
294
309
|
}
|
|
295
310
|
|
|
296
|
-
const {
|
|
297
|
-
|
|
298
|
-
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
299
|
-
}
|
|
311
|
+
const { organisationUuid: ORGANISATION_UUID, workspaceUuid: WORKSPACE_UUID, sandbox } =
|
|
312
|
+
resolveRequestAuth(accessToken, sandboxParams, decodeTokenSafely);
|
|
300
313
|
|
|
301
314
|
const apiBaseUrl = baseUrl || BASE_URL;
|
|
302
315
|
|
|
303
|
-
const url =
|
|
316
|
+
const url = new URL(
|
|
317
|
+
`${apiBaseUrl}/public/organisations/${ORGANISATION_UUID}/workspaces/${WORKSPACE_UUID}/notices/${noticeUuid}/audio/${language}`
|
|
318
|
+
);
|
|
319
|
+
// Sandbox mode: the acting identity travels as query params
|
|
320
|
+
// (primary_email / primary_mobile); the credential is the X-Consent-Token header.
|
|
321
|
+
if (sandbox) {
|
|
322
|
+
for (const [key, value] of Object.entries(sandboxQueryIdentity(sandbox))) {
|
|
323
|
+
url.searchParams.append(key, value);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
304
326
|
|
|
305
|
-
const response = await fetch(url, {
|
|
327
|
+
const response = await fetch(url.toString(), {
|
|
306
328
|
method: "GET",
|
|
307
329
|
headers: {
|
|
308
|
-
|
|
330
|
+
...buildAuthHeaders(accessToken, sandbox),
|
|
309
331
|
Accept: "application/json",
|
|
310
332
|
},
|
|
311
333
|
signal,
|
|
@@ -343,6 +365,9 @@ export const initiateGuardianVerification = async ({
|
|
|
343
365
|
signal,
|
|
344
366
|
}: InitiateGuardianVerificationParams): Promise<InitiateGuardianVerificationResponse> => {
|
|
345
367
|
try {
|
|
368
|
+
// Guardian verification is JWT-only: sandbox sessions have no minor-notice /
|
|
369
|
+
// guardian support (the server 403s them, and C1 blocks minor notices at
|
|
370
|
+
// render), so no sandbox identity is wired through here.
|
|
346
371
|
if (!accessToken) {
|
|
347
372
|
throw new Error("accessToken is required");
|
|
348
373
|
}
|
|
@@ -363,8 +388,10 @@ export const initiateGuardianVerification = async ({
|
|
|
363
388
|
if (!decodedToken) {
|
|
364
389
|
throw new Error("Invalid access token");
|
|
365
390
|
}
|
|
366
|
-
|
|
367
|
-
|
|
391
|
+
const {
|
|
392
|
+
organisation_uuid: ORGANISATION_UUID,
|
|
393
|
+
workspace_uuid: WORKSPACE_UUID,
|
|
394
|
+
} = decodedToken;
|
|
368
395
|
if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
|
|
369
396
|
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
370
397
|
}
|
|
@@ -458,6 +485,9 @@ export const verifyGuardianStatus = async ({
|
|
|
458
485
|
sessionToken,
|
|
459
486
|
signal,
|
|
460
487
|
}: VerifyGuardianStatusParams): Promise<VerifyGuardianStatusResponse> => {
|
|
488
|
+
// Guardian verification is JWT-only: sandbox sessions have no minor-notice /
|
|
489
|
+
// guardian support (the server 403s them, and C1 blocks minor notices at
|
|
490
|
+
// render), so no sandbox identity is wired through here.
|
|
461
491
|
if (!accessToken || !sessionToken) {
|
|
462
492
|
throw new Error("accessToken and sessionToken are required");
|
|
463
493
|
}
|
|
@@ -466,8 +496,10 @@ export const verifyGuardianStatus = async ({
|
|
|
466
496
|
if (!decodedToken) {
|
|
467
497
|
throw new Error("Invalid access token");
|
|
468
498
|
}
|
|
469
|
-
|
|
470
|
-
|
|
499
|
+
const {
|
|
500
|
+
organisation_uuid: ORGANISATION_UUID,
|
|
501
|
+
workspace_uuid: WORKSPACE_UUID,
|
|
502
|
+
} = decodedToken;
|
|
471
503
|
if (!ORGANISATION_UUID || !WORKSPACE_UUID) {
|
|
472
504
|
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
473
505
|
}
|
|
@@ -482,9 +514,7 @@ export const verifyGuardianStatus = async ({
|
|
|
482
514
|
Authorization: `Bearer ${accessToken}`,
|
|
483
515
|
"Content-Type": "application/json",
|
|
484
516
|
},
|
|
485
|
-
body: JSON.stringify({
|
|
486
|
-
session_token: sessionToken,
|
|
487
|
-
}),
|
|
517
|
+
body: JSON.stringify({ session_token: sessionToken }),
|
|
488
518
|
signal,
|
|
489
519
|
}
|
|
490
520
|
);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox-mode auth primitives for the consent PoC.
|
|
3
|
+
*
|
|
4
|
+
* In sandbox mode the SDK talks to the consent-server with a pasted static
|
|
5
|
+
* token and an explicit test subject, bypassing the JWT entirely. Org/workspace
|
|
6
|
+
* UUIDs come from props (they still build the `/public/organisations/{org}/
|
|
7
|
+
* workspaces/{ws}/...` URL paths) instead of being decoded from a token.
|
|
8
|
+
*
|
|
9
|
+
* Auth is the single `X-Consent-Token` header carrying the static token; its
|
|
10
|
+
* presence alone marks the request as `environment=test`. There is no subject
|
|
11
|
+
* header. The acting identity (the test subject) rides the request's own
|
|
12
|
+
* payload: query params (`primary_email` / `primary_mobile`) on GET/query reads,
|
|
13
|
+
* and the body on POSTs. The server normalizes and namespaces it `test::`
|
|
14
|
+
* server-side. Call sites resolve the credential via {@link buildAuthHeaders}
|
|
15
|
+
* and the identity via {@link sandboxQueryIdentity} / {@link sandboxBodyIdentity}.
|
|
16
|
+
*
|
|
17
|
+
* This file exists so every API call site resolves its org/workspace UUIDs and
|
|
18
|
+
* builds its auth header through one place, keeping sandbox vs. JWT branching
|
|
19
|
+
* from drifting across the ~6 fetch sites.
|
|
20
|
+
*/
|
|
21
|
+
import { SANDBOX_TOKEN_HEADER } from "../../shared/sandbox";
|
|
22
|
+
import type {
|
|
23
|
+
RedactoJwtPayload,
|
|
24
|
+
ResolvedRequestAuth,
|
|
25
|
+
SandboxAuth,
|
|
26
|
+
SandboxAuthParams,
|
|
27
|
+
SandboxIdentityPayload,
|
|
28
|
+
} from "./types";
|
|
29
|
+
|
|
30
|
+
export { SANDBOX_TOKEN_HEADER };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a {@link SandboxAuth} from the raw sandbox props, or return null when
|
|
34
|
+
* sandbox mode is not active (no token).
|
|
35
|
+
*
|
|
36
|
+
* @throws Error when a sandbox token is provided but org/workspace or the acting
|
|
37
|
+
* identity are incomplete — sandbox mode cannot build URLs or identify the
|
|
38
|
+
* acting principal without them, so failing loudly beats a silent malformed
|
|
39
|
+
* request.
|
|
40
|
+
*/
|
|
41
|
+
export const resolveSandboxAuth = (
|
|
42
|
+
params: SandboxAuthParams
|
|
43
|
+
): SandboxAuth | null => {
|
|
44
|
+
const token = params.token?.trim();
|
|
45
|
+
if (!token) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const organisationUuid = params.organisationUuid?.trim();
|
|
50
|
+
const workspaceUuid = params.workspaceUuid?.trim();
|
|
51
|
+
const ucic = params.sandboxUcic?.trim();
|
|
52
|
+
const subject = params.sandboxSubject?.trim();
|
|
53
|
+
|
|
54
|
+
if (!organisationUuid || !workspaceUuid) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Sandbox mode requires organisationUuid and workspaceUuid props"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (!ucic && !subject) {
|
|
60
|
+
throw new Error("Sandbox mode requires a UCIC, email, or mobile");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { token, ucic, subject: subject ?? "", organisationUuid, workspaceUuid };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the org/workspace UUIDs and sandbox auth for a request. In sandbox
|
|
68
|
+
* mode these come straight from props; otherwise they are decoded from the JWT
|
|
69
|
+
* (preserving the pre-sandbox behavior and error messages exactly). The JWT
|
|
70
|
+
* decoder is injected so this module stays free of a direct jwt-decode import.
|
|
71
|
+
*
|
|
72
|
+
* @throws Error when neither a valid sandbox config nor a decodable token with
|
|
73
|
+
* org/workspace UUIDs is available.
|
|
74
|
+
*/
|
|
75
|
+
export const resolveRequestAuth = (
|
|
76
|
+
accessToken: string | undefined,
|
|
77
|
+
sandboxParams: SandboxAuthParams,
|
|
78
|
+
decodeTokenSafely: (token: string) => RedactoJwtPayload | null
|
|
79
|
+
): ResolvedRequestAuth => {
|
|
80
|
+
const sandbox = resolveSandboxAuth(sandboxParams);
|
|
81
|
+
if (sandbox) {
|
|
82
|
+
return {
|
|
83
|
+
organisationUuid: sandbox.organisationUuid,
|
|
84
|
+
workspaceUuid: sandbox.workspaceUuid,
|
|
85
|
+
sandbox,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const decodedToken = accessToken ? decodeTokenSafely(accessToken) : null;
|
|
90
|
+
if (!decodedToken) {
|
|
91
|
+
throw new Error("Invalid access token");
|
|
92
|
+
}
|
|
93
|
+
const { organisation_uuid, workspace_uuid } = decodedToken;
|
|
94
|
+
if (!organisation_uuid || !workspace_uuid) {
|
|
95
|
+
throw new Error("Invalid token: missing organization or workspace UUID");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
organisationUuid: organisation_uuid,
|
|
99
|
+
workspaceUuid: workspace_uuid,
|
|
100
|
+
sandbox: null,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the request headers for an authenticated call. In sandbox mode this is
|
|
106
|
+
* the single `X-Consent-Token` credential (its presence marks the request as
|
|
107
|
+
* `environment=test`); otherwise the standard `Authorization: Bearer`. There is
|
|
108
|
+
* no subject header — the acting identity travels in the request's own payload
|
|
109
|
+
* (see {@link sandboxQueryIdentity} for reads, {@link sandboxBodyIdentity} for
|
|
110
|
+
* POSTs).
|
|
111
|
+
*/
|
|
112
|
+
export const buildAuthHeaders = (
|
|
113
|
+
accessToken: string | undefined,
|
|
114
|
+
sandbox: SandboxAuth | null
|
|
115
|
+
): Record<string, string> => {
|
|
116
|
+
if (sandbox) {
|
|
117
|
+
return { [SANDBOX_TOKEN_HEADER]: sandbox.token };
|
|
118
|
+
}
|
|
119
|
+
return { Authorization: `Bearer ${accessToken}` };
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The identity payload fragment for the sandbox acting principal:
|
|
124
|
+
* `{ org_user_id }` when a UCIC is set (it takes precedence), else
|
|
125
|
+
* `{ primary_email }` when the subject looks like an email (contains "@"),
|
|
126
|
+
* otherwise `{ primary_mobile }`. Merge this into a JSON request body (POST) or
|
|
127
|
+
* into query params (GET) so the server can resolve the acting test identity.
|
|
128
|
+
*/
|
|
129
|
+
export const sandboxBodyIdentity = (
|
|
130
|
+
sandbox: SandboxAuth
|
|
131
|
+
): SandboxIdentityPayload => {
|
|
132
|
+
if (sandbox.ucic) {
|
|
133
|
+
return { org_user_id: sandbox.ucic };
|
|
134
|
+
}
|
|
135
|
+
return sandbox.subject.includes("@")
|
|
136
|
+
? { primary_email: sandbox.subject }
|
|
137
|
+
: { primary_mobile: sandbox.subject };
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The identity query fragment for GET/query reads: `{ org_user_id }` when a UCIC
|
|
142
|
+
* is set (it takes precedence), else `{ primary_email }` when the subject looks
|
|
143
|
+
* like an email (contains "@"), otherwise `{ primary_mobile }`. Append these to
|
|
144
|
+
* the request's query string so the server can resolve the acting test identity
|
|
145
|
+
* (the server reads `org_user_id` / `primary_email` / `primary_mobile` query
|
|
146
|
+
* params on sandbox reads).
|
|
147
|
+
*/
|
|
148
|
+
export const sandboxQueryIdentity = (
|
|
149
|
+
sandbox: SandboxAuth
|
|
150
|
+
): SandboxIdentityPayload => sandboxBodyIdentity(sandbox);
|
|
@@ -6,6 +6,52 @@ export type RedactoJwtPayload = {
|
|
|
6
6
|
iat?: number;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Optional sandbox-mode auth fields shared by every API param type. When
|
|
11
|
+
* `token` is set the call sends the single `X-Consent-Token` header instead of
|
|
12
|
+
* `Authorization: Bearer`, injects the subject into each request's own payload
|
|
13
|
+
* (body for POSTs, query param for GETs), and takes org/workspace from these
|
|
14
|
+
* props instead of decoding the JWT. All optional so existing JWT callers are
|
|
15
|
+
* unaffected.
|
|
16
|
+
*/
|
|
17
|
+
export type SandboxAuthParams = {
|
|
18
|
+
token?: string;
|
|
19
|
+
/** UCIC — the client's own user id (`org_user_id`); takes precedence over the subject. */
|
|
20
|
+
sandboxUcic?: string;
|
|
21
|
+
sandboxSubject?: string;
|
|
22
|
+
organisationUuid?: string;
|
|
23
|
+
workspaceUuid?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sandbox authentication context. Present only when the host opts into sandbox
|
|
28
|
+
* mode by passing a sandbox token; the token/org/workspace are always required
|
|
29
|
+
* at that point because there is no JWT to fall back on. The acting identity is
|
|
30
|
+
* a UCIC (the client's own `org_user_id`) or a `subject` (email/mobile) — at
|
|
31
|
+
* least one is present (UCIC takes precedence when both are set).
|
|
32
|
+
*/
|
|
33
|
+
export type SandboxAuth = {
|
|
34
|
+
token: string;
|
|
35
|
+
/** UCIC — the client's own user id (`org_user_id`). Takes precedence over `subject`. */
|
|
36
|
+
ucic?: string;
|
|
37
|
+
subject: string;
|
|
38
|
+
organisationUuid: string;
|
|
39
|
+
workspaceUuid: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The identity payload fragment for the sandbox acting principal. */
|
|
43
|
+
export type SandboxIdentityPayload =
|
|
44
|
+
| { org_user_id: string }
|
|
45
|
+
| { primary_email: string }
|
|
46
|
+
| { primary_mobile: string };
|
|
47
|
+
|
|
48
|
+
/** Org/workspace UUIDs plus the resolved sandbox context for a request. */
|
|
49
|
+
export type ResolvedRequestAuth = {
|
|
50
|
+
organisationUuid: string;
|
|
51
|
+
workspaceUuid: string;
|
|
52
|
+
sandbox: SandboxAuth | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
9
55
|
export type PurposeSelection = {
|
|
10
56
|
selected: boolean;
|
|
11
57
|
status: string; // ACTIVE | EXPIRED | WITHDRAW | DECLINED
|
|
@@ -157,10 +203,10 @@ export type Settings = {
|
|
|
157
203
|
font?: string;
|
|
158
204
|
};
|
|
159
205
|
|
|
160
|
-
export type FetchConsentContentParams = {
|
|
206
|
+
export type FetchConsentContentParams = SandboxAuthParams & {
|
|
161
207
|
noticeId: string;
|
|
162
|
-
accessToken
|
|
163
|
-
refreshToken
|
|
208
|
+
accessToken?: string;
|
|
209
|
+
refreshToken?: string;
|
|
164
210
|
baseUrl?: string;
|
|
165
211
|
language?: string;
|
|
166
212
|
specific_uuid?: string;
|
|
@@ -207,8 +253,8 @@ export type Purpose = {
|
|
|
207
253
|
data_elements: Array<DataElement>;
|
|
208
254
|
};
|
|
209
255
|
|
|
210
|
-
export type SubmitConsentEventParams = {
|
|
211
|
-
accessToken
|
|
256
|
+
export type SubmitConsentEventParams = SandboxAuthParams & {
|
|
257
|
+
accessToken?: string;
|
|
212
258
|
baseUrl?: string;
|
|
213
259
|
ledgerBaseUrl?: string;
|
|
214
260
|
noticeUuid: string;
|
|
@@ -331,8 +377,8 @@ export type DpoInfoAudio = {
|
|
|
331
377
|
grievance_email_audio_url?: string;
|
|
332
378
|
};
|
|
333
379
|
|
|
334
|
-
export type FetchTTSAudioUrlsParams = {
|
|
335
|
-
accessToken
|
|
380
|
+
export type FetchTTSAudioUrlsParams = SandboxAuthParams & {
|
|
381
|
+
accessToken?: string;
|
|
336
382
|
baseUrl?: string;
|
|
337
383
|
noticeUuid: string;
|
|
338
384
|
language: string;
|
|
@@ -346,8 +392,8 @@ export type FetchTTSAudioUrlsParams = {
|
|
|
346
392
|
* Parameters for initiating guardian verification
|
|
347
393
|
* POST /guardian/initiate-verification
|
|
348
394
|
*/
|
|
349
|
-
export type InitiateGuardianVerificationParams = {
|
|
350
|
-
accessToken
|
|
395
|
+
export type InitiateGuardianVerificationParams = SandboxAuthParams & {
|
|
396
|
+
accessToken?: string;
|
|
351
397
|
baseUrl?: string;
|
|
352
398
|
guardianName: string;
|
|
353
399
|
guardianContact: string;
|
|
@@ -382,8 +428,8 @@ export type InitiateGuardianVerificationResponse = {
|
|
|
382
428
|
* Parameters for checking verification status
|
|
383
429
|
* POST /guardian/verify-status
|
|
384
430
|
*/
|
|
385
|
-
export type VerifyGuardianStatusParams = {
|
|
386
|
-
accessToken
|
|
431
|
+
export type VerifyGuardianStatusParams = SandboxAuthParams & {
|
|
432
|
+
accessToken?: string;
|
|
387
433
|
baseUrl?: string;
|
|
388
434
|
sessionToken: string;
|
|
389
435
|
signal?: AbortSignal;
|
|
@@ -2,8 +2,34 @@ import type { Settings, DpoInfoTranslation } from "./api/types";
|
|
|
2
2
|
|
|
3
3
|
export type Props = Readonly<{
|
|
4
4
|
noticeId: string;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* JWT access token. Optional so sandbox integrations (see `token`) can
|
|
7
|
+
* omit it. Required in the normal JWT flow.
|
|
8
|
+
*/
|
|
9
|
+
accessToken?: string;
|
|
10
|
+
/** JWT refresh token. Optional; accepted for source-compat, currently unused. */
|
|
11
|
+
refreshToken?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Sandbox PoC: a pasted static token. When set, the SDK runs against the
|
|
14
|
+
* sandbox with no JWT — it sends the single `X-Consent-Token` header, carries
|
|
15
|
+
* the acting identity (`email` || `mobile`) in each request's own payload,
|
|
16
|
+
* takes org/workspace from `organisationUuid`/`workspaceUuid`, and does no
|
|
17
|
+
* token refresh.
|
|
18
|
+
*/
|
|
19
|
+
token?: string;
|
|
20
|
+
/** Sandbox test subject email (carried in the request payload when set). */
|
|
21
|
+
email?: string;
|
|
22
|
+
/** Sandbox test subject mobile (fallback subject when `email` is absent). */
|
|
23
|
+
mobile?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Sandbox UCIC: the client's own user id / org_user_id, e.g. a UUID or
|
|
26
|
+
* customer code; optional third identifier alongside email/mobile.
|
|
27
|
+
*/
|
|
28
|
+
ucic?: string;
|
|
29
|
+
/** Sandbox org UUID (replaces the JWT-decoded org in sandbox mode). */
|
|
30
|
+
organisationUuid?: string;
|
|
31
|
+
/** Sandbox workspace UUID (replaces the JWT-decoded workspace in sandbox mode). */
|
|
32
|
+
workspaceUuid?: string;
|
|
7
33
|
baseUrl?: string;
|
|
8
34
|
ledgerBaseUrl?: string;
|
|
9
35
|
settings?: Partial<Settings>;
|