@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/README.md ADDED
@@ -0,0 +1,489 @@
1
+ # @consentera/consent-sdk
2
+
3
+ Consent management for the web, against the [Consentera](https://docs.consentera.in)
4
+ platform. India's **DPDP Act** first, with TCF 2.2 and GPP surfaces for sites that
5
+ also need them.
6
+
7
+ - **Consent lifecycle** — create a consent session, validate a purpose, update,
8
+ withdraw, renew, verify the artefact.
9
+ - **Cookie banner + preference centre** — a drop-in surface for cookie consent.
10
+ - **React** — a provider, hooks and a `<ConsentGate>` that closes when consent is
11
+ withdrawn.
12
+
13
+ ```
14
+ npm install @consentera/consent-sdk
15
+ ```
16
+
17
+ Node ≥ 20. React ≥ 18 (optional peer, only for `@consentera/consent-sdk/react`).
18
+
19
+ ---
20
+
21
+ ## Quickstart (10 minutes)
22
+
23
+ ### 1. Put your secret key on YOUR server, never in the page
24
+
25
+ A Data Fiduciary credential (`tiq_live_…` / `tiq_test_…`) is a **secret**. Every
26
+ consent lifecycle road needs one, and a browser bundle is public, so the browser
27
+ talks to **your** server and your server talks to us.
28
+
29
+ ```ts
30
+ // app/api/consentera/[...path]/route.ts (Next.js — any server framework works)
31
+ export async function POST(req: Request, { params }: { params: { path: string[] } }) {
32
+ // CONSENTERA_API_URL is YOUR tenant's API origin — the SDK ships no default.
33
+ const upstream = `${process.env.CONSENTERA_API_URL}/api/v1/public/${params.path.join('/')}`;
34
+ const res = await fetch(upstream, {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ 'X-API-Key': process.env.CONSENTERA_API_KEY!, // the secret, server-side only
39
+ 'X-Tenant-Id': process.env.CONSENTERA_TENANT_ID!,
40
+ // pass these through so the SDK's guarantees survive the hop
41
+ 'Idempotency-Key': req.headers.get('Idempotency-Key') ?? '',
42
+ 'X-Consentera-SDK': req.headers.get('X-Consentera-SDK') ?? '',
43
+ },
44
+ body: await req.text(),
45
+ });
46
+ // expose the request id so SDK errors can carry it
47
+ const out = new Response(res.body, { status: res.status });
48
+ const rid = res.headers.get('X-Request-Id');
49
+ if (rid) out.headers.set('X-Request-Id', rid);
50
+ return out;
51
+ }
52
+ ```
53
+
54
+ ### 2. Point the SDK at your route
55
+
56
+ ```ts
57
+ import { ConsentEraClient } from '@consentera/consent-sdk';
58
+
59
+ const ce = new ConsentEraClient({
60
+ proxyEndpoint: '/api/consentera', // in a browser this is required
61
+ callbackUrl: 'https://your-site.example/consent/done',
62
+ });
63
+ ```
64
+
65
+ No `tenantId` behind a proxy: your route supplies the tenant (the
66
+ `X-Tenant-Id` above) and the SDK sends no tenant header through it. With
67
+ `apiEndpoint` (direct, server-side) `tenantId` is required, and a client
68
+ without it is refused with `TENANT_ID_REQUIRED`.
69
+
70
+ Putting `apiKey` here in a browser is **refused at construction** with
71
+ `SECRET_KEY_IN_BROWSER`. That is deliberate — see *The credential model* below.
72
+
73
+ ### 3. Ask for consent
74
+
75
+ ```ts
76
+ const session = await ce.consent.createSession({
77
+ // The identifiers, keyed by YOUR organisation's locked integration key.
78
+ // A field outside the key is 400 UNKNOWN_IDENTIFIER_FIELD, and the message
79
+ // lists the fields your key allows.
80
+ data_principal: { email: 'riya@example.in' },
81
+ notice_internal_name: 'bnb_consent_v2',
82
+ age: { date_of_birth: '1998-04-12' }, // the one age signal
83
+ });
84
+
85
+ ce.consent.redirectToConsent(session); // or openConsentPopup(session)
86
+ ```
87
+
88
+ `createSession` adds **its own `state`** to your `callbackUrl` — 32 random bytes,
89
+ fresh per session, kept in this browser's session storage — so the return can
90
+ be tied to the browser that started it. The platform keeps every parameter
91
+ already on `callback_url` when it builds the return, so the state comes back.
92
+ `state` is therefore reserved: a `callbackUrl` that already carries one is
93
+ refused with `CALLBACK_STATE_RESERVED`. Pick a different name for your own
94
+ parameter. `callbackUrl` must be absolute (`INVALID_CALLBACK_URL` otherwise).
95
+
96
+ `openConsentPopup(session)` shows the consent page **in a dialog on your page**
97
+ and resolves when the person decides:
98
+
99
+ ```ts
100
+ const r = await ce.consent.openConsentPopup(session);
101
+ if (r.outcome === 'decided') {
102
+ // r.status: 'granted' | 'partial' | 'denied'; r.pending: the record is still
103
+ // being written (redirect pending=1), so the read-back may answer 202 first
104
+ await confirmOnYourServer(r.session_id, r.artifact_id); // the artefact is the record
105
+ }
106
+ // r.outcome === 'dismissed': the person closed the dialog
107
+ ```
108
+
109
+ It listens for the one message the consent page posts to the page that frames
110
+ it, `consentera:submitted` / `consentera:declined`, and only from the consent
111
+ page's origin and the frame it opened.
112
+
113
+ **The consent page posts that message only when it is framed.** A consent
114
+ page in its own window or tab (`window.open`, a redirect) posts nothing. It
115
+ sends the person to your `callback_url` instead, and that is what
116
+ `redirectToConsent` + `handleCallback` are for. That is why the popup is a
117
+ dialog with the page in an iframe, not a browser window. Two further
118
+ conditions, both set by the platform:
119
+
120
+ - the consent page addresses that message to the **origin of the session's
121
+ `callback_url`**, so your page must be on that origin. The SDK refuses up
122
+ front with `CALLBACK_ORIGIN_MISMATCH` rather than wait for a message the
123
+ browser would drop;
124
+ - the platform lets only the **Allowed Domains** of your integration client
125
+ frame the page.
126
+
127
+ The message is the consent page's report, not the consent record. Confirm the
128
+ artefact (step 4) before you act on a grant.
129
+
130
+ ### 4. Verify the return trip — the artefact, not the URL
131
+
132
+ ```ts
133
+ // on https://your-site.example/consent/done
134
+ // arriving as ?artifact_id=…&pending=1&session_id=…&state=<ours>&status=granted|partial|denied
135
+ const result = await ce.consent.handleCallback();
136
+
137
+ if (result.status === 'completed') {
138
+ // the artefact was read from the platform FOR THIS SESSION and names the
139
+ // person this browser's session was created for
140
+ proceed(result.artifact);
141
+ } else {
142
+ // 'unverified' | 'denied' | 'pending' | 'error' — result.reason says which and why
143
+ askAgain(result.reason);
144
+ }
145
+ ```
146
+
147
+ `handleCallback()` is **async** and returns `completed` **only** after confirming
148
+ the artefact with the platform. A query string can never produce `completed`.
149
+
150
+ **What the platform puts on the return URL** — and nothing else:
151
+
152
+ ```
153
+ <your callback_url>?artifact_id=<uuid>&pending=1&session_id=<uuid>&status=granted|partial|denied
154
+ ```
155
+
156
+ plus `&sig=<hex>` only when your integration client holds a callback signing
157
+ secret (a hosted consent page's return is never signed). `status` is one of
158
+ exactly three values, `granted`, `partial` or `denied`; the SDK exposes it as
159
+ `result.claimed_status`, and any other value (`completed`, `success`, `expired`,
160
+ a missing status) is `'unknown'` and is **never** confirmed — the result is
161
+ `unverified`. `pending=1` (`result.claimed_pending`) means the consent was
162
+ recorded and its record is **still being written**; the platform sets it on
163
+ every capture, and it is why the first artefact read may answer 202.
164
+
165
+ **Both are hints, not proof.** They are query parameters, and `pending` is not
166
+ signed. Confirm the consent by reading the record back through your backend —
167
+ which is what `handleCallback()` does on the `granted`/`partial` road — and read
168
+ the artefact's purposes for what was granted: `partial` means some purposes
169
+ were declined.
170
+
171
+ **Statuses**: `completed` (verified; `claimed_status` says `granted` or
172
+ `partial`), `denied`, `pending` (the consent WAS recorded, the artefact is not
173
+ readable yet — see below), `error`, and `unverified` (nothing is proven; treat
174
+ exactly as "no consent").
175
+
176
+ #### The signature
177
+
178
+ When you register a `callback_signing_secret` on your m2m client, the platform
179
+ signs the callback:
180
+
181
+ ```
182
+ sig = hex( HMAC_SHA256( callback_signing_secret,
183
+ session_id + "|" + artifact_id + "|" + status ) )
184
+ ```
185
+
186
+ **That key is yours and must not be in a browser.** So the browser cannot verify
187
+ it: point `verifyCallbackSignature` at your own server, which does.
188
+
189
+ ```ts
190
+ // browser
191
+ const ce = new ConsentEraClient({
192
+ tenantId: TENANT,
193
+ proxyEndpoint: '/api/consentera',
194
+ verifyCallbackSignature: async (p) =>
195
+ (await fetch('/api/consentera/verify-callback', { method: 'POST', body: JSON.stringify(p) })).ok,
196
+ });
197
+
198
+ // your server route
199
+ import { verifyCallbackSignature } from '@consentera/consent-sdk';
200
+ const ok = await verifyCallbackSignature(process.env.CONSENTERA_CALLBACK_SECRET!, params);
201
+ ```
202
+
203
+ A callback that **carries** a `sig` with no verifier configured is `unverified`.
204
+ A signature nobody checks is not a control, so the SDK will not quietly ignore
205
+ one the platform bothered to produce. If you have registered no signing secret,
206
+ no `sig` is sent and the state plus the artefact confirmation are the controls.
207
+
208
+ **The order of checks.** The `state` must come back and must match the one
209
+ stored at create. If it is missing or different, the result is `unverified` and
210
+ nothing is read. Only then is `status` looked at, and only as a hint:
211
+ `granted`/`partial` → the artefact is read back for this session; `denied` →
212
+ `denied`; anything else → `unverified`. The server's `challengeNonce` is not
213
+ part of this: it is the hosted page's own credential, carried on `consent_url`,
214
+ and the return never carries it.
215
+
216
+ #### How the artefact is confirmed
217
+
218
+ The SDK reads `GET /consent/artifacts/{artifact_id}?session_id={session_id}`
219
+ through your proxy. **The `session_id` is what gives the answer its meaning:**
220
+
221
+ | Platform answer | `handleCallback()` |
222
+ |---|---|
223
+ | **200** with the artefact | `completed` — once the artefact's `data_principal_id` equals the one your session create returned |
224
+ | **202** + `Retry-After` — recorded, still being written | waits `Retry-After` inside `artifactWaitMs` (default 15 000), else **`pending`** with `retryAfterMs` |
225
+ | **404** `ARTIFACT_NOT_FOUND` | `unverified` at once: the id was never issued for this session. A forged `artifact_id` looks exactly like this |
226
+ | anything else | `unverified` |
227
+
228
+ Without the `session_id` the platform answers 404 for a consent whose artefact
229
+ is still being written as well as for an id it never issued, which is why the
230
+ SDK always sends it.
231
+
232
+ **Why the person is compared.** The platform checks `session_id` only while
233
+ the artefact is still being written. Once it exists, the 200 is returned for
234
+ *any* session id (measured on the platform; walk finding F077). The artefact
235
+ carries no session id. It does carry `data_principal_id`, and the session
236
+ create returned the same field for the person the session is about, so the
237
+ SDK compares the two.
238
+
239
+ `pending` is **not** a failure and **not** a consent that did not happen: the
240
+ decision is recorded. Read it again after `retryAfterMs`:
241
+
242
+ ```ts
243
+ const r = await ce.consent.getArtifact(artifactId, { sessionId });
244
+ if (r.state === 'pending') setTimeout(retry, r.retryAfterMs); // 202
245
+ else use(r.artifact); // 200
246
+ // a 404 throws ConsenteraNotFoundError (code ARTIFACT_NOT_FOUND)
247
+ ```
248
+
249
+ ### 5. Gate on consent later
250
+
251
+ ```ts
252
+ if (await ce.consent.isAllowed({ data_principal_identifiers: { email } }, 'product_analytics')) {
253
+ loadAnalytics();
254
+ }
255
+ ```
256
+
257
+ `validate()` returns the platform's whole answer, including
258
+ `data_principal_id`, the platform's id for the person you asked about. Keep it:
259
+ the next call can name the person by `{ data_principal_id }` and send no
260
+ identifier at all.
261
+
262
+ ### Naming a Data Principal
263
+
264
+ `data_principal_identifiers` is an **open map keyed by your organisation's own
265
+ locked integration key** — the same shape `data_principal` takes on session
266
+ create, validated by the same validator. It is not a fixed vocabulary, so this
267
+ SDK does not enumerate one and does not allow-list: a tenant keyed on
268
+ `{customer_id}` names people by `customer_id`, one keyed on `{email, mobile}`
269
+ by those.
270
+
271
+ **One spelling, both roads.** The mobile atom is **`mobile`** on session create
272
+ *and* on the lifecycle roads. It used to be `phone` here and `mobile` there,
273
+ folded server-side; F015 removed the fold, so **`phone` is now refused by name**
274
+ — the refusal even tells you the atom to use.
275
+
276
+ Only the wire KEY differs between the two roads: create spells the object
277
+ `data_principal` (and may mint a person), the lifecycle roads spell it
278
+ `data_principal_identifiers` (resolve-only, never creates anybody).
279
+
280
+ The refusals you will meet, all from that one validator:
281
+
282
+ | code | meaning |
283
+ |---|---|
284
+ | `UNKNOWN_IDENTIFIER_FIELD` | a field outside your key, named — and for a vernacular spelling, the atom to use instead |
285
+ | `IDENTIFIER_REQUIRED` | nothing named a person |
286
+ | `INVALID_IDENTIFIER_FORMAT` | a value that cannot be an identifier of its type (a raw 12-digit Aadhaar lives here — send the Aadhaar-linked token) |
287
+ | `SCHEME_NOT_CONFIGURED` | the organisation has not locked how it identifies people yet |
288
+
289
+ They arrive as `err.code` on a `ConsenteraError` with `err.kind === 'identity'`
290
+ (`'guardian'` for the age/guardian family), so you can branch on the class and
291
+ still read the platform's exact word.
292
+
293
+ ---
294
+
295
+ ## React
296
+
297
+ ```tsx
298
+ 'use client';
299
+ import { ConsentEraProvider, useConsentEra, ConsentGate } from '@consentera/consent-sdk/react';
300
+
301
+ export function App({ children }) {
302
+ return (
303
+ <ConsentEraProvider config={{ tenantId: TENANT, proxyEndpoint: '/api/consentera' }}>
304
+ {children}
305
+ </ConsentEraProvider>
306
+ );
307
+ }
308
+
309
+ function Marketing({ email }: { email: string }) {
310
+ return (
311
+ <ConsentGate
312
+ who={{ data_principal_identifiers: { email } }}
313
+ purposeCode="marketing_email"
314
+ fallback={<AskForConsent />}
315
+ >
316
+ <MarketingContent />
317
+ </ConsentGate>
318
+ );
319
+ }
320
+ ```
321
+
322
+ The built bundle carries `'use client'`, so it works in the Next.js App Router.
323
+ `<ConsentGate>` **fails closed** — unknown, loading and errored all render the
324
+ fallback — and it **re-checks when consent changes**, so a withdrawal anywhere in
325
+ the app closes the gate without a remount.
326
+
327
+ **No hook throws during render.** When the provider's configuration is refused
328
+ (a secret key in a browser, no endpoint), or there is no provider at all,
329
+ `useConsentEraClient()` returns `client: null`. `useConsentEra()` returns
330
+ `ready: false` and `null` namespaces. `error` says why, and a
331
+ `ConsenteraError` carries its `code`. `<ConsentGate>` renders its fallback. A
332
+ configuration mistake therefore closes the gate instead of blanking the page:
333
+
334
+ ```tsx
335
+ const { consent, error } = useConsentEra();
336
+ if (!consent) return <p>Consent is unavailable: {error?.message}</p>;
337
+ ```
338
+
339
+ ---
340
+
341
+ ## The credential model
342
+
343
+ | credential | where it may live | what it opens |
344
+ |---|---|---|
345
+ | `tiq_live_` / `tiq_test_` secret key | your server only | every consent lifecycle road |
346
+ | `tiq_pub_` site key | a browser bundle | public roads (notice fetch, consent-page config/submit) |
347
+ | consent session id + nonce | the consent page | that one session's render/submit |
348
+
349
+ **In a browser, lifecycle roads must go through `proxyEndpoint`.** The SDK
350
+ enforces this: `apiKey` with a `window` present is refused at construction, and a
351
+ `df` road with no proxy raises `SECRET_KEY_IN_BROWSER` naming the fix.
352
+
353
+ If you are certain your code never reaches a browser but a `window` exists anyway
354
+ (a jsdom harness, an SSR shim), `unsafeAllowSecretKeyInBrowser: true` allows it
355
+ and every request warns.
356
+
357
+ ### What the platform must guarantee
358
+
359
+ This is now enforced by **route membership plus a fail-closed origin binding**,
360
+ not by the four legacy permission names — that mechanism (F017, platform PR
361
+ #1781) supersedes the earlier finding that a site key authorised *zero* roads.
362
+ For a browser to reach the SDK's `public` and `session` roads directly, without a
363
+ proxy, the platform guarantees are:
364
+
365
+ - **A site key opens a fixed SET of routes by membership**, not by carrying one
366
+ of `consent.render / widget.render / session.submit / session.render`. Those
367
+ four permission names are checked by no route (`RequireDFPermission("…")` grep:
368
+ 0 hits) and are no longer how access is decided.
369
+ - **`allowed_domains` on the m2m client must be NON-EMPTY.** The key is bound to
370
+ its registered origins and refused fail-closed everywhere else, so a leaked
371
+ site key works nowhere the DF did not list — but a key with an EMPTY
372
+ `allowed_domains` therefore works **nowhere at all**. Register your origins, or
373
+ the browser calls 403 with a correct key.
374
+ - **Render and submit need NO credential.** `GET /consent/sessions/{id}/render`,
375
+ `POST /consent/sessions/{id}/submit` and `GET
376
+ /consent/sessions/{id}/widget-template` authorise on the **session id + its
377
+ nonce** — the SDK's `session` road sends no key and no `X-Tenant-Id`, because
378
+ the session is the capability.
379
+ - The DF read roads (`/df/config`, `/df/purposes`, `/df/notice/purposes`,
380
+ `/df/notice/template`) are the SDK's `public` road: openable by a site key
381
+ whose `allowed_domains` includes the calling origin.
382
+
383
+ What this SDK still enforces on its side: a **secret** key (`tiq_live_` /
384
+ `tiq_test_`) is refused in a browser at construction — that is orthogonal to the
385
+ above and does not change. The `df` lifecycle roads (create session, validate,
386
+ withdraw, …) remain server-to-server and go through `proxyEndpoint` in a browser.
387
+
388
+ ---
389
+
390
+ ## Reliability
391
+
392
+ Every request carries a deadline, retries safely, and can be cancelled.
393
+
394
+ ```ts
395
+ const ce = new ConsentEraClient({
396
+ tenantId: TENANT,
397
+ proxyEndpoint: '/api/consentera',
398
+ timeoutMs: 10_000, // default
399
+ retry: { attempts: 3, baseDelayMs: 250, maxDelayMs: 4000 }, // default
400
+ });
401
+
402
+ // one key per logical operation — a double-clicked Save is ONE consent write
403
+ await ce.consent.update(id, updates, context, 'btn_save', { idempotencyKey: formSubmissionId });
404
+
405
+ // cancel from your own code
406
+ const ac = new AbortController();
407
+ await ce.consent.validate(who, 'analytics', { signal: ac.signal });
408
+ ```
409
+
410
+ Retries happen on network failure, 5xx and 429, with exponential backoff and full
411
+ jitter, honouring `Retry-After`. The idempotency key is minted **once per logical
412
+ operation** and reused across every retry of it, so a retry can never write twice.
413
+
414
+ ## Errors
415
+
416
+ ```ts
417
+ import { ConsenteraError } from '@consentera/consent-sdk';
418
+
419
+ try {
420
+ await ce.consent.createSession({ ... });
421
+ } catch (err) {
422
+ if (err instanceof ConsenteraError) {
423
+ err.kind; // 'identity' | 'guardian' | 'rate_limit' | 'auth' | … (closed set)
424
+ err.code; // 'UNKNOWN_IDENTIFIER_FIELD' — the platform's canonical code
425
+ err.status; // 400
426
+ err.requestId; // quote this in a support ticket
427
+ err.retryAfterMs;
428
+ }
429
+ }
430
+ ```
431
+
432
+ Switch on `kind` (closed, exhaustive); read `code` for the platform's exact word.
433
+
434
+ ## Privacy defaults
435
+
436
+ - **`collectContext: 'minimal'`** by default: platform, device type, browser
437
+ family, OS family. No UA string, no screen size, no timezone, no page URL, no
438
+ referrer. `'full'` adds them, with the page URL's **query and fragment removed**;
439
+ `'none'` sends no context at all.
440
+ - **`beforeSend`** gets the last look at every request body. Return it to send,
441
+ return `null` to refuse (which raises — it never silently sends nothing).
442
+ - Debug logging **never prints a request or response body**. The body of a session
443
+ create is the Data Principal's identifiers.
444
+
445
+ ## How the SDK identifies itself
446
+
447
+ Every request carries the pair agreed across all six Consentera SDKs:
448
+
449
+ ```
450
+ User-Agent: ConsenteraSDK/2.0.0 (<platform>; <runtime>)
451
+ X-Consentera-SDK: js/2.0.0
452
+ ```
453
+
454
+ `ConsenteraSDK/<version>` is the form the platform's audit pipeline already
455
+ parses. **In a browser only the second is sent**: `User-Agent` is a forbidden
456
+ fetch header, so the browser drops any attempt to set it — the Node build and
457
+ the CLI send both.
458
+
459
+ ```ts
460
+ new ConsentEraClient({
461
+ tenantId: TENANT,
462
+ proxyEndpoint: '/api/consentera',
463
+ collectContext: 'none',
464
+ beforeSend: ({ body }) => redactForYourPolicy(body),
465
+ });
466
+ ```
467
+
468
+ ## Content Security Policy
469
+
470
+ The SDK makes no `eval` and inserts no `<script>`. It does inject a `<style>`
471
+ element for the banner and preference centre, so a strict `style-src` needs
472
+ either `'unsafe-inline'` or the SDK's styles disabled (bring your own UI).
473
+
474
+ The CDN build is at `dist/consentera-consent.min.js` (`unpkg`/`jsdelivr` point
475
+ there). Pin the version and use the published SRI hash.
476
+
477
+ ## Migrating from 1.x
478
+
479
+ See [CHANGELOG.md](./CHANGELOG.md). In short: `handleCallback()` is async and
480
+ fails closed, a failed consent sync now throws instead of reporting success,
481
+ `apiKey` is refused in a browser, identifiers replaced `data_principal_ref`,
482
+ telemetry is off by default, and the cookie banner (`ConsenteraConsent`, the
483
+ default export) **requires `apiEndpoint`** — there is no default server, and a
484
+ missing one is refused at construction with `ENDPOINT_REQUIRED` (script tag:
485
+ `data-api-endpoint`).
486
+
487
+ ## Licence
488
+
489
+ MIT — see [LICENSE](./LICENSE).