Repository navigation
Conversation
Add two components with a shared configuration (ComplianceConfig), so that apps add cookie consent and a compliance footer from configuration alone: - ecc-utils-design-cookie-consent: banner on the first visit (accept all, necessary only, customize), preferences dialog per category, consent in localStorage, asked again when the categories or the version change; hasConsent(), onConsentChange(), openCookiePreferences() and a window event for other code. - ecc-utils-design-compliance-footer: default legal items (privacy, terms, cookie policy, cookie preferences, imprint, accessibility, contact, privacy choices) that operators change, hide or extend; actions link, mailto, preferences, Markdown page (escaped subset) and https embed (sandboxed, optionally gated by consent). Configured URLs are checked (no javascript: links). Docs pages with previews and operator notes; tests for both components. Closes #458 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the compliance components: - Consent signature includes whether a category is required, so a category that becomes optional asks users again. - Consent that can't be written takes precedence over an older stored choice, which is removed, so revoking works with full storage. - onConsentChange() takes the config and also reports changes from other tabs (storage events); both components follow them. - The cookie consent keeps unsaved choices when an equal config object is passed (e.g. on every React render). - mailto items accept only plain addresses (no percent-encoding). - Same-origin embeds get no allow-same-origin in their sandbox. - Markdown links open a new tab only for other origins. - Footer dialog texts (texts.close); listeners removed on disconnect; URL tests; docs on consent-less embeds and Cookie Preferences. Refs #458 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A storage event drops consent kept in memory after a failed write, since the other tab's write is newer; a cleared storage (key null) counts too. - The cookie consent refreshes when its preferences close and when it is connected again, so changes made meanwhile aren't missed. Refs #458 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideData structuresDefine the shared consent, category, footer-item, and configuration data structures that underpin both components and their public utilities.
APIExpose the consent and footer component entry points, shared utilities, and typed component events to package consumers.
ImplementationImplement consent persistence, validation, cross-tab notifications, and footer-item merging used by the public components and helper functions.
Implement URL validation for links, embeds, external-origin checks, and mailto actions used by both components.
Render the supported Markdown subset as escaped Lit templates for legal pages in the footer dialog.
UIProvide the cookie banner and preferences dialog, including saving choices, following consent updates, and opening preferences on request.
Provide the configurable legal footer, its actions and dialog, consent-gated embeds, and item-selection event.
DocumentationDocument footer configuration, supported actions, safety constraints, and examples for React and HTML consumers.
Document cookie-consent behavior, configuration, integration examples, and operator guidance for loading optional scripts only with consent.
Register the new component documentation pages in the design components navigation.
TestsTest footer item merging, Markdown safety, footer actions, dialogs, consent-gated embeds, and listener cleanup.
Test consent storage, signatures, category edge cases, cross-tab updates, component behavior, and URL safety.
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Documentation build overview
53 files changed ·
|
There was a problem hiding this comment.
Hey - I've found 9 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="packages/ecc-utils-design/src/components/compliance-footer/compliance-footer.ts" line_range="178" />
<code_context>
+ // a same-origin frame with both flags could lift its own sandbox
+ const sandbox = isSameOrigin(src)
+ ? "allow-scripts allow-popups allow-popups-to-escape-sandbox"
+ : "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox";
+ return html`<iframe
+ class="mt-4 h-[60vh] w-full rounded-md border border-border"
</code_context>
<issue_to_address>
**Embedded content escapes sandbox restrictions**
When a visitor interacts with a malicious or compromised external embed, the external-frame sandbox allows popups to escape sandboxing. A malicious or compromised embed can open an unsandboxed popup after a user interaction and use it to navigate the app's top-level window, bypassing the frame's sandbox restrictions.
Omit `allow-popups-to-escape-sandbox` or disallow popups for embedded content.
</issue_to_address>
### Comment 2
<location path="packages/ecc-utils-design/src/components/cookie-consent/consent.ts" line_range="342-343" />
<code_context>
+ config?: CookieConsentConfig
+): () => void {
+ const handler = (event: Event) => {
+ const { detail } = event as CustomEvent<{ consent: CookieConsent }>;
+ if (detail?.consent) listener(detail.consent);
+ };
+ const storageHandler = (event: StorageEvent) => {
</code_context>
<issue_to_address>
**Other configurations receive consent**
When multiple consent configurations with different storage keys are used on the same page and one saves consent, `onConsentChange`'s same-window handler forwards every `CONSENT_CHANGED_EVENT` payload without matching the subscribed config's storage key or signature, so listeners receive unrelated choices and can enable optional behavior without consent for their configuration.
Match each event's consent to the subscribed configuration's storage key or signature before invoking its listener.
</issue_to_address>
### Comment 3
<location path="packages/ecc-utils-design/src/components/cookie-consent/cookie-consent.ts" line_range="108" />
<code_context>
+ private listen(): void {
+ this.stopListening?.();
+ this.stopListening = onConsentChange(() => {
+ if (!this.dialog?.open) this.refresh();
+ }, this.config?.cookies);
+ }
</code_context>
<issue_to_address>
**Stale preferences overwrite newer consent**
When one tab changes consent while another tab's preferences dialog is open, and the second tab then saves, `listen()` skips `refresh()` while the dialog is open, so this tab's `choices` remain stale; saving them calls `saveConsent()` for every category and overwrites the other tab's newer choices.
Reconcile incoming consent with the dialog's edits before saving, or detect that consent changed since the dialog opened and refresh or ask the user to resolve it.
</issue_to_address>
### Comment 4
<location path="packages/ecc-utils-design/src/components/cookie-consent/consent.ts" line_range="323-324" />
<code_context>
+ updatedAt: new Date().toISOString(),
+ };
+ writeItem(consentStorageKey(config), JSON.stringify(consent));
+ window.dispatchEvent(
+ new CustomEvent(CONSENT_CHANGED_EVENT, { detail: { consent } })
+ );
+ return consent;
</code_context>
<issue_to_address>
**Reentrant saves deliver stale consent**
When an earlier consent-change listener saves consent while handling an event, and a later listener also reacts to consent changes, `dispatchEvent()` invokes listeners synchronously, so an earlier listener that calls `saveConsent()` dispatches a newer event before the outer dispatch reaches later listeners; those listeners then receive the newer consent followed by the stale one and can re-enable behavior after a revocation.
Queue consent notifications and drain them in save order rather than allowing nested dispatches to overtake the outer event.
</issue_to_address>
### Comment 5
<location path="packages/ecc-utils-design/src/components/cookie-consent/consent.ts" line_range="314" />
<code_context>
+ }
+ const categories: Record<string, boolean> = {};
+ cookieCategories(config).forEach((category) => {
+ categories[category.id] =
+ !!category.required || consent.categories?.[category.id] === true;
+ });
</code_context>
<issue_to_address>
**The special category choice is lost**
When a configured category has the ID `__proto__`, `saveConsent()` assigns the category into ordinary objects, so the inherited `__proto__` setter prevents the choice from being stored or serialized; reads then miss the consent and the preference can appear enabled regardless of the user's choice.
Use prototype-safe records or equivalent own-property handling for category choices and saved consent.
Also at `packages/ecc-utils-design/src/components/cookie-consent/cookie-consent.ts:138-139`, `packages/ecc-utils-design/src/components/cookie-consent/consent.ts:273-276`.
</issue_to_address>
### Comment 6
<location path="packages/ecc-utils-design/src/components/cookie-consent/consent.ts" line_range="190" />
<code_context>
+ * @param config Cookie consent configuration
+ * @returns The categories
+ */
+export function cookieCategories(
+ config?: CookieConsentConfig
+): CookieCategory[] {
</code_context>
<issue_to_address>
**Duplicate categories conflict**
When configured categories contain duplicate IDs with conflicting `required` values, including a non-required `necessary` category when the default is added, `cookieCategories()` retains duplicate IDs, so the preferences can show conflicting switches while `saveConsent()` stores one value per ID and `hasConsent()` can treat the category as granted even after rejection.
Deduplicate category IDs while preserving the required category's consent semantics.
Also at `packages/ecc-utils-design/src/components/cookie-consent/consent.ts:193-198`, `packages/ecc-utils-design/src/components/cookie-consent/consent.ts:292-298`, `packages/ecc-utils-design/src/components/cookie-consent/consent.ts:314-315`.
</issue_to_address>
### Comment 7
<location path="packages/ecc-utils-design/src/components/cookie-consent/consent.ts" line_range="193" />
<code_context>
+export function cookieCategories(
+ config?: CookieConsentConfig
+): CookieCategory[] {
+ const categories = (config?.categories ?? []).filter(
+ (category) => category && typeof category.id === "string" && category.id
+ );
</code_context>
<issue_to_address>
**Malformed config breaks rendering**
When runtime deployment configuration supplies a non-array `cookies.categories` or `footer.items` value, `cookieCategories` or `footerItems` calls `.filter()` on the configured value, so a deployment setting with `categories` or `items` set to a non-array throws during component updates and the consent or footer UI fails to render. No runtime shape check catches this.
Validate these fields as arrays before filtering and fall back safely or report the invalid setting.
Also at `packages/ecc-utils-design/src/components/cookie-consent/consent.ts:374`.
</issue_to_address>
### Comment 8
<location path="packages/ecc-utils-design/src/components/cookie-consent/cookie-consent.ts" line_range="124" />
<code_context>
+ private async openDialog(item: DialogItem): Promise<void> {
+ this.openItem = item;
+ await this.updateComplete;
+ if (this.dialog && !this.dialog.open) this.dialog.showModal();
+ }
+
</code_context>
<issue_to_address>
**Detached dialogs reject opening**
When the component is removed after an open request schedules an update but before `updateComplete` resolves, `showModal()` runs after an `await` without checking whether the component is still connected; removing the component while its update is pending leaves the dialog detached, so `showModal()` throws and the preferences or footer page does not open.
After awaiting `updateComplete`, return without opening the dialog if the component is no longer connected.
Also at `packages/ecc-utils-design/src/components/compliance-footer/compliance-footer.ts:107`.
</issue_to_address>
### Comment 9
<location path="apps/documentation/pages/docs/design/components/cookie-consent.mdx" line_range="99" />
<code_context>
+ </Tabs.Tab>
+ <Tabs.Tab>
+ ```html showLineNumbers copy filename="index.html"
+ <script type="module" src="https://esm.sh/@elixir-cloud/design@2.0.0-alpha.50/components/cookie-consent"></script>
+ <ecc-utils-design-cookie-consent></ecc-utils-design-cookie-consent>
+ <script type="module">
</code_context>
<issue_to_address>
**HTML examples load unavailable release**
The package version is `2.0.0-alpha.49`, so these URLs request `2.0.0-alpha.50`, which is not the version built by this PR. Users following either HTML example fail to load the component or load no implementation of the new feature.
Point the examples at the package version that contains these components, or bump the package version to `2.0.0-alpha.50` through the release process.
Also at `apps/documentation/pages/docs/design/components/compliance-footer.mdx:70`.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 9 findings to address first, and if consent checks or their configuration are wrong, an optional embed or script could load before the user has agreed, sending browsing data to a third party. Reverting can stop future loads, but it cannot undo data already disclosed.
Blocking findings: packages/ecc-utils-design/src/components/compliance-footer/compliance-footer.ts:178, packages/ecc-utils-design/src/components/cookie-consent/consent.ts:343, packages/ecc-utils-design/src/components/cookie-consent/cookie-consent.ts:108, packages/ecc-utils-design/src/components/cookie-consent/consent.ts:324, packages/ecc-utils-design/src/components/cookie-consent/consent.ts:314, and 4 more
Fixes from the automated review: - onConsentChange() reports only consent of its config's storage key; the window event's detail carries the storage key. - Consent events are dispatched in save order, also when a listener saves consent itself. - Categories with the same ID are merged (required wins); __proto__ and non-array settings are skipped, also for footer items. - Consent from another tab is merged into open preferences: categories the user switched stay, the others follow. - Dialogs don't open once the element was removed. - Embeds lose allow-popups-to-escape-sandbox. Refs #458 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the conflict in the design package's test scripts: take dev's port 8000, as assigned to the package in #446. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Linked issues
#458 feat: cookie consent and compliance footer · 9 of 10 met
| # | Requirement | Status | Notes |
|---|---|---|---|
| 1 | Show a consent banner on the first visit | 🟡 Partly met | The banner is suppressed when there are no optional categories unless notice is enabled. |
| 2 | Collect consent per category, including necessary and configurable categories | ✅ Met | Categories are configurable, and a required necessary category is added when none is marked required. |
| 3 | Store consent in the browser and ask again when configured categories change | ✅ Met | Consent is stored in localStorage, and the signature includes category IDs, required status, and policy version. |
| 4 | Provide a way to reopen cookie preferences | ✅ Met | The footer preferences action and openCookiePreferences() open the preferences dialog. |
| 5 | Provide an API or event for checking consent before loading optional scripts | ✅ Met | The change exports hasConsent(), onConsentChange(), and consent events. |
| 6 | Provide a configurable compliance footer with the listed default items | ✅ Met | The defaults are defined in the shared consent module; entries without actions are not displayed. |
| 7 | Let operators remove, change, and add footer items | ✅ Met | Footer configuration supports overrides, hidden items, additional items, and replacing the defaults. |
| 8 | Support Markdown pages, embeds, mailto, preferences, and external links as item actions | ✅ Met | Each action is supported; embed actions can name a consent category and wait for that consent before rendering the frame. |
| 9 | Document a sample configuration and what operators must provide | ✅ Met | The component docs include configuration examples and operator notes on policies, consent, and embed CSP. |
| 10 | Allow apps to add consent management and a footer from configuration alone | ✅ Met | Both components consume the shared ComplianceConfig. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #509 +/- ##
==========================================
+ Coverage 73.53% 76.79% +3.26%
==========================================
Files 33 40 +7
Lines 8123 9403 +1280
Branches 381 542 +161
==========================================
+ Hits 5973 7221 +1248
- Misses 2141 2169 +28
- Partials 9 13 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Linked issuesChecked at commit c79f40e #458 feat: cookie consent and compliance footer · 7 of 7 met
|
Description
Apps built on the components (first the Cloud Registry UI, elixir-cloud-aai/cloud-registry-ui#38) need cookie consent and links to legal information that operators configure per deployment, without code changes. This adds two components to
@elixir-cloud/designthat read one shared configuration (ComplianceConfig, e.g. from a JSON setting):ecc-utils-design-cookie-consent(config.cookies): a banner on the first visit with equally prominent "Accept all", "Necessary only" and "Customize"; a preferences dialog per category (native<dialog>); consent stored inlocalStorageand asked again when the categories (IDs,required) orversionchange. Without optional categories no banner is shown, unlessnoticeis set. Firesecc-cookie-consent-changed(on the element, not bubbling, and onwindow).ecc-utils-design-compliance-footer(config.footer): default items (Privacy Policy, Terms & Conditions, Cookie Policy, Cookie Preferences, Imprint, Accessibility Statement, Contact Us, Your Privacy Choices) that operators give an action, rename, hide or extend. Actions:link,mailto,preferences,page(Markdown subset, rendered with Lit, so all text is escaped) andembed(anhttpspage in a sandboxed frame, e.g. from Iubenda, Termly or GetTerms; withconsent, only after consent to that category). Fires the cancelableecc-compliance-item-selected, so apps can route items themselves.components/cookie-consent/index.js, also from the main entry):hasConsent(),readConsent(),saveConsent(),onConsentChange()(also reports changes from other tabs),openCookiePreferences(),footerItems().http(s),mailtoand relative links only, nojavascript:/data:; embeds onlyhttps, orhttpon localhost;mailtoonly plain addresses). Same-origin embeds get noallow-same-origin, so they can't lift their sandbox. Consent that can't be stored applies to the page only and never falls back to an older stored choice.frame-srcfor embeds, embeds that set cookies).testscript (--port 8003) and thetsconfigoption of the shared test config, as fix(cloud-registry): react to provider changes #502, feat(cloud-registry): expose problem details #504 and feat: add surface, font and dark mode controls #446 do.No existing API changes; the components and types are new.
Closes #458
How it was verified
npm run lint(husky, whole repo): 0 errors, warnings unchanged.npx turbo run build --filter=@elixir-cloud/designand--filter=ecc-docs: pass (React wrappers generated).npx web-test-runner --group defaultinpackages/ecc-utils-designwith Chromium and Firefox: 47/47 pass. WebKit can't start on my machine; CI runs it.mailtoand external links, no page errors.Checklist
Comments
@elixir-cloud/designrather than a new package, because a new npm package needs its own trusted-publishing and release setup. Happy to move them into a separate package (e.g.@elixir-cloud/compliance) if you prefer.<script>that injects a policy) aren't supported, since configuration must not run code on the page; most policy services also offer a page URL to embed or link.ecc-cookie-consent-changedis written as a literal innew CustomEvent(...), not via the exported constant, because the React wrapper generator reads the event name from that call.Declined review findings
consent.ts,urls.ts) fromcomponents/cookie-consent/to a neutral folder: the footer imports it from there; the package has no shared folder for non-component code yet, and the cookie consent is its natural owner.🤖 Generated with Claude Code
Summary by Sourcery
Add configurable cookie consent and compliance footer components to provide deployment-specific privacy controls and legal information without application code changes.
New Features:
Enhancements:
Documentation:
Tests: