@gradial/aci 0.1.28 → 0.1.30

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 (73) hide show
  1. package/README.md +47 -0
  2. package/bin/aci-darwin-arm64 +0 -0
  3. package/dist/astro/index.d.ts +1 -0
  4. package/dist/astro/index.js +7 -0
  5. package/dist/collect/api.d.ts +24 -0
  6. package/dist/collect/api.js +63 -0
  7. package/dist/collect/batcher.d.ts +65 -0
  8. package/dist/collect/batcher.js +127 -0
  9. package/dist/collect/config.d.ts +30 -0
  10. package/dist/collect/config.js +53 -0
  11. package/dist/collect/consent/adapters/c15t.d.ts +2 -0
  12. package/dist/collect/consent/adapters/c15t.js +43 -0
  13. package/dist/collect/consent/adapters/cookiebot.d.ts +2 -0
  14. package/dist/collect/consent/adapters/cookiebot.js +48 -0
  15. package/dist/collect/consent/adapters/onetrust.d.ts +2 -0
  16. package/dist/collect/consent/adapters/onetrust.js +54 -0
  17. package/dist/collect/consent/adapters/trustarc.d.ts +2 -0
  18. package/dist/collect/consent/adapters/trustarc.js +65 -0
  19. package/dist/collect/consent/envelope.d.ts +49 -0
  20. package/dist/collect/consent/envelope.js +100 -0
  21. package/dist/collect/consent/mapping.d.ts +62 -0
  22. package/dist/collect/consent/mapping.js +129 -0
  23. package/dist/collect/declarative.d.ts +35 -0
  24. package/dist/collect/declarative.js +100 -0
  25. package/dist/collect/gapbuffer.d.ts +55 -0
  26. package/dist/collect/gapbuffer.js +134 -0
  27. package/dist/collect/index.d.ts +6 -0
  28. package/dist/collect/index.js +154 -0
  29. package/dist/collect/linkclicks.d.ts +12 -0
  30. package/dist/collect/linkclicks.js +35 -0
  31. package/dist/collect/pageviews.d.ts +22 -0
  32. package/dist/collect/pageviews.js +42 -0
  33. package/dist/collect/transport.d.ts +22 -0
  34. package/dist/collect/transport.js +52 -0
  35. package/dist/collect/ulid.d.ts +6 -0
  36. package/dist/collect/ulid.js +36 -0
  37. package/dist/collect/variationviews.d.ts +31 -0
  38. package/dist/collect/variationviews.js +80 -0
  39. package/dist/collect/webvitals.d.ts +38 -0
  40. package/dist/collect/webvitals.js +99 -0
  41. package/dist/content/index.d.ts +1 -0
  42. package/dist/content/index.js +1 -0
  43. package/dist/content/personalization.d.ts +57 -0
  44. package/dist/content/personalization.js +54 -0
  45. package/dist/content/types.d.ts +3 -0
  46. package/dist/next/collect-script.d.ts +12 -0
  47. package/dist/next/collect-script.js +15 -0
  48. package/dist/next/config.js +1 -0
  49. package/dist/next/index.d.ts +1 -0
  50. package/dist/next/index.js +1 -0
  51. package/dist/next/page.d.ts +2 -0
  52. package/dist/next/page.js +5 -2
  53. package/dist/providers/file.js +1 -8
  54. package/dist/react/VideoPlayer.d.ts +3 -3
  55. package/dist/react/index.d.ts +4 -4
  56. package/dist/react/index.js +1 -1
  57. package/dist/types/image.d.ts +27 -13
  58. package/dist/types/image.js +17 -17
  59. package/dist/types/media.d.ts +10 -4
  60. package/dist/types/video.d.ts +5 -2
  61. package/package.json +8 -9
  62. package/src/cli/validate-content.mjs +0 -0
  63. package/src/types/image.ts +32 -29
  64. package/templates/astro/template/src/fixtures/sections.ts +1 -1
  65. package/templates/astro/template/src/layouts/SiteLayout.astro +77 -1
  66. package/templates/astro/template/src/pages/[...slug].astro +3 -2
  67. package/templates/common/.content/config/site.json +5 -0
  68. package/templates/common/.content/schemas/site.schema.json +15 -0
  69. package/templates/common/README.md.tmpl +15 -0
  70. package/templates/nextjs/template/package.json.tmpl +2 -0
  71. package/templates/nextjs/template/src/app/_components/ConsentManager.tsx +132 -0
  72. package/templates/nextjs/template/src/app/layout.tsx +3 -1
  73. package/templates/nextjs/template/src/components/sections/HomeHero.tsx +1 -1
