@redacto.io/consent-sdk-js 0.1.1 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/redacto.js CHANGED
@@ -303,6 +303,7 @@ var fetchConsentContentPublic = async ({
303
303
  var submitConsentEvent = async ({
304
304
  accessToken,
305
305
  baseUrl,
306
+ ledgerBaseUrl,
306
307
  noticeUuid,
307
308
  purposes,
308
309
  declined,
@@ -318,7 +319,7 @@ var submitConsentEvent = async ({
318
319
  if (!decodedToken) throw new Error("Invalid access token");
319
320
  const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
320
321
  if (!ORG || !WS) throw new Error("Invalid token: missing organization or workspace UUID");
321
- const apiBaseUrl = baseUrl || BASE_URL;
322
+ const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL;
322
323
  const validatedPurposes = purposes.map((purpose) => {
323
324
  if (!purpose.uuid) throw new Error("Invalid purpose data: missing uuid");
324
325
  const dataElements = (purpose.data_elements || []).filter((element) => element.enabled).map((element) => ({
@@ -377,13 +378,14 @@ var submitConsentEventPublic = async ({
377
378
  workspace_uuid,
378
379
  accessToken,
379
380
  baseUrl,
381
+ ledgerBaseUrl,
380
382
  noticeUuid,
381
383
  purposes,
382
384
  declined,
383
385
  meta_data
384
386
  }) => {
385
387
  var _a, _b;
386
- const apiBaseUrl = baseUrl || BASE_URL_PUBLIC;
388
+ const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL_PUBLIC;
387
389
  const payload = {
388
390
  notice_uuid: noticeUuid,
389
391
  purposes: purposes.map((purpose) => {
@@ -616,6 +618,95 @@ var verifyGuardianStatus = async ({
616
618
  };
617
619
  };
618
620
 
621
+ // src/api/notice-consent-status.ts
622
+ var fetchNoticeConsentStatusForScope = async ({
623
+ accessToken,
624
+ ledgerBaseUrl,
625
+ organisationUuid,
626
+ workspaceUuid,
627
+ noticeUuid,
628
+ signal
629
+ }) => {
630
+ if (!accessToken || !ledgerBaseUrl || !organisationUuid || !workspaceUuid || !noticeUuid) {
631
+ return null;
632
+ }
633
+ try {
634
+ const response = await fetch(
635
+ `${ledgerBaseUrl}/public/organisations/${organisationUuid}/workspaces/${workspaceUuid}/notices/${noticeUuid}/check-consent`,
636
+ {
637
+ method: "POST",
638
+ headers: {
639
+ Authorization: `Bearer ${accessToken}`,
640
+ "Content-Type": "application/json"
641
+ },
642
+ body: "{}",
643
+ signal
644
+ }
645
+ );
646
+ if (!response.ok) return null;
647
+ const data = await response.json();
648
+ const body = data && typeof data === "object" && "detail" in data && data.detail ? data.detail : data;
649
+ if (!body || !Array.isArray(body.purposes)) return null;
650
+ return body;
651
+ } catch {
652
+ return null;
653
+ }
654
+ };
655
+ var fetchNoticeConsentStatus = async ({
656
+ accessToken,
657
+ ledgerBaseUrl,
658
+ noticeUuid,
659
+ signal
660
+ }) => {
661
+ if (!accessToken || !ledgerBaseUrl || !noticeUuid) return null;
662
+ const decodedToken = decodeTokenSafely(accessToken);
663
+ if (!decodedToken) return null;
664
+ const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
665
+ if (!ORG || !WS) return null;
666
+ return fetchNoticeConsentStatusForScope({
667
+ accessToken,
668
+ ledgerBaseUrl,
669
+ organisationUuid: ORG,
670
+ workspaceUuid: WS,
671
+ noticeUuid,
672
+ signal
673
+ });
674
+ };
675
+ var isNoticeFullyConsented = (status) => status.consented && status.all_mandatory_active;
676
+ var buildLedgerConsentOverlay = (status, configPurposes) => {
677
+ var _a, _b;
678
+ const byUuid = new Map(status.purposes.map((p) => [p.uuid, p]));
679
+ const hasHistory = status.purposes.some((p) => (p.status || "INACTIVE") !== "INACTIVE");
680
+ if (!hasHistory) return null;
681
+ const purpose_selections = {};
682
+ for (const cp of configPurposes) {
683
+ const lp = byUuid.get(cp.uuid);
684
+ const purposeStatus = (lp == null ? void 0 : lp.status) || "INACTIVE";
685
+ const selectedByUuid = new Map(((lp == null ? void 0 : lp.data_elements) || []).map((de) => [de.uuid, de.selected]));
686
+ const data_elements = {};
687
+ for (const de of cp.data_elements) {
688
+ data_elements[de.uuid] = {
689
+ selected: (_a = selectedByUuid.get(de.uuid)) != null ? _a : false,
690
+ enabled: de.enabled,
691
+ required: de.required
692
+ };
693
+ }
694
+ purpose_selections[cp.uuid] = {
695
+ selected: (_b = lp == null ? void 0 : lp.selected) != null ? _b : false,
696
+ status: purposeStatus,
697
+ needs_reconsent: purposeStatus !== "ACTIVE",
698
+ data_elements
699
+ };
700
+ }
701
+ const reconsent_required = configPurposes.some(
702
+ (cp) => {
703
+ var _a2;
704
+ return (((_a2 = purpose_selections[cp.uuid]) == null ? void 0 : _a2.status) || "INACTIVE") !== "ACTIVE";
705
+ }
706
+ );
707
+ return { reconsent_required, purpose_selections };
708
+ };
709
+
619
710
  // src/styles/inject.ts
620
711
  var STYLE_ID = "redacto-consent-checkbox-styles";
621
712
  var STYLE_CONTENT = `
@@ -1355,7 +1446,6 @@ var LANGUAGE_NATIVE_LABELS = {
1355
1446
  English: "English",
1356
1447
  Assamese: "\u0985\u09B8\u09AE\u09C0\u09AF\u09BC\u09BE (Assamese)",
1357
1448
  Bengali: "\u09AC\u09BE\u0982\u09B2\u09BE (Bengali)",
1358
- Bhojpuri: "\u092D\u094B\u091C\u092A\u0941\u0930\u0940 (Bhojpuri)",
1359
1449
  Bodo: "\u092C\u0921\u093C\u094B (Bodo)",
1360
1450
  Dogri: "\u0921\u094B\u0917\u0930\u0940 (Dogri)",
1361
1451
  "Goan Konkani": "\u0915\u094B\u0902\u0915\u0923\u0940 (Goan Konkani)",
@@ -2758,7 +2848,7 @@ var ModalController = class {
2758
2848
  }
2759
2849
  // ─── Private ───
2760
2850
  async fetchNotice() {
2761
- var _a, _b, _c;
2851
+ var _a, _b, _c, _d;
2762
2852
  this.abortController = new AbortController();
2763
2853
  this.store.setState({ isLoading: true, fetchError: null });
2764
2854
  try {
@@ -2774,7 +2864,23 @@ var ModalController = class {
2774
2864
  signal: this.abortController.signal
2775
2865
  });
2776
2866
  const activeConfig = data.detail.active_config;
2777
- const purposeSelections = data.detail.purpose_selections;
2867
+ let purposeSelections = data.detail.purpose_selections;
2868
+ let reconsentRequired = data.detail.reconsent_required;
2869
+ let ledgerOverlayApplied = false;
2870
+ if (this.options.ledgerBaseUrl && !this.options.applicationId) {
2871
+ const ledgerStatus = await fetchNoticeConsentStatus({
2872
+ accessToken: this.options.accessToken,
2873
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
2874
+ noticeUuid: this.options.noticeId,
2875
+ signal: this.abortController.signal
2876
+ });
2877
+ if (ledgerStatus) {
2878
+ const overlay = buildLedgerConsentOverlay(ledgerStatus, activeConfig.purposes);
2879
+ reconsentRequired = (_a = overlay == null ? void 0 : overlay.reconsent_required) != null ? _a : false;
2880
+ purposeSelections = overlay == null ? void 0 : overlay.purpose_selections;
2881
+ ledgerOverlayApplied = Boolean(overlay);
2882
+ }
2883
+ }
2778
2884
  const initialCollapsed = {};
2779
2885
  const initialPurposes = {};
2780
2886
  const initialElements = {};
@@ -2782,7 +2888,7 @@ var ModalController = class {
2782
2888
  let needsConsentPurposes = [];
2783
2889
  let alreadyConsentedPurposes = [];
2784
2890
  let isFullyConsentedReviewMode = false;
2785
- if (purposeSelections && data.detail.reconsent_required) {
2891
+ if (purposeSelections && (reconsentRequired || ledgerOverlayApplied)) {
2786
2892
  for (const purpose of activeConfig.purposes) {
2787
2893
  const selection = purposeSelections[purpose.uuid];
2788
2894
  initialCollapsed[purpose.uuid] = true;
@@ -2791,7 +2897,7 @@ var ModalController = class {
2791
2897
  initialPurposes[purpose.uuid] = true;
2792
2898
  for (const de of purpose.data_elements) {
2793
2899
  const combinedId = `${purpose.uuid}-${de.uuid}`;
2794
- const deSelection = (_a = selection.data_elements) == null ? void 0 : _a[de.uuid];
2900
+ const deSelection = (_b = selection.data_elements) == null ? void 0 : _b[de.uuid];
2795
2901
  const wasSelected = (deSelection == null ? void 0 : deSelection.selected) || false;
2796
2902
  initialElements[combinedId] = wasSelected;
2797
2903
  initialDataElementSelections[combinedId] = wasSelected;
@@ -2880,7 +2986,7 @@ var ModalController = class {
2880
2986
  this.options.onAccept();
2881
2987
  } else {
2882
2988
  this.store.setState({ fetchError: error, isLoading: false });
2883
- (_c = (_b = this.options).onError) == null ? void 0 : _c.call(_b, error);
2989
+ (_d = (_c = this.options).onError) == null ? void 0 : _d.call(_c, error);
2884
2990
  }
2885
2991
  }
2886
2992
  }
@@ -3588,6 +3694,7 @@ var ModalController = class {
3588
3694
  await submitConsentEvent({
3589
3695
  accessToken: this.options.accessToken,
3590
3696
  baseUrl: this.options.baseUrl,
3697
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
3591
3698
  noticeUuid: state.content.detail.uuid,
3592
3699
  purposes,
3593
3700
  declined: false,
@@ -4338,6 +4445,7 @@ var InlineController = class {
4338
4445
  this.purposeRefs = null;
4339
4446
  this.errorEl = null;
4340
4447
  this.hasSubmitted = false;
4448
+ this.hasCheckedLedgerConsent = false;
4341
4449
  this.unsubscribe = null;
4342
4450
  this.options = options;
4343
4451
  this.store = new Store({
@@ -4349,6 +4457,7 @@ var InlineController = class {
4349
4457
  selectedPurposes: {},
4350
4458
  selectedDataElements: {},
4351
4459
  hasAlreadyConsented: false,
4460
+ isCheckingConsent: false,
4352
4461
  fetchError: null,
4353
4462
  errorMessage: null
4354
4463
  });
@@ -4391,6 +4500,10 @@ var InlineController = class {
4391
4500
  }
4392
4501
  updateToken(accessToken) {
4393
4502
  this.options = { ...this.options, accessToken };
4503
+ if (this.shouldCheckLedgerConsent()) {
4504
+ void this.checkLedgerConsent();
4505
+ return;
4506
+ }
4394
4507
  this.tryAutoSubmit();
4395
4508
  }
4396
4509
  // ─── Private ───
@@ -4417,6 +4530,7 @@ var InlineController = class {
4417
4530
  initialElements[`${purpose.uuid}-${de.uuid}`] = false;
4418
4531
  }
4419
4532
  }
4533
+ void this.checkLedgerConsent();
4420
4534
  this.store.setState({
4421
4535
  content: activeConfig,
4422
4536
  isLoading: false,
@@ -4436,6 +4550,35 @@ var InlineController = class {
4436
4550
  }
4437
4551
  }
4438
4552
  }
4553
+ shouldCheckLedgerConsent() {
4554
+ return Boolean(this.options.ledgerBaseUrl) && !this.options.applicationId && Boolean(this.options.accessToken) && !this.hasCheckedLedgerConsent;
4555
+ }
4556
+ // Ask the Go ledger whether this principal has already fully consented —
4557
+ // the ledger equivalent of the legacy Python 409. Skipped without a ledger
4558
+ // (the 409 path governs) and for the magic-link applicationId flow. Any
4559
+ // failure returns null and falls through to the normal flow.
4560
+ async checkLedgerConsent() {
4561
+ var _a, _b;
4562
+ if (!this.shouldCheckLedgerConsent()) return;
4563
+ this.hasCheckedLedgerConsent = true;
4564
+ this.store.setState({ isCheckingConsent: true });
4565
+ try {
4566
+ const status = await fetchNoticeConsentStatusForScope({
4567
+ accessToken: this.options.accessToken,
4568
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
4569
+ organisationUuid: this.options.org_uuid,
4570
+ workspaceUuid: this.options.workspace_uuid,
4571
+ noticeUuid: this.options.notice_uuid
4572
+ });
4573
+ if (status && isNoticeFullyConsented(status)) {
4574
+ this.store.setState({ hasAlreadyConsented: true });
4575
+ (_b = (_a = this.options).onAccept) == null ? void 0 : _b.call(_a);
4576
+ }
4577
+ } finally {
4578
+ this.store.setState({ isCheckingConsent: false });
4579
+ this.tryAutoSubmit();
4580
+ }
4581
+ }
4439
4582
  handleStateChange(state, prevState) {
4440
4583
  if (prevState.isLoading && !state.isLoading && state.content) {
4441
4584
  this.rebuildUI();
@@ -4657,7 +4800,7 @@ var InlineController = class {
4657
4800
  var _a, _b;
4658
4801
  const state = this.store.getState();
4659
4802
  const accessToken = this.options.accessToken;
4660
- if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !this.hasSubmitted) {
4803
+ if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !state.isCheckingConsent && !state.hasAlreadyConsented && !this.hasSubmitted) {
4661
4804
  this.hasSubmitted = true;
4662
4805
  try {
4663
4806
  await this.handleSubmit(accessToken);
@@ -4682,6 +4825,7 @@ var InlineController = class {
4682
4825
  workspace_uuid: this.options.workspace_uuid,
4683
4826
  accessToken: tokenToUse,
4684
4827
  baseUrl: this.options.baseUrl,
4828
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
4685
4829
  noticeUuid: content.notice_uuid,
4686
4830
  purposes: content.purposes.map((purpose) => {
4687
4831
  const hasSelectedRequired = purpose.data_elements.some(
package/dist/redacto.mjs CHANGED
@@ -273,6 +273,7 @@ var fetchConsentContentPublic = async ({
273
273
  var submitConsentEvent = async ({
274
274
  accessToken,
275
275
  baseUrl,
276
+ ledgerBaseUrl,
276
277
  noticeUuid,
277
278
  purposes,
278
279
  declined,
@@ -288,7 +289,7 @@ var submitConsentEvent = async ({
288
289
  if (!decodedToken) throw new Error("Invalid access token");
289
290
  const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
290
291
  if (!ORG || !WS) throw new Error("Invalid token: missing organization or workspace UUID");
291
- const apiBaseUrl = baseUrl || BASE_URL;
292
+ const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL;
292
293
  const validatedPurposes = purposes.map((purpose) => {
293
294
  if (!purpose.uuid) throw new Error("Invalid purpose data: missing uuid");
294
295
  const dataElements = (purpose.data_elements || []).filter((element) => element.enabled).map((element) => ({
@@ -347,13 +348,14 @@ var submitConsentEventPublic = async ({
347
348
  workspace_uuid,
348
349
  accessToken,
349
350
  baseUrl,
351
+ ledgerBaseUrl,
350
352
  noticeUuid,
351
353
  purposes,
352
354
  declined,
353
355
  meta_data
354
356
  }) => {
355
357
  var _a, _b;
356
- const apiBaseUrl = baseUrl || BASE_URL_PUBLIC;
358
+ const apiBaseUrl = ledgerBaseUrl || baseUrl || BASE_URL_PUBLIC;
357
359
  const payload = {
358
360
  notice_uuid: noticeUuid,
359
361
  purposes: purposes.map((purpose) => {
@@ -586,6 +588,95 @@ var verifyGuardianStatus = async ({
586
588
  };
587
589
  };
588
590
 
591
+ // src/api/notice-consent-status.ts
592
+ var fetchNoticeConsentStatusForScope = async ({
593
+ accessToken,
594
+ ledgerBaseUrl,
595
+ organisationUuid,
596
+ workspaceUuid,
597
+ noticeUuid,
598
+ signal
599
+ }) => {
600
+ if (!accessToken || !ledgerBaseUrl || !organisationUuid || !workspaceUuid || !noticeUuid) {
601
+ return null;
602
+ }
603
+ try {
604
+ const response = await fetch(
605
+ `${ledgerBaseUrl}/public/organisations/${organisationUuid}/workspaces/${workspaceUuid}/notices/${noticeUuid}/check-consent`,
606
+ {
607
+ method: "POST",
608
+ headers: {
609
+ Authorization: `Bearer ${accessToken}`,
610
+ "Content-Type": "application/json"
611
+ },
612
+ body: "{}",
613
+ signal
614
+ }
615
+ );
616
+ if (!response.ok) return null;
617
+ const data = await response.json();
618
+ const body = data && typeof data === "object" && "detail" in data && data.detail ? data.detail : data;
619
+ if (!body || !Array.isArray(body.purposes)) return null;
620
+ return body;
621
+ } catch {
622
+ return null;
623
+ }
624
+ };
625
+ var fetchNoticeConsentStatus = async ({
626
+ accessToken,
627
+ ledgerBaseUrl,
628
+ noticeUuid,
629
+ signal
630
+ }) => {
631
+ if (!accessToken || !ledgerBaseUrl || !noticeUuid) return null;
632
+ const decodedToken = decodeTokenSafely(accessToken);
633
+ if (!decodedToken) return null;
634
+ const { organisation_uuid: ORG, workspace_uuid: WS } = decodedToken;
635
+ if (!ORG || !WS) return null;
636
+ return fetchNoticeConsentStatusForScope({
637
+ accessToken,
638
+ ledgerBaseUrl,
639
+ organisationUuid: ORG,
640
+ workspaceUuid: WS,
641
+ noticeUuid,
642
+ signal
643
+ });
644
+ };
645
+ var isNoticeFullyConsented = (status) => status.consented && status.all_mandatory_active;
646
+ var buildLedgerConsentOverlay = (status, configPurposes) => {
647
+ var _a, _b;
648
+ const byUuid = new Map(status.purposes.map((p) => [p.uuid, p]));
649
+ const hasHistory = status.purposes.some((p) => (p.status || "INACTIVE") !== "INACTIVE");
650
+ if (!hasHistory) return null;
651
+ const purpose_selections = {};
652
+ for (const cp of configPurposes) {
653
+ const lp = byUuid.get(cp.uuid);
654
+ const purposeStatus = (lp == null ? void 0 : lp.status) || "INACTIVE";
655
+ const selectedByUuid = new Map(((lp == null ? void 0 : lp.data_elements) || []).map((de) => [de.uuid, de.selected]));
656
+ const data_elements = {};
657
+ for (const de of cp.data_elements) {
658
+ data_elements[de.uuid] = {
659
+ selected: (_a = selectedByUuid.get(de.uuid)) != null ? _a : false,
660
+ enabled: de.enabled,
661
+ required: de.required
662
+ };
663
+ }
664
+ purpose_selections[cp.uuid] = {
665
+ selected: (_b = lp == null ? void 0 : lp.selected) != null ? _b : false,
666
+ status: purposeStatus,
667
+ needs_reconsent: purposeStatus !== "ACTIVE",
668
+ data_elements
669
+ };
670
+ }
671
+ const reconsent_required = configPurposes.some(
672
+ (cp) => {
673
+ var _a2;
674
+ return (((_a2 = purpose_selections[cp.uuid]) == null ? void 0 : _a2.status) || "INACTIVE") !== "ACTIVE";
675
+ }
676
+ );
677
+ return { reconsent_required, purpose_selections };
678
+ };
679
+
589
680
  // src/styles/inject.ts
590
681
  var STYLE_ID = "redacto-consent-checkbox-styles";
591
682
  var STYLE_CONTENT = `
@@ -1325,7 +1416,6 @@ var LANGUAGE_NATIVE_LABELS = {
1325
1416
  English: "English",
1326
1417
  Assamese: "\u0985\u09B8\u09AE\u09C0\u09AF\u09BC\u09BE (Assamese)",
1327
1418
  Bengali: "\u09AC\u09BE\u0982\u09B2\u09BE (Bengali)",
1328
- Bhojpuri: "\u092D\u094B\u091C\u092A\u0941\u0930\u0940 (Bhojpuri)",
1329
1419
  Bodo: "\u092C\u0921\u093C\u094B (Bodo)",
1330
1420
  Dogri: "\u0921\u094B\u0917\u0930\u0940 (Dogri)",
1331
1421
  "Goan Konkani": "\u0915\u094B\u0902\u0915\u0923\u0940 (Goan Konkani)",
@@ -2728,7 +2818,7 @@ var ModalController = class {
2728
2818
  }
2729
2819
  // ─── Private ───
2730
2820
  async fetchNotice() {
2731
- var _a, _b, _c;
2821
+ var _a, _b, _c, _d;
2732
2822
  this.abortController = new AbortController();
2733
2823
  this.store.setState({ isLoading: true, fetchError: null });
2734
2824
  try {
@@ -2744,7 +2834,23 @@ var ModalController = class {
2744
2834
  signal: this.abortController.signal
2745
2835
  });
2746
2836
  const activeConfig = data.detail.active_config;
2747
- const purposeSelections = data.detail.purpose_selections;
2837
+ let purposeSelections = data.detail.purpose_selections;
2838
+ let reconsentRequired = data.detail.reconsent_required;
2839
+ let ledgerOverlayApplied = false;
2840
+ if (this.options.ledgerBaseUrl && !this.options.applicationId) {
2841
+ const ledgerStatus = await fetchNoticeConsentStatus({
2842
+ accessToken: this.options.accessToken,
2843
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
2844
+ noticeUuid: this.options.noticeId,
2845
+ signal: this.abortController.signal
2846
+ });
2847
+ if (ledgerStatus) {
2848
+ const overlay = buildLedgerConsentOverlay(ledgerStatus, activeConfig.purposes);
2849
+ reconsentRequired = (_a = overlay == null ? void 0 : overlay.reconsent_required) != null ? _a : false;
2850
+ purposeSelections = overlay == null ? void 0 : overlay.purpose_selections;
2851
+ ledgerOverlayApplied = Boolean(overlay);
2852
+ }
2853
+ }
2748
2854
  const initialCollapsed = {};
2749
2855
  const initialPurposes = {};
2750
2856
  const initialElements = {};
@@ -2752,7 +2858,7 @@ var ModalController = class {
2752
2858
  let needsConsentPurposes = [];
2753
2859
  let alreadyConsentedPurposes = [];
2754
2860
  let isFullyConsentedReviewMode = false;
2755
- if (purposeSelections && data.detail.reconsent_required) {
2861
+ if (purposeSelections && (reconsentRequired || ledgerOverlayApplied)) {
2756
2862
  for (const purpose of activeConfig.purposes) {
2757
2863
  const selection = purposeSelections[purpose.uuid];
2758
2864
  initialCollapsed[purpose.uuid] = true;
@@ -2761,7 +2867,7 @@ var ModalController = class {
2761
2867
  initialPurposes[purpose.uuid] = true;
2762
2868
  for (const de of purpose.data_elements) {
2763
2869
  const combinedId = `${purpose.uuid}-${de.uuid}`;
2764
- const deSelection = (_a = selection.data_elements) == null ? void 0 : _a[de.uuid];
2870
+ const deSelection = (_b = selection.data_elements) == null ? void 0 : _b[de.uuid];
2765
2871
  const wasSelected = (deSelection == null ? void 0 : deSelection.selected) || false;
2766
2872
  initialElements[combinedId] = wasSelected;
2767
2873
  initialDataElementSelections[combinedId] = wasSelected;
@@ -2850,7 +2956,7 @@ var ModalController = class {
2850
2956
  this.options.onAccept();
2851
2957
  } else {
2852
2958
  this.store.setState({ fetchError: error, isLoading: false });
2853
- (_c = (_b = this.options).onError) == null ? void 0 : _c.call(_b, error);
2959
+ (_d = (_c = this.options).onError) == null ? void 0 : _d.call(_c, error);
2854
2960
  }
2855
2961
  }
2856
2962
  }
@@ -3558,6 +3664,7 @@ var ModalController = class {
3558
3664
  await submitConsentEvent({
3559
3665
  accessToken: this.options.accessToken,
3560
3666
  baseUrl: this.options.baseUrl,
3667
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
3561
3668
  noticeUuid: state.content.detail.uuid,
3562
3669
  purposes,
3563
3670
  declined: false,
@@ -4308,6 +4415,7 @@ var InlineController = class {
4308
4415
  this.purposeRefs = null;
4309
4416
  this.errorEl = null;
4310
4417
  this.hasSubmitted = false;
4418
+ this.hasCheckedLedgerConsent = false;
4311
4419
  this.unsubscribe = null;
4312
4420
  this.options = options;
4313
4421
  this.store = new Store({
@@ -4319,6 +4427,7 @@ var InlineController = class {
4319
4427
  selectedPurposes: {},
4320
4428
  selectedDataElements: {},
4321
4429
  hasAlreadyConsented: false,
4430
+ isCheckingConsent: false,
4322
4431
  fetchError: null,
4323
4432
  errorMessage: null
4324
4433
  });
@@ -4361,6 +4470,10 @@ var InlineController = class {
4361
4470
  }
4362
4471
  updateToken(accessToken) {
4363
4472
  this.options = { ...this.options, accessToken };
4473
+ if (this.shouldCheckLedgerConsent()) {
4474
+ void this.checkLedgerConsent();
4475
+ return;
4476
+ }
4364
4477
  this.tryAutoSubmit();
4365
4478
  }
4366
4479
  // ─── Private ───
@@ -4387,6 +4500,7 @@ var InlineController = class {
4387
4500
  initialElements[`${purpose.uuid}-${de.uuid}`] = false;
4388
4501
  }
4389
4502
  }
4503
+ void this.checkLedgerConsent();
4390
4504
  this.store.setState({
4391
4505
  content: activeConfig,
4392
4506
  isLoading: false,
@@ -4406,6 +4520,35 @@ var InlineController = class {
4406
4520
  }
4407
4521
  }
4408
4522
  }
4523
+ shouldCheckLedgerConsent() {
4524
+ return Boolean(this.options.ledgerBaseUrl) && !this.options.applicationId && Boolean(this.options.accessToken) && !this.hasCheckedLedgerConsent;
4525
+ }
4526
+ // Ask the Go ledger whether this principal has already fully consented —
4527
+ // the ledger equivalent of the legacy Python 409. Skipped without a ledger
4528
+ // (the 409 path governs) and for the magic-link applicationId flow. Any
4529
+ // failure returns null and falls through to the normal flow.
4530
+ async checkLedgerConsent() {
4531
+ var _a, _b;
4532
+ if (!this.shouldCheckLedgerConsent()) return;
4533
+ this.hasCheckedLedgerConsent = true;
4534
+ this.store.setState({ isCheckingConsent: true });
4535
+ try {
4536
+ const status = await fetchNoticeConsentStatusForScope({
4537
+ accessToken: this.options.accessToken,
4538
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
4539
+ organisationUuid: this.options.org_uuid,
4540
+ workspaceUuid: this.options.workspace_uuid,
4541
+ noticeUuid: this.options.notice_uuid
4542
+ });
4543
+ if (status && isNoticeFullyConsented(status)) {
4544
+ this.store.setState({ hasAlreadyConsented: true });
4545
+ (_b = (_a = this.options).onAccept) == null ? void 0 : _b.call(_a);
4546
+ }
4547
+ } finally {
4548
+ this.store.setState({ isCheckingConsent: false });
4549
+ this.tryAutoSubmit();
4550
+ }
4551
+ }
4409
4552
  handleStateChange(state, prevState) {
4410
4553
  if (prevState.isLoading && !state.isLoading && state.content) {
4411
4554
  this.rebuildUI();
@@ -4627,7 +4770,7 @@ var InlineController = class {
4627
4770
  var _a, _b;
4628
4771
  const state = this.store.getState();
4629
4772
  const accessToken = this.options.accessToken;
4630
- if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !this.hasSubmitted) {
4773
+ if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !state.isCheckingConsent && !state.hasAlreadyConsented && !this.hasSubmitted) {
4631
4774
  this.hasSubmitted = true;
4632
4775
  try {
4633
4776
  await this.handleSubmit(accessToken);
@@ -4652,6 +4795,7 @@ var InlineController = class {
4652
4795
  workspace_uuid: this.options.workspace_uuid,
4653
4796
  accessToken: tokenToUse,
4654
4797
  baseUrl: this.options.baseUrl,
4798
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
4655
4799
  noticeUuid: content.notice_uuid,
4656
4800
  purposes: content.purposes.map((purpose) => {
4657
4801
  const hasSelectedRequired = purpose.data_elements.some(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redacto.io/consent-sdk-js",
3
- "version": "0.1.1",
3
+ "version": "1.0.0-beta.0",
4
4
  "main": "dist/redacto.js",
5
5
  "module": "dist/redacto.mjs",
6
6
  "types": "dist/redacto.d.ts",
@@ -33,11 +33,11 @@
33
33
  "jwt-decode": "^4.0.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@vitest/coverage-v8": "3.1.3",
36
+ "@vitest/coverage-v8": "^4.1.8",
37
37
  "jsdom": "^26.1.0",
38
38
  "tsup": "^8.4.0",
39
39
  "typescript": "5.8.2",
40
- "vitest": "^3.1.3"
40
+ "vitest": "^4.1.8"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "tsup",