@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
@@ -0,0 +1,804 @@
1
+ /**
2
+ * ConsentEra Consent SDK — Consent Lifecycle Types
3
+ * Types matching the ConsentEra Go API request/response shapes
4
+ */
5
+ import type { RetryPolicy } from '../core/http';
6
+ import type { ContextMode } from '../utils/context';
7
+ export interface ConsentEraClientConfig {
8
+ /** Secret API key for server-side use (tiq_live_*). Do NOT embed in frontend code. */
9
+ apiKey?: string;
10
+ /** Public site key for frontend use (tiq_pub_*). Safe to embed in client-side code. */
11
+ siteKey?: string;
12
+ /**
13
+ * Your tenant id. REQUIRED with `apiEndpoint` (direct mode), where it is sent
14
+ * as X-Tenant-Id. OPTIONAL with `proxyEndpoint`: your server holds the key and
15
+ * the key names the tenant, and the SDK sends no tenant header through a proxy
16
+ * (SDK register WEB-036).
17
+ */
18
+ tenantId?: string;
19
+ /** ConsentEra API base URL (for direct mode, e.g. 'http://localhost:8080') */
20
+ apiEndpoint?: string;
21
+ /** Proxy endpoint (for proxy mode, e.g. '/api/consentera') — API key injected server-side */
22
+ proxyEndpoint?: string;
23
+ /**
24
+ * Absolute URL to redirect back to after consent collection. createSession
25
+ * adds this SDK's own `state` to it (the callback binding), so `state` is
26
+ * reserved: a URL that already carries one is refused.
27
+ */
28
+ callbackUrl?: string;
29
+ /**
30
+ * Default language for the consent session. It is sent as the wire field
31
+ * `language`; when it is absent NOTHING is sent, which means the TENANT'S
32
+ * own default language.
33
+ *
34
+ * THE WIRE NAME IS `language` SINCE U58. It was `locale_pref`, and
35
+ * `notice_language` and `template_language` stood beside it — three fields
36
+ * for one question. The API decodes exactly one now
37
+ * (`core/dpintake/dpintake.go:109`) and the other three are deleted from the
38
+ * request struct. A caller still sending them has them dropped in silence,
39
+ * because the handler decodes without `DisallowUnknownFields`.
40
+ */
41
+ language?: string;
42
+ /** Enable debug logging. Never logs a request or response BODY — see core/http.ts. */
43
+ debug?: boolean;
44
+ /** Custom headers to include in every request. Can be a static object or a function for dynamic values. */
45
+ customHeaders?: Record<string, string> | (() => Record<string, string>);
46
+ /**
47
+ * Allow a SECRET `apiKey` although a `window` exists. Refused by default —
48
+ * see the header of core/ConsentEraClient.ts. Every request then warns.
49
+ */
50
+ unsafeAllowSecretKeyInBrowser?: boolean;
51
+ /** Per-request deadline in ms. Default 10000. */
52
+ timeoutMs?: number;
53
+ /** Retry policy: attempts (default 3), baseDelayMs (250), maxDelayMs (4000). */
54
+ retry?: Partial<RetryPolicy>;
55
+ /**
56
+ * How much browser environment travels with a consent mutation.
57
+ * Default `minimal`; 1.x behaved as `full` with no way to turn it off.
58
+ */
59
+ collectContext?: ContextMode;
60
+ /**
61
+ * Last look at every request body before it leaves. Return the body to send;
62
+ * return null to refuse the call (which raises, never sends nothing quietly).
63
+ */
64
+ beforeSend?: (req: {
65
+ method: string;
66
+ path: string;
67
+ body: unknown;
68
+ }) => unknown | null;
69
+ /** Inject a fetch implementation (tests, a polyfill, a traced fetch). */
70
+ fetchImpl?: typeof fetch;
71
+ /**
72
+ * Verify the platform's `sig` on a consent callback. The signing key is the
73
+ * DF's callback_signing_secret and must NOT be in a browser, so in a browser
74
+ * this calls your own server. See {@link CallbackSignatureVerifier}.
75
+ */
76
+ verifyCallbackSignature?: import('../consent/CallbackHandler').CallbackSignatureVerifier;
77
+ /**
78
+ * How long the callback road waits for the consent artifact to become
79
+ * readable. It is written asynchronously after submit (5.2–10.8s measured),
80
+ * so a read at callback time 404s. Default 15000; 0 disables the wait and
81
+ * returns `pending` at once.
82
+ */
83
+ artifactWaitMs?: number;
84
+ }
85
+ /**
86
+ * `data_principal` — the person's identifiers, KEYED BY THE FIELD NAMES OF THE
87
+ * TENANT'S LOCKED INTEGRATION KEY.
88
+ *
89
+ * A MAP AND NOT AN INTERFACE, and that is the point, not a shortcut. The
90
+ * admissible key set is a PER-TENANT fact the platform reads at request time:
91
+ * a tenant keyed on `{email, mobile}` accepts exactly `email` and `mobile`. A
92
+ * closed interface would have to name every identifier type the platform
93
+ * knows, and then every tenant's request object would carry every OTHER
94
+ * tenant's fields as optional — which is the second, unchecked identifier door
95
+ * U58 exists to delete. The server's own type is
96
+ * `map[string]string` (`core/dpintake/dpintake.go:82`).
97
+ *
98
+ * THE TYPE COMES FROM THE FIELD NAME. There is no declared type to disagree
99
+ * with the value and no shape to guess from, so `data_principal_ref_type` and
100
+ * the shape-guesser behind it are both gone. A vernacular spelling is NOT
101
+ * folded any more either: on a tenant keyed on `{email, mobile}`, `phone` is an
102
+ * UNKNOWN FIELD (400 `UNKNOWN_IDENTIFIER_FIELD`, which lists the allowed
103
+ * fields) rather than a spelling of `mobile`.
104
+ *
105
+ * { email: 'riya@example.in' }
106
+ * { customer_id: 'CUST-90210', mobile: '+919876500000' }
107
+ */
108
+ export type DataPrincipal = Record<string, string>;
109
+ /**
110
+ * `age` — the ONE age signal, and it is a date.
111
+ *
112
+ * `is_minor`, `is_declared_adult` and `self_declared_adult` are deleted from
113
+ * this request, because a flag cannot graduate anyone at eighteen: a person
114
+ * marked `is_minor` in March is still marked one the following March, while a
115
+ * date of birth answers the question again every time it is asked. The date is
116
+ * SERVER-AUTHORITATIVE — the platform derives the age and is never told it
117
+ * (`core/dpintake/dpintake.go:92-94`).
118
+ */
119
+ export interface Age {
120
+ /**
121
+ * `YYYY-MM-DD` and nothing else. Any other format, a future date, or an
122
+ * implausible one is refused 400 `INVALID_DATE_OF_BIRTH`.
123
+ *
124
+ * SEND IT. With no date the person's age is UNKNOWN — the session is still
125
+ * created and the response carries a warning, because the consequence is
126
+ * otherwise invisible: purposes restricted for children are refused, and if
127
+ * this person IS a child no parental consent can be sought.
128
+ *
129
+ * If the date IS a child's, the request must also carry a guardian channel
130
+ * (`guardian_email` or `guardian_phone`) or it is refused 412
131
+ * `GUARDIAN_REQUIRED`.
132
+ */
133
+ date_of_birth?: string;
134
+ }
135
+ /**
136
+ * Session-create request — POST /api/v1/public/consent/sessions
137
+ *
138
+ * ─── U58 (IK-WIRE): THIS IS A PUBLIC WIRE BREAK ───────────────────────────
139
+ *
140
+ * The identity, age and language halves of this body are the platform's
141
+ * `core/dpintake.Object`, EMBEDDED in the request struct
142
+ * (`consent/collection.go:152`), so `data_principal`, `data_principal_id`,
143
+ * `age` and `language` decode at the TOP LEVEL. The same four fields identify
144
+ * a person on all four consent-collection roads — the hosted page, the kiosk,
145
+ * QR identify and this endpoint.
146
+ *
147
+ * WHAT WENT, AND WHY:
148
+ *
149
+ * data_principal_ref, data_principal_ref_type
150
+ * -> data_principal. THE TYPE COMES FROM THE FIELD NAME.
151
+ * data_principal_details.{email,phone,customer_id,aadhaar,pan}
152
+ * -> deleted. A second, unchecked identifier door: these became refs
153
+ * ALONGSIDE data_principal_ref, judged against no scheme.
154
+ * data_principal_details.name -> read by nothing.
155
+ * data_principal_details.guardian_ref -> names a DIFFERENT person; the
156
+ * guardian channel below replaces it.
157
+ * is_minor, is_declared_adult -> age.date_of_birth.
158
+ * locale_pref, notice_language, template_language -> language.
159
+ * purpose_ids -> deleted. The NOTICE decides the
160
+ * purposes (DPDP §5); the field bought only a fail-fast 400 on ids
161
+ * that were never going to be used.
162
+ *
163
+ * THERE IS NO OVERLAP WINDOW. The handler decodes with a plain `json.Decoder`
164
+ * and NO `DisallowUnknownFields` on both wires, so a body in the other shape is
165
+ * not refused for being the other shape — its identifiers are dropped without a
166
+ * word and the request is then refused for carrying none. This SDK version
167
+ * talks to an API carrying U58 and to no other.
168
+ *
169
+ * IDENTITY IS REQUIRED AND THE TYPE SAYS SO. Send `data_principal` (the
170
+ * identifiers) or `data_principal_id` (the platform's own uuid for the person,
171
+ * returned by every session create), or both — sent together they must agree,
172
+ * or the call is refused 409 `IDENTITY_MISMATCH`. A request carrying neither
173
+ * falls to the key's floor (400 `IDENTIFIER_REQUIRED`), so the union below
174
+ * makes that a COMPILE error instead of a round trip.
175
+ */
176
+ export type CreateSessionRequest = CreateSessionRequestFields & ({
177
+ data_principal: DataPrincipal;
178
+ data_principal_id?: string;
179
+ } | {
180
+ data_principal?: DataPrincipal;
181
+ data_principal_id: string;
182
+ });
183
+ /** The non-identity half of {@link CreateSessionRequest}. */
184
+ export interface CreateSessionRequestFields {
185
+ /**
186
+ * The person's identifiers, by the field names of this organisation's locked
187
+ * integration key. See {@link DataPrincipal}.
188
+ *
189
+ * A field outside the key is refused 400 `UNKNOWN_IDENTIFIER_FIELD` and the
190
+ * message LISTS the allowed fields, because an integrator cannot read the key
191
+ * from outside. Too few of them is 400 `IDENTIFIER_REQUIRED`, whose message
192
+ * names the fields and never the mode.
193
+ */
194
+ data_principal?: DataPrincipal;
195
+ /** The platform's own uuid for the person, from any previous CreateSessionResponse. */
196
+ data_principal_id?: string;
197
+ /** The ONE age signal. See {@link Age}. */
198
+ age?: Age;
199
+ /**
200
+ * Preferred language for the consent page, as an ISO code. It goes to the
201
+ * FRONT of the locale fallback chain, AHEAD of the tenant's own default — so
202
+ * send it only when a language was actually asked for. A language the
203
+ * platform does not serve is refused 400 `UNSUPPORTED_LANGUAGE`, and that
204
+ * message names the list.
205
+ *
206
+ * ONE FIELD. It replaces `locale_pref`, `notice_language` and
207
+ * `template_language` together.
208
+ */
209
+ language?: string;
210
+ session_ref?: string;
211
+ notice_internal_name?: string;
212
+ /**
213
+ * Bind exactly this version NUMBER of the notice code, which must be active;
214
+ * omit to bind the code's default version.
215
+ *
216
+ * This field was `notice_version_id` (a UUID string) and the API has never
217
+ * had such a key: it decoded into nothing and was silently dropped, so every
218
+ * caller that set it silently got the default version and no error.
219
+ */
220
+ notice_version_number?: number;
221
+ ui_mode?: 'redirect' | 'embedded_popup' | 'embedded_page' | 'api_only';
222
+ /** Absolute return URL. createSession adds its own `state` to it; `state` is reserved. */
223
+ callback_url?: string;
224
+ /** HMAC-signed token from a campaign SMS URL, linking this session to an existing customer record. */
225
+ customer_token?: string;
226
+ /** Where the guardian's verification request is sent. Not with `guardian_phone`. */
227
+ guardian_email?: string;
228
+ /** Where the guardian's verification request is sent. Not with `guardian_email`. */
229
+ guardian_phone?: string;
230
+ /**
231
+ * What the child SAYS the guardian is to them ('mother', 'father', 'legal
232
+ * guardian', …). Recorded as a CLAIM on the pending link and confirmed or
233
+ * corrected by the guardian themselves on the verification road; nothing
234
+ * downstream treats it as proven.
235
+ *
236
+ * There is no `guardian_name`: a name is a claim about a second person made
237
+ * by somebody else, held in the child's own record, with no way to reach them
238
+ * and nobody able to correct it.
239
+ */
240
+ guardian_relationship?: string;
241
+ }
242
+ /**
243
+ * Session-create response.
244
+ *
245
+ * THE FIELD NAMES ARE NOT UNIFORM AND THAT IS THE WIRE, not a transcription
246
+ * error here: `expiresAt`, `languageCode` and `challengeNonce` are camelCase on
247
+ * the wire and the rest are snake_case. They are pinned that way in the API's
248
+ * own struct as an entrenched contract
249
+ * (`consent/collection.go:238-260`, `canonical-field:waive`).
250
+ */
251
+ export interface CreateSessionResponse {
252
+ consent_session_id: string;
253
+ /** Always returned, so a caller can store it and send it next time. */
254
+ data_principal_id: string;
255
+ consent_url: string;
256
+ /** camelCase on the wire. */
257
+ expiresAt: string;
258
+ notice_version_id: string;
259
+ notice_hash: string;
260
+ /** The language the session actually resolved to. camelCase on the wire. */
261
+ languageCode: string;
262
+ /** camelCase on the wire. */
263
+ challengeNonce: string;
264
+ ui_schema_version: string;
265
+ /**
266
+ * Consequences that did not refuse the request — an unknown age, for one.
267
+ * Read them: the effect is otherwise invisible.
268
+ */
269
+ warnings?: string[];
270
+ /**
271
+ * Present when this session is a child's and an invitation went out to the
272
+ * guardian channel this request carried. The consent is NOT recorded until
273
+ * that guardian verifies.
274
+ */
275
+ guardian_verification?: GuardianVerificationPending;
276
+ }
277
+ /**
278
+ * The guardian invitation raised by a session create that carried a channel.
279
+ *
280
+ * IT CARRIES NO LINK AND NO TOKEN, deliberately. The verification URL is a
281
+ * bearer credential — address-bound, 72 hours, single use — and it goes to the
282
+ * guardian's own address. Returning it on this response would put it in the
283
+ * organisation's logs, and the organisation is not the party the link is for.
284
+ */
285
+ export interface GuardianVerificationPending {
286
+ /** `'pending'`: the guardianship exists and nothing about it is proven yet. */
287
+ status: string;
288
+ /**
289
+ * Which channel the invitation went out on — `'email'` or `'sms'`. Never the
290
+ * address, which the organisation already has.
291
+ */
292
+ channel: string;
293
+ /** Names the guardianship, so it can be followed on the guardian APIs. */
294
+ link_id: string;
295
+ }
296
+ export interface SessionRenderResponse {
297
+ session_id: string;
298
+ notice: NoticeContent;
299
+ purposes: RenderPurpose[];
300
+ notice_details: NoticeDetails;
301
+ branding: BrandingConfig;
302
+ }
303
+ export interface NoticeContent {
304
+ title: string;
305
+ description: string;
306
+ version: string;
307
+ }
308
+ export interface RenderPurpose {
309
+ id: string;
310
+ code: string;
311
+ name: string;
312
+ description: string;
313
+ mandatory: boolean;
314
+ default_status: string;
315
+ sort_order: number;
316
+ legal_basis?: string;
317
+ retention_period?: string;
318
+ }
319
+ export interface NoticeDetails {
320
+ dpo_name?: string;
321
+ dpo_email?: string;
322
+ dpo_phone?: string;
323
+ grievance_url?: string;
324
+ privacy_policy_url?: string;
325
+ }
326
+ export interface BrandingConfig {
327
+ logo_url?: string;
328
+ primary_color?: string;
329
+ secondary_color?: string;
330
+ font_family?: string;
331
+ }
332
+ /**
333
+ * GET /v1/consent/artifacts/{artifact_id}.
334
+ *
335
+ * TRANSCRIBED FROM ArtifactDetailsResponse
336
+ * (consent/collection.go:4270-4278). It used to declare `data_principal_ref`,
337
+ * `notice_version_id`, `status` and `updated_at` — the API sends NONE of those
338
+ * four, so each read back `undefined` and nothing said so. `artifact_type` and
339
+ * `data_principal_id`, which it DOES send, were missing.
340
+ */
341
+ export interface ConsentArtifact {
342
+ artifact_id: string;
343
+ artifact_version: number;
344
+ artifact_type: string;
345
+ /** The platform's own uuid for the person. The artifact names no identifier. */
346
+ data_principal_id: string;
347
+ purposes: ArtifactPurpose[];
348
+ created_at: string;
349
+ }
350
+ /**
351
+ * The 202 body of GET /v1/consent/artifacts/{artifact_id}?session_id=… — the
352
+ * consent WAS recorded and acknowledged, and its artifact is still being
353
+ * written (walk finding F018).
354
+ *
355
+ * Transcribed from ArtifactPendingResponse
356
+ * (consent/collection.go:4373-4400 at 4fda7e3d05). A DIFFERENT SHAPE FROM
357
+ * {@link ConsentArtifact} on purpose: a pending notice read as an artifact
358
+ * would be an artifact with no purposes, which looks like "consented to
359
+ * nothing". The status CODE is the platform's contract; `status: 'pending'`
360
+ * is the stable body discriminant beside it.
361
+ */
362
+ export interface ArtifactPending {
363
+ status: 'pending';
364
+ /** The id that WILL carry this consent — the one the callback named. */
365
+ artifact_id: string;
366
+ /** The session the read presented, echoed. */
367
+ session_id: string;
368
+ /**
369
+ * Diagnostic, not the decision: `projection_pending`, `projection_committing`,
370
+ * or `parked_<reason>` when an operator owes the projection.
371
+ */
372
+ reason: string;
373
+ /**
374
+ * Seconds, as on the Retry-After header (1 while the worker owes it, 60 when
375
+ * parked). The wire name is `retry_after`.
376
+ */
377
+ retry_after: number;
378
+ /** Human-readable, and not a contract. */
379
+ message: string;
380
+ }
381
+ /**
382
+ * What an artifact read can answer, as a union the compiler makes you branch
383
+ * on. A 404 is not in it: that is a thrown ConsenteraNotFoundError, and with a
384
+ * `session_id` it means the id was never issued for that session.
385
+ */
386
+ export type ArtifactReadResult = {
387
+ state: 'recorded';
388
+ artifact: ConsentArtifact;
389
+ requestId?: string;
390
+ } | {
391
+ state: 'pending';
392
+ pending: ArtifactPending;
393
+ /** When to read again, from Retry-After (or `retry_after`), in ms. */
394
+ retryAfterMs: number;
395
+ requestId?: string;
396
+ };
397
+ export interface ArtifactPurpose {
398
+ purpose_id: string;
399
+ purpose_code: string;
400
+ purpose_name?: string;
401
+ status: ConsentStatus;
402
+ effective_at?: string;
403
+ expires_at?: string;
404
+ legal_basis?: string;
405
+ }
406
+ export type ConsentStatus = 'granted' | 'denied' | 'withdrawn' | 'not_set' | 'expired';
407
+ /**
408
+ * `data_principal_identifiers` — how every consent LIFECYCLE road names a
409
+ * person.
410
+ *
411
+ * ─── IT IS THE SAME SHAPE AS `data_principal` ON SESSION CREATE (F015) ─────
412
+ *
413
+ * An OPEN map keyed by THE TENANT'S OWN LOCKED INTEGRATION KEY — the same
414
+ * `core/dpintake.DataPrincipal` create takes, validated by the same
415
+ * `dpintake.Validate` (consent/update_context.go:104-119). An earlier turn of
416
+ * this SDK typed it as a CLOSED five-field object; F015
417
+ * (consent/one-identifier-vocabulary-20260922) deleted that struct, because a
418
+ * closed struct has three faults a per-tenant vocabulary cannot carry:
419
+ *
420
+ * * ONE VOCABULARY NOW, ONE SPELLING. The mobile atom is `mobile` on BOTH
421
+ * roads. It used to be `phone` here and `mobile` on create, folded
422
+ * server-side; F015 removed the fold, so `phone` is refused BY NAME
423
+ * (400 UNKNOWN_IDENTIFIER_FIELD, "…This platform spells that identifier
424
+ * 'mobile': use mobile"). Do NOT send `phone`.
425
+ * * THE ADMISSIBLE SET IS PER-TENANT, so this SDK cannot know it and must
426
+ * NOT allow-list. A tenant keyed on `{customer_id}` names people by
427
+ * `customer_id`; one keyed on `{email, mobile}` by those. Send the fields
428
+ * the organisation locked; the SERVER answers UNKNOWN_IDENTIFIER_FIELD,
429
+ * naming the field and listing the key\'s actual fields, when you get it
430
+ * wrong. A closed type here would reject a legal key at compile time.
431
+ * * THERE IS NO `pan` KEY. `pan` is an evidence-class identifier and can
432
+ * never be a scheme field (tenant_identifier_scheme_types_chk, 00697), so
433
+ * it is not an example and not a member of any allow-list.
434
+ *
435
+ * ─── THE KEY DIFFERS FROM CREATE; THE VALUE DOES NOT ──────────────────────
436
+ *
437
+ * Create spells the object `data_principal`; these roads keep
438
+ * `data_principal_identifiers`. Only the wire KEY differs — the value is the
439
+ * same open scheme-keyed map. The two answer different questions:
440
+ * `data_principal` on create MAY MINT a person, while this one is resolve-only
441
+ * and never creates anybody (lifecycle_identity.go). Unifying the key would be
442
+ * a wire break for every live integration.
443
+ *
444
+ * ANY ONE FIELD IS ENOUGH. A raw 12-digit `aadhaar` value is refused
445
+ * 400 INVALID_IDENTIFIER_FORMAT (F015 folded the old AADHAAR_RAW_REFUSED into
446
+ * the one format refusal create already answers); send the Aadhaar-LINKED
447
+ * token, never the number.
448
+ *
449
+ * THE REFUSALS A CALLER MEETS, all from the ONE validator create shares:
450
+ * - `UNKNOWN_IDENTIFIER_FIELD` 400 a field outside the tenant's key, by
451
+ * name, with the atom to use for a
452
+ * vernacular spelling ("phone" -> mobile)
453
+ * - `IDENTIFIER_REQUIRED` 400 nothing named a person
454
+ * - `INVALID_IDENTIFIER_FORMAT` 400 a value that cannot be an identifier of
455
+ * its type (raw Aadhaar lives here now)
456
+ * - `SCHEME_NOT_CONFIGURED` the organisation has not locked how it
457
+ * identifies people, so no field means
458
+ * anything yet
459
+ */
460
+ export type DataPrincipalIdentifiers = Record<string, string>;
461
+ /**
462
+ * POST /v1/consent/validate.
463
+ *
464
+ * ─── data_principal_ref IS REFUSED OUTRIGHT ──────────────────────────────
465
+ *
466
+ * Owner ruling, 2026-09-21: no transition period and no deprecation window
467
+ * (consent/lifecycle_identity.go:8-11). A request carrying it is answered
468
+ * 400 VALIDATION_ERROR whose message begins `DATA_PRINCIPAL_REF_REFUSED`
469
+ * (the one machine-readable token, lifecycle_identity.go:68) — and the refusal
470
+ * fires EVEN IF `data_principal_id` is also present, because two fields naming
471
+ * a person can disagree and the caller would never learn which one the answer
472
+ * was about.
473
+ *
474
+ * WHAT WAS WRONG WITH IT, since it is easy to get backwards: the old field was
475
+ * ONE OPAQUE UNTYPED HANDLE. An untyped value with no locked scheme to narrow
476
+ * it is fanned across every probe order, and under a two-identifier scheme a
477
+ * bare 12-digit value is undecidable between `aadhaar` and `customer_id` — so
478
+ * the spine refused it rather than guess, because guessing wrong writes a
479
+ * person split in two. This is OPAQUE HANDLE -> TYPED IDENTIFIERS.
480
+ *
481
+ * Name the person with `data_principal_id`, or with
482
+ * `data_principal_identifiers`; the union below makes sending neither a
483
+ * COMPILE error instead of a round trip.
484
+ */
485
+ export type ValidateRequest = ValidateRequestFields & ({
486
+ data_principal_id: string;
487
+ data_principal_identifiers?: DataPrincipalIdentifiers;
488
+ } | {
489
+ data_principal_id?: string;
490
+ data_principal_identifiers: DataPrincipalIdentifiers;
491
+ });
492
+ /** The non-identity half of {@link ValidateRequest}. */
493
+ export interface ValidateRequestFields {
494
+ data_principal_id?: string;
495
+ data_principal_identifiers?: DataPrincipalIdentifiers;
496
+ purpose_code?: string;
497
+ purpose_id?: string;
498
+ /** Optional: validate against the purpose's allowed_channels. */
499
+ channel?: string;
500
+ }
501
+ /**
502
+ * POST /v1/consent/validate — the platform's answer, field for field.
503
+ *
504
+ * TRANSCRIBED FROM ValidateResponse (consent/validate.go:142-165 at
505
+ * 4fda7e3d05) and checked against a live answer from setup.consentera.in
506
+ * (fixtures/platform-wire/validate.200.allow.json). It used to lack
507
+ * `data_principal_id`, the field the platform returns so a caller can drop
508
+ * the identifiers on the next call, and which the runbook's validate step
509
+ * reads (SDK register WEB-037). It also declared an `artifact_id` that the
510
+ * platform never sends. That field is gone.
511
+ */
512
+ export interface ValidateResponse {
513
+ decision: 'ALLOW' | 'DENY';
514
+ reason_code: ConsentReasonCode;
515
+ /** The platform's uuid for the person asked about. Hold it; later calls need no identifier. */
516
+ data_principal_id: string;
517
+ purpose_id: string;
518
+ /** Omitted by the platform when the purpose has no code. */
519
+ purpose_code?: string;
520
+ effective_at?: string;
521
+ expires_at?: string;
522
+ /** Server time of the decision. The request's own timestamp is ignored. */
523
+ validated_at: string;
524
+ /** DPDP §4 basis: `consent`, `legitimate_use`, … */
525
+ legal_basis?: string;
526
+ is_mandatory?: boolean;
527
+ /** The §7 / §17 limb invoked when decision is ALLOW and legal_basis is not consent. */
528
+ dpdp_exemption_basis?: string;
529
+ /** Human-readable, and not a contract. */
530
+ message?: string;
531
+ }
532
+ /**
533
+ * `reason_code` on a validate response — the DPDP reason the decision came out
534
+ * the way it did. Transcribed from the Reason* constants in
535
+ * consentera-api/internal/modules/consent/consent/validate.go.
536
+ *
537
+ * NOT A CLOSED UNION, and the `string` arm is not laziness: validate.go mints
538
+ * `"CONSENT_" + strings.ToUpper(currentStatus)` for any consent status it has
539
+ * no named constant for, so the set genuinely cannot be enumerated ahead of
540
+ * time. A closed union would make a legitimate response a type error.
541
+ *
542
+ * TWO MEMBERS THIS UNION USED TO CARRY WERE NOT REASON CODES.
543
+ * `PURPOSE_MANDATORY` appears nowhere in the platform — not in a Go file, not
544
+ * in a doc — and the code it was presumably meant to be is `PURPOSE_INACTIVE`.
545
+ * `CONSENT_NOT_FOUND` is real but belongs to a DIFFERENT vocabulary: it is an
546
+ * HTTP error code (apierrors/errors.go, a 404 on a receipt or a withdrawal),
547
+ * never a validate reason. The reason code for "no consent record exists" is
548
+ * `NO_CONSENT_FOUND`, which this union did not have — so the one outcome an
549
+ * integrator most needs to branch on was the one it could not name.
550
+ */
551
+ export type ConsentReasonCode = 'CONSENT_GRANTED' | 'LEGITIMATE_USE' | 'LEGAL_OBLIGATION' | 'MEDICAL_EMERGENCY' | 'EMPLOYMENT_PURPOSE' | 'PUBLIC_INTEREST' | 'NO_CONSENT_FOUND' | 'CONSENT_DENIED' | 'CONSENT_WITHDRAWN' | 'CONSENT_EXPIRED' | 'CONSENT_NOT_EFFECTIVE' | 'CONSENT_PENDING_CONFIRMATION' | 'DATA_PRINCIPAL_NOT_FOUND' | 'PURPOSE_INACTIVE' | 'MINOR_NO_GUARDIAN' | 'CHANNEL_NOT_ALLOWED' | 'LU_CONTRACT_LAPSED' | (string & {});
552
+ /**
553
+ * POST /v1/consent/validate/bulk.
554
+ *
555
+ * `data_principal_ref` AND `data_principal_refs` are both refused, with the
556
+ * same `DATA_PRINCIPAL_REF_REFUSED` prefix (consent/validate.go:249,257).
557
+ * One person by `data_principal_id` or `data_principal_identifiers`; many by
558
+ * `data_principal_ids` or `data_principal_identifiers_list`.
559
+ */
560
+ export type BulkValidateRequest = BulkValidateRequestFields & ({
561
+ data_principal_id: string;
562
+ } | {
563
+ data_principal_identifiers: DataPrincipalIdentifiers;
564
+ } | {
565
+ data_principal_ids: string[];
566
+ } | {
567
+ data_principal_identifiers_list: DataPrincipalIdentifiers[];
568
+ });
569
+ /** The non-identity half of {@link BulkValidateRequest}. */
570
+ export interface BulkValidateRequestFields {
571
+ /** One person. */
572
+ data_principal_id?: string;
573
+ data_principal_identifiers?: DataPrincipalIdentifiers;
574
+ /** Many people, one per element. */
575
+ data_principal_ids?: string[];
576
+ data_principal_identifiers_list?: DataPrincipalIdentifiers[];
577
+ purpose_codes?: string[];
578
+ purpose_code?: string;
579
+ purpose_id?: string;
580
+ }
581
+ /** BulkValidateResponse (consent/validate.go:290-295). */
582
+ export interface BulkValidateResponse {
583
+ results: ValidateResponse[];
584
+ total_count: number;
585
+ allow_count: number;
586
+ deny_count: number;
587
+ }
588
+ export interface UpdateContextResponse {
589
+ data_principal_id: string;
590
+ consents: ConsentContextItem[];
591
+ notice_version_id: string;
592
+ notice_hash: string;
593
+ language_code: string;
594
+ }
595
+ export interface ConsentContextItem {
596
+ purpose_id: string;
597
+ purpose_code?: string;
598
+ purpose_name: string;
599
+ current_status: ConsentStatus;
600
+ mandatory: boolean;
601
+ withdrawable: boolean;
602
+ last_updated_at?: string;
603
+ artifact_id?: string;
604
+ artifact_version?: number;
605
+ }
606
+ export interface UpdateConsentRequest {
607
+ data_principal_id: string;
608
+ notice_version_id: string;
609
+ notice_hash: string;
610
+ language_code?: string;
611
+ updates: ConsentUpdate[];
612
+ affirmative_action: AffirmativeAction;
613
+ client_context?: ClientContext;
614
+ }
615
+ export interface ConsentUpdate {
616
+ purpose_id: string;
617
+ new_status: 'granted' | 'denied';
618
+ }
619
+ export interface WithdrawalContextResponse {
620
+ data_principal_id: string;
621
+ consents: ConsentContextItem[];
622
+ notice_version_id: string;
623
+ notice_hash: string;
624
+ }
625
+ export interface WithdrawRequest {
626
+ data_principal_id: string;
627
+ purposes: string[];
628
+ reason?: string;
629
+ affirmative_action: AffirmativeAction;
630
+ client_context?: ClientContext;
631
+ }
632
+ export interface BulkWithdrawRequest {
633
+ data_principal_id: string;
634
+ purposes: string[];
635
+ reason?: string;
636
+ affirmative_action: AffirmativeAction;
637
+ client_context?: ClientContext;
638
+ }
639
+ export interface RenewalContextResponse {
640
+ data_principal_id: string;
641
+ expiring_consents: ExpiringConsent[];
642
+ notice_version_id: string;
643
+ notice_hash: string;
644
+ }
645
+ export interface ExpiringConsent {
646
+ purpose_id: string;
647
+ purpose_name: string;
648
+ current_status: ConsentStatus;
649
+ expires_at: string;
650
+ days_remaining: number;
651
+ renewable: boolean;
652
+ }
653
+ export interface RenewRequest {
654
+ data_principal_id: string;
655
+ purpose_ids: string[];
656
+ notice_hash: string;
657
+ /** Client observation time; the server owns the recorded time. */
658
+ captured_at?: string;
659
+ client_context?: ClientContext;
660
+ }
661
+ /** The canonical renewal evidence returned by the existing renewal roads. */
662
+ export interface RenewedConsentInfo {
663
+ purpose_id: string;
664
+ previous_expiry?: string;
665
+ new_expiry?: string;
666
+ }
667
+ export interface RenewResponse {
668
+ /** Present when durable acceptance is awaiting the canonical stored artifact. */
669
+ status?: 'pending';
670
+ /** Server guidance for the existing bound artifact GET; never repeat the POST. */
671
+ retry_after_seconds?: number;
672
+ artifact_id: string;
673
+ artifact_version: number;
674
+ data_principal_id: string;
675
+ renewed_consents: RenewedConsentInfo[];
676
+ /** Existing artifact-read binding; retain it when the POST is accepted with 202. */
677
+ session_id?: string;
678
+ /** Only present when the server returned the canonical stored artifact hash. */
679
+ integrity_hash?: string;
680
+ }
681
+ export interface BulkRenewResponse extends RenewResponse {
682
+ success_count: number;
683
+ failure_count: number;
684
+ failed_renewals?: Array<{
685
+ purpose_id: string;
686
+ error: string;
687
+ }>;
688
+ }
689
+ export interface BulkRenewRequest {
690
+ data_principal_id: string;
691
+ renewals: Array<{
692
+ purpose_id: string;
693
+ new_expiry_days?: number;
694
+ }>;
695
+ notice_hash: string;
696
+ /** Client observation time; the server owns the recorded time. */
697
+ captured_at?: string;
698
+ client_context?: ClientContext;
699
+ }
700
+ export interface AffirmativeAction {
701
+ type: 'button' | 'toggle' | 'checkbox';
702
+ ui_event_id: string;
703
+ captured_at: string;
704
+ }
705
+ export interface ClientContext {
706
+ ip?: string;
707
+ user_agent?: string;
708
+ platform: 'web' | 'mobile' | 'api';
709
+ device_type?: 'desktop' | 'mobile' | 'tablet';
710
+ screen_resolution?: string;
711
+ timezone?: string;
712
+ browser_name?: string;
713
+ browser_version?: string;
714
+ os_name?: string;
715
+ os_version?: string;
716
+ session_id?: string;
717
+ page_url?: string;
718
+ referrer?: string;
719
+ }
720
+ export interface DFConfig {
721
+ tenant_id: string;
722
+ tenant_name: string;
723
+ purposes: DFPurpose[];
724
+ notices: DFNotice[];
725
+ regulations: string[];
726
+ languages: string[];
727
+ branding: BrandingConfig;
728
+ }
729
+ export interface DFPurpose {
730
+ id: string;
731
+ code: string;
732
+ name: string;
733
+ description: string;
734
+ category: string;
735
+ mandatory: boolean;
736
+ default_status: string;
737
+ retention_period?: string;
738
+ legal_basis?: string;
739
+ sort_order: number;
740
+ }
741
+ export interface DFNotice {
742
+ id: string;
743
+ internal_name: string;
744
+ title: string;
745
+ version: string;
746
+ status: string;
747
+ }
748
+ export interface NoticePurposesResponse {
749
+ notice_id: string;
750
+ notice_internal_name: string;
751
+ notice_title: string;
752
+ notice_version: string;
753
+ purposes: NoticePurpose[];
754
+ }
755
+ export interface NoticePurpose {
756
+ id: string;
757
+ code: string;
758
+ name: string;
759
+ description: string;
760
+ mandatory: boolean;
761
+ sort_order: number;
762
+ legal_basis?: string;
763
+ retention_period?: string;
764
+ data_categories?: string[];
765
+ }
766
+ /**
767
+ * POST /v1/df/principal-token.
768
+ *
769
+ * THIS ROAD STILL TAKES data_principal_ref, AND THAT IS CORRECT — do not
770
+ * "fix" it. It lives in identity/dfclient (handlers.go:1418-1428), not in the
771
+ * consent module, so the 2026-09-21 ruling that refuses the field across the
772
+ * consent surface does not reach it: it still accepts data_principal_id OR
773
+ * data_principal_ref and requires one of the two.
774
+ */
775
+ export interface PrincipalTokenRequest {
776
+ data_principal_ref: string;
777
+ redirect_path?: string;
778
+ }
779
+ export interface PrincipalTokenResponse {
780
+ token: string;
781
+ redirect_url: string;
782
+ expires_in: number;
783
+ }
784
+ export interface WithdrawalAnalytics {
785
+ total_withdrawals: number;
786
+ by_purpose: Record<string, number>;
787
+ by_period: Record<string, number>;
788
+ reasons: WithdrawalReason[];
789
+ }
790
+ export interface WithdrawalReason {
791
+ reason: string;
792
+ count: number;
793
+ percentage: number;
794
+ }
795
+ export interface ApiResponse<T> {
796
+ data?: T;
797
+ error?: ApiError;
798
+ }
799
+ export interface ApiError {
800
+ code: string;
801
+ message: string;
802
+ details?: Record<string, unknown>;
803
+ }
804
+ export type ConsentLifecycleEvent = 'session:created' | 'session:completed' | 'session:expired' | 'consent:validated' | 'consent:updated' | 'consent:withdrawn' | 'consent:renewed' | 'config:loaded' | 'principal:redirected' | 'error';