@atlasprivacy/consent-sdk 0.0.0-stage → 0.0.1-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/CHANGELOG.md ADDED
@@ -0,0 +1,155 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 (in beta)
4
+
5
+ First public release, published to npm as `@atlasprivacy/consent-sdk` under the MIT license. Betas are `1.0.0-beta.N` on the npm `beta` tag. The internal builds listed below were never released; their changes are all part of 1.0.0.
6
+
7
+ - Synchronize saved decisions across tabs and SDK instances, reconcile server choices, and expose `onPreferenceChange` and `atlas-consent:change` without credentials.
8
+ - Display every configured purpose when notice sections are incomplete, and reject malformed notice structures before mounting.
9
+ - Keep dialogs dismissible during saves and rendering failures. Use native modality for dynamically added page content and restore focus through nested Shadow DOM.
10
+ - Allow a cancelled launch to be reopened immediately; ignore stale session responses and form callbacks.
11
+ - Sanitize notice HTML with bundled DOMPurify and support enforcing Trusted Types without a permissive default policy.
12
+ - Preserve HTTP status on invalid JSON responses. Add `refresh()` and a reload action for notices changed while editing.
13
+ - Test Chromium, Firefox, WebKit, and mobile viewports; gate CI packages on dependency auditing; reject changed assets at existing published versions.
14
+
15
+ ## Internal builds before 1.0.0
16
+
17
+ These versions were built for testing only and are superseded by 1.0.0.
18
+
19
+ ### Internal 1.8.0
20
+
21
+ - Render the consent form inside the SDK, in the website's page, in Shadow DOM. Websites no longer load an Atlas iframe. The Atlas API is unchanged and still stores every decision.
22
+ - Make all session, decision, and view calls from the SDK. Save failures now report the real error code and HTTP status through `onError`.
23
+ - Remove the `frame_timeout` and `result_mismatch` error codes. An invalid save result now reports `invalid_response`.
24
+ - Keep `ui.html` and `atlas-consent-ui.js` only for the Atlas dashboard preview.
25
+ - Keep one session, and so one consent record, when the notice, its language or the website list changes. Sessions renew on use and last a year from last use once they hold a consent. `getClaim()` keeps working across these changes.
26
+ - Name the main button by what it saves: "Accept Necessary" when no optional purpose is ticked, "Accept Selected" when any is. It previously could read "Accept Necessary" while saving optional purposes.
27
+ - Show notice text as written: names keep their case, and paragraphs, lists, emphasis and links are kept.
28
+ - Mark purposes, collected data and shared data with their own language when it differs from the notice language.
29
+ - `open()` on a mounted panel scrolls the panel into view instead of doing nothing.
30
+ - Report every failure to `onError` and `atlas-consent:error` once, including failures of `mount()` and `open()`. Errors use the specific `code` Atlas now sends, such as `consent_linked` and `notice_changed`.
31
+ - `theme: "dark"` uses dark colours unless colours are set. Unknown theme values fall back to light.
32
+ - Script tags accept `data-auto-show` and `data-appearance`, use the script's own Atlas address before the build default, report `missing_container` for a panel without `data-target`, and fire `atlas-consent:ready` after the configuration loads.
33
+ - Send the SDK version with each save, for the audit trail.
34
+ - CI publishes `shipping` packages as well as `dev` and `staging`; the 1.7.0 note that shipping stays disabled no longer applies.
35
+
36
+ ### Internal 1.7.1
37
+
38
+ - Send the save operation's key in the `Idempotency-Key` header, the same name the Atlas API uses, instead of `submission_id` in the body. Requires a backend that allows this header.
39
+
40
+ ### Internal 1.7.0
41
+
42
+ - Select build-time backend defaults from DEV_URL, STAGING_URL, and PRODUCTION_URL. Explicit API URLs still override the default.
43
+ - Publish dev and staging packages to the private GitLab Generic Package Registry after checks pass. Keep shipping publication disabled.
44
+ - Expire all pipeline artifacts after one week.
45
+ - Use the ci-cogserver runner, fresh checkouts, and npm caches outside the source directory.
46
+
47
+ ### Internal 1.6.2
48
+
49
+ - Remove the docs directory and package references.
50
+ - Cancel automatic startup when the client is closed before configuration loads.
51
+ - Scope saved preferences by backend base URL and app ID. Old app-only preferences require a fresh choice.
52
+ - Validate configuration, session choices, and saved results before use. Report invalid notice rendering.
53
+ - Preserve browser initialization in consumer bundles.
54
+ - Keep Shift+Tab inside the dialog from its initial focus position.
55
+ - Choose button text by contrast and handle non-string appearance overrides safely.
56
+
57
+ ### Internal 1.6.1
58
+
59
+ - Separate client lifecycle, storage, transport, dialog accessibility, protocol, and hosted UI modules.
60
+ - Separate UI styles, theme handling, rendering, and session API calls.
61
+ - Group tests by unit, integration, browser, dashboard, package, and fixtures.
62
+ - Add formatting, lint, import-boundary, and package-consumer checks to the GitLab build.
63
+ - Clean generated output before each build. Preserve public exports and asset filenames.
64
+
65
+ ### Internal 1.6.0
66
+
67
+ - Add System theme. The form follows device light and dark preferences and updates when they change. Brand color stays the same.
68
+
69
+ ### Internal 1.5.5
70
+
71
+ - Cancel pending form launches on close. Reject calls after destroy before sending requests.
72
+ - Restore page access when two clients open dialogs. Open panel clients in a popup when `open()` is called.
73
+ - Close dialogs after completion even when the completion callback throws.
74
+ - Time out form reads and saves after 15 seconds. Keep the submission ID for retries.
75
+ - Disable editable choices during a save. Keep required choices selected.
76
+ - Validate appearance keys and support IPv6 localhost base URLs.
77
+ - Add GitLab build, browser test, and package jobs with distributable artifacts.
78
+ - Verify asset version and integrity before backend sync. Build before package creation.
79
+
80
+ ### Internal 1.5.4
81
+
82
+ - Fix: group tick boxes were cleared after every save.
83
+ - Fix: opening a popup or full page no longer puts a focus ring on the close button. Tab still reaches it first.
84
+ - Script-tag start now reports a clear error when the script address does not contain `/consent-sdk/assets/`, instead of calling a wrong address.
85
+ - The build fails if the SDK version in `src/protocol.ts` differs from `package.json`.
86
+ - Requests time out after 15 seconds. A panel that never loads now reports `frame_timeout`, like the popup.
87
+ - A mismatched or unexpected completion message now raises `onError` (`result_mismatch`) instead of being ignored.
88
+ - `onClose` runs only when a form was open, and not on `destroy()`.
89
+ - The page behind a popup or full-page form is inert while it is open, and restored on close.
90
+ - Saved preferences in storage are checked before use.
91
+
92
+ ### Internal 1.5.3
93
+
94
+ - Embedded panel fills its container, with no grey side bands, and uses a calmer 14px text size.
95
+
96
+ ### Internal 1.5.2
97
+
98
+ - Spacing now changes the whole form: text size, gaps, row padding and button height. Compact is clearly tighter.
99
+ - The rounded font uses a humanist stack that looks different from Sans on every device.
100
+
101
+ ### Internal 1.5.1
102
+
103
+ - Fix: colors and style passed in code were replaced by the saved look once the form loaded.
104
+
105
+ ### Internal 1.5.0
106
+
107
+ - Full page mode opens over the same page instead of a new browser tab, and closes back to it. Show full-page consent in the current tab. Restore website scrolling and focus on close.
108
+
109
+ ### Internal 1.4.0
110
+
111
+ - Match the consent screen used in templates and levels: bordered group cards with a chevron, a tick box per group and per purpose, "+N more" data summaries, a shared-data list, a fade above stacked footer buttons, and the same type sizes.
112
+ - Match its corner size by default. The "soft" corners are 6px.
113
+ - Skip the link section, as the template screen does.
114
+
115
+ ### Internal 1.3.1
116
+
117
+ - Show the three buttons in one row when the form is wide.
118
+ - Remove the in-form preview banner. The dashboard already says nothing is saved.
119
+
120
+ ### Internal 1.3.0
121
+
122
+ - Add the `appearance` option. Colors and style set in code override the look saved in Atlas.
123
+
124
+ ### Internal 1.2.3
125
+
126
+ - Default to the FRS theme: pink `#ae0a60` and text `#1a202c`, the same colors as the consent level screen.
127
+
128
+ ### Internal 1.2.2
129
+
130
+ - Do not move focus into the form in dashboard previews.
131
+
132
+ ### Internal 1.2.1
133
+
134
+ - Do not move focus or scroll when an embedded panel opens.
135
+
136
+ ### Internal 1.2.0
137
+
138
+ - Render the consent form from the consent level: title, sections, data groups and button names.
139
+ - Add collapsible data groups, pinned buttons and "Accept Selected" after a choice is changed.
140
+ - Remove text and button label settings from appearance. They now come from the consent level.
141
+
142
+ ### Internal 1.1.0
143
+
144
+ - Add appearance settings from the dashboard: theme, colors, corner style, font, density, brand label, text and button labels.
145
+ - Apply appearance with CSS variables. The SDK accepts hex colors and fixed choices only.
146
+ - Add a live `appearance` message so the dashboard preview updates while a person edits.
147
+ - Report one anonymous "form shown" count per opened form, for the dashboard funnel.
148
+ - Appearance changes do not change the consent version, so saved choices stay valid.
149
+
150
+ ### Internal 1.0.0
151
+
152
+ - Add panel, automatic popup and button-opened full page modes.
153
+ - Add scoped anonymous sessions, saved choices and verified backend linking.
154
+ - Add shared hosted UI, TypeScript types, lifecycle methods and completion events.
155
+ - Add versioned browser/ESM bundles and asset integrity manifest.
@@ -0,0 +1,85 @@
1
+ # Contributing
2
+
3
+ Use Node.js 22.13 or later in the Node.js 22 series. `.nvmrc` selects Node.js 22. Commit the npm lock file with dependency changes.
4
+
5
+ ## Local checks
6
+
7
+ ```sh
8
+ npm ci
9
+ npm run check
10
+ npx playwright install chromium firefox webkit
11
+ npm run test:browser
12
+ ```
13
+
14
+ `check` runs formatting, lint, unit and integration tests, TypeScript checks, a clean build, and an installed-package consumer check. The browser suite uses a local API fixture and the built SDK. `src/ui/index.ts` builds `ui.html` for the Atlas dashboard preview only. The adjacent demo check runs only when that demo checkout is present.
15
+
16
+ Use `npm run format` to format files. Use `npm run lint:fix` for supported lint fixes. Review the resulting changes before commit.
17
+
18
+ ## Code changes
19
+
20
+ - Put lifecycle and API code in `src/client/`, protocol helpers in `src/shared/`, and form code in `src/ui/`.
21
+ - Keep public exports in `src/index.ts`. Keep script startup in `src/browser.ts`.
22
+ - Use explicit types at public and network boundaries. Use `import type` for type imports.
23
+ - Use `.js` extensions for relative TypeScript imports. The compiler resolves them to source files and preserves valid ESM declaration imports.
24
+ - Keep shared code independent of the client and form. The client may import the form. The form must not import the client: it receives its API calls as callbacks. Lint checks these boundaries. Integration tests check dependency cycles.
25
+ - Use one operation per statement. Use names that explain the value or action. Add comments for constraints and security decisions.
26
+ - Add regression tests for behavior changes. Keep unit tests independent of a running backend. Use browser tests for focus, DOM, messaging, and theme behavior.
27
+ - Do not put organization credentials, browser session tokens, or local environment files in source control.
28
+
29
+ ## Compatibility and releases
30
+
31
+ Keep the `create`, `ConsentClient`, `ConsentError`, and `VERSION` exports stable in a compatible release. Keep public type names and built asset filenames stable. The package consumer test checks exports and declaration resolution from an installed archive.
32
+
33
+ `package.json` and `src/shared/constants.ts` hold `X.Y.Z`, the next release; CI adds the pre-release suffix (see below). To start the next release, set both, the lock file, README examples, and the backend version constant to the new `X.Y.Z` together. Run the checks before syncing assets. Use a new version directory; preserve assets used by existing integrations.
34
+
35
+ Backend asset sync stages the complete build before publishing it. An identical repeat sync is allowed; different contents at an existing version are rejected. Bump the SDK version for changed release assets rather than overwriting URLs used by installed integrations.
36
+
37
+ ## Dependency maintenance
38
+
39
+ DOMPurify is bundled into the SDK for safe notice parsing and Trusted Types compatibility. It is a build dependency; installed SDK consumers need no separate runtime package. The build preserves license notices and includes `dist/DOMPurify-LICENSE.txt`. Keep the sanitizer current and run the hostile-markup and enforcing-CSP browser tests when changing it. Formatting and lint tools are development dependencies.
40
+
41
+ CI runs `npm audit --audit-level=moderate` as a separate release gate. Browser checks run the current Chromium, Firefox, and WebKit engines, including mobile Chromium and WebKit viewport projects. Use `npm run test:browser -- --project=chromium` for a focused local run.
42
+
43
+ The `minimatch` override pins `11.0.1` for the lint toolchain. Earlier releases have a [pattern amplification advisory](https://github.com/advisories/GHSA-g3hv-75wg-m249). Version 11 changes internal APIs, as described in its [changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md). The lint checks exercise the current toolchain with this override. Recheck and remove the override when upstream dependencies use a patched version directly. Run `npm audit` and all checks when changing dependencies.
44
+
45
+ ## Branches and releases
46
+
47
+ ```text
48
+ feature ──MR──▶ dev ──promote──▶ staging ──promote──▶ shipping ──▶ npm + tag vVERSION
49
+ ▲ ▲ │
50
+ └──── back-merge ──┴──── back-merge ◀───┘ (after every release)
51
+ ```
52
+
53
+ Merge requests go into `dev` only. Promotions are fast-forwards that only Maintainers can run:
54
+
55
+ 1. On a `dev` pipeline, press **promote:staging**. It starts a staging pipeline that fast-forwards `staging` to that commit; the normal `staging` pipeline then builds and publishes the release candidate.
56
+ 2. On the `staging` pipeline, once it has published, press **promote:shipping**. `shipping` moves to that commit, which must be staging's latest.
57
+ 3. The `shipping` pipeline rebuilds with `PRODUCTION_URL`, publishes to npm and the GitLab registry, tags `vVERSION`, creates the GitLab release, and runs **back-merge**.
58
+
59
+ A hotfix can be pushed to `shipping` directly; back-merge brings it into `staging` and `dev`. Review pipeline changes before promoting: `git diff origin/staging...origin/dev -- .gitlab-ci.yml scripts/ci`.
60
+
61
+ The `version` job chooses each pipeline's version (`scripts/lib/release-version.mjs`) and every later job stamps it into the package (`scripts/ci/stamp-version.mjs`):
62
+
63
+ - `dev` and other branches build `X.Y.Z-dev.<pipeline>`; `staging` builds `X.Y.Z-rc.<pipeline>`.
64
+ - `shipping` builds `X.Y.Z-beta.N` while `RELEASE_CHANNEL` in `.gitlab-ci.yml` is `beta`, where `N` is one more than the highest beta on npm. Nobody bumps `N` by hand.
65
+ - For the final release, set `RELEASE_CHANNEL: final` in a commit and promote it. `shipping` then publishes `X.Y.Z` on the npm `latest` tag. After that, set `package.json` to the next version (and `RELEASE_CHANNEL` back to `beta` for its betas).
66
+ - A shipping pipeline for a commit npm already holds keeps that version, so retrying it publishes nothing twice. A version number on npm can never be reused, even after unpublishing.
67
+
68
+ ### CI/CD variables
69
+
70
+ | Variable | Value | Settings |
71
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ |
72
+ | `DEV_URL`, `STAGING_URL`, `PRODUCTION_URL` | Backend API base URLs (see README) | Environment scope `*` |
73
+ | `NPM_TOKEN` | npm granular access token, read and write, for `@atlasprivacy/consent-sdk` (the whole `@atlasprivacy` scope for the first publish), with an expiry and **Bypass two-factor authentication** ticked; the npm account requires 2FA for writes, so a token without it fails with `EOTP` | Masked, protected, environment scope `npm` |
74
+ | `PROMOTE_TOKEN` | Project access token, Maintainer role, `write_repository` scope | Masked, protected, environment scope `promotion` |
75
+
76
+ ### Project settings
77
+
78
+ - **Protected branches:** `dev`, `staging` and `shipping`; merge and push: Maintainers.
79
+ - **Protected tags:** `v*`, create: Maintainers. Release tags then come only from the `release` job.
80
+ - **Merge request pipelines:** "Allow merge request pipelines to access protected variables and runners" off.
81
+ - **npm:** on the package's settings page, set publishing access to "Require two-factor authentication or a granular access token". Give the org's `developers` team read-only access so only the token and owners can publish.
82
+
83
+ ## Merge requests
84
+
85
+ Describe the problem, the final change, and the checks you ran. State changes to public behavior or deployment requirements. Keep unrelated edits in a separate merge request. The GitLab pipeline publishes packages as described in [Branches and releases](#branches-and-releases); it does not deploy the backend.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FRS Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,225 @@
1
- # Temporary Holding Version
1
+ # Atlas Consent SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A browser SDK with a built-in consent form and three display modes: embedded panel, automatic popup, and full page opened by a button. The form runs in your page. Atlas provides the API that loads the notice and stores decisions.
4
+
5
+ In Atlas, open **Consents → Integrations**, create an app and select a published consent level. Open the integration to add your website origins and enable it. You can create multiple apps for the same level. Copy the app ID and API base URL from its setup guide. Keep your organization API token on your server.
6
+
7
+ ## Add the SDK
8
+
9
+ Load the script from npm through jsDelivr, or use the versioned asset URL from Atlas. Pin an exact version; a version URL never changes. Replace the example host, version and app ID below. Until 1.0.0 is final, releases are betas such as `1.0.0-beta.1` (see [npm](https://www.npmjs.com/package/@atlasprivacy/consent-sdk?activeTab=versions) for the current one).
10
+
11
+ ```html
12
+ <script src="https://cdn.jsdelivr.net/npm/@atlasprivacy/consent-sdk@1.0.0/dist/atlas-consent.js"></script>
13
+ ```
14
+
15
+ With a bundler, install the package and import it:
16
+
17
+ ```sh
18
+ npm install @atlasprivacy/consent-sdk@beta
19
+ ```
20
+
21
+ ```ts
22
+ import { create } from "@atlasprivacy/consent-sdk";
23
+ ```
24
+
25
+ The npm package defaults to the production Atlas API. Pass `apiBaseUrl` to use another Atlas deployment. The examples below use the Atlas-hosted asset URL:
26
+
27
+ ```html
28
+ <script src="https://atlas.example/consent-sdk/assets/1.0.0/atlas-consent.js"></script>
29
+ <script>
30
+ const consent = AtlasConsent.create({
31
+ appId: "ac_app_REPLACE_WITH_YOUR_APP_ID",
32
+ apiBaseUrl: "https://atlas.example",
33
+ mode: "popup",
34
+ onComplete(result, claim) {
35
+ console.log(result.choices);
36
+ // Send claim to your authenticated backend when the visitor logs in.
37
+ },
38
+ onError(error) {
39
+ console.error(error.code, error.message);
40
+ },
41
+ });
42
+ </script>
43
+ ```
44
+
45
+ The API base URL must include any deployment path prefix. Do not add `/api/v1` or `/consent-sdk` to it.
46
+
47
+ For a script-only automatic popup:
48
+
49
+ ```html
50
+ <script
51
+ defer
52
+ src="https://atlas.example/consent-sdk/assets/1.0.0/atlas-consent.js"
53
+ data-atlas-app="ac_app_REPLACE_WITH_YOUR_APP_ID"
54
+ data-mode="popup"
55
+ ></script>
56
+ ```
57
+
58
+ Script tag attributes:
59
+
60
+ | Attribute | Meaning |
61
+ | ------------------- | ------------------------------------------------------------------------------ |
62
+ | `data-atlas-app` | Required. The app ID. |
63
+ | `data-mode` | `panel`, `popup` or `page`. Defaults to the mode saved in Atlas. |
64
+ | `data-target` | CSS selector of the panel container. Required when the mode is `panel`. |
65
+ | `data-language` | Notice language. Defaults to `en`. |
66
+ | `data-auto-show` | `false` stops a popup from opening by itself. |
67
+ | `data-appearance` | The `appearance` object as JSON, for example `'{"theme":"dark"}'`. |
68
+ | `data-api-base-url` | The Atlas API base URL. Normally not needed: the script's own address is used. |
69
+
70
+ `window` receives `atlas-consent:ready` once the configuration has loaded, with the client in `event.detail.client`. If the mode is `panel` and there is no `data-target`, the SDK reports a `missing_container` error instead of showing nothing. Use either automatic initialization or `create()`, so you do not create duplicate instances.
71
+
72
+ ## Choose a display mode
73
+
74
+ ### Embedded panel
75
+
76
+ Place the container before initialization. The SDK inserts the form into this element.
77
+
78
+ ```html
79
+ <div id="consent-panel"></div>
80
+ <script>
81
+ const consent = AtlasConsent.create({
82
+ appId: "ac_app_REPLACE_WITH_YOUR_APP_ID",
83
+ apiBaseUrl: "https://atlas.example",
84
+ mode: "panel",
85
+ target: "#consent-panel",
86
+ });
87
+ </script>
88
+ ```
89
+
90
+ ### Popup on page load
91
+
92
+ Use `mode: 'popup'`. The SDK opens it when there is no current saved choice for this app and configuration. To open only on demand, set `autoShow: false` and call `consent.open()` from your button.
93
+
94
+ ### Full page on a button click
95
+
96
+ ```js
97
+ const consent = AtlasConsent.create({
98
+ appId: "ac_app_REPLACE_WITH_YOUR_APP_ID",
99
+ apiBaseUrl: "https://atlas.example",
100
+ mode: "page",
101
+ });
102
+ document.querySelector("#privacy-choice").addEventListener("click", () => {
103
+ consent.open().catch(console.error);
104
+ });
105
+ ```
106
+
107
+ `open()` shows a full-screen consent page over your current page, so the visitor never leaves your site, and returns to it when done.
108
+
109
+ ## Read and change a choice
110
+
111
+ ```js
112
+ await consent.ready;
113
+ const preference = consent.getPreference(); // null until a current choice is saved
114
+ const claim = consent.getClaim(); // scoped proof; keep private
115
+ await consent.open(); // show the form again
116
+ consent.close(); // close the UI
117
+ await consent.refresh(); // reload the notice and session, keeping an embedded panel in its container
118
+ consent.destroy(); // remove the form, listeners and pending requests
119
+ ```
120
+
121
+ `open()` opens the form as a dialog. Panel clients use the popup layout for it. If a panel is already mounted, `open()` scrolls it into view and focuses it instead of opening a second form.
122
+
123
+ `ready` resolves to the published configuration. `onComplete(result, claim)` runs after a successful save. Results include `ref_id`, `app_id`, `level_id`, `config_version`, and a purpose-ID-to-boolean `choices` map. Necessary purposes stay selected. Saves use the existing Atlas consent history and audit services.
124
+
125
+ Use `onPreferenceChange(preference)` to update optional processing when the SDK observes a changed choice, including a withdrawal in another tab. It receives a preference snapshot or `null` when no current consent exists. It also reports an initially stored choice after configuration loads. The equivalent window event is `atlas-consent:change`, with `{ appId, preference }` in `event.detail`; it contains no session or claim token. `onComplete` remains specific to saves from this instance's active form.
126
+
127
+ Clients synchronize choices through local storage, including multiple SDK instances in the same page. A form with stale choices is closed when another instance or tab changes its saved decision; reopen it to edit the latest choice. Clearing storage invalidates the cached preference. Direct storage edits in the same document are noticed on the next preference or claim read. When storage is blocked, clients retain their own choices in memory for that page only.
128
+
129
+ Session reads reconcile cached choices with the server. Choices from an older notice are kept for editing and claiming, but do not count as consent to a new notice. A `notice_changed` save error provides a **Reload consent form** action, which fetches the current notice before another save. Code integrations can also call `refresh()`.
130
+
131
+ Every configured purpose is displayed, even when the notice omits a purpose group or section. Malformed notice structures are rejected before mounting. Rendering errors retain a dismissal control, and saving leaves Close available. Closing an in-flight save dismisses the form; it does not undo a decision the server may already have recorded.
132
+
133
+ The main button saves exactly the boxes that are ticked. Its name follows them: the level's "Accept Necessary" text when no optional purpose is ticked, and its "Accept Selected" text when any is.
134
+
135
+ Use `appearance` to set colors and style in code: `appearance: { theme: 'light', primary_color: '#ae0a60', background_color: '#ffffff', text_color: '#1a202c', radius: 'soft', font: 'system', density: 'comfortable' }`. Values in code override the look saved in Atlas. Colors must be 6-digit hex values; anything else falls back to the default. Wording and button names always come from the consent level, shown as written, with the notice's paragraphs, lists, emphasis and links.
136
+
137
+ `theme` is `light`, `dark` or `system`. `dark` uses a dark background and light text unless you set your own colors. `system` follows the device's light or dark preference and updates when it changes. Both keep your brand color.
138
+
139
+ Use `language: 'en'` (default), or a language supported by the configured level. The notice's title, sections and buttons follow that language. Atlas keeps purposes, collected data and shared data in one language (English), so those lists are marked with their own `lang` for screen readers. The SDK's few own labels, such as "Your choices are saved.", are English.
140
+
141
+ ## Link a verified customer
142
+
143
+ A browser can save anonymously. Page loads create no consent record; the first decision creates one. A reference ID alone does not authorize access or prove identity.
144
+
145
+ After your application verifies the customer, send `consent.getClaim()` to your own authenticated backend. Your backend must choose the verified customer ID from its login session, not trust a customer ID supplied by the browser. Then call:
146
+
147
+ ```http
148
+ POST /api/v1/consents/records/{ref_id}/claim
149
+ Authorization: Bearer YOUR_ORGANIZATION_API_TOKEN
150
+ Content-Type: application/json
151
+
152
+ {
153
+ "app_id": "ac_app_REPLACE_WITH_YOUR_APP_ID",
154
+ "claim_token": "THE_BROWSER_CLAIM_TOKEN",
155
+ "customer_id": "YOUR_VERIFIED_ID"
156
+ }
157
+ ```
158
+
159
+ You can also supply verified applicant email/mobile fields supported by the applicant API. The token needs `consents.records.applicant.update`. Claiming preserves the anonymous evidence, writes the link to the record's audit trail, and ends browser access to the record. A repeat claim for the same customer is safe; a conflicting customer is rejected. If the customer already has a record on this consent's system, the claim returns 409 and the anonymous record stays unlinked. The error names the existing record, so your backend can save the website choices there with `PUT /api/v1/consents/records/{ref_id}/decisions`. Provide a verified customer flow for later changes after linking. The anonymous SDK cannot read a customer's other records.
160
+
161
+ The claim keeps working when the notice, its language or your website list changes, because the browser keeps the same session and the same record. After linking, opening the form in that browser reports `consent_linked`; send the visitor to your signed-in flow.
162
+
163
+ ## Storage and cookie integrations
164
+
165
+ The SDK stores a backend-and-app-specific session capability and choices in your website's local storage. It sets no cookies. If storage is unavailable, the UI still works but cannot remember a choice after reload.
166
+
167
+ A browser session that never saved a choice lasts 30 days. Once it holds a consent, it lasts a year from its last use. Opening the form renews it, and so does a page load in its last 60 days. A changed notice or language asks the visitor again, with their earlier choices ticked, and saves to the same record. Stored consent evidence remains in Atlas after a session ends. Clearing browser storage, or a session ending, loses the anonymous proof: the next save creates a new record. Treat the claim token as a credential: do not put it in URLs, analytics, logs, or third-party scripts.
168
+
169
+ The popup is a consent form. It does **not** scan cookies or automatically block scripts, trackers, tags, or server-side processing. Your site must keep optional processing disabled until the matching purpose choice is true, and stop it when the visitor changes that choice. Map purpose IDs explicitly. `getPreference()` is a browser convenience, not authorization for backend access.
170
+
171
+ ## Origins and errors
172
+
173
+ Register exact origins, including scheme and non-default port. `https://example.com` and `https://www.example.com` are separate. Wildcards and paths are rejected. HTTP is supported only for local development origins. Deploy the API and assets over HTTPS.
174
+
175
+ Errors are `ConsentError` objects with `code`, `message`, and HTTP `status` (0 when no response arrived). Every failure goes to `onError` and to the `atlas-consent:error` event on `window`, once. `ready`, `open()`, and `mount()` also reject with it.
176
+
177
+ | Code | Meaning |
178
+ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
179
+ | `session_invalid` | The session expired or is unknown. The SDK starts a new one by itself. |
180
+ | `consent_linked` | This browser's consent is linked to a customer. Use your signed-in flow. |
181
+ | `notice_changed` | The notice changed while the form was open. Open the form again. |
182
+ | `not_allowed` | This website's origin is not allowed, or the integration is disabled. |
183
+ | `not_found` | Unknown app ID. |
184
+ | `invalid_request`, `request_too_large`, `conflict` | Atlas rejected the request. |
185
+ | `rate_limited` | HTTP 429. Wait before trying again. |
186
+ | `service_unavailable`, `internal_error` | HTTP 503 or 500. Try again later. |
187
+ | `request_timeout`, `network_error`, `invalid_response` | Atlas did not answer, or answered with something unusable. |
188
+ | `invalid_app`, `invalid_api_base_url`, `invalid_appearance`, `missing_container` | Integration mistakes on your page. |
189
+ | `form_error` | The notice could not be drawn. |
190
+ | `destroyed` | The client was destroyed. Not reported to `onError`. |
191
+ | `consent_sdk_error` | Sent by Atlas backends older than this SDK, in place of the codes above. |
192
+
193
+ Do not automatically retry writes without a delay. The form keeps the same `Idempotency-Key` when retrying a failed save, so a retry cannot save twice.
194
+
195
+ Build with `npm run check`. Run `npm run test:browser` before a release. Copy versioned assets with `npm run sync:backend -- /path/to/webpy`. Keep previous asset versions available.
196
+
197
+ ## Security notes
198
+
199
+ - Session and claim tokens are kept in the visitor's `localStorage` under `atlas-consent:v2:<encoded-api-base-url>:<appId>`, so they are readable by any script on your site. Keep third-party scripts on pages that use the SDK to a minimum and use a Content Security Policy.
200
+ - The consent form runs in your page, in Shadow DOM. Shadow DOM keeps your CSS and the form's CSS apart. It is not a security boundary: any script on your page can read or change the form, so what Atlas records is what your page submitted. Keep third-party scripts off pages that collect consent.
201
+ - Allow your Atlas API host in your Content Security Policy `connect-src`. The form needs no inline style permission.
202
+ - Notice HTML is sanitized with bundled DOMPurify, then rebuilt using the SDK's restricted formatting and link rules. On sites enforcing Trusted Types, allow the `dompurify` policy name in `trusted-types`; for example, `require-trusted-types-for 'script'; trusted-types dompurify`. No permissive default policy is needed. A CSP that disallows the sanitizer policy fails closed and leaves the dialog dismissible.
203
+ - Atlas writes each save to the record's audit trail with how it arrived: the kind of form (drawn on your website, the older Atlas-hosted form, or no browser origin at all), the SDK version the browser reported, your website's origin, the session, the notice version and language, and the caller's IP address, with a hash of those details. Treat a save as what your page sent, not as proof of what the person saw.
204
+
205
+ ## Development
206
+
207
+ See [Contributing](CONTRIBUTING.md) for local checks and release rules.
208
+
209
+ The SDK targets current evergreen Chrome/Edge, Firefox, and Safari, using native modal dialogs and Shadow DOM. Automated tests cover Chromium, Firefox, WebKit, and mobile Chromium/WebKit viewports. Mobile projects emulate browser devices; they do not replace testing on physical phones or embedded webviews. Destroy an instance when a framework component unmounts; create a new instance when it mounts again. The ESM entry point can be imported during server rendering, but `create()` must run in the browser.
210
+
211
+ ## Packages and API URLs
212
+
213
+ Set GitLab CI/CD variables `DEV_URL`, `STAGING_URL`, and `PRODUCTION_URL` to the backend API base URLs, including the service prefix. Use environment scope `*` so the variables are available to build jobs. `dev` and other branches use `DEV_URL`. `staging` uses `STAGING_URL`. `shipping` uses `PRODUCTION_URL`.
214
+
215
+ These URLs become the default `apiBaseUrl` in compiled packages. Explicit `create({apiBaseUrl})` options take priority. Script-tag integrations use `data-api-base-url` first, then the Atlas address the script was loaded from, then the build URL. Local builds can set `ATLAS_SDK_API_BASE_URL` or continue to supply the API URL at runtime. API URLs are public build configuration; do not put secrets in them.
216
+
217
+ | Branch | Version | Published to |
218
+ | ---------- | ---------------------- | ----------------------------------------------------- |
219
+ | other | `X.Y.Z-dev.<pipeline>` | job artifacts only (one week) |
220
+ | `dev` | `X.Y.Z-dev.<pipeline>` | private GitLab Generic Package Registry |
221
+ | `staging` | `X.Y.Z-rc.<pipeline>` | private GitLab Generic Package Registry |
222
+ | `shipping` | `X.Y.Z-beta.N` | npm (`beta` tag), GitLab registry, `v` tag, release |
223
+ | `shipping` | `X.Y.Z` when final | npm (`latest` tag), GitLab registry, `v` tag, release |
224
+
225
+ Packages contain scripts, the dashboard preview page, declarations, and an integrity manifest with SHA-384 hashes for Subresource Integrity. [Contributing](CONTRIBUTING.md#branches-and-releases) describes the release flow.