@redacto.io/consent-sdk-js 0.1.2-beta.0 → 1.0.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.
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 = `
@@ -2757,7 +2848,7 @@ var ModalController = class {
2757
2848
  }
2758
2849
  // ─── Private ───
2759
2850
  async fetchNotice() {
2760
- var _a, _b, _c;
2851
+ var _a, _b, _c, _d;
2761
2852
  this.abortController = new AbortController();
2762
2853
  this.store.setState({ isLoading: true, fetchError: null });
2763
2854
  try {
@@ -2773,7 +2864,23 @@ var ModalController = class {
2773
2864
  signal: this.abortController.signal
2774
2865
  });
2775
2866
  const activeConfig = data.detail.active_config;
2776
- 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
+ }
2777
2884
  const initialCollapsed = {};
2778
2885
  const initialPurposes = {};
2779
2886
  const initialElements = {};
@@ -2781,7 +2888,7 @@ var ModalController = class {
2781
2888
  let needsConsentPurposes = [];
2782
2889
  let alreadyConsentedPurposes = [];
2783
2890
  let isFullyConsentedReviewMode = false;
2784
- if (purposeSelections && data.detail.reconsent_required) {
2891
+ if (purposeSelections && (reconsentRequired || ledgerOverlayApplied)) {
2785
2892
  for (const purpose of activeConfig.purposes) {
2786
2893
  const selection = purposeSelections[purpose.uuid];
2787
2894
  initialCollapsed[purpose.uuid] = true;
@@ -2790,7 +2897,7 @@ var ModalController = class {
2790
2897
  initialPurposes[purpose.uuid] = true;
2791
2898
  for (const de of purpose.data_elements) {
2792
2899
  const combinedId = `${purpose.uuid}-${de.uuid}`;
2793
- 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];
2794
2901
  const wasSelected = (deSelection == null ? void 0 : deSelection.selected) || false;
2795
2902
  initialElements[combinedId] = wasSelected;
2796
2903
  initialDataElementSelections[combinedId] = wasSelected;
@@ -2879,7 +2986,7 @@ var ModalController = class {
2879
2986
  this.options.onAccept();
2880
2987
  } else {
2881
2988
  this.store.setState({ fetchError: error, isLoading: false });
2882
- (_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);
2883
2990
  }
2884
2991
  }
2885
2992
  }
@@ -3587,6 +3694,7 @@ var ModalController = class {
3587
3694
  await submitConsentEvent({
3588
3695
  accessToken: this.options.accessToken,
3589
3696
  baseUrl: this.options.baseUrl,
3697
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
3590
3698
  noticeUuid: state.content.detail.uuid,
3591
3699
  purposes,
3592
3700
  declined: false,
@@ -4337,7 +4445,9 @@ var InlineController = class {
4337
4445
  this.purposeRefs = null;
4338
4446
  this.errorEl = null;
4339
4447
  this.hasSubmitted = false;
4448
+ this.hasCheckedLedgerConsent = false;
4340
4449
  this.unsubscribe = null;
4450
+ this.abortController = null;
4341
4451
  this.options = options;
4342
4452
  this.store = new Store({
4343
4453
  content: null,
@@ -4348,12 +4458,14 @@ var InlineController = class {
4348
4458
  selectedPurposes: {},
4349
4459
  selectedDataElements: {},
4350
4460
  hasAlreadyConsented: false,
4461
+ isCheckingConsent: false,
4351
4462
  fetchError: null,
4352
4463
  errorMessage: null
4353
4464
  });
4354
4465
  this.mediaQuery = createMediaQuery("(max-width: 768px)");
4355
4466
  }
4356
4467
  async init() {
4468
+ this.abortController = new AbortController();
4357
4469
  if (typeof this.options.target === "string") {
4358
4470
  this.targetEl = document.querySelector(this.options.target);
4359
4471
  if (!this.targetEl) {
@@ -4376,8 +4488,9 @@ var InlineController = class {
4376
4488
  await this.fetchNotice();
4377
4489
  }
4378
4490
  destroy() {
4379
- var _a;
4380
- (_a = this.unsubscribe) == null ? void 0 : _a.call(this);
4491
+ var _a, _b;
4492
+ (_a = this.abortController) == null ? void 0 : _a.abort();
4493
+ (_b = this.unsubscribe) == null ? void 0 : _b.call(this);
4381
4494
  this.mediaQuery.destroy();
4382
4495
  this.store.destroy();
4383
4496
  if (this.hostEl && this.hostEl.parentNode) {
@@ -4390,9 +4503,19 @@ var InlineController = class {
4390
4503
  }
4391
4504
  updateToken(accessToken) {
4392
4505
  this.options = { ...this.options, accessToken };
4506
+ if (this.shouldCheckLedgerConsent()) {
4507
+ void this.checkLedgerConsent();
4508
+ return;
4509
+ }
4393
4510
  this.tryAutoSubmit();
4394
4511
  }
4395
4512
  // ─── Private ───
4513
+ // destroy() aborts the controller, so an aborted signal is the marker that
4514
+ // teardown has happened and async continuations must not touch the host.
4515
+ isDestroyed() {
4516
+ var _a;
4517
+ return Boolean((_a = this.abortController) == null ? void 0 : _a.signal.aborted);
4518
+ }
4396
4519
  async fetchNotice() {
4397
4520
  var _a, _b, _c, _d;
4398
4521
  this.store.setState({ isLoading: true, fetchError: null });
@@ -4405,6 +4528,9 @@ var InlineController = class {
4405
4528
  language: this.options.language,
4406
4529
  specific_uuid: this.options.applicationId
4407
4530
  });
4531
+ if (this.isDestroyed()) {
4532
+ return;
4533
+ }
4408
4534
  const activeConfig = data.detail.active_config;
4409
4535
  const initialCollapsed = {};
4410
4536
  const initialPurposes = {};
@@ -4416,6 +4542,7 @@ var InlineController = class {
4416
4542
  initialElements[`${purpose.uuid}-${de.uuid}`] = false;
4417
4543
  }
4418
4544
  }
4545
+ void this.checkLedgerConsent();
4419
4546
  this.store.setState({
4420
4547
  content: activeConfig,
4421
4548
  isLoading: false,
@@ -4425,6 +4552,9 @@ var InlineController = class {
4425
4552
  selectedDataElements: initialElements
4426
4553
  });
4427
4554
  } catch (err) {
4555
+ if (this.isDestroyed()) {
4556
+ return;
4557
+ }
4428
4558
  const error = err;
4429
4559
  if (error.status === 409) {
4430
4560
  this.store.setState({ hasAlreadyConsented: true, isLoading: false });
@@ -4435,6 +4565,41 @@ var InlineController = class {
4435
4565
  }
4436
4566
  }
4437
4567
  }
4568
+ shouldCheckLedgerConsent() {
4569
+ return Boolean(this.options.ledgerBaseUrl) && !this.options.applicationId && Boolean(this.options.accessToken) && !this.hasCheckedLedgerConsent;
4570
+ }
4571
+ // Ask the Go ledger whether this principal has already fully consented —
4572
+ // the ledger equivalent of the legacy Python 409. Skipped without a ledger
4573
+ // (the 409 path governs) and for the magic-link applicationId flow. Any
4574
+ // failure returns null and falls through to the normal flow.
4575
+ async checkLedgerConsent() {
4576
+ var _a, _b, _c;
4577
+ if (!this.shouldCheckLedgerConsent()) return;
4578
+ this.hasCheckedLedgerConsent = true;
4579
+ this.store.setState({ isCheckingConsent: true });
4580
+ try {
4581
+ const status = await fetchNoticeConsentStatusForScope({
4582
+ accessToken: this.options.accessToken,
4583
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
4584
+ organisationUuid: this.options.org_uuid,
4585
+ workspaceUuid: this.options.workspace_uuid,
4586
+ noticeUuid: this.options.notice_uuid,
4587
+ signal: (_a = this.abortController) == null ? void 0 : _a.signal
4588
+ });
4589
+ if (this.isDestroyed()) {
4590
+ return;
4591
+ }
4592
+ if (status && isNoticeFullyConsented(status)) {
4593
+ this.store.setState({ hasAlreadyConsented: true });
4594
+ (_c = (_b = this.options).onAccept) == null ? void 0 : _c.call(_b);
4595
+ }
4596
+ } finally {
4597
+ if (!this.isDestroyed()) {
4598
+ this.store.setState({ isCheckingConsent: false });
4599
+ this.tryAutoSubmit();
4600
+ }
4601
+ }
4602
+ }
4438
4603
  handleStateChange(state, prevState) {
4439
4604
  if (prevState.isLoading && !state.isLoading && state.content) {
4440
4605
  this.rebuildUI();
@@ -4654,9 +4819,12 @@ var InlineController = class {
4654
4819
  }
4655
4820
  async tryAutoSubmit() {
4656
4821
  var _a, _b;
4822
+ if (this.isDestroyed()) {
4823
+ return;
4824
+ }
4657
4825
  const state = this.store.getState();
4658
4826
  const accessToken = this.options.accessToken;
4659
- if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !this.hasSubmitted) {
4827
+ if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !state.isCheckingConsent && !state.hasAlreadyConsented && !this.hasSubmitted) {
4660
4828
  this.hasSubmitted = true;
4661
4829
  try {
4662
4830
  await this.handleSubmit(accessToken);
@@ -4681,6 +4849,7 @@ var InlineController = class {
4681
4849
  workspace_uuid: this.options.workspace_uuid,
4682
4850
  accessToken: tokenToUse,
4683
4851
  baseUrl: this.options.baseUrl,
4852
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
4684
4853
  noticeUuid: content.notice_uuid,
4685
4854
  purposes: content.purposes.map((purpose) => {
4686
4855
  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 = `
@@ -2727,7 +2818,7 @@ var ModalController = class {
2727
2818
  }
2728
2819
  // ─── Private ───
2729
2820
  async fetchNotice() {
2730
- var _a, _b, _c;
2821
+ var _a, _b, _c, _d;
2731
2822
  this.abortController = new AbortController();
2732
2823
  this.store.setState({ isLoading: true, fetchError: null });
2733
2824
  try {
@@ -2743,7 +2834,23 @@ var ModalController = class {
2743
2834
  signal: this.abortController.signal
2744
2835
  });
2745
2836
  const activeConfig = data.detail.active_config;
2746
- 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
+ }
2747
2854
  const initialCollapsed = {};
2748
2855
  const initialPurposes = {};
2749
2856
  const initialElements = {};
@@ -2751,7 +2858,7 @@ var ModalController = class {
2751
2858
  let needsConsentPurposes = [];
2752
2859
  let alreadyConsentedPurposes = [];
2753
2860
  let isFullyConsentedReviewMode = false;
2754
- if (purposeSelections && data.detail.reconsent_required) {
2861
+ if (purposeSelections && (reconsentRequired || ledgerOverlayApplied)) {
2755
2862
  for (const purpose of activeConfig.purposes) {
2756
2863
  const selection = purposeSelections[purpose.uuid];
2757
2864
  initialCollapsed[purpose.uuid] = true;
@@ -2760,7 +2867,7 @@ var ModalController = class {
2760
2867
  initialPurposes[purpose.uuid] = true;
2761
2868
  for (const de of purpose.data_elements) {
2762
2869
  const combinedId = `${purpose.uuid}-${de.uuid}`;
2763
- 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];
2764
2871
  const wasSelected = (deSelection == null ? void 0 : deSelection.selected) || false;
2765
2872
  initialElements[combinedId] = wasSelected;
2766
2873
  initialDataElementSelections[combinedId] = wasSelected;
@@ -2849,7 +2956,7 @@ var ModalController = class {
2849
2956
  this.options.onAccept();
2850
2957
  } else {
2851
2958
  this.store.setState({ fetchError: error, isLoading: false });
2852
- (_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);
2853
2960
  }
2854
2961
  }
2855
2962
  }
@@ -3557,6 +3664,7 @@ var ModalController = class {
3557
3664
  await submitConsentEvent({
3558
3665
  accessToken: this.options.accessToken,
3559
3666
  baseUrl: this.options.baseUrl,
3667
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
3560
3668
  noticeUuid: state.content.detail.uuid,
3561
3669
  purposes,
3562
3670
  declined: false,
@@ -4307,7 +4415,9 @@ var InlineController = class {
4307
4415
  this.purposeRefs = null;
4308
4416
  this.errorEl = null;
4309
4417
  this.hasSubmitted = false;
4418
+ this.hasCheckedLedgerConsent = false;
4310
4419
  this.unsubscribe = null;
4420
+ this.abortController = null;
4311
4421
  this.options = options;
4312
4422
  this.store = new Store({
4313
4423
  content: null,
@@ -4318,12 +4428,14 @@ var InlineController = class {
4318
4428
  selectedPurposes: {},
4319
4429
  selectedDataElements: {},
4320
4430
  hasAlreadyConsented: false,
4431
+ isCheckingConsent: false,
4321
4432
  fetchError: null,
4322
4433
  errorMessage: null
4323
4434
  });
4324
4435
  this.mediaQuery = createMediaQuery("(max-width: 768px)");
4325
4436
  }
4326
4437
  async init() {
4438
+ this.abortController = new AbortController();
4327
4439
  if (typeof this.options.target === "string") {
4328
4440
  this.targetEl = document.querySelector(this.options.target);
4329
4441
  if (!this.targetEl) {
@@ -4346,8 +4458,9 @@ var InlineController = class {
4346
4458
  await this.fetchNotice();
4347
4459
  }
4348
4460
  destroy() {
4349
- var _a;
4350
- (_a = this.unsubscribe) == null ? void 0 : _a.call(this);
4461
+ var _a, _b;
4462
+ (_a = this.abortController) == null ? void 0 : _a.abort();
4463
+ (_b = this.unsubscribe) == null ? void 0 : _b.call(this);
4351
4464
  this.mediaQuery.destroy();
4352
4465
  this.store.destroy();
4353
4466
  if (this.hostEl && this.hostEl.parentNode) {
@@ -4360,9 +4473,19 @@ var InlineController = class {
4360
4473
  }
4361
4474
  updateToken(accessToken) {
4362
4475
  this.options = { ...this.options, accessToken };
4476
+ if (this.shouldCheckLedgerConsent()) {
4477
+ void this.checkLedgerConsent();
4478
+ return;
4479
+ }
4363
4480
  this.tryAutoSubmit();
4364
4481
  }
4365
4482
  // ─── Private ───
4483
+ // destroy() aborts the controller, so an aborted signal is the marker that
4484
+ // teardown has happened and async continuations must not touch the host.
4485
+ isDestroyed() {
4486
+ var _a;
4487
+ return Boolean((_a = this.abortController) == null ? void 0 : _a.signal.aborted);
4488
+ }
4366
4489
  async fetchNotice() {
4367
4490
  var _a, _b, _c, _d;
4368
4491
  this.store.setState({ isLoading: true, fetchError: null });
@@ -4375,6 +4498,9 @@ var InlineController = class {
4375
4498
  language: this.options.language,
4376
4499
  specific_uuid: this.options.applicationId
4377
4500
  });
4501
+ if (this.isDestroyed()) {
4502
+ return;
4503
+ }
4378
4504
  const activeConfig = data.detail.active_config;
4379
4505
  const initialCollapsed = {};
4380
4506
  const initialPurposes = {};
@@ -4386,6 +4512,7 @@ var InlineController = class {
4386
4512
  initialElements[`${purpose.uuid}-${de.uuid}`] = false;
4387
4513
  }
4388
4514
  }
4515
+ void this.checkLedgerConsent();
4389
4516
  this.store.setState({
4390
4517
  content: activeConfig,
4391
4518
  isLoading: false,
@@ -4395,6 +4522,9 @@ var InlineController = class {
4395
4522
  selectedDataElements: initialElements
4396
4523
  });
4397
4524
  } catch (err) {
4525
+ if (this.isDestroyed()) {
4526
+ return;
4527
+ }
4398
4528
  const error = err;
4399
4529
  if (error.status === 409) {
4400
4530
  this.store.setState({ hasAlreadyConsented: true, isLoading: false });
@@ -4405,6 +4535,41 @@ var InlineController = class {
4405
4535
  }
4406
4536
  }
4407
4537
  }
4538
+ shouldCheckLedgerConsent() {
4539
+ return Boolean(this.options.ledgerBaseUrl) && !this.options.applicationId && Boolean(this.options.accessToken) && !this.hasCheckedLedgerConsent;
4540
+ }
4541
+ // Ask the Go ledger whether this principal has already fully consented —
4542
+ // the ledger equivalent of the legacy Python 409. Skipped without a ledger
4543
+ // (the 409 path governs) and for the magic-link applicationId flow. Any
4544
+ // failure returns null and falls through to the normal flow.
4545
+ async checkLedgerConsent() {
4546
+ var _a, _b, _c;
4547
+ if (!this.shouldCheckLedgerConsent()) return;
4548
+ this.hasCheckedLedgerConsent = true;
4549
+ this.store.setState({ isCheckingConsent: true });
4550
+ try {
4551
+ const status = await fetchNoticeConsentStatusForScope({
4552
+ accessToken: this.options.accessToken,
4553
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
4554
+ organisationUuid: this.options.org_uuid,
4555
+ workspaceUuid: this.options.workspace_uuid,
4556
+ noticeUuid: this.options.notice_uuid,
4557
+ signal: (_a = this.abortController) == null ? void 0 : _a.signal
4558
+ });
4559
+ if (this.isDestroyed()) {
4560
+ return;
4561
+ }
4562
+ if (status && isNoticeFullyConsented(status)) {
4563
+ this.store.setState({ hasAlreadyConsented: true });
4564
+ (_c = (_b = this.options).onAccept) == null ? void 0 : _c.call(_b);
4565
+ }
4566
+ } finally {
4567
+ if (!this.isDestroyed()) {
4568
+ this.store.setState({ isCheckingConsent: false });
4569
+ this.tryAutoSubmit();
4570
+ }
4571
+ }
4572
+ }
4408
4573
  handleStateChange(state, prevState) {
4409
4574
  if (prevState.isLoading && !state.isLoading && state.content) {
4410
4575
  this.rebuildUI();
@@ -4624,9 +4789,12 @@ var InlineController = class {
4624
4789
  }
4625
4790
  async tryAutoSubmit() {
4626
4791
  var _a, _b;
4792
+ if (this.isDestroyed()) {
4793
+ return;
4794
+ }
4627
4795
  const state = this.store.getState();
4628
4796
  const accessToken = this.options.accessToken;
4629
- if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !this.hasSubmitted) {
4797
+ if (accessToken && state.content && this.areAllRequiredElementsChecked() && !state.isSubmitting && !state.isCheckingConsent && !state.hasAlreadyConsented && !this.hasSubmitted) {
4630
4798
  this.hasSubmitted = true;
4631
4799
  try {
4632
4800
  await this.handleSubmit(accessToken);
@@ -4651,6 +4819,7 @@ var InlineController = class {
4651
4819
  workspace_uuid: this.options.workspace_uuid,
4652
4820
  accessToken: tokenToUse,
4653
4821
  baseUrl: this.options.baseUrl,
4822
+ ledgerBaseUrl: this.options.ledgerBaseUrl,
4654
4823
  noticeUuid: content.notice_uuid,
4655
4824
  purposes: content.purposes.map((purpose) => {
4656
4825
  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.2-beta.0",
3
+ "version": "1.0.0-beta.1",
4
4
  "main": "dist/redacto.js",
5
5
  "module": "dist/redacto.mjs",
6
6
  "types": "dist/redacto.d.ts",