@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.
- package/CHANGELOG.md +245 -0
- package/LICENSE +21 -0
- package/README.md +489 -0
- package/dist/consentera-consent.cjs +4919 -0
- package/dist/consentera-consent.cjs.map +1 -0
- package/dist/consentera-consent.min.js +2 -0
- package/dist/consentera-consent.min.js.map +1 -0
- package/dist/consentera-consent.mjs +4864 -0
- package/dist/consentera-consent.mjs.map +1 -0
- package/dist/react/index.cjs +2731 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.mjs +2724 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/types/consent/CallbackHandler.d.ts +246 -0
- package/dist/types/consent/ConsentManager.d.ts +128 -0
- package/dist/types/consent/ConsentSession.d.ts +127 -0
- package/dist/types/consent/ConsentValidator.d.ts +63 -0
- package/dist/types/consent/artifactRead.d.ts +48 -0
- package/dist/types/consent/consentPopup.d.ts +115 -0
- package/dist/types/core/ConsentEraClient.d.ts +106 -0
- package/dist/types/core/ConsenteraConsent.d.ts +163 -0
- package/dist/types/core/errors.d.ts +108 -0
- package/dist/types/core/http.d.ts +176 -0
- package/dist/types/core/version.d.ts +36 -0
- package/dist/types/df/DFConfigClient.d.ts +59 -0
- package/dist/types/gcm/ConsentModeBridge.d.ts +54 -0
- package/dist/types/gpp/GPPManager.d.ts +62 -0
- package/dist/types/index.d.mts +5 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/principal/PrincipalClient.d.ts +34 -0
- package/dist/types/react/ConsentEraProvider.d.ts +58 -0
- package/dist/types/react/ConsentGate.d.ts +40 -0
- package/dist/types/react/index.d.mts +4 -0
- package/dist/types/react/index.d.ts +10 -0
- package/dist/types/react/useConsentEra.d.ts +65 -0
- package/dist/types/react/useConsentValidation.d.ts +23 -0
- package/dist/types/storage/ConsentStorage.d.ts +39 -0
- package/dist/types/tcf/TCFManager.d.ts +46 -0
- package/dist/types/types/consent-lifecycle.d.ts +804 -0
- package/dist/types/types/index.d.ts +311 -0
- package/dist/types/ui/ConsentBanner.d.ts +22 -0
- package/dist/types/ui/PreferenceCenter.d.ts +24 -0
- package/dist/types/utils/EventEmitter.d.ts +32 -0
- package/dist/types/utils/Logger.d.ts +16 -0
- package/dist/types/utils/browserStorage.d.ts +35 -0
- package/dist/types/utils/context.d.ts +81 -0
- package/dist/types/utils/helpers.d.ts +48 -0
- package/package.json +132 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsentEra Consent SDK — Session Management
|
|
3
|
+
* Create consent sessions, retrieve artifacts, handle redirect flows
|
|
4
|
+
*/
|
|
5
|
+
import { ConsentEraClientConfig, CreateSessionRequest, CreateSessionResponse, ArtifactReadResult } from '../types/consent-lifecycle';
|
|
6
|
+
import { type ArtifactReadOptions } from './artifactRead';
|
|
7
|
+
import { Logger } from '../utils/Logger';
|
|
8
|
+
import { type RequestOptions } from '../core/http';
|
|
9
|
+
import { type ConsentPopupOptions, type ConsentPopupResult } from './consentPopup';
|
|
10
|
+
type RequestFn = <T>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: unknown, queryParams?: Record<string, string | number | boolean | undefined | null>, options?: RequestOptions) => Promise<T>;
|
|
11
|
+
export declare class ConsentSession {
|
|
12
|
+
private request;
|
|
13
|
+
private config;
|
|
14
|
+
private logger;
|
|
15
|
+
/**
|
|
16
|
+
* The callback_url each session was created with, in this page. The popup
|
|
17
|
+
* needs it: /collect posts its decision to that URL's origin.
|
|
18
|
+
*/
|
|
19
|
+
private callbackUrls;
|
|
20
|
+
constructor(request: RequestFn, config: ConsentEraClientConfig, logger: Logger);
|
|
21
|
+
/**
|
|
22
|
+
* Create a new consent session for consent collection.
|
|
23
|
+
* Returns a session with consent_url for redirect/popup flow.
|
|
24
|
+
*
|
|
25
|
+
* createSession({
|
|
26
|
+
* data_principal: { email: 'riya@example.in' },
|
|
27
|
+
* notice_internal_name: 'bnb_consent_v2',
|
|
28
|
+
* age: { date_of_birth: '1998-04-12' },
|
|
29
|
+
* })
|
|
30
|
+
*
|
|
31
|
+
* Identity is `data_principal` — the identifiers, keyed by THIS
|
|
32
|
+
* ORGANISATION'S locked integration key — or `data_principal_id`; the type of
|
|
33
|
+
* {@link CreateSessionRequest} requires at least one of the two, because the
|
|
34
|
+
* API refuses a request with neither.
|
|
35
|
+
*
|
|
36
|
+
* ─── THIS IS THE U58 WIRE, AND THERE IS NO OVERLAP WINDOW ────────────────
|
|
37
|
+
*
|
|
38
|
+
* `data_principal_ref`, `data_principal_ref_type`, `data_principal_details`,
|
|
39
|
+
* `locale_pref`, `notice_language`, `template_language` and `purpose_ids` are
|
|
40
|
+
* DELETED from the API's request struct. This SDK version talks to an API
|
|
41
|
+
* carrying U58 and to no other, in both directions:
|
|
42
|
+
*
|
|
43
|
+
* old SDK -> new API the identifiers are dropped, then 400
|
|
44
|
+
* new SDK -> old API the identifiers are dropped, then 400
|
|
45
|
+
*
|
|
46
|
+
* EVERY FIELD THIS BUILDS IS A FIELD THE API DECODES. The handler decodes
|
|
47
|
+
* with a plain `json.Decoder` and NO `DisallowUnknownFields` — on BOTH wires
|
|
48
|
+
* — so a key it does not know is dropped in silence rather than refused. That
|
|
49
|
+
* is why the two wires cannot be mixed and why the failure above is a 400
|
|
50
|
+
* about a MISSING identifier rather than about the field you sent. Check a
|
|
51
|
+
* field against the request struct before adding it.
|
|
52
|
+
*/
|
|
53
|
+
createSession(params: CreateSessionRequest, options?: {
|
|
54
|
+
idempotencyKey?: string;
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
}): Promise<CreateSessionResponse>;
|
|
58
|
+
/**
|
|
59
|
+
* Redirect the user to the ConsentEra consent collection widget.
|
|
60
|
+
* Only works in browser environments.
|
|
61
|
+
*/
|
|
62
|
+
redirectToConsent(session: CreateSessionResponse): void;
|
|
63
|
+
/**
|
|
64
|
+
* Open the consent page in a dialog ON THIS PAGE and resolve with the
|
|
65
|
+
* decision it reports, or `{ outcome: 'dismissed' }` when the person closes
|
|
66
|
+
* it. Rejects after `timeoutMs` (default 10 minutes).
|
|
67
|
+
*
|
|
68
|
+
* const r = await consent.openConsentPopup(session);
|
|
69
|
+
* if (r.outcome === 'decided') confirmOnServer(r.session_id, r.artifact_id);
|
|
70
|
+
*
|
|
71
|
+
* It listens for the ONE message /collect posts, `consentera:submitted` /
|
|
72
|
+
* `consentera:declined`, from the /collect origin and the frame it opened.
|
|
73
|
+
* See consent/consentPopup.ts for the wire and for why this is a dialog and
|
|
74
|
+
* not window.open (a top-level /collect posts nothing) (SDK register WEB-035).
|
|
75
|
+
*
|
|
76
|
+
* THIS PAGE MUST BE ON THE CALLBACK'S ORIGIN: /collect addresses the message
|
|
77
|
+
* to the session's callback_url origin. A session created by this SDK is
|
|
78
|
+
* checked before anything opens (`CALLBACK_ORIGIN_MISMATCH`); pass
|
|
79
|
+
* `callbackUrl` when your server set a different one. The page must also be
|
|
80
|
+
* in your integration client's Allowed Domains, or the platform refuses to
|
|
81
|
+
* be framed (frame-ancestors).
|
|
82
|
+
*
|
|
83
|
+
* The result is what the platform page reported, not the consent record:
|
|
84
|
+
* confirm the artefact before acting on a grant.
|
|
85
|
+
*/
|
|
86
|
+
openConsentPopup(session: CreateSessionResponse, options?: ConsentPopupOptions): Promise<ConsentPopupResult>;
|
|
87
|
+
/**
|
|
88
|
+
* Read a consent artifact.
|
|
89
|
+
*
|
|
90
|
+
* const r = await consent.getArtifact(artifactId, { sessionId });
|
|
91
|
+
* if (r.state === 'pending') retryIn(r.retryAfterMs); // 202: recorded, still being written
|
|
92
|
+
* else use(r.artifact); // 200
|
|
93
|
+
*
|
|
94
|
+
* PASS THE SESSION ID the callback gave you. The platform answers 202 for a
|
|
95
|
+
* consent whose artifact is still being written only when it can see which
|
|
96
|
+
* session is asking; without it that consent is a 404, the same answer a
|
|
97
|
+
* guessed id gets. A 404 WITH a session id means the artifact was never
|
|
98
|
+
* issued for that session: it throws a ConsenteraNotFoundError.
|
|
99
|
+
*
|
|
100
|
+
* A 200 is not proof the artifact belongs to your session — the platform
|
|
101
|
+
* checks the session only while the artifact is missing (walk finding F077).
|
|
102
|
+
* Compare `artifact.data_principal_id` with your session's.
|
|
103
|
+
*/
|
|
104
|
+
getArtifact(artifactId: string, options?: ArtifactReadOptions): Promise<ArtifactReadResult>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A fresh per-attempt callback state: 32 random bytes, hex. From the platform
|
|
108
|
+
* CSPRNG only — there is no Math.random fallback, because a guessable state is
|
|
109
|
+
* a forgeable callback. (`newRequestId` may fall back; this must not.)
|
|
110
|
+
*/
|
|
111
|
+
export declare function newCallbackState(): string;
|
|
112
|
+
/**
|
|
113
|
+
* callback_url with this SDK's `state` added to its query.
|
|
114
|
+
*
|
|
115
|
+
* WHY IT SURVIVES THE ROUND TRIP — a platform fact, read on pre-main
|
|
116
|
+
* 35cd853ac7: addRedirectParams appends session_id/artifact_id to callback_url
|
|
117
|
+
* with `&` when it already has a `?` (redirect_params.go:17), and
|
|
118
|
+
* setRedirectParam then parses, SETS status / pending / sig and re-encodes —
|
|
119
|
+
* every other key, `state` included, is kept. The mobile SDKs have bound their
|
|
120
|
+
* callbacks this way since 2.0.0.
|
|
121
|
+
*
|
|
122
|
+
* `state` IS RESERVED. A callback_url that already carries one is refused
|
|
123
|
+
* rather than overwritten: overwriting would silently break the Data
|
|
124
|
+
* Fiduciary's own use of it, and keeping theirs would bind nothing.
|
|
125
|
+
*/
|
|
126
|
+
export declare function withCallbackState(callbackUrl: string, state: string): string;
|
|
127
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsentEra Consent SDK — Consent Validation
|
|
3
|
+
* Validate consent status for single or multiple purposes
|
|
4
|
+
*/
|
|
5
|
+
import { ValidateResponse, BulkValidateResponse } from '../types/consent-lifecycle';
|
|
6
|
+
import { Logger } from '../utils/Logger';
|
|
7
|
+
import { type PrincipalRef } from '../utils/context';
|
|
8
|
+
import type { RequestOptions } from '../core/http';
|
|
9
|
+
type RequestFn = <T>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: unknown, queryParams?: Record<string, string | number | boolean | undefined | null>, options?: RequestOptions) => Promise<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Per-check knobs. `signal` is what lets a React hook drop a validation whose
|
|
12
|
+
* answer it no longer wants — without it an out-of-order response can overwrite
|
|
13
|
+
* a newer decision, which on a consent gate fails OPEN.
|
|
14
|
+
*/
|
|
15
|
+
export interface ValidateOptions {
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class ConsentValidator {
|
|
20
|
+
private request;
|
|
21
|
+
private logger;
|
|
22
|
+
constructor(request: RequestFn, logger: Logger);
|
|
23
|
+
/**
|
|
24
|
+
* Validate consent for a single purpose.
|
|
25
|
+
* Returns ALLOW or DENY with reason code.
|
|
26
|
+
*
|
|
27
|
+
* check({ data_principal_identifiers: { email: 'riya@example.in' } },
|
|
28
|
+
* 'product_analytics')
|
|
29
|
+
* check({ data_principal_id: '…' }, 'product_analytics')
|
|
30
|
+
*
|
|
31
|
+
* ─── data_principal_ref IS REFUSED OUTRIGHT ────────────────────────────
|
|
32
|
+
*
|
|
33
|
+
* Owner ruling 2026-09-21, no transition period
|
|
34
|
+
* (consent/lifecycle_identity.go:8-11). A request carrying it is answered
|
|
35
|
+
* 400 VALIDATION_ERROR whose message begins `DATA_PRINCIPAL_REF_REFUSED`,
|
|
36
|
+
* and the refusal fires even when `data_principal_id` is also present.
|
|
37
|
+
*
|
|
38
|
+
* NAME PEOPLE BY THE ORGANISATION'S LOCKED KEY FIELDS (F015): the mobile atom
|
|
39
|
+
* is `mobile` on this road AND on create — `phone` is refused by name. See
|
|
40
|
+
* {@link DataPrincipalIdentifiers}.
|
|
41
|
+
*/
|
|
42
|
+
check(who: PrincipalRef, purposeCode: string, options?: ValidateOptions): Promise<ValidateResponse>;
|
|
43
|
+
/**
|
|
44
|
+
* Validate consent for multiple purposes at once.
|
|
45
|
+
* Returns results map indexed by purpose code.
|
|
46
|
+
*
|
|
47
|
+
* `data_principal_ref` AND `data_principal_refs` are both refused here
|
|
48
|
+
* (consent/validate.go:249,257).
|
|
49
|
+
*/
|
|
50
|
+
checkBulk(who: PrincipalRef, purposeCodes: string[], options?: ValidateOptions): Promise<BulkValidateResponse>;
|
|
51
|
+
/**
|
|
52
|
+
* Quick boolean check — is this purpose allowed?
|
|
53
|
+
*
|
|
54
|
+
* isAllowed({ data_principal_identifiers: { email: 'riya@example.in' } },
|
|
55
|
+
* 'product_analytics')
|
|
56
|
+
*/
|
|
57
|
+
isAllowed(who: PrincipalRef, purposeCode: string, options?: ValidateOptions): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* Check multiple purposes and return a map of purpose → boolean.
|
|
60
|
+
*/
|
|
61
|
+
areAllowed(who: PrincipalRef, purposeCodes: string[]): Promise<Map<string, boolean>>;
|
|
62
|
+
}
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consentera Consent SDK — THE artifact read, in one place.
|
|
3
|
+
*
|
|
4
|
+
* `ConsentSession.getArtifact` and the callback handler's confirmation both
|
|
5
|
+
* read GET /consent/artifacts/{artifact_id}, and before this file each spelled
|
|
6
|
+
* the request itself. Both spelled it WITHOUT `?session_id=`, which on the
|
|
7
|
+
* platform that ships is the difference between two answers:
|
|
8
|
+
*
|
|
9
|
+
* without session_id a consent whose artifact is still being written is a
|
|
10
|
+
* 404 ARTIFACT_NOT_FOUND — the same word a guessed id
|
|
11
|
+
* gets. The callback handler therefore had to read
|
|
12
|
+
* every 404 as "maybe pending" and poll it, so a FORGED
|
|
13
|
+
* artifact_id came back `pending` instead of refused.
|
|
14
|
+
* with session_id 202 + Retry-After while the projection is owed; 404
|
|
15
|
+
* only when the id was never issued for that session or
|
|
16
|
+
* will never be written. (GetArtifactHandler,
|
|
17
|
+
* consent/collection.go:4409-4520 at 4fda7e3d05; walk
|
|
18
|
+
* finding F018; SDK register WEB-033.)
|
|
19
|
+
*
|
|
20
|
+
* Measured on setup.consentera.in (4fda7e3d05): an unknown artifact read with a
|
|
21
|
+
* real session is 404; the walk's real artifact with its session is 200. See
|
|
22
|
+
* fixtures/platform-wire/artifact-read.*.json at the repo root.
|
|
23
|
+
*
|
|
24
|
+
* WHAT THE session_id DOES NOT DO (walk finding F077, measured): once the
|
|
25
|
+
* artifact row exists the 200 path reads by tenant + artifact_id ALONE. The
|
|
26
|
+
* same artifact read with a random session_id is still 200. So a 200 does not
|
|
27
|
+
* prove the artifact belongs to your session; compare its `data_principal_id`
|
|
28
|
+
* with the one your session create returned (the callback handler does).
|
|
29
|
+
*/
|
|
30
|
+
import type { ArtifactReadResult } from '../types/consent-lifecycle';
|
|
31
|
+
import type { RequestOptions } from '../core/http';
|
|
32
|
+
type RequestFn = <T>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: unknown, queryParams?: Record<string, string | number | boolean | undefined | null>, options?: RequestOptions) => Promise<T>;
|
|
33
|
+
export interface ArtifactReadOptions {
|
|
34
|
+
/**
|
|
35
|
+
* The session the callback named. SEND IT: without it the platform cannot
|
|
36
|
+
* tell "still being written" from "never issued", and answers 404 for both.
|
|
37
|
+
*/
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
export declare function artifactPath(artifactId: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Read one artifact. Resolves `recorded` (200) or `pending` (202); a 404, a
|
|
45
|
+
* 403 and everything else is the transport's typed ConsenteraError.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readArtifact(request: RequestFn, artifactId: string, options?: ArtifactReadOptions): Promise<ArtifactReadResult>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consentera Consent SDK — the consent popup, and the ONE message it listens for.
|
|
3
|
+
*
|
|
4
|
+
* ─── WHAT /collect ACTUALLY POSTS (SDK register WEB-035) ────────────────────
|
|
5
|
+
*
|
|
6
|
+
* After a decision, an EMBEDDED /collect page (window.parent !== window) posts
|
|
7
|
+
* exactly one message to its parent. That is decisionMessage() in
|
|
8
|
+
* consentera-ui/src/app/collect/[sessionId]/decision-message.ts, posted at
|
|
9
|
+
* page.tsx:1535-1545 (grant) and :1670-1680 (decline) on the served commit
|
|
10
|
+
* 4fda7e3d05:
|
|
11
|
+
*
|
|
12
|
+
* { type: 'consentera:submitted' | 'consentera:declined',
|
|
13
|
+
* session_id, artifact_id, status: 'granted'|'partial'|'denied', pending,
|
|
14
|
+
* sessionId, redirectUrl, choices } // the three legacy fields
|
|
15
|
+
*
|
|
16
|
+
* `choices` rides only on a submit. The TARGET ORIGIN is the origin of the
|
|
17
|
+
* session's callback_url (redirect-trust.ts parentOriginFor), so the page that
|
|
18
|
+
* frames /collect must be on the callback's origin or the browser drops the
|
|
19
|
+
* message without a word.
|
|
20
|
+
*
|
|
21
|
+
* Before this file the SDK opened the page with window.open and listened for
|
|
22
|
+
* `consentera:consent-result`, a type the platform has never sent. That was
|
|
23
|
+
* wrong twice over, because a window.open popup is a TOP-LEVEL page. It posts
|
|
24
|
+
* nothing: it navigates to the callback instead. The storage fallback polled
|
|
25
|
+
* sessionStorage, which a separate window does not share. So the promise could
|
|
26
|
+
* end only when the window closed, as `unverified`, or at the 10-minute
|
|
27
|
+
* timeout.
|
|
28
|
+
*
|
|
29
|
+
* So the popup is now what the platform's own website popup is: a dialog on
|
|
30
|
+
* THIS page with /collect in an iframe (consentera-ui/src/lib/onboarding/
|
|
31
|
+
* website-consent.ts, createDialog). It is the presentation in which /collect
|
|
32
|
+
* reports the decision.
|
|
33
|
+
*
|
|
34
|
+
* ─── WHAT THE RESULT IS, AND WHAT IT IS NOT ────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* The message comes from the platform's origin and from the frame this SDK
|
|
37
|
+
* opened (both are checked), so it is the platform page's report of the
|
|
38
|
+
* decision. It is not the consent record. The record is the artefact: confirm
|
|
39
|
+
* it with getArtifact(artifact_id, { sessionId }) through your proxy, or on
|
|
40
|
+
* your server, before you act on a grant.
|
|
41
|
+
*/
|
|
42
|
+
export type DecisionStatus = 'granted' | 'partial' | 'denied';
|
|
43
|
+
/** The decision /collect reported. */
|
|
44
|
+
export interface ConsentPopupDecision {
|
|
45
|
+
outcome: 'decided';
|
|
46
|
+
type: 'consentera:submitted' | 'consentera:declined';
|
|
47
|
+
session_id: string;
|
|
48
|
+
/** '' only when an older platform build posted the legacy fields alone and its redirectUrl named none. */
|
|
49
|
+
artifact_id: string;
|
|
50
|
+
status: DecisionStatus;
|
|
51
|
+
/**
|
|
52
|
+
* The PROJECTION state, as the platform means it (redirect `pending=1`,
|
|
53
|
+
* collection.go:4274): the consent is recorded and the artifact named here is
|
|
54
|
+
* still being written, so a read may answer 202 + Retry-After. It is not a
|
|
55
|
+
* consent state and not a double opt-in (the platform names that
|
|
56
|
+
* `awaiting_confirmation`). A hint, not proof: confirm through your backend.
|
|
57
|
+
*/
|
|
58
|
+
pending: boolean;
|
|
59
|
+
/** The callback URL with the callback query on it, when the session has a callback. */
|
|
60
|
+
redirect_url?: string;
|
|
61
|
+
/** purpose → granted, on a submit. */
|
|
62
|
+
choices?: Record<string, boolean>;
|
|
63
|
+
}
|
|
64
|
+
/** The person closed the dialog without deciding. Nothing was recorded by this dialog. */
|
|
65
|
+
export interface ConsentPopupDismissed {
|
|
66
|
+
outcome: 'dismissed';
|
|
67
|
+
session_id: string;
|
|
68
|
+
}
|
|
69
|
+
export type ConsentPopupResult = ConsentPopupDecision | ConsentPopupDismissed;
|
|
70
|
+
/**
|
|
71
|
+
* Read a MessageEvent as /collect's decision — or null when it is not one, or
|
|
72
|
+
* not from where it must come from.
|
|
73
|
+
*
|
|
74
|
+
* Exported for a Data Fiduciary that frames consent_url itself: the same
|
|
75
|
+
* checks, one implementation.
|
|
76
|
+
*
|
|
77
|
+
* origin must equal the /collect origin (the consent_url's origin)
|
|
78
|
+
* source when given, must be the frame that was opened
|
|
79
|
+
* type consentera:submitted | consentera:declined
|
|
80
|
+
* session `session_id` (or the legacy `sessionId`) must be this session;
|
|
81
|
+
* when both are present they must agree
|
|
82
|
+
*/
|
|
83
|
+
export declare function readDecisionMessage(event: Pick<MessageEvent, 'origin' | 'data' | 'source'>, expected: {
|
|
84
|
+
origin: string;
|
|
85
|
+
sessionId: string;
|
|
86
|
+
source?: MessageEventSource | null;
|
|
87
|
+
}): ConsentPopupDecision | null;
|
|
88
|
+
export interface ConsentPopupOptions {
|
|
89
|
+
/** Reject after this long with no decision. Default 10 minutes. */
|
|
90
|
+
timeoutMs?: number;
|
|
91
|
+
/**
|
|
92
|
+
* The callback_url the session was created with, when it is not the one
|
|
93
|
+
* this SDK sent (a server that sets its own). /collect posts the decision to
|
|
94
|
+
* THAT origin, so this page must be on it; the SDK refuses up front when it
|
|
95
|
+
* is not, instead of waiting for a message the browser will drop.
|
|
96
|
+
*/
|
|
97
|
+
callbackUrl?: string;
|
|
98
|
+
/** The dialog's accessible name. */
|
|
99
|
+
title?: string;
|
|
100
|
+
/** Where to attach the dialog. Default document.body. */
|
|
101
|
+
container?: HTMLElement;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Refuse a callback that cannot deliver the decision to this page.
|
|
105
|
+
* Returns the refusal, or null when the callback is fine or unknown.
|
|
106
|
+
*/
|
|
107
|
+
export declare function callbackOriginProblem(callbackUrl: string | undefined, pageOrigin: string): string | null;
|
|
108
|
+
/**
|
|
109
|
+
* Open consent_url in a dialog on this page and resolve with the decision
|
|
110
|
+
* /collect posts, or `dismissed` when the person closes it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function openConsentDialog(session: {
|
|
113
|
+
consent_session_id: string;
|
|
114
|
+
consent_url: string;
|
|
115
|
+
}, options?: ConsentPopupOptions): Promise<ConsentPopupResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consentera Consent SDK — the client.
|
|
3
|
+
*
|
|
4
|
+
* ─── THE CREDENTIAL RULE, AND IT IS ENFORCED HERE AT CONSTRUCTION ──────────
|
|
5
|
+
*
|
|
6
|
+
* A BROWSER BUNDLE NEVER CARRIES A SECRET (owner ruling 2026-09-22). Before
|
|
7
|
+
* 2.0.0 this file said the opposite in a comment — "Prefer site key (public,
|
|
8
|
+
* safe for frontend) over API key (secret, server-side only)" — and then sent
|
|
9
|
+
* whichever of the two it was given, from wherever it was running. Both halves
|
|
10
|
+
* of that sentence were wrong in a way that only showed up in production:
|
|
11
|
+
*
|
|
12
|
+
* the site key does not work. The platform replaces a site key's permissions
|
|
13
|
+
* with exactly {consent.render, widget.render, session.submit, session.render}
|
|
14
|
+
* (core/auth/df/api_usage.go:105-110, :1115) and NO ROUTE REQUIRES ANY OF THEM
|
|
15
|
+
* — a grep for RequireDFPermission of those four names over the whole API
|
|
16
|
+
* returns nothing. Every consent lifecycle road requires
|
|
17
|
+
* engagement.consent.{collect,read,validate,update,withdraw,renew}
|
|
18
|
+
* (cmd/api/routes_consent.go:377-569). The intersection is empty, so a browser
|
|
19
|
+
* configured the recommended way got 403 on every call.
|
|
20
|
+
*
|
|
21
|
+
* the secret key does work — which is worse. It is the only credential that
|
|
22
|
+
* authorises those roads, so "make it work" meant putting a tiq_live_ key in
|
|
23
|
+
* a public bundle.
|
|
24
|
+
*
|
|
25
|
+
* So the rule is now structural rather than advisory:
|
|
26
|
+
*
|
|
27
|
+
* in a browser a Data Fiduciary road MUST go through `proxyEndpoint` — your
|
|
28
|
+
* own server route, which holds the secret and forwards. An
|
|
29
|
+
* `apiKey` in a browser is REFUSED AT CONSTRUCTION.
|
|
30
|
+
* on a server `apiKey` directly, as always.
|
|
31
|
+
* public roads may carry `siteKey`; they carry no secret.
|
|
32
|
+
* session roads carry no key at all: the session id and its nonce are the
|
|
33
|
+
* capability.
|
|
34
|
+
*
|
|
35
|
+
* WHAT THE PLATFORM MUST STILL DO for the public half to be usable from a
|
|
36
|
+
* browser without a proxy is a separate unit and is written down in
|
|
37
|
+
* README.md → "What the platform must guarantee".
|
|
38
|
+
*/
|
|
39
|
+
import { ConsentEraClientConfig } from '../types/consent-lifecycle';
|
|
40
|
+
import { ConsentSession } from '../consent/ConsentSession';
|
|
41
|
+
import { ConsentValidator } from '../consent/ConsentValidator';
|
|
42
|
+
import { ConsentManager } from '../consent/ConsentManager';
|
|
43
|
+
import { CallbackHandler } from '../consent/CallbackHandler';
|
|
44
|
+
import { DFConfigClient } from '../df/DFConfigClient';
|
|
45
|
+
import { PrincipalClient } from '../principal/PrincipalClient';
|
|
46
|
+
import { EventEmitter } from '../utils/EventEmitter';
|
|
47
|
+
import { buildClientContext, buildAffirmativeAction } from '../utils/context';
|
|
48
|
+
import { type RequestOptions } from './http';
|
|
49
|
+
export declare class ConsentEraClient extends EventEmitter {
|
|
50
|
+
private config;
|
|
51
|
+
private logger;
|
|
52
|
+
private transport;
|
|
53
|
+
/** Consent session creation and artifact retrieval */
|
|
54
|
+
readonly session: ConsentSession;
|
|
55
|
+
/** Consent validation (single + bulk) */
|
|
56
|
+
readonly validate: ConsentValidator;
|
|
57
|
+
/** Consent update, withdrawal, and renewal */
|
|
58
|
+
readonly manage: ConsentManager;
|
|
59
|
+
/** Callback handler for redirect flows */
|
|
60
|
+
readonly callback: CallbackHandler;
|
|
61
|
+
/** DF configuration and notice purposes */
|
|
62
|
+
readonly df: DFConfigClient;
|
|
63
|
+
/** Data principal rights and portal SSO */
|
|
64
|
+
readonly principal: PrincipalClient;
|
|
65
|
+
/**
|
|
66
|
+
* Unified consent namespace — aggregates session, validate, manage, callback.
|
|
67
|
+
*/
|
|
68
|
+
readonly consent: {
|
|
69
|
+
createSession: ConsentSession['createSession'];
|
|
70
|
+
redirectToConsent: ConsentSession['redirectToConsent'];
|
|
71
|
+
openConsentPopup: ConsentSession['openConsentPopup'];
|
|
72
|
+
getArtifact: ConsentSession['getArtifact'];
|
|
73
|
+
validate: ConsentValidator['check'];
|
|
74
|
+
validateBulk: ConsentValidator['checkBulk'];
|
|
75
|
+
isAllowed: ConsentValidator['isAllowed'];
|
|
76
|
+
areAllowed: ConsentValidator['areAllowed'];
|
|
77
|
+
handleCallback: CallbackHandler['parseCallback'];
|
|
78
|
+
isCallback: CallbackHandler['isCallback'];
|
|
79
|
+
getContext: ConsentManager['getUpdateContext'];
|
|
80
|
+
update: ConsentManager['update'];
|
|
81
|
+
grant: ConsentManager['grant'];
|
|
82
|
+
deny: ConsentManager['deny'];
|
|
83
|
+
getWithdrawalContext: ConsentManager['getWithdrawalContext'];
|
|
84
|
+
withdraw: ConsentManager['withdraw'];
|
|
85
|
+
withdrawBulk: ConsentManager['withdrawBulk'];
|
|
86
|
+
getRenewalContext: ConsentManager['getRenewalContext'];
|
|
87
|
+
renew: ConsentManager['renew'];
|
|
88
|
+
renewBulk: ConsentManager['renewBulk'];
|
|
89
|
+
};
|
|
90
|
+
constructor(config: ConsentEraClientConfig);
|
|
91
|
+
/**
|
|
92
|
+
* Central HTTP request method. Delegates to {@link HttpTransport}, which owns
|
|
93
|
+
* the deadline, the retry policy, the idempotency key and the credential rule.
|
|
94
|
+
*
|
|
95
|
+
* @param queryParams query string values (the legacy 4th-argument position)
|
|
96
|
+
* @param options per-request road, idempotency key, signal, timeout, retry
|
|
97
|
+
*/
|
|
98
|
+
request<T>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, body?: unknown, queryParams?: Record<string, string | number | boolean | undefined | null>, options?: RequestOptions): Promise<T>;
|
|
99
|
+
/** Build client context — delegates to shared helper */
|
|
100
|
+
static buildClientContext: typeof buildClientContext;
|
|
101
|
+
/** Build affirmative action — delegates to shared helper */
|
|
102
|
+
static buildAffirmativeAction: typeof buildAffirmativeAction;
|
|
103
|
+
/** Get the current configuration. The credentials are redacted. */
|
|
104
|
+
getConfig(): ConsentEraClientConfig;
|
|
105
|
+
}
|
|
106
|
+
export { ConsenteraError, ConsentEraApiError, ConsenteraConfigError, ConsenteraAuthError, ConsenteraPermissionError, ConsenteraValidationError, ConsenteraIdentityError, ConsenteraGuardianError, ConsenteraNotFoundError, ConsenteraConflictError, ConsenteraRateLimitError, ConsenteraServerError, ConsenteraNetworkError, ConsenteraTimeoutError, type ConsenteraErrorKind, } from './errors';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsentEra Consent SDK Core Module
|
|
3
|
+
* Main entry point for consent management
|
|
4
|
+
*/
|
|
5
|
+
import { ConsentEraConfig, ConsentData, ConsentPreferences, ConsentResponse, ConfigResponse, PurposeCategory, CookieConsentConfigResponse } from '../types';
|
|
6
|
+
import { EventEmitter } from '../utils/EventEmitter';
|
|
7
|
+
export declare class ConsentEraConsent extends EventEmitter {
|
|
8
|
+
private config;
|
|
9
|
+
private storage;
|
|
10
|
+
private banner;
|
|
11
|
+
private preferenceCenter;
|
|
12
|
+
private tcfManager;
|
|
13
|
+
private gppManager;
|
|
14
|
+
private logger;
|
|
15
|
+
private initialized;
|
|
16
|
+
private initPromise;
|
|
17
|
+
private destroyed;
|
|
18
|
+
private remoteConfig;
|
|
19
|
+
private remotePolicyVersion;
|
|
20
|
+
private ageGateEnabled;
|
|
21
|
+
private ageGatePrompt;
|
|
22
|
+
private pendingIsAdult;
|
|
23
|
+
private readonly deviceId;
|
|
24
|
+
constructor(config: ConsentEraConfig);
|
|
25
|
+
private mergeDefaults;
|
|
26
|
+
private getOrCreateDeviceId;
|
|
27
|
+
/**
|
|
28
|
+
* Supply a self-declared adult/minor signal (DPDP §9(3)) before calling
|
|
29
|
+
* acceptAll()/rejectAll()/savePreferences(). Optional — if never called,
|
|
30
|
+
* the server treats the visitor as unknown-age and force-denies tracking
|
|
31
|
+
* categories regardless of what the visitor otherwise selects.
|
|
32
|
+
*/
|
|
33
|
+
setAgeDeclaration(isAdult: boolean): void;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the tenant has DPDP §9(3) age assurance turned on for this
|
|
36
|
+
* banner. Populated from the real config once init() resolves; false
|
|
37
|
+
* before that.
|
|
38
|
+
*/
|
|
39
|
+
isAgeGateEnabled(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The tenant-configured age-gate prompt text, if any — for an integrator
|
|
42
|
+
* building their own age-check UI ahead of calling setAgeDeclaration().
|
|
43
|
+
*/
|
|
44
|
+
getAgeGatePrompt(): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Initialize the SDK
|
|
47
|
+
*/
|
|
48
|
+
init(): Promise<void>;
|
|
49
|
+
private doInit;
|
|
50
|
+
private authHeaders;
|
|
51
|
+
/**
|
|
52
|
+
* Fetch configuration from server — the real, public, unauthenticated
|
|
53
|
+
* (beyond X-Tenant-Id) /api/v1/cookie-consent/config surface. Previously
|
|
54
|
+
* this called {apiEndpoint}/consent/config/{tenantId}, a route that does
|
|
55
|
+
* not exist anywhere in the API, so every real embed silently fell back to
|
|
56
|
+
* getDefaultConfig() and never showed the tenant's actual categories.
|
|
57
|
+
*/
|
|
58
|
+
private fetchConfig;
|
|
59
|
+
/**
|
|
60
|
+
* Show consent banner
|
|
61
|
+
*/
|
|
62
|
+
showBanner(): void;
|
|
63
|
+
/**
|
|
64
|
+
* Hide consent banner
|
|
65
|
+
*/
|
|
66
|
+
hideBanner(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Show preference center
|
|
69
|
+
*/
|
|
70
|
+
showPreferences(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Hide preference center
|
|
73
|
+
*/
|
|
74
|
+
hidePreferences(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Accept all consent purposes
|
|
77
|
+
*/
|
|
78
|
+
acceptAll(): Promise<ConsentResponse>;
|
|
79
|
+
/**
|
|
80
|
+
* Reject all non-essential purposes
|
|
81
|
+
*/
|
|
82
|
+
rejectAll(): Promise<ConsentResponse>;
|
|
83
|
+
/**
|
|
84
|
+
* Save consent preferences
|
|
85
|
+
*/
|
|
86
|
+
savePreferences(preferences: ConsentPreferences): Promise<ConsentResponse>;
|
|
87
|
+
private saveConsent;
|
|
88
|
+
/** Client-requested selections, keyed by category/purpose id — required
|
|
89
|
+
* (mandatory) categories are always true regardless of the input. This is
|
|
90
|
+
* what gets POSTed; the server may still override individual entries in
|
|
91
|
+
* its response (see effective_selections above). */
|
|
92
|
+
private buildSelections;
|
|
93
|
+
private selectionsToPreferences;
|
|
94
|
+
private buildConsentData;
|
|
95
|
+
/**
|
|
96
|
+
* POST /api/v1/cookie-consent/preferences — the real submit endpoint.
|
|
97
|
+
* Previously this posted to {apiEndpoint}/consent, a route that does not
|
|
98
|
+
* exist anywhere in the API, so no real embed ever actually recorded a
|
|
99
|
+
* consent decision server-side (it silently failed and kept only the
|
|
100
|
+
* local copy).
|
|
101
|
+
*
|
|
102
|
+
* ─── IT THROWS. IT USED TO SWALLOW, AND THAT WAS THE WORST DEFECT HERE ────
|
|
103
|
+
*
|
|
104
|
+
* Until 2.0.0 every failure — offline, 403, a wrong host, a 500 — was caught,
|
|
105
|
+
* logged at warn, and turned into `return null`. The caller then wrote the
|
|
106
|
+
* local cookie anyway, hid the banner, fired `consent_given` and reported
|
|
107
|
+
* success. The visitor had clicked Reject All; the platform had no record of
|
|
108
|
+
* it; and because the cookie existed the banner never came back to ask again.
|
|
109
|
+
* A withdrawal was lost the same way. The SDK's own log line was the only
|
|
110
|
+
* trace, at a level nobody reads in production.
|
|
111
|
+
*
|
|
112
|
+
* Now the failure reaches the caller, the local copy is NOT written, the
|
|
113
|
+
* banner stays up, and the host hears about it on `sync_failed` and
|
|
114
|
+
* `callbacks.onSyncFailed`.
|
|
115
|
+
*/
|
|
116
|
+
private syncConsent;
|
|
117
|
+
/**
|
|
118
|
+
* Get current consent data
|
|
119
|
+
*/
|
|
120
|
+
getConsent(): ConsentData | null;
|
|
121
|
+
/**
|
|
122
|
+
* Check if consent has been given
|
|
123
|
+
*/
|
|
124
|
+
hasConsent(): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Withdraw consent for specified purposes (or all optional purposes if
|
|
127
|
+
* none given). There is no separate public withdraw endpoint for the
|
|
128
|
+
* cookie widget — the real API expresses withdrawal the same way a
|
|
129
|
+
* changed-mind re-visit is expressed: resubmitting selections via
|
|
130
|
+
* POST /api/v1/cookie-consent/preferences with the withdrawn categories
|
|
131
|
+
* set to false. Previously this called {apiEndpoint}/consent/withdraw,
|
|
132
|
+
* a route that does not exist anywhere in the API.
|
|
133
|
+
*/
|
|
134
|
+
withdrawConsent(purposes?: string[]): Promise<ConsentResponse>;
|
|
135
|
+
/**
|
|
136
|
+
* Check if consent is given for a specific purpose
|
|
137
|
+
*/
|
|
138
|
+
isConsentGiven(purpose: string): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Get current preferences
|
|
141
|
+
*/
|
|
142
|
+
getPreferences(): ConsentPreferences;
|
|
143
|
+
/**
|
|
144
|
+
* Set language
|
|
145
|
+
*/
|
|
146
|
+
setLanguage(language: string): void;
|
|
147
|
+
/**
|
|
148
|
+
* Destroy SDK instance
|
|
149
|
+
*/
|
|
150
|
+
destroy(): void;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Translates the real /api/v1/cookie-consent/config response into the SDK's
|
|
154
|
+
* own internal ConfigResponse/BannerConfig/PurposeConfig shape that
|
|
155
|
+
* ConsentBanner and PreferenceCenter render from. Isolated here (rather than
|
|
156
|
+
* rewriting those two renderers around the real field names) so the
|
|
157
|
+
* rendering layer's contract stays stable while only the network layer
|
|
158
|
+
* changes — the real backend's vocabulary (cookie "categories" with a
|
|
159
|
+
* mandatory/sort_order flag) is a different shape than the SDK's generic
|
|
160
|
+
* "purposes", not just a rename.
|
|
161
|
+
*/
|
|
162
|
+
export declare function mapCookieConfigToSDKConfig(real: CookieConsentConfigResponse, config: ConsentEraConfig): ConfigResponse;
|
|
163
|
+
export declare function mapCategoryKeyToPurposeCategory(key: string): PurposeCategory;
|