@consentera/consent-sdk 2.0.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/LICENSE +21 -0
  3. package/README.md +489 -0
  4. package/dist/consentera-consent.cjs +4919 -0
  5. package/dist/consentera-consent.cjs.map +1 -0
  6. package/dist/consentera-consent.min.js +2 -0
  7. package/dist/consentera-consent.min.js.map +1 -0
  8. package/dist/consentera-consent.mjs +4864 -0
  9. package/dist/consentera-consent.mjs.map +1 -0
  10. package/dist/react/index.cjs +2731 -0
  11. package/dist/react/index.cjs.map +1 -0
  12. package/dist/react/index.mjs +2724 -0
  13. package/dist/react/index.mjs.map +1 -0
  14. package/dist/types/consent/CallbackHandler.d.ts +246 -0
  15. package/dist/types/consent/ConsentManager.d.ts +128 -0
  16. package/dist/types/consent/ConsentSession.d.ts +127 -0
  17. package/dist/types/consent/ConsentValidator.d.ts +63 -0
  18. package/dist/types/consent/artifactRead.d.ts +48 -0
  19. package/dist/types/consent/consentPopup.d.ts +115 -0
  20. package/dist/types/core/ConsentEraClient.d.ts +106 -0
  21. package/dist/types/core/ConsenteraConsent.d.ts +163 -0
  22. package/dist/types/core/errors.d.ts +108 -0
  23. package/dist/types/core/http.d.ts +176 -0
  24. package/dist/types/core/version.d.ts +36 -0
  25. package/dist/types/df/DFConfigClient.d.ts +59 -0
  26. package/dist/types/gcm/ConsentModeBridge.d.ts +54 -0
  27. package/dist/types/gpp/GPPManager.d.ts +62 -0
  28. package/dist/types/index.d.mts +5 -0
  29. package/dist/types/index.d.ts +28 -0
  30. package/dist/types/principal/PrincipalClient.d.ts +34 -0
  31. package/dist/types/react/ConsentEraProvider.d.ts +58 -0
  32. package/dist/types/react/ConsentGate.d.ts +40 -0
  33. package/dist/types/react/index.d.mts +4 -0
  34. package/dist/types/react/index.d.ts +10 -0
  35. package/dist/types/react/useConsentEra.d.ts +65 -0
  36. package/dist/types/react/useConsentValidation.d.ts +23 -0
  37. package/dist/types/storage/ConsentStorage.d.ts +39 -0
  38. package/dist/types/tcf/TCFManager.d.ts +46 -0
  39. package/dist/types/types/consent-lifecycle.d.ts +804 -0
  40. package/dist/types/types/index.d.ts +311 -0
  41. package/dist/types/ui/ConsentBanner.d.ts +22 -0
  42. package/dist/types/ui/PreferenceCenter.d.ts +24 -0
  43. package/dist/types/utils/EventEmitter.d.ts +32 -0
  44. package/dist/types/utils/Logger.d.ts +16 -0
  45. package/dist/types/utils/browserStorage.d.ts +35 -0
  46. package/dist/types/utils/context.d.ts +81 -0
  47. package/dist/types/utils/helpers.d.ts +48 -0
  48. package/package.json +132 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,245 @@