package/README.md CHANGED
@@ -94,6 +94,53 @@ List available templates:
94
94
  npx @gradial/aci@0.1.27 init --list
95
95
  ```
96
96
 
97
+ ## Personalization: The One-Tag Install
98
+
99
+ Personalization collection is one script tag. Externally rendered sites paste
100
+ it once, verbatim; Gradial-rendered sites (Next `createPage`, the Astro
101
+ starter layout) emit the same tag automatically and never hand-write it:
102
+
103
+ ```html
104
+ <script defer src="/_aci/aci.js" data-aci-consent="c15t"></script>
105
+ ```
106
+
107
+ **Boundary (v1):** the site MUST be served through a Gradial edge binding
108
+ (CloudFront, Vercel, or the local adapter). The edge serves the SDK at the
109
+ stable `/_aci/aci.js` URL, provides `/collect`, and mints visitor identity —
110
+ none of which exist on fully external hosting, which is out of scope pending
111
+ OQ-6 (REQ-13).
112
+
113
+ Tag attributes (the tag is the only per-site configuration surface):
114
+
115
+ | Attribute | Meaning |
116
+ | --- | --- |
117
+ | `data-aci-consent` | CMP preset: `c15t`, `onetrust`, `cookiebot`, `trustarc`, or `manual` (default `manual` — only the JS consent API `aci('consent', …)` writes envelopes) |
118
+ | `data-aci-consent-categories` | Flat comma list of CMP categories that imply the personalization purpose (e.g. `measurement,marketing`); each preset ships a sane default |
119
+ | `data-aci-site` | Optional site ID, debug only — the edge binding is the site authority |
120
+ | `data-aci-debug` | `1` enables structured `console.debug` diagnostics |
121
+
122
+ For Gradial-rendered sites the tag is driven by an optional block on the
123
+ site config doc (`/config/site.json`); absent block = disabled, no tag:
124
+
125
+ ```json
126
+ {
127
+ "personalization": {
128
+ "collect": true,
129
+ "consentPreset": "c15t",
130
+ "consentCategories": "measurement,marketing"
131
+ }
132
+ }
133
+ ```
134
+
135
+ The starter kits default to the `c15t` preset. The Next kit ships a working
136
+ c15t setup (`ConsentManager` in `src/app/_components/`, offline mode by
137
+ default, hosted mode via `NEXT_PUBLIC_C15T_BACKEND_URL`) plus the bridge
138
+ that mirrors the c15t store onto `window.c15t`, where the SDK reads it. c15t
139
+ has no Astro UI package, so the Astro kit ships the bridge only (in
140
+ `SiteLayout.astro`): add a banner via a React island (`@astrojs/react` +
141
+ `@c15t/react`) or any setup built on c15t's store and the bridge picks it
142
+ up; until then only the `manual` JS consent API writes envelopes.
143
+
97
144
  ## Verify Locally
98
145
 
99
146
  From `packages/sdk`:
Binary file
@@ -38,6 +38,7 @@ export declare function getPageRuntimeRenderInput(request: Request): RenderInput
38
38
  export { createRegistry, registryLookup } from '../registry.js';
39
39
  export type { Registry, RegistryEntry, AnyComponent } from '../registry.js';
40
40
  export type { BlockRef } from '../types/block-ref.js';
41
+ export { collectScriptTagHTML, collectTagAttributes, COLLECT_SCRIPT_SRC, type CollectTagAttributes, type PersonalizationSiteConfig, type SitePersonalizationConfig, } from '../content/personalization.js';
41
42
  export interface RenderableBlock {
42
43
  Component: AnyComponent | undefined;
43
44
  props: Record<string, unknown>;
@@ -36,6 +36,13 @@ export function getPageRuntimeRenderInput(request) {
36
36
  // Registry helpers
37
37
  // ---------------------------------------------------------------------------
38
38
  export { createRegistry, registryLookup } from '../registry.js';
39
+ // ---------------------------------------------------------------------------
40
+ // Personalization collect tag — the Astro integration has no attribute-
41
+ // bearing HTML-emission seam (injectScript injects script *content*, not a
42
+ // tag with data attributes), so the kit layout emits the contract tag via
43
+ // this snippet builder (design/04; design/09 Task 9b).
44
+ // ---------------------------------------------------------------------------
45
+ export { collectScriptTagHTML, collectTagAttributes, COLLECT_SCRIPT_SRC, } from '../content/personalization.js';
39
46
  export function createRenderChildren(registry) {
40
47
  function renderChildren(blocks) {
41
48
  return blocks.map((block) => {
@@ -0,0 +1,24 @@
1
+ import type { DebugFn } from "./config.js";
2
+ export interface CommandHandlers {
3
+ /** Receives the normalized custom.* event name. */
4
+ track: (name: string, properties?: Record<string, unknown>) => void;
5
+ /** Manual consent write; only reachable under the manual preset. */
6
+ consent: (envelope: {
7
+ p: unknown;
8
+ src: unknown;
9
+ }) => void;
10
+ }
11
+ export interface CommandQueueConfig {
12
+ preset: string;
13
+ }
14
+ interface AciHost {
15
+ aci?: unknown;
16
+ }
17
+ /**
18
+ * installCommandQueue replaces any pre-load stub on window.aci with the live
19
+ * dispatcher, draining queued calls in order first. Malformed or unknown
20
+ * commands are ignored after a debug record; the CMP preset is authoritative
21
+ * over consent commands (manual mode only).
22
+ */
23
+ export declare function installCommandQueue(win: AciHost, cfg: CommandQueueConfig, handlers: CommandHandlers, debug?: DebugFn): void;
24
+ export {};
@@ -0,0 +1,63 @@
1
+ // Browser command API (design/04): window.aci is a write-only command
2
+ // queue. Pre-load calls push onto aci.q via the documented array-push stub;
3
+ // installCommandQueue drains that queue in order and becomes the live
4
+ // dispatcher. Commands return undefined — nothing is readable back.
5
+ import { toCustomEventName } from "./declarative.js";
6
+ function isPlainObjectLike(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ /**
10
+ * installCommandQueue replaces any pre-load stub on window.aci with the live
11
+ * dispatcher, draining queued calls in order first. Malformed or unknown
12
+ * commands are ignored after a debug record; the CMP preset is authoritative
13
+ * over consent commands (manual mode only).
14
+ */
15
+ export function installCommandQueue(win, cfg, handlers, debug) {
16
+ const dispatch = (...args) => {
17
+ try {
18
+ const [command, ...rest] = args;
19
+ if (command === "track") {
20
+ const name = toCustomEventName(rest[0]);
21
+ if (name === undefined) {
22
+ debug?.("capture", { reason: "malformed_track_name", name: rest[0] });
23
+ return undefined;
24
+ }
25
+ const properties = rest[1];
26
+ if (properties !== undefined && !isPlainObjectLike(properties)) {
27
+ debug?.("capture", { reason: "malformed_track_properties", name });
28
+ return undefined;
29
+ }
30
+ handlers.track(name, properties);
31
+ return undefined;
32
+ }
33
+ if (command === "consent") {
34
+ if (cfg.preset !== "manual") {
35
+ // The CMP is authoritative (design/04 consent-writer precedence).
36
+ debug?.("consent", { reason: "preset_authoritative", preset: cfg.preset });
37
+ return undefined;
38
+ }
39
+ const envelope = rest[0];
40
+ if (!isPlainObjectLike(envelope)) {
41
+ debug?.("consent", { reason: "malformed_consent_envelope" });
42
+ return undefined;
43
+ }
44
+ handlers.consent({ p: envelope.p, src: envelope.src });
45
+ return undefined;
46
+ }
47
+ debug?.("capture", { reason: "unknown_command", command });
48
+ }
49
+ catch (err) {
50
+ debug?.("capture", { reason: "command_failed", error: String(err) });
51
+ }
52
+ return undefined;
53
+ };
54
+ const stub = win.aci;
55
+ const queued = stub?.q;
56
+ win.aci = dispatch;
57
+ if (queued == null)
58
+ return;
59
+ for (const call of Array.from(queued)) {
60
+ const args = Array.from(call);
61
+ dispatch(...args);
62
+ }
63
+ }
@@ -0,0 +1,65 @@
1
+ import type { DebugFn } from "./config.js";
2
+ /** One collect event on the wire (personalization-data-plane design/04). */
3
+ export interface CollectEvent {
4
+ eventId: string;
5
+ eventName: string;
6
+ occurredAt?: string;
7
+ properties?: Record<string, unknown>;
8
+ url?: string;
9
+ referrer?: string;
10
+ }
11
+ /** An event at capture time, before the eventId is stamped. */
12
+ export type PendingEvent = Omit<CollectEvent, "eventId">;
13
+ /** Planning threshold: full serialized envelope, UTF-8 bytes. */
14
+ export declare const MAX_BATCH_BYTES: number;
15
+ /** Flush interval (build-time constant, design/04 config table). */
16
+ export declare const FLUSH_INTERVAL_MS = 5000;
17
+ export interface BatcherTimers {
18
+ setInterval(fn: () => void, ms: number): unknown;
19
+ clearInterval(id: unknown): void;
20
+ }
21
+ interface ListenerTarget {
22
+ addEventListener(type: string, fn: () => void): void;
23
+ }
24
+ export interface BatcherOptions {
25
+ /** Transport for one serialized batch envelope. */
26
+ send: (body: string) => void;
27
+ debug?: DebugFn;
28
+ /** Runs at the start of every flush tick (gap-buffer marker re-check). */
29
+ onTick?: () => void;
30
+ maxBatchBytes?: number;
31
+ flushIntervalMs?: number;
32
+ timers?: BatcherTimers;
33
+ documentTarget?: ListenerTarget & {
34
+ visibilityState?: string;
35
+ };
36
+ windowTarget?: ListenerTarget;
37
+ }
38
+ export declare class Batcher {
39
+ private readonly options;
40
+ private readonly maxBytes;
41
+ private queue;
42
+ private queuedBytes;
43
+ private flushing;
44
+ private intervalId;
45
+ constructor(options: BatcherOptions);
46
+ get pending(): number;
47
+ push(event: CollectEvent): void;
48
+ /**
49
+ * flush is one tick: run the onTick hook (which may push more events, e.g.
50
+ * a gap-buffer drain), then send everything queued in threshold-sized
51
+ * envelopes. Re-entrant pushes are drained in the same tick.
52
+ */
53
+ flush(): void;
54
+ /**
55
+ * start installs the interval and lifecycle flush triggers, then flushes
56
+ * immediately: init queues the initial page view before start (index.ts),
57
+ * and holding it for the first interval tick would delay the session's
58
+ * first signal by up to FLUSH_INTERVAL_MS.
59
+ */
60
+ start(): void;
61
+ stop(): void;
62
+ private envelopeBytes;
63
+ private drain;
64
+ }
65
+ export {};
@@ -0,0 +1,127 @@
1
+ // Batcher (design/05): queue events, split so no request exceeds the 60 KiB
2
+ // planning threshold (64 KiB wire ceiling), flush on start, size, interval,
3
+ // visibilitychange and pagehide. Byte accounting is incremental — a running
4
+ // per-event size computed at enqueue — so a flush stays O(n).
5
+ /** Planning threshold: full serialized envelope, UTF-8 bytes. */
6
+ export const MAX_BATCH_BYTES = 60 * 1024;
7
+ /** Flush interval (build-time constant, design/04 config table). */
8
+ export const FLUSH_INTERVAL_MS = 5000;
9
+ const ENVELOPE_PREFIX = '{"events":[';
10
+ const ENVELOPE_SUFFIX = "]}";
11
+ const ENVELOPE_OVERHEAD = ENVELOPE_PREFIX.length + ENVELOPE_SUFFIX.length;
12
+ const encoder = new TextEncoder();
13
+ const utf8ByteLength = (s) => encoder.encode(s).length;
14
+ export class Batcher {
15
+ options;
16
+ maxBytes;
17
+ queue = [];
18
+ queuedBytes = 0;
19
+ flushing = false;
20
+ intervalId;
21
+ constructor(options) {
22
+ this.options = options;
23
+ this.maxBytes = options.maxBatchBytes ?? MAX_BATCH_BYTES;
24
+ }
25
+ get pending() {
26
+ return this.queue.length;
27
+ }
28
+ push(event) {
29
+ const json = JSON.stringify(event);
30
+ const bytes = utf8ByteLength(json);
31
+ this.queue.push({ json, bytes, eventId: event.eventId });
32
+ this.queuedBytes += bytes;
33
+ if (!this.flushing && this.envelopeBytes() >= this.maxBytes) {
34
+ this.flush();
35
+ }
36
+ }
37
+ /**
38
+ * flush is one tick: run the onTick hook (which may push more events, e.g.
39
+ * a gap-buffer drain), then send everything queued in threshold-sized
40
+ * envelopes. Re-entrant pushes are drained in the same tick.
41
+ */
42
+ flush() {
43
+ if (this.flushing)
44
+ return;
45
+ this.flushing = true;
46
+ try {
47
+ this.options.onTick?.();
48
+ this.drain();
49
+ }
50
+ finally {
51
+ this.flushing = false;
52
+ }
53
+ }
54
+ /**
55
+ * start installs the interval and lifecycle flush triggers, then flushes
56
+ * immediately: init queues the initial page view before start (index.ts),
57
+ * and holding it for the first interval tick would delay the session's
58
+ * first signal by up to FLUSH_INTERVAL_MS.
59
+ */
60
+ start() {
61
+ const timers = this.options.timers ?? {
62
+ setInterval: (fn, ms) => setInterval(fn, ms),
63
+ clearInterval: (id) => clearInterval(id),
64
+ };
65
+ this.intervalId = timers.setInterval(() => this.flush(), this.options.flushIntervalMs ?? FLUSH_INTERVAL_MS);
66
+ const doc = this.options.documentTarget ??
67
+ (typeof document !== "undefined" ? document : undefined);
68
+ doc?.addEventListener("visibilitychange", () => {
69
+ if (doc.visibilityState === "hidden")
70
+ this.flush();
71
+ });
72
+ const win = this.options.windowTarget ??
73
+ (typeof window !== "undefined" ? window : undefined);
74
+ win?.addEventListener("pagehide", () => this.flush());
75
+ this.flush();
76
+ }
77
+ stop() {
78
+ if (this.intervalId !== undefined) {
79
+ const timers = this.options.timers;
80
+ if (timers) {
81
+ timers.clearInterval(this.intervalId);
82
+ }
83
+ else {
84
+ clearInterval(this.intervalId);
85
+ }
86
+ this.intervalId = undefined;
87
+ }
88
+ }
89
+ envelopeBytes() {
90
+ const commas = this.queue.length > 1 ? this.queue.length - 1 : 0;
91
+ return ENVELOPE_OVERHEAD + this.queuedBytes + commas;
92
+ }
93
+ drain() {
94
+ while (this.queue.length > 0) {
95
+ const head = this.queue[0];
96
+ if (ENVELOPE_OVERHEAD + head.bytes > this.maxBytes) {
97
+ // A single event whose own encoding exceeds the threshold: drop it
98
+ // with a debug record and keep going — never loop on it.
99
+ this.queue.shift();
100
+ this.queuedBytes -= head.bytes;
101
+ this.options.debug?.("flush", {
102
+ reason: "oversize_event",
103
+ eventId: head.eventId,
104
+ bytes: head.bytes,
105
+ });
106
+ continue;
107
+ }
108
+ const parts = [];
109
+ let size = ENVELOPE_OVERHEAD;
110
+ while (this.queue.length > 0) {
111
+ const next = this.queue[0];
112
+ if (ENVELOPE_OVERHEAD + next.bytes > this.maxBytes)
113
+ break; // oversize: outer loop drops it
114
+ const nextSize = size + next.bytes + (parts.length > 0 ? 1 : 0);
115
+ if (nextSize > this.maxBytes)
116
+ break;
117
+ parts.push(next.json);
118
+ size = nextSize;
119
+ this.queue.shift();
120
+ this.queuedBytes -= next.bytes;
121
+ }
122
+ if (parts.length === 0)
123
+ continue;
124
+ this.options.send(ENVELOPE_PREFIX + parts.join(",") + ENVELOPE_SUFFIX);
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,30 @@
1
+ export type ConsentPreset = "c15t" | "onetrust" | "cookiebot" | "trustarc" | "manual";
2
+ export interface TagConfig {
3
+ /** Optional site id, for debug and a future multi-site surface. */
4
+ site: string | undefined;
5
+ /** CMP preset; "manual" means only the JS consent API writes envelopes. */
6
+ preset: ConsentPreset;
7
+ /** Raw flat comma list of CMP categories implying personalization. */
8
+ categories: string | undefined;
9
+ /** Structured console.debug diagnostics enabled (data-aci-debug="1"). */
10
+ debug: boolean;
11
+ }
12
+ /**
13
+ * Structured diagnostics kinds (design/05): stable event-shaped records a
14
+ * future debug overlay can render. "init" covers configuration and swallowed
15
+ * init failures.
16
+ */
17
+ export type DebugKind = "init" | "capture" | "consent" | "flush" | "buffer";
18
+ export type DebugFn = (kind: DebugKind, detail: unknown) => void;
19
+ /**
20
+ * readTagConfig parses the data-aci-* attributes off the SDK's own script
21
+ * element (document.currentScript). Missing or unrecognized values fall back
22
+ * to safe defaults; unknown attributes are tolerated.
23
+ */
24
+ export declare function readTagConfig(script: Element | null | undefined): TagConfig;
25
+ /**
26
+ * createDebug returns the structured debug emitter: {t, kind, detail}
27
+ * records via console.debug when enabled, a no-op otherwise. The sink and
28
+ * clock are injectable for tests. Diagnostics never throw.
29
+ */
30
+ export declare function createDebug(enabled: boolean, log?: (record: unknown) => void, now?: () => number): DebugFn;
@@ -0,0 +1,53 @@
1
+ // Tag configuration (design/04): the script tag is the only per-site
2
+ // configuration surface. Unknown attributes are ignored.
3
+ const PRESETS = new Set([
4
+ "c15t",
5
+ "onetrust",
6
+ "cookiebot",
7
+ "trustarc",
8
+ "manual",
9
+ ]);
10
+ function attribute(script, name) {
11
+ const value = script?.getAttribute?.(name);
12
+ if (value == null)
13
+ return undefined;
14
+ const trimmed = value.trim();
15
+ return trimmed === "" ? undefined : trimmed;
16
+ }
17
+ /**
18
+ * readTagConfig parses the data-aci-* attributes off the SDK's own script
19
+ * element (document.currentScript). Missing or unrecognized values fall back
20
+ * to safe defaults; unknown attributes are tolerated.
21
+ */
22
+ export function readTagConfig(script) {
23
+ const rawPreset = attribute(script, "data-aci-consent")?.toLowerCase();
24
+ const preset = rawPreset !== undefined && PRESETS.has(rawPreset)
25
+ ? rawPreset
26
+ : "manual";
27
+ return {
28
+ site: attribute(script, "data-aci-site"),
29
+ preset,
30
+ categories: attribute(script, "data-aci-consent-categories"),
31
+ debug: attribute(script, "data-aci-debug") === "1",
32
+ };
33
+ }
34
+ /**
35
+ * createDebug returns the structured debug emitter: {t, kind, detail}
36
+ * records via console.debug when enabled, a no-op otherwise. The sink and
37
+ * clock are injectable for tests. Diagnostics never throw.
38
+ */
39
+ export function createDebug(enabled, log, now) {
40
+ if (!enabled) {
41
+ return () => { };
42
+ }
43
+ const emit = log ?? ((record) => console.debug("[aci]", record));
44
+ const clock = now ?? Date.now;
45
+ return (kind, detail) => {
46
+ try {
47
+ emit({ t: clock(), kind, detail });
48
+ }
49
+ catch {
50
+ // Diagnostics must never affect the page.
51
+ }
52
+ };
53
+ }
@@ -0,0 +1,2 @@
1
+ import { type AdapterDeps } from "../mapping.js";
2
+ export declare function start(deps: AdapterDeps): void;
@@ -0,0 +1,43 @@
1
+ // c15t adapter (P0, dogfood): a thin subscription over the shared mapping
2
+ // core. Reads window.c15t's consents record (category -> boolean, modeled on
3
+ // @c15t/nextjs's ConsentState) once on discovery, then follows
4
+ // onConsentChange payloads (wrapped {consents} or a bare record).
5
+ import { guard, handleCmpState, pollForGlobal, } from "../mapping.js";
6
+ const PRESET = "c15t";
7
+ function normalizeConsents(record) {
8
+ if (typeof record !== "object" || record === null)
9
+ return undefined;
10
+ const categories = {};
11
+ for (const [key, value] of Object.entries(record)) {
12
+ if (typeof value === "boolean")
13
+ categories[key.toLowerCase()] = value;
14
+ }
15
+ return { categories };
16
+ }
17
+ function payloadConsents(payload, c15t) {
18
+ if (typeof payload === "object" && payload !== null) {
19
+ if ("consents" in payload) {
20
+ return payload.consents;
21
+ }
22
+ return payload; // bare consents record
23
+ }
24
+ return c15t.consents; // unknown payload shape: re-read the global
25
+ }
26
+ export function start(deps) {
27
+ pollForGlobal(() => {
28
+ const c15t = deps.window.c15t;
29
+ return typeof c15t === "object" && c15t !== null
30
+ ? c15t
31
+ : undefined;
32
+ }, (c15t) => {
33
+ guard(deps.debug, () => {
34
+ const handle = (record) => {
35
+ const state = normalizeConsents(record);
36
+ if (state)
37
+ handleCmpState(state, PRESET, deps);
38
+ };
39
+ handle(c15t.consents); // read the current state once
40
+ c15t.onConsentChange?.((payload) => guard(deps.debug, () => handle(payloadConsents(payload, c15t))));
41
+ });
42
+ }, deps);
43
+ }
@@ -0,0 +1,2 @@
1
+ import { type AdapterDeps } from "../mapping.js";
2
+ export declare function start(deps: AdapterDeps): void;
@@ -0,0 +1,48 @@
1
+ // Cookiebot adapter: reads window.Cookiebot.consent (a category -> boolean
2
+ // record: preferences/statistics/marketing) and follows the documented
3
+ // window events CookiebotOnConsentReady / CookiebotOnAccept /
4
+ // CookiebotOnDecline. Before the visitor has responded
5
+ // (Cookiebot.hasResponse !== true) the state is not a decision — the
6
+ // adapter writes nothing (fail closed).
7
+ import { guard, handleCmpState, pollForGlobal, } from "../mapping.js";
8
+ const PRESET = "cookiebot";
9
+ const CONSENT_EVENTS = [
10
+ "CookiebotOnConsentReady",
11
+ "CookiebotOnAccept",
12
+ "CookiebotOnDecline",
13
+ ];
14
+ function readGlobal(w) {
15
+ const cookiebot = w.Cookiebot;
16
+ return typeof cookiebot === "object" && cookiebot !== null
17
+ ? cookiebot
18
+ : undefined;
19
+ }
20
+ function normalizeConsent(record) {
21
+ if (typeof record !== "object" || record === null)
22
+ return undefined;
23
+ const categories = {};
24
+ for (const [key, value] of Object.entries(record)) {
25
+ if (typeof value === "boolean")
26
+ categories[key.toLowerCase()] = value;
27
+ }
28
+ return { categories };
29
+ }
30
+ export function start(deps) {
31
+ const readAndHandle = () => {
32
+ const cookiebot = readGlobal(deps.window);
33
+ if (!cookiebot)
34
+ return;
35
+ if (cookiebot.hasResponse !== true)
36
+ return; // no decision yet: fail closed
37
+ const state = normalizeConsent(cookiebot.consent);
38
+ if (state)
39
+ handleCmpState(state, PRESET, deps);
40
+ };
41
+ const target = deps.window;
42
+ if (typeof target.addEventListener === "function") {
43
+ for (const name of CONSENT_EVENTS) {
44
+ target.addEventListener(name, () => guard(deps.debug, readAndHandle));
45
+ }
46
+ }
47
+ pollForGlobal(() => readGlobal(deps.window), () => guard(deps.debug, readAndHandle), deps);
48
+ }
@@ -0,0 +1,2 @@
1
+ import { type AdapterDeps } from "../mapping.js";
2
+ export declare function start(deps: AdapterDeps): void;
@@ -0,0 +1,54 @@
1
+ // OneTrust adapter: reads the window.OnetrustActiveGroups string (the
2
+ // active-group list, e.g. ",C0001,C0002,") and chains OptanonWrapper —
3
+ // OneTrust invokes it on load and on every consent change.
4
+ //
5
+ // The active-groups surface only enumerates GRANTED groups, so denial is
6
+ // inferred: a configured category shaped like a standard group id (C0002,
7
+ // C0004, ...) that is absent from the list reads as recognized-but-denied,
8
+ // which lets a genuine revoke propagate. Custom-named categories that never
9
+ // appear stay unrecognized and write nothing (fail closed).
10
+ import { guard, handleCmpState, pollForGlobal, resolveCategories, } from "../mapping.js";
11
+ const PRESET = "onetrust";
12
+ const STANDARD_GROUP_ID = /^c\d{4}$/;
13
+ function normalizeGroups(groups, configured) {
14
+ const categories = {};
15
+ for (const token of groups.split(",")) {
16
+ const group = token.trim().toLowerCase();
17
+ if (group !== "")
18
+ categories[group] = true;
19
+ }
20
+ for (const category of configured ?? []) {
21
+ if (!(category in categories) && STANDARD_GROUP_ID.test(category)) {
22
+ categories[category] = false;
23
+ }
24
+ }
25
+ return { categories };
26
+ }
27
+ export function start(deps) {
28
+ const w = deps.window;
29
+ const readAndHandle = () => {
30
+ const groups = w.OnetrustActiveGroups;
31
+ if (typeof groups !== "string")
32
+ return;
33
+ const configured = resolveCategories(PRESET, deps.categories);
34
+ handleCmpState(normalizeGroups(groups, configured), PRESET, deps);
35
+ };
36
+ // Chain the wrapper immediately so consent changes reach us even when
37
+ // OneTrust loads (or the visitor decides) after this adapter starts.
38
+ const previous = w.OptanonWrapper;
39
+ w.OptanonWrapper = () => {
40
+ if (typeof previous === "function") {
41
+ try {
42
+ previous();
43
+ }
44
+ catch {
45
+ // The site's own wrapper is third-party code; never let it stop us.
46
+ }
47
+ }
48
+ guard(deps.debug, readAndHandle);
49
+ };
50
+ pollForGlobal(() => {
51
+ const groups = w.OnetrustActiveGroups;
52
+ return typeof groups === "string" ? groups : undefined;
53
+ }, () => guard(deps.debug, readAndHandle), deps);
54
+ }
@@ -0,0 +1,2 @@
1
+ import { type AdapterDeps } from "../mapping.js";
2
+ export declare function start(deps: AdapterDeps): void;