@redacto.io/consent-sdk-react 10.1.0 → 10.2.0-beta.1

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.
@@ -113,7 +113,9 @@ declare function createConsentClient(baseUrl: string, ledgerBaseUrl?: string): {
113
113
  * `selected: true` to regrant/renew, and `partial: true` so the rest of the
114
114
  * notice's purposes are left untouched. The principal is resolved from the
115
115
  * JWT, so no `contact`/identifiers are sent in the body, and
116
- * `consent_event_uuid` is omitted so the ledger mints a fresh event.
116
+ * `consent_event_uuid` is omitted so the ledger mints a fresh event, and
117
+ * `product_uuid` pins it to one card -- unscoped, the ledger fans the
118
+ * answer out over every product the notice covers.
117
119
  *
118
120
  * Without a ledger, the legacy Python `/manage-consent?status=` endpoint is
119
121
  * used with its `{ purpose_uuid, contact, ... }` payload.
@@ -113,7 +113,9 @@ declare function createConsentClient(baseUrl: string, ledgerBaseUrl?: string): {
113
113
  * `selected: true` to regrant/renew, and `partial: true` so the rest of the
114
114
  * notice's purposes are left untouched. The principal is resolved from the
115
115
  * JWT, so no `contact`/identifiers are sent in the body, and
116
- * `consent_event_uuid` is omitted so the ledger mints a fresh event.
116
+ * `consent_event_uuid` is omitted so the ledger mints a fresh event, and
117
+ * `product_uuid` pins it to one card -- unscoped, the ledger fans the
118
+ * answer out over every product the notice covers.
117
119
  *
118
120
  * Without a ledger, the legacy Python `/manage-consent?status=` endpoint is
119
121
  * used with its `{ purpose_uuid, contact, ... }` payload.
@@ -10032,6 +10032,64 @@ var useAuth = () => {
10032
10032
  // src/RedactoPrivacyCenter/context/BaseUrlContext.tsx
10033
10033
  var import_react4 = require("react");
10034
10034
 
10035
+ // src/shared/idempotency.ts
10036
+ var HEADER = "Idempotency-Key";
10037
+ var pending = /* @__PURE__ */ new Map();
10038
+ var MAX_PENDING = 16;
10039
+ var mintKey = () => {
10040
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
10041
+ return crypto.randomUUID();
10042
+ }
10043
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}-${Math.random().toString(36).slice(2, 14)}`;
10044
+ };
10045
+ var statusOf = (outcome) => {
10046
+ if (typeof Response !== "undefined" && outcome instanceof Response) {
10047
+ return outcome.status;
10048
+ }
10049
+ if (outcome && typeof outcome === "object" && "status" in outcome) {
10050
+ const status = outcome.status;
10051
+ if (typeof status === "number") return status;
10052
+ }
10053
+ return void 0;
10054
+ };
10055
+ var settledOnResolve = (result) => {
10056
+ const status = statusOf(result);
10057
+ return status === void 0 || status < 500;
10058
+ };
10059
+ var settledOnReject = (error) => {
10060
+ const status = statusOf(error);
10061
+ return status !== void 0 && status < 500;
10062
+ };
10063
+ var withIdempotencyKey = async (url, body, send) => {
10064
+ const signature = `${url}
10065
+ ${body}`;
10066
+ let entry = pending.get(signature);
10067
+ if (!entry) {
10068
+ entry = { key: mintKey(), inFlight: 0, keep: false };
10069
+ if (pending.size >= MAX_PENDING) {
10070
+ const oldest = pending.keys().next().value;
10071
+ if (oldest !== void 0) pending.delete(oldest);
10072
+ }
10073
+ pending.set(signature, entry);
10074
+ } else if (entry.inFlight === 0) {
10075
+ entry.keep = false;
10076
+ }
10077
+ entry.inFlight += 1;
10078
+ const settle = (settled) => {
10079
+ entry.inFlight -= 1;
10080
+ if (!settled) entry.keep = true;
10081
+ if (entry.inFlight === 0 && !entry.keep) pending.delete(signature);
10082
+ };
10083
+ try {
10084
+ const result = await send({ [HEADER]: entry.key });
10085
+ settle(settledOnResolve(result));
10086
+ return result;
10087
+ } catch (error) {
10088
+ settle(settledOnReject(error));
10089
+ throw error;
10090
+ }
10091
+ };
10092
+
10035
10093
  // src/RedactoPrivacyCenter/api/client.ts
10036
10094
  function createConsentClient(baseUrl, ledgerBaseUrl) {
10037
10095
  const ledgerConfigured = Boolean(ledgerBaseUrl);
@@ -10180,8 +10238,15 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10180
10238
  const queryParams = new URLSearchParams();
10181
10239
  if (language) queryParams.append("language", language);
10182
10240
  const queryString = queryParams.toString();
10183
- const url = privacyCenterUrl(baseRequest, `/form/data${queryString ? `?${queryString}` : ""}`);
10184
- const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
10241
+ const url = privacyCenterUrl(
10242
+ baseRequest,
10243
+ `/form/data${queryString ? `?${queryString}` : ""}`
10244
+ );
10245
+ const {
10246
+ useSessionAuth = false,
10247
+ getAuthToken,
10248
+ ...fetchOptions
10249
+ } = options || {};
10185
10250
  if (useSessionAuth) {
10186
10251
  return apiFetch(url, {
10187
10252
  ...fetchOptions,
@@ -10202,7 +10267,11 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10202
10267
  */
10203
10268
  async createCaseRequest(baseRequest, payload, authToken, options) {
10204
10269
  const url = privacyCenterUrl(baseRequest, "/case");
10205
- const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
10270
+ const {
10271
+ useSessionAuth = false,
10272
+ getAuthToken,
10273
+ ...fetchOptions
10274
+ } = options || {};
10206
10275
  const contact = sandboxContact();
10207
10276
  const requestPayload = contact && payload && typeof payload === "object" ? { ...payload, contact } : payload;
10208
10277
  if (useSessionAuth) {
@@ -10256,7 +10325,7 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10256
10325
  const url = `${baseUrl}/public/tokens/token-exchange?token=${encodeURIComponent(token)}`;
10257
10326
  const response = await fetch(url, {
10258
10327
  method: "GET",
10259
- headers: { "Accept": "application/json" }
10328
+ headers: { Accept: "application/json" }
10260
10329
  });
10261
10330
  if (!response.ok) {
10262
10331
  const errorData = await response.json().catch(() => null);
@@ -10296,13 +10365,24 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10296
10365
  */
10297
10366
  async getUserConsents(baseRequest, params, authToken, options) {
10298
10367
  const queryParams = new URLSearchParams();
10299
- if (params.offset !== void 0) queryParams.append("offset", params.offset.toString());
10300
- if (params.limit !== void 0) queryParams.append("limit", params.limit.toString());
10301
- if (params.nominated_offset !== void 0) queryParams.append("nominated_offset", params.nominated_offset.toString());
10302
- if (params.nominated_limit !== void 0) queryParams.append("nominated_limit", params.nominated_limit.toString());
10368
+ if (params.offset !== void 0)
10369
+ queryParams.append("offset", params.offset.toString());
10370
+ if (params.limit !== void 0)
10371
+ queryParams.append("limit", params.limit.toString());
10372
+ if (params.nominated_offset !== void 0)
10373
+ queryParams.append(
10374
+ "nominated_offset",
10375
+ params.nominated_offset.toString()
10376
+ );
10377
+ if (params.nominated_limit !== void 0)
10378
+ queryParams.append(
10379
+ "nominated_limit",
10380
+ params.nominated_limit.toString()
10381
+ );
10303
10382
  if (params.search) queryParams.append("search", params.search);
10304
10383
  if (params.status) queryParams.append("status", params.status);
10305
- if (params.product_uuid) queryParams.append("product_uuid", params.product_uuid);
10384
+ if (params.product_uuid)
10385
+ queryParams.append("product_uuid", params.product_uuid);
10306
10386
  if (params.language) queryParams.append("language", params.language);
10307
10387
  appendSandboxIdentity(queryParams);
10308
10388
  const queryString = queryParams.toString();
@@ -10321,12 +10401,15 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10321
10401
  */
10322
10402
  async getGroupPurposes(baseRequest, productUuid, params, authToken, options) {
10323
10403
  const queryParams = new URLSearchParams();
10324
- if (params.offset !== void 0) queryParams.append("offset", params.offset.toString());
10325
- if (params.limit !== void 0) queryParams.append("limit", params.limit.toString());
10404
+ if (params.offset !== void 0)
10405
+ queryParams.append("offset", params.offset.toString());
10406
+ if (params.limit !== void 0)
10407
+ queryParams.append("limit", params.limit.toString());
10326
10408
  if (params.status) queryParams.append("status", params.status);
10327
10409
  if (params.search) queryParams.append("search", params.search);
10328
10410
  if (params.language) queryParams.append("language", params.language);
10329
- if (params.nominator_uuid) queryParams.append("nominator_uuid", params.nominator_uuid);
10411
+ if (params.nominator_uuid)
10412
+ queryParams.append("nominator_uuid", params.nominator_uuid);
10330
10413
  appendSandboxIdentity(queryParams);
10331
10414
  const queryString = queryParams.toString();
10332
10415
  const suffix = `/user-consents/${productUuid}/purposes${queryString ? `?${queryString}` : ""}`;
@@ -10348,7 +10431,9 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10348
10431
  * `selected: true` to regrant/renew, and `partial: true` so the rest of the
10349
10432
  * notice's purposes are left untouched. The principal is resolved from the
10350
10433
  * JWT, so no `contact`/identifiers are sent in the body, and
10351
- * `consent_event_uuid` is omitted so the ledger mints a fresh event.
10434
+ * `consent_event_uuid` is omitted so the ledger mints a fresh event, and
10435
+ * `product_uuid` pins it to one card -- unscoped, the ledger fans the
10436
+ * answer out over every product the notice covers.
10352
10437
  *
10353
10438
  * Without a ledger, the legacy Python `/manage-consent?status=` endpoint is
10354
10439
  * used with its `{ purpose_uuid, contact, ... }` payload.
@@ -10356,8 +10441,14 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10356
10441
  async manageConsent(baseRequest, purposeId, contact, status, authToken, options, nominatorContact, dataElementUuids, productUuid, noticeUuid, dataElements, language) {
10357
10442
  var _a;
10358
10443
  if (!useLedger()) {
10359
- const url2 = privacyCenterUrl(baseRequest, `/manage-consent?status=${status}`);
10360
- const body2 = { purpose_uuid: purposeId, contact };
10444
+ const url2 = privacyCenterUrl(
10445
+ baseRequest,
10446
+ `/manage-consent?status=${status}`
10447
+ );
10448
+ const body2 = {
10449
+ purpose_uuid: purposeId,
10450
+ contact
10451
+ };
10361
10452
  if (nominatorContact) {
10362
10453
  body2.nominator_contact = nominatorContact;
10363
10454
  }
@@ -10378,7 +10469,9 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10378
10469
  });
10379
10470
  }
10380
10471
  if (!noticeUuid) {
10381
- throw new Error("notice_uuid is required to manage consent via the ledger");
10472
+ throw new Error(
10473
+ "notice_uuid is required to manage consent via the ledger"
10474
+ );
10382
10475
  }
10383
10476
  const purposeSelected = status !== "revoke";
10384
10477
  const elements = (dataElements != null ? dataElements : []).map((de) => {
@@ -10397,6 +10490,12 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10397
10490
  // Sandbox has no JWT, so the ledger resolves the acting test principal
10398
10491
  // from this identity in the body; null (spread no-op) on the live JWT path.
10399
10492
  ...(_a = sandboxBodyIdentity()) != null ? _a : {},
10493
+ // Pins the action to the card the user acted on. Without it the ledger
10494
+ // scopes by the notice and applies the answer to every product the
10495
+ // notice covers, so revoking a purpose held on two products withdrew
10496
+ // both. Stamped on the envelope and the selection, mirroring the Python
10497
+ // relay this path was modelled on.
10498
+ ...productUuid ? { product_uuid: productUuid } : {},
10400
10499
  partial: true,
10401
10500
  declined: false,
10402
10501
  select_all_mandatory: false,
@@ -10404,6 +10503,7 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10404
10503
  purposes: [
10405
10504
  {
10406
10505
  purpose_uuid: purposeId,
10506
+ ...productUuid ? { product_uuid: productUuid } : {},
10407
10507
  selected: purposeSelected,
10408
10508
  data_elements: elements
10409
10509
  }
@@ -10411,11 +10511,19 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10411
10511
  ...language ? { language } : {}
10412
10512
  };
10413
10513
  const url = ledgerUrl(baseRequest, "/submit-consent");
10414
- return privacyCenterApiFetch(url, {
10415
- ...options,
10416
- method: "POST",
10417
- body: JSON.stringify(body),
10418
- authToken: resolveAuthToken(authToken, baseRequest)
10514
+ const serialisedBody = JSON.stringify(body);
10515
+ return withIdempotencyKey(url, serialisedBody, (idempotencyHeaders) => {
10516
+ const headers = new Headers(options == null ? void 0 : options.headers);
10517
+ Object.entries(idempotencyHeaders).forEach(
10518
+ ([name, value]) => headers.set(name, value)
10519
+ );
10520
+ return privacyCenterApiFetch(url, {
10521
+ ...options,
10522
+ method: "POST",
10523
+ body: serialisedBody,
10524
+ headers,
10525
+ authToken: resolveAuthToken(authToken, baseRequest)
10526
+ });
10419
10527
  });
10420
10528
  },
10421
10529
  /**
@@ -10428,7 +10536,10 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10428
10536
  if (language) queryParams.append("language", language);
10429
10537
  appendSandboxIdentity(queryParams);
10430
10538
  const queryString = queryParams.toString();
10431
- const url = privacyCenterUrl(baseRequest, `/activities${queryString ? `?${queryString}` : ""}`);
10539
+ const url = privacyCenterUrl(
10540
+ baseRequest,
10541
+ `/activities${queryString ? `?${queryString}` : ""}`
10542
+ );
10432
10543
  return privacyCenterApiFetch(url, {
10433
10544
  ...options,
10434
10545
  authToken: resolveAuthToken(authToken, baseRequest)
@@ -10466,13 +10577,19 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10466
10577
  */
10467
10578
  async getReceipts(baseRequest, params, authToken, options) {
10468
10579
  const queryParams = new URLSearchParams();
10469
- if (params.skip !== void 0) queryParams.append("skip", params.skip.toString());
10470
- if (params.limit !== void 0) queryParams.append("limit", params.limit.toString());
10471
- if (params.notice_uuid) queryParams.append("notice_uuid", params.notice_uuid);
10472
- if (params.event_type) queryParams.append("event_type", params.event_type);
10580
+ if (params.skip !== void 0)
10581
+ queryParams.append("skip", params.skip.toString());
10582
+ if (params.limit !== void 0)
10583
+ queryParams.append("limit", params.limit.toString());
10584
+ if (params.notice_uuid)
10585
+ queryParams.append("notice_uuid", params.notice_uuid);
10586
+ if (params.event_type)
10587
+ queryParams.append("event_type", params.event_type);
10473
10588
  if (params.status) queryParams.append("status", params.status);
10474
- if (params.created_after) queryParams.append("created_after", params.created_after);
10475
- if (params.created_before) queryParams.append("created_before", params.created_before);
10589
+ if (params.created_after)
10590
+ queryParams.append("created_after", params.created_after);
10591
+ if (params.created_before)
10592
+ queryParams.append("created_before", params.created_before);
10476
10593
  if (params.language) queryParams.append("language", params.language);
10477
10594
  appendSandboxIdentity(queryParams);
10478
10595
  const queryString = queryParams.toString();
@@ -10514,7 +10631,10 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10514
10631
  trimmedToken.startsWith("Bearer ") ? trimmedToken : `Bearer ${trimmedToken}`
10515
10632
  );
10516
10633
  }
10517
- const response = await fetch(url, { method: "GET", headers: requestHeaders });
10634
+ const response = await fetch(url, {
10635
+ method: "GET",
10636
+ headers: requestHeaders
10637
+ });
10518
10638
  if (!response.ok) {
10519
10639
  let errorMessage = `Request failed with status ${response.status}`;
10520
10640
  try {
@@ -10551,7 +10671,11 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10551
10671
  * Get messages for a case (privacy center)
10552
10672
  */
10553
10673
  async getCaseMessages(baseRequest, caseUuid, authToken) {
10554
- const url = caseUrl(baseRequest, caseUuid, `/messages${sandboxIdentitySuffix()}`);
10674
+ const url = caseUrl(
10675
+ baseRequest,
10676
+ caseUuid,
10677
+ `/messages${sandboxIdentitySuffix()}`
10678
+ );
10555
10679
  return privacyCenterApiFetch(url, {
10556
10680
  authToken: resolveAuthToken(authToken, baseRequest)
10557
10681
  });
@@ -10560,7 +10684,11 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10560
10684
  * Send a message on a case (privacy center)
10561
10685
  */
10562
10686
  async sendCaseMessage(baseRequest, caseUuid, body, documentUuids, authToken) {
10563
- const url = caseUrl(baseRequest, caseUuid, `/messages${sandboxIdentitySuffix()}`);
10687
+ const url = caseUrl(
10688
+ baseRequest,
10689
+ caseUuid,
10690
+ `/messages${sandboxIdentitySuffix()}`
10691
+ );
10564
10692
  return privacyCenterApiFetch(url, {
10565
10693
  method: "POST",
10566
10694
  body: JSON.stringify({ body, document_uuids: documentUuids }),
@@ -10571,7 +10699,11 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10571
10699
  * Upload a document on a case (multipart/form-data, privacy center)
10572
10700
  */
10573
10701
  async uploadCaseDocument(baseRequest, caseUuid, file, authToken) {
10574
- const url = caseUrl(baseRequest, caseUuid, `/upload-document${sandboxIdentitySuffix()}`);
10702
+ const url = caseUrl(
10703
+ baseRequest,
10704
+ caseUuid,
10705
+ `/upload-document${sandboxIdentitySuffix()}`
10706
+ );
10575
10707
  const formData = new FormData();
10576
10708
  formData.append("file", file, file.name);
10577
10709
  formData.append("payload", JSON.stringify({ metadata: null }));
@@ -10600,7 +10732,11 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10600
10732
  * Get document info/download URL for a case document (privacy center)
10601
10733
  */
10602
10734
  async getCaseDocument(baseRequest, caseUuid, documentUuid, authToken) {
10603
- const url = caseUrl(baseRequest, caseUuid, `/documents/${documentUuid}${sandboxIdentitySuffix()}`);
10735
+ const url = caseUrl(
10736
+ baseRequest,
10737
+ caseUuid,
10738
+ `/documents/${documentUuid}${sandboxIdentitySuffix()}`
10739
+ );
10604
10740
  return privacyCenterApiFetch(url, {
10605
10741
  authToken: resolveAuthToken(authToken, baseRequest)
10606
10742
  });
@@ -10633,7 +10769,10 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10633
10769
  queryParams.append("language", params.language);
10634
10770
  }
10635
10771
  const queryString = queryParams.toString();
10636
- const url = notificationUrl(baseRequest, queryString ? `?${queryString}` : "");
10772
+ const url = notificationUrl(
10773
+ baseRequest,
10774
+ queryString ? `?${queryString}` : ""
10775
+ );
10637
10776
  return privacyCenterApiFetch(url, {
10638
10777
  authToken: resolveAuthToken(authToken, baseRequest)
10639
10778
  });
@@ -10680,7 +10819,10 @@ function createConsentClient(baseUrl, ledgerBaseUrl) {
10680
10819
  * Acknowledge a notification (sets is_read and is_acknowledged to true)
10681
10820
  */
10682
10821
  async acknowledgeNotification(baseRequest, notificationUuid, authToken) {
10683
- const url = notificationUrl(baseRequest, `/${notificationUuid}/acknowledge`);
10822
+ const url = notificationUrl(
10823
+ baseRequest,
10824
+ `/${notificationUuid}/acknowledge`
10825
+ );
10684
10826
  return privacyCenterApiFetch(url, {
10685
10827
  method: "POST",
10686
10828
  authToken: resolveAuthToken(authToken, baseRequest)