1
+ ## Unreleased — renewal request wire correction
2
+
3
+ Existing `renewBulk(principalId, purposeIds, noticeHash, uiEventId, options)`
4
+ keeps its call signature and maps purpose IDs to the platform's `renewals`
5
+ items. Both renewal methods send top-level `captured_at`; the server does not
6
+ consume a nested `affirmative_action` on these roads. The legacy `uiEventId`
7
+ argument remains accepted but is not a server-stored renewal field. Exported
8
+ request types now describe these actual wire bodies. Response metadata and
9
+ idempotency/cancellation options remain available.
10
+
11
+ # Changelog
12
+
13
+ All notable changes to `@consentera/consent-sdk`.
14
+ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
15
+
16
+ ## Unreleased — callback status contract (not published)
17
+
18
+ ### Changed
19
+
20
+ - **`handleCallback()` reads the platform's own callback vocabulary.** The
21
+ platform sets `status` to exactly `granted`, `partial` or `denied`, and
22
+ `pending=1` on every capture (consent/collection.go:4094-4098, :4243, :4274
23
+ on platform pre-main). The handler knew `completed|denied|expired|error`
24
+ instead: it had branches for `expired`, `rejected` and `timeout`, which the
25
+ platform never sends, did not know `partial`, never read `pending`, and sent
26
+ any unrecognised or missing status down the success road. Now
27
+ `readCallbackParams()` returns `claimed_status`
28
+ (`'granted' | 'partial' | 'denied' | 'unknown'`) and `pending`, the result
29
+ carries `claimed_status` and `claimed_pending`, and an `unknown` status is
30
+ `unverified` without an artefact read. `claimedCallbackStatus()` and
31
+ `CallbackClaimedStatus` are exported.
32
+ - **The redirect return can now verify against the real platform.**
33
+ `handleCallback()` compared the server's `challengeNonce`, stored at create,
34
+ with a `nonce`/`state` on the return. The platform's return never carries it:
35
+ it adds only `session_id`, `artifact_id`, `status`, `pending` and, when signed,
36
+ `sig` (collection.go:4225-4288). `challengeNonce` is the hosted page's
37
+ credential and rides `consent_url`. So every genuine redirect came back
38
+ `unverified`. `createSession` now adds its own random `state` to
39
+ `callback_url`, the way the mobile SDKs do. The platform builds the return on
40
+ top of that URL's query, so the state comes back, and `handleCallback()`
41
+ requires it. The stored record holds `state` and no longer holds the nonce.
42
+ `CallbackParams.nonce` is now `CallbackParams.state`. A `callback_url` that
43
+ already carries `state` is refused with `CALLBACK_STATE_RESERVED`, and a
44
+ relative one with `INVALID_CALLBACK_URL`. A session created before this
45
+ change, or without a callback, is `unverified` on return. Read it back
46
+ through your backend.
47
+ - **`CallbackStatus` no longer includes `'expired'`.** Nothing on the platform
48
+ produced it; a return URL saying `status=expired` is now `unverified`.
49
+
50
+ ## Unreleased — M4 integration
51
+
52
+ ### Changed
53
+
54
+ - Existing `ConsentManager.renew` and `renewBulk` now return
55
+ `TransportResponse<RenewResponse>` and `TransportResponse<BulkRenewResponse>`:
56
+ the HTTP status, response body, request ID and retry delay are preserved.
57
+ A `202` records durable acceptance without claiming a completed artifact.
58
+ Retain its `artifact_id` and `session_id` for the existing bound artifact read;
59
+ do not repeat the renewal POST as a polling operation. A stored
60
+ `integrity_hash` is returned unchanged when available.
61
+ - Renewal change events invalidate caches for the purposes the response actually
62
+ renewed, including accepted `202` responses. The event is not artifact-readiness
63
+ evidence. Bulk requests with no successful renewal emit no change event.
64
+ - **Type migration:** callers that ignore the result can continue to do so, but
65
+ explicit function assignments returning `Promise<void>` must be updated to
66
+ the new response type. This is not universal source compatibility.
67
+
68
+ ## [2.0.0] — 2026-09-22
69
+
70
+ A security and correctness release. **Every breaking change below exists because
71
+ the 1.x behaviour could report success for a consent that was never recorded, or
72
+ put a secret in a public bundle.** Read *Breaking* before upgrading.
73
+
74
+ ### Breaking
75
+
76
+ - **`apiEndpoint` is required by the cookie SDK (`ConsenteraConsent`, the
77
+ default export), and there is no default server.** A missing or blank value
78
+ is refused at construction with `ENDPOINT_REQUIRED` — the code
79
+ `ConsentEraClient` already used. 1.x silently fell back to a built-in host on
80
+ the `.io` domain, which the company does not own: it did not resolve, so every
81
+ such embed failed, and it was one DNS record away from receiving the visitor's
82
+ consent payload and the site key. The script-tag auto-init now reads
83
+ `data-api-endpoint` and, without it, logs why and initialises nothing instead
84
+ of taking the bundle down.
85
+ - **`handleCallback()` / `CallbackHandler.parseCallback()` is now `async` and
86
+ fails closed.** 1.x read `session_id`, `artifact_id` and `status` from the
87
+ query string, logged *"Callback verified against stored session"*, and verified
88
+ **nothing** — it never parsed the record it had stored, never compared the
89
+ nonce, and returned `completed` for any URL carrying an `artifact_id`. A link
90
+ to `/consent/done?session_id=x&artifact_id=y` made the SDK answer "completed"
91
+ with no consent given. It now requires the stored session and its nonce to
92
+ match, and reaches `completed` **only** by fetching the artefact from the
93
+ platform and confirming it belongs to that session. Everything else is
94
+ `unverified`, never `completed`. The synchronous
95
+ `readCallbackParams()` returns the URL's claims for callers who want them.
96
+ - **The artefact read carries `?session_id=`, and `getArtifact` returns a
97
+ union** (SDK register WEB-033, WEB-034). `getArtifact(id, { sessionId })`
98
+ resolves `{ state: 'recorded', artifact }` on a 200 and
99
+ `{ state: 'pending', pending, retryAfterMs }` on the platform's 202; a 404
100
+ throws `ConsenteraNotFoundError`. `handleCallback()` reads with the session,
101
+ so a 404 is `unverified` at once instead of being polled as "maybe pending",
102
+ and it binds the artefact to the person the session was created for
103
+ (`data_principal_id`, now kept in the stored session record) instead of to a
104
+ `consent_session_id` field the platform's artefact does not have.
105
+ - **A failed consent sync throws.** 1.x caught every failure in `syncConsent`,
106
+ logged it at `warn`, wrote the local cookie anyway, hid the banner and fired
107
+ `consent_given`. The visitor's decision was lost and the banner never came back
108
+ to ask again. Now the error propagates, nothing is stored, the banner stays up,
109
+ and the host is told on the `sync_failed` event and `callbacks.onSyncFailed`.
110
+ - **No fabricated notice.** `getDefaultConfig()` is deleted. When the tenant's
111
+ configuration cannot be fetched, 1.x rendered a hardcoded "We value your
112
+ privacy" banner with invented purpose ids and an empty `policy_version`, and
113
+ collected consent against it. `init()` now rejects and no banner is shown.
114
+ - **`apiKey` is refused in a browser** (`SECRET_KEY_IN_BROWSER`) — at BOTH entry
115
+ points. The lifecycle client and the cookie SDK (`ConsentEraConsent`, the
116
+ package default export) now share one guard; before, the cookie SDK had no
117
+ browser check and shipped a `tiq_live_` secret to `/api/v1/cookie-consent/*`
118
+ silently — that plane ignores the key, so nothing refused it and nothing
119
+ failed. Consent
120
+ lifecycle roads need a Data Fiduciary secret; in a browser they must go through
121
+ `proxyEndpoint`. `unsafeAllowSecretKeyInBrowser: true` overrides, loudly.
122
+ 1.x's advice — *"prefer site key (public, safe for frontend)"* — was wrong in
123
+ both directions: a site key authorises no lifecycle road at all (its
124
+ permissions are replaced with four that no route requires), so the only
125
+ credential that worked was the secret one.
126
+ - **One error type.** `ConsenteraError` with `kind`, `code`, `status`,
127
+ `requestId`, `retryAfterMs`, plus subclasses for `instanceof`.
128
+ `ConsentEraApiError` is an alias of it and `.statusCode` still reads, so most
129
+ 1.x error handling keeps working.
130
+ - **Telemetry is off by default.** `collectContext` defaults to `'minimal'`. 1.x
131
+ sent `page_url: location.href`, the referrer, the raw User-Agent, screen size
132
+ and timezone on every mutation with no way to turn it off. `'full'` restores
133
+ them, with the page URL's query and fragment stripped.
134
+ - **`ConsentStorage.save()` returns `boolean`** (false when nothing durable was
135
+ written).
136
+ - `peerDependencies.react` is `^18 || ^19`; `engines.node` is `>=20`.
137
+ - The UMD global is `Consentera`. `window.ConsentEraConsent` is still set.
138
+
139
+ ### Added
140
+
141
+ - **Deadlines, retries and idempotency.** Every request has an
142
+ `AbortController` timeout (default 10s), retries network failures / 5xx / 429
143
+ with exponential backoff and full jitter honouring `Retry-After`, and carries
144
+ **one idempotency key per logical operation** reused across retries.
145
+ `idempotencyKey`, `signal` and `timeoutMs` are caller-supplyable.
146
+ - **`X-Request-ID` surfaced** on every error.
147
+ - **SDK identification**, as the pair agreed across all six surfaces:
148
+ `User-Agent: ConsenteraSDK/<version> (<platform>; <runtime>)` plus
149
+ `X-Consentera-SDK: js/<version>`. In a browser only the second is sent —
150
+ `User-Agent` is a forbidden fetch header there.
151
+ - **The platform's callback signature is verified.** When a
152
+ `callback_signing_secret` is registered the platform appends
153
+ `sig = hex(HMAC_SHA256(secret, session_id|artifact_id|status))`; the SDK
154
+ checks it through `verifyCallbackSignature`, and a callback that carries a
155
+ `sig` with no verifier configured is `unverified`. The signing key is the
156
+ DF's, so in a browser the verifier calls your own server.
157
+ - **`pending`**, a new callback status. The consent artefact is written
158
+ asynchronously after submit (5.2–10.8 s measured), so the callback road polls
159
+ with backoff inside a bounded window (`artifactWaitMs`, default 15 s) and
160
+ reports `pending` when it expires — never `completed` on a 404, and never a
161
+ failure for a consent that was recorded.
162
+ - **React: no hook throws during render** (SDK register WEB-038). A refused
163
+ provider configuration, or a missing provider, used to throw out of
164
+ `useConsentEraClient()` and blank the page. Now `client` is `null` with the
165
+ reason in `error`, `useConsentEra()` returns `ready: false` and `null`
166
+ namespaces, and `<ConsentGate>` renders its fallback. Code that called a
167
+ namespace unconditionally must check it first.
168
+ - **`'use client'`** on the built React bundle — the package now works in the
169
+ Next.js App Router, where 1.x failed the build.
170
+ - **`beforeSend`** hook: the last look at every request body.
171
+ - **Guarded storage.** `localStorage`/`sessionStorage` throw in private mode,
172
+ with cookies blocked, and in sandboxed iframes; every access is wrapped and
173
+ degrades to memory. A cookie over the 4096-byte browser limit is refused
174
+ loudly instead of being dropped silently.
175
+ - **Popup flow: `openConsentPopup` opens a dialog on your page and hears the
176
+ message `/collect` actually posts** (SDK register WEB-035). It frames
177
+ `consent_url` and resolves on `consentera:submitted` / `consentera:declined`
178
+ from the `/collect` origin and the frame it opened. The result is
179
+ `{ outcome: 'decided', session_id, artifact_id, status, pending, … }` or
180
+ `{ outcome: 'dismissed' }`, with a timeout. The first 2.0.0 cut used
181
+ `window.open` and listened for `consentera:consent-result`, which the
182
+ platform never sends. A top-level `/collect` posts nothing, and that
183
+ promise could only end at the timeout or as `unverified`. Your page must be
184
+ on the session callback's origin (`CALLBACK_ORIGIN_MISMATCH` otherwise) and
185
+ in the client's Allowed Domains. `readDecisionMessage` is exported for hosts
186
+ that frame the page themselves.
187
+ - **`tenantId` is optional behind `proxyEndpoint`** (SDK register WEB-036).
188
+ The Data Fiduciary's server supplies the tenant there and the SDK sends no
189
+ tenant header, so requiring the value made integrators invent one. It is
190
+ still required with `apiEndpoint` (`TENANT_ID_REQUIRED`).
191
+ - **`ValidateResponse` is the platform's answer** (SDK register WEB-037):
192
+ `data_principal_id`, `purpose_id`, `validated_at`, `effective_at`,
193
+ `legal_basis`, `is_mandatory`, `dpdp_exemption_basis` and `message` are
194
+ typed, and the `artifact_id` the platform never sends is gone.
195
+ `purpose_code` is optional, as on the wire. `BulkValidateResponse` gains
196
+ `total_count`, `allow_count` and `deny_count`.
197
+ - **Type declarations actually ship.** `npm run build` now runs `build:types`;
198
+ 1.x's `build` did not, so the published tarball declared `types` at a path that
199
+ did not exist. `scripts/verify-pack.sh` asserts it from the tarball.
200
+ - **The ESM and CJS bundles are `.mjs` and `.cjs`.** They were
201
+ `consentera-consent.esm.js` and `.cjs.js`, and Node decides a file's module
202
+ system from its EXTENSION plus the package `type`, not from the exports
203
+ condition that reached it — so with `"type": "commonjs"` **every named import
204
+ from an ESM project failed**: `SyntaxError: Named export 'CallbackHandler'
205
+ not found`. Found by installing the tarball, which is now a gate
206
+ (`scripts/install-smoke.sh`).
207
+ - `exports` carries per-condition `types` (with a `.d.mts` entry for ESM, so the
208
+ types are no longer "masquerading as CJS"), plus `sideEffects`, `engines`,
209
+ `unpkg`, `jsdelivr`, `homepage`, `bugs`, `typesVersions`, and README/LICENSE
210
+ in `files`. `publint` and `@arethetypeswrong/cli` are both clean.
211
+ - ESLint 9 flat config that runs (1.x's `lint` script had no eslint installed and
212
+ no config file — it printed `eslint: not found`).
213
+ - React tests, a coverage floor, `publint` and `@arethetypeswrong/cli`.
214
+
215
+ ### Fixed
216
+
217
+ - `useConsentValidation` cancels in-flight checks and drops out-of-order
218
+ responses. A stale `ALLOW` could overwrite a fresh `DENY` — a consent gate
219
+ failing **open**.
220
+ - `useConsentValidation` subscribes to `consent.changed`, so `<ConsentGate>`
221
+ closes on withdrawal without a remount.
222
+ - `useConsentValidation` depends on `who` **by value**. With the object-shaped
223
+ prop, an inline literal made a new reference every render and the effect
224
+ re-fired every render — an unbounded request loop.
225
+ - `ConsentEraProvider` no longer calls `setState` during render, and keeps
226
+ providing context when construction fails, so hooks report the real cause
227
+ instead of "must be used within `<ConsentEraProvider>`" inside a provider.
228
+ - A 200 response whose body is not JSON is a server error, not a network error.
229
+ - Identifiers are never logged; request/response bodies are never logged.
230
+ - `tsc --strict` is clean, and `build:types` no longer disables the two strict
231
+ flags that were failing.
232
+ - Idempotency keys and generated ids use `crypto.randomUUID()`, not
233
+ `Math.random()`.
234
+ - The transport's `public` and `session` road classes are now WIRED — the DF
235
+ reads ride `public` (site-key-openable) and the session widget template rides
236
+ `session` (nonce, no credential). Nothing set a road before, so both classes
237
+ were dead and every DF read demanded a secret it did not need.
238
+ - `getWidgetTemplate()` (`GET /df/widget-template`, a 404 — no such route) is
239
+ replaced by `getSessionWidgetTemplate(sessionId)` (`GET
240
+ /consent/sessions/{id}/widget-template`). Use `getNoticeTemplate()` for the
241
+ notice-authoring template.
242
+ - `getArtifact` encodes the artifact id in the path (it did not; the
243
+ CallbackHandler copy did).
244
+
245
+ [2.0.0]: https://github.com/consentera-platform/consentera-sdks/releases/tag/v2.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Consentera
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.