@kywi-software/js 0.13.0 → 0.15.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.
@@ -1 +1 @@
1
- {"version":3,"file":"dom-patcher.d.ts","sourceRoot":"","sources":["../../src/audience/dom-patcher.ts"],"names":[],"mappings":"AACA,wBAAgB,sBAAsB,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAiC7E"}
1
+ {"version":3,"file":"dom-patcher.d.ts","sourceRoot":"","sources":["../../src/audience/dom-patcher.ts"],"names":[],"mappings":"AACA,wBAAgB,sBAAsB,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAwC7E"}
@@ -26,6 +26,14 @@ export interface AudienceBootConfig {
26
26
  * the host page passes the resolved config value through.
27
27
  */
28
28
  transparencyNotice?: boolean;
29
+ /**
30
+ * `theme.personalization.requireConsent` (kywi-cms#91). Defaults to true —
31
+ * the resolved audience is only remembered for a visitor who consented to
32
+ * personalization. `false` turns that gate off for a site whose consent is
33
+ * collected by external tooling; the server applies the same flag, so the two
34
+ * must agree or the request is made and then refused persistence.
35
+ */
36
+ requireConsent?: boolean;
29
37
  }
30
38
  export declare function bootAudienceEngine(config: AudienceBootConfig): Promise<void>;
31
39
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audience/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAA;AASnF,OAAO,EAA0C,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAGpG,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IACvC,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,GAAG,cAAc,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;KAAE,CAAC,CAAA;IAC9J,YAAY,CAAC,EAAE,kBAAkB,CAAA;IACjC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAUD,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+FlF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audience/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAA;AAUnF,OAAO,EAA0C,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAGpG,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IACvC,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,QAAQ,GAAG,cAAc,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;KAAE,CAAC,CAAA;IAC9J,YAAY,CAAC,EAAE,kBAAkB,CAAA;IACjC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAUD,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiIlF"}
@@ -31,13 +31,22 @@ export declare function optIn(): void;
31
31
  export declare function resetToAutomatic(): void;
32
32
  /**
33
33
  * Pins the visitor to `audienceId` via the public runtime route — the same
34
- * endpoint and payload the boot path already uses to persist a client-resolved
35
- * audience. The server re-validates the id against the active audiences.
34
+ * endpoint the boot path uses, but marked `source: 'switcher'`. That flag is
35
+ * what tells the server this is the visitor operating a control by hand rather
36
+ * than the runtime remembering something automatically, which is why it is
37
+ * honoured as an `essential` preference and not gated on personalization consent
38
+ * (kywi-cms#91; the same argument as kywi_optout — see the block above).
39
+ *
40
+ * The server re-validates the id against the active audiences.
41
+ *
42
+ * Returns whether the pin will SURVIVE this page: false when the server declined
43
+ * to store it (an older server that predates `source`, or a refusal), so the
44
+ * caller can say so instead of reloading into an unchanged page.
36
45
  */
37
46
  export declare function pinAudience(audienceId: string, opts?: {
38
47
  apiBase?: string | undefined;
39
48
  csrfToken?: string | null | undefined;
40
- }): Promise<void>;
49
+ }): Promise<boolean>;
41
50
  /**
42
51
  * Renders (or removes) the transparency surface for `state`. Safe to call on
43
52
  * every boot; it replaces whatever is currently mounted.
@@ -1 +1 @@
1
- {"version":3,"file":"transparency.d.ts","sourceRoot":"","sources":["../../src/audience/transparency.ts"],"names":[],"mappings":"AAiCA,6DAA6D;AAC7D,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,cAAc,EAAE,OAAO,CAAA;IACvB,UAAU,EAAE,OAAO,CAAA;IACnB,8EAA8E;IAC9E,iBAAiB,EAAE,OAAO,CAAA;IAC1B,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,2EAA2E;IAC3E,SAAS,CAAC,EAAE,oBAAoB,EAAE,CAAA;IAClC,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAoBD,8EAA8E;AAC9E,wBAAgB,WAAW,IAAI,OAAO,CASrC;AAQD,wBAAgB,MAAM,IAAI,IAAI,CAG7B;AAED,wBAAgB,KAAK,IAAI,IAAI,CAE5B;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAMD;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAO,GACjF,OAAO,CAAC,IAAI,CAAC,CASf;AAiCD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CA6HtE;AAwBD,kDAAkD;AAClD,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAED,oFAAoF;AACpF,wBAAgB,aAAa,IAAI,IAAI,CAIpC;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAU5C;AAED,wEAAwE;AACxE,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C"}
1
+ {"version":3,"file":"transparency.d.ts","sourceRoot":"","sources":["../../src/audience/transparency.ts"],"names":[],"mappings":"AAiCA,6DAA6D;AAC7D,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,cAAc,EAAE,OAAO,CAAA;IACvB,UAAU,EAAE,OAAO,CAAA;IACnB,8EAA8E;IAC9E,iBAAiB,EAAE,OAAO,CAAA;IAC1B,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,2EAA2E;IAC3E,SAAS,CAAC,EAAE,oBAAoB,EAAE,CAAA;IAClC,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAoBD,8EAA8E;AAC9E,wBAAgB,WAAW,IAAI,OAAO,CASrC;AAeD,wBAAgB,MAAM,IAAI,IAAI,CAG7B;AAED,wBAAgB,KAAK,IAAI,IAAI,CAE5B;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAAO,GACjF,OAAO,CAAC,OAAO,CAAC,CAkBlB;AA+CD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAqItE;AAwBD,kDAAkD;AAClD,wBAAgB,WAAW,IAAI,IAAI,CAGlC;AAED,oFAAoF;AACpF,wBAAgB,aAAa,IAAI,IAAI,CAIpC;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAU5C;AAED,wEAAwE;AACxE,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C"}
package/dist/consent.d.ts CHANGED
@@ -1,5 +1,32 @@
1
1
  export declare const CONSENT_COOKIE: "kywi_consent";
2
+ /** The cookie an audience pin lives in. */
3
+ export declare const AUDIENCE_PIN_COOKIE: "kywi_audience";
4
+ /**
5
+ * The `kywi_audience` value prefix marking a pin the visitor chose BY HAND in
6
+ * the transparency panel, rather than one the runtime remembered on its own.
7
+ * Mirrors `SWITCHER_PIN_PREFIX` in core's `audiences/consent.ts`.
8
+ */
9
+ export declare const SWITCHER_PIN_PREFIX: "switcher:";
2
10
  export type ConsentCategory = 'essential' | 'analytics' | 'marketing' | 'personalization';
11
+ /**
12
+ * Every cookie the engine writes → the consent category it requires.
13
+ *
14
+ * A MIRROR of `COOKIE_CONSENT_CATEGORY` in `@kywi-software/core`'s
15
+ * `audiences/consent.ts`, which is the authority. This package cannot import
16
+ * core (neither depends on the other), so the copy exists to let a decision made
17
+ * here REVOKE the cookies it denies without a round trip.
18
+ * `__tests__/consent-parity.test.ts` asserts the two maps are identical.
19
+ */
20
+ export declare const COOKIE_CONSENT_CATEGORY: Record<string, ConsentCategory>;
21
+ /** Mirrors `ConsentGateOptions` in core's `audiences/consent.ts`. */
22
+ export interface ConsentGateOptions {
23
+ /**
24
+ * `theme.personalization.requireConsent`. Defaults to true. When explicitly
25
+ * `false` the gate is off and every category reads as allowed — for a site
26
+ * whose consent is collected by external tooling (kywi-cms#91).
27
+ */
28
+ requireConsent?: boolean;
29
+ }
3
30
  export interface ConsentState {
4
31
  essential: boolean;
5
32
  analytics: boolean;
@@ -9,6 +36,19 @@ export interface ConsentState {
9
36
  /**
10
37
  * Read the current consent state from the `kywi_consent` cookie.
11
38
  * Returns the parsed ConsentState, or null if the cookie is missing or malformed.
39
+ *
40
+ * The payload is NORMALIZED, not trusted: `essential` is forced true and every
41
+ * other category is `=== true` or false. The cookie is client-writable, so it
42
+ * can arrive partial (written by an older version), tampered with, or carrying
43
+ * `"yes"` where a boolean belongs. Casting it to `ConsentState` and hoping left
44
+ * callers reading `state.marketing` a truthy string. This mirrors
45
+ * `parseConsentValue` in core's `audiences/consent.ts` exactly, so the client
46
+ * and the server can never disagree about what a given cookie means
47
+ * (`__tests__/consent-parity.test.ts` pins that).
48
+ *
49
+ * Unlike core's version this does NOT decodeURIComponent: `readCookies` has
50
+ * already decoded the value, and decoding twice would corrupt a payload
51
+ * containing a literal `%`.
12
52
  */
13
53
  export declare function getConsentState(): ConsentState | null;
14
54
  /**
@@ -16,9 +56,36 @@ export declare function getConsentState(): ConsentState | null;
16
56
  * The cookie is stored with a 1-year Max-Age.
17
57
  */
18
58
  export declare function setConsent(prefs: Partial<Omit<ConsentState, 'essential'>>): void;
59
+ /**
60
+ * Delete every cookie whose category `state` does not grant, and return the
61
+ * names deleted.
62
+ *
63
+ * Refusing to WRITE new cookies is only half of "Reject All": a visitor who
64
+ * accepted last month and rejects today is still carrying a `kywi_visitor` id,
65
+ * and a gate that only blocked writes would keep reading it forever
66
+ * (kywi-cms#91).
67
+ *
68
+ * `kywi_signals` is HttpOnly and cannot be removed from script; the attempt is
69
+ * harmless, the server stops writing it, and it expires on its own 60-second
70
+ * horizon. Core's server-side read paths also ignore personalization cookies
71
+ * whenever the gate denies, so a cookie script cannot clear is inert anyway.
72
+ */
73
+ export declare function clearDeniedCookies(state: ConsentState | null): string[];
19
74
  /**
20
75
  * Check whether a specific consent category has been granted.
21
- * Returns false if no consent cookie exists or the category is unknown.
76
+ *
77
+ * `essential` is ALWAYS granted, decision or not — necessary cookies (the
78
+ * consent record itself, the personalization opt-out flag) are never gated, and
79
+ * before kywi-cms#91 that was true only by accident because nothing called this
80
+ * function at all. Every other category needs an explicit `true`, so an
81
+ * undecided visitor grants nothing and an unknown category is denied.
82
+ *
83
+ * `requireConsent: false` (from `theme.personalization.requireConsent`) turns the
84
+ * gate off entirely for a site whose consent is collected by external tooling.
85
+ *
86
+ * The authoritative cookie→category map lives in `@kywi-software/core`'s
87
+ * `audiences/consent.ts` (`COOKIE_CONSENT_CATEGORY`) — this package cannot
88
+ * import core, so `__tests__/consent-parity.test.ts` asserts the two agree.
22
89
  */
23
- export declare function hasConsent(category: ConsentCategory): boolean;
90
+ export declare function hasConsent(category: ConsentCategory, options?: ConsentGateOptions): boolean;
24
91
  //# sourceMappingURL=consent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consent.d.ts","sourceRoot":"","sources":["../src/consent.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,EAAG,cAAuB,CAAA;AAErD,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,iBAAiB,CAAA;AAEzF,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,eAAe,EAAE,OAAO,CAAA;CACzB;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,YAAY,GAAG,IAAI,CAYrD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC,GAC9C,IAAI,CASN;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAI7D"}
1
+ {"version":3,"file":"consent.d.ts","sourceRoot":"","sources":["../src/consent.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,EAAG,cAAuB,CAAA;AAErD,2CAA2C;AAC3C,eAAO,MAAM,mBAAmB,EAAG,eAAwB,CAAA;AAE3D;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAG,WAAoB,CAAA;AAEvD,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,iBAAiB,CAAA;AAEzF;;;;;;;;GAQG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CASnE,CAAA;AAED,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,eAAe,EAAE,OAAO,CAAA;CACzB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,IAAI,YAAY,GAAG,IAAI,CAmBrD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC,GAC9C,IAAI,CAYN;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,GAAG,MAAM,EAAE,CAYvE;AAyBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,eAAe,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAM/F"}
package/dist/index.d.ts CHANGED
@@ -23,4 +23,5 @@ export { bootAudienceEngine } from './audience/index.js';
23
23
  export { openTransparencyPanel, renderTransparencyPanel, type TransparencyState, type TransparencyAudience, } from './audience/transparency.js';
24
24
  export { bootExperiments, type ExperimentRuntime, type ConversionSignal } from './experiments/index.js';
25
25
  export { initNavMenus, enhanceNav, NAV_BREAKPOINT } from './nav/menu.js';
26
+ export { getConsentState, setConsent, hasConsent, clearDeniedCookies, COOKIE_CONSENT_CATEGORY, CONSENT_COOKIE, AUDIENCE_PIN_COOKIE, SWITCHER_PIN_PREFIX, type ConsentCategory, type ConsentGateOptions, type ConsentState, } from './consent.js';
26
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAUnE,KAAK,eAAe,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,KAAK,IAAI,CAAA;AAIlD,eAAO,MAAM,IAAI;;aAEa,WAAW;YACzB,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC;gBACtC,eAAe,GAAG,IAAI;;2BAIb,oBAAoB,GAAG,oBAAoB;;;;oBAM9C,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;4BAC7C,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,IAAI;;;eAK5C,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG;aACvB,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG;gBAClB,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI;YAC7C,IAAI;CAkBb,CAAA;AAWD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,GAC1B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACvG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAUnE,KAAK,eAAe,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,KAAK,IAAI,CAAA;AAIlD,eAAO,MAAM,IAAI;;aAEa,WAAW;YACzB,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC;gBACtC,eAAe,GAAG,IAAI;;2BAIb,oBAAoB,GAAG,oBAAoB;;;;oBAM9C,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;4BAC7C,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,IAAI;;;eAK5C,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG;aACvB,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG;gBAClB,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI;YAC7C,IAAI;CAkBb,CAAA;AAWD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACxD,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,GAC1B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AACvG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAIxE,OAAO,EACL,eAAe,EACf,UAAU,EACV,UAAU,EACV,kBAAkB,EAClB,uBAAuB,EACvB,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAA"}
package/dist/kywi.esm.js CHANGED
@@ -871,7 +871,9 @@ function patchVariantContainers(winningAudienceId) {
871
871
  el.removeAttribute("aria-busy");
872
872
  const skeleton = el.querySelector(".kywi-variant-skeleton");
873
873
  if (skeleton) skeleton.remove();
874
- const target = winningAudienceId ?? "default";
874
+ const requested = winningAudienceId ?? "default";
875
+ const hasMatch = arms.some((child) => child.getAttribute("data-kywi-variant") === requested);
876
+ const target = hasMatch ? requested : "default";
875
877
  el.setAttribute("data-variant", target);
876
878
  for (const child of arms) {
877
879
  const h = child;
@@ -966,14 +968,26 @@ function hasPin() {
966
968
  async function pinAudience(audienceId, opts = {}) {
967
969
  const apiBase = opts.apiBase ?? DEFAULT_API_BASE;
968
970
  try {
969
- await fetch(`${apiBase}/kywi/audience`, {
971
+ const res = await fetch(`${apiBase}/kywi/audience`, {
970
972
  method: "POST",
971
973
  headers: { "Content-Type": "application/json", ...opts.csrfToken ? { "X-CSRF-Token": opts.csrfToken } : {} },
972
- body: JSON.stringify({ audienceId })
974
+ body: JSON.stringify({ audienceId, source: "switcher" })
973
975
  });
976
+ if (!res.ok) return false;
977
+ const body = await res.json().catch(() => null);
978
+ return body?.data?.persisted !== false;
974
979
  } catch {
980
+ return false;
975
981
  }
976
982
  }
983
+ function showSwitcherError(group) {
984
+ group.parentElement?.querySelector(".kywi-transparency-switcher-error")?.remove();
985
+ const note = document.createElement("p");
986
+ note.className = "kywi-transparency-switcher-error";
987
+ note.setAttribute("role", "status");
988
+ note.textContent = "Couldn't save that choice. It won't be remembered on the next page.";
989
+ group.parentElement?.appendChild(note);
990
+ }
977
991
  function reload() {
978
992
  try {
979
993
  window.location.reload();
@@ -1078,7 +1092,12 @@ function renderTransparencyPanel(state) {
1078
1092
  const isCurrent = option.id === state.audienceId;
1079
1093
  const optionBtn = button("kywi-transparency-option", option.name, () => {
1080
1094
  if (isCurrent) return;
1081
- void pinAudience(option.id, { apiBase: state.apiBase, csrfToken: state.csrfToken }).then(reload);
1095
+ void pinAudience(option.id, { apiBase: state.apiBase, csrfToken: state.csrfToken }).then(
1096
+ (persisted) => {
1097
+ if (persisted) reload();
1098
+ else showSwitcherError(group);
1099
+ }
1100
+ );
1082
1101
  });
1083
1102
  optionBtn.dataset.kywiAudienceId = option.id;
1084
1103
  optionBtn.setAttribute("aria-pressed", String(isCurrent));
@@ -1209,6 +1228,74 @@ function readServerMeta() {
1209
1228
  return { audienceId: m("audience-id"), experimentId: m("experiment-id"), variantId: m("variant-id"), visitorId: m("visitor-id") ?? "" };
1210
1229
  }
1211
1230
 
1231
+ // src/consent.ts
1232
+ var CONSENT_COOKIE = "kywi_consent";
1233
+ var AUDIENCE_PIN_COOKIE = "kywi_audience";
1234
+ var SWITCHER_PIN_PREFIX = "switcher:";
1235
+ var COOKIE_CONSENT_CATEGORY = {
1236
+ kywi_consent: "essential",
1237
+ kywi_optout: "essential",
1238
+ kywi_preview_init: "essential",
1239
+ kywi_signals: "personalization",
1240
+ kywi_visitor: "personalization",
1241
+ kywi_utm: "personalization",
1242
+ kywi_audience: "personalization",
1243
+ kywi_known: "personalization"
1244
+ };
1245
+ function getConsentState() {
1246
+ const cookies = readCookies();
1247
+ const raw = cookies.get(CONSENT_COOKIE);
1248
+ if (!raw) return null;
1249
+ let parsed;
1250
+ try {
1251
+ parsed = JSON.parse(raw);
1252
+ } catch {
1253
+ return null;
1254
+ }
1255
+ if (typeof parsed !== "object" || parsed === null) return null;
1256
+ const obj = parsed;
1257
+ return {
1258
+ essential: true,
1259
+ analytics: obj.analytics === true,
1260
+ marketing: obj.marketing === true,
1261
+ personalization: obj.personalization === true
1262
+ };
1263
+ }
1264
+ function setConsent(prefs) {
1265
+ const state = {
1266
+ essential: true,
1267
+ analytics: prefs.analytics ?? false,
1268
+ marketing: prefs.marketing ?? false,
1269
+ personalization: prefs.personalization ?? false
1270
+ };
1271
+ setCookie(CONSENT_COOKIE, JSON.stringify(state), 31536e3);
1272
+ clearDeniedCookies(state);
1273
+ }
1274
+ function clearDeniedCookies(state) {
1275
+ if (typeof document === "undefined") return [];
1276
+ const cookies = readCookies();
1277
+ const deleted = [];
1278
+ for (const [name, category] of Object.entries(COOKIE_CONSENT_CATEGORY)) {
1279
+ if (category === "essential") continue;
1280
+ if (state && state[category] === true) continue;
1281
+ if (isDeliberatePreference(name, cookies.get(name))) continue;
1282
+ deleteCookie(name);
1283
+ deleted.push(name);
1284
+ }
1285
+ return deleted;
1286
+ }
1287
+ function isDeliberatePreference(name, value) {
1288
+ if (name !== AUDIENCE_PIN_COOKIE || value === void 0) return false;
1289
+ return value.startsWith(SWITCHER_PIN_PREFIX) && value.length > SWITCHER_PIN_PREFIX.length;
1290
+ }
1291
+ function hasConsent(category, options = {}) {
1292
+ if (options.requireConsent === false) return true;
1293
+ if (category === "essential") return true;
1294
+ const state = getConsentState();
1295
+ if (!state) return false;
1296
+ return state[category] === true;
1297
+ }
1298
+
1212
1299
  // src/audience/adapter-runner.ts
1213
1300
  var CACHE_PREFIX = "kywi_adapter_";
1214
1301
  function getCached(adapterId) {
@@ -1554,9 +1641,12 @@ async function bootAudienceEngine(config) {
1554
1641
  return;
1555
1642
  }
1556
1643
  const cookies = readCookies();
1644
+ const consentGate = config.requireConsent === void 0 ? {} : { requireConsent: config.requireConsent };
1645
+ const mayPersonalize = hasConsent("personalization", consentGate);
1646
+ const storedVisitorId = mayPersonalize ? cookies.get(COOKIE_NAMES.VISITOR) : void 0;
1557
1647
  let merged = {
1558
1648
  ...serverSignals,
1559
- identity: { visitorId: serverMeta.visitorId || cookies.get(COOKIE_NAMES.VISITOR) || "", isKnown: cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
1649
+ identity: { visitorId: serverMeta.visitorId || storedVisitorId || "", isKnown: mayPersonalize && cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
1560
1650
  meta: { isOptedOut: cookies.has(COOKIE_NAMES.OPTOUT), isPreview: false, previewAudienceId: null, resolvedAt: "server" }
1561
1651
  };
1562
1652
  if (config.adapters && config.adapters.length > 0) {
@@ -1564,7 +1654,8 @@ async function bootAudienceEngine(config) {
1564
1654
  merged.adapters = { ...merged.adapters, ...adapterResults };
1565
1655
  }
1566
1656
  const result = evaluateClientSide(audiences, merged, currentPath, pageCategory);
1567
- const pinnedId = cookies.get(COOKIE_NAMES.AUDIENCE) ?? null;
1657
+ const rawPin = cookies.get(COOKIE_NAMES.AUDIENCE) ?? null;
1658
+ const pinnedId = rawPin?.startsWith("switcher:") ? rawPin.slice("switcher:".length) || null : rawPin;
1568
1659
  const isPinned = !result.isOptedOut && pinnedId !== null && activeAudiences.some((a) => a.id === pinnedId);
1569
1660
  const effectiveAudienceId = isPinned ? pinnedId : result.winningAudienceId;
1570
1661
  patchVariantContainers(effectiveAudienceId);
@@ -1581,11 +1672,14 @@ async function bootAudienceEngine(config) {
1581
1672
  renderPersonalizationBadge({ audienceName: nameOf(effectiveAudienceId) });
1582
1673
  populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: effectiveAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
1583
1674
  fireKywiReady();
1584
- if (!isPinned && result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1675
+ if (!isPinned && mayPersonalize && result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
1585
1676
  fetch(`${apiBase}/kywi/audience`, {
1586
1677
  method: "POST",
1587
1678
  headers: { "Content-Type": "application/json", ...Kywi.context?.csrfToken ? { "X-CSRF-Token": Kywi.context.csrfToken } : {} },
1588
- body: JSON.stringify({ audienceId: result.winningAudienceId })
1679
+ // Explicitly the AUTOMATIC write, so the server applies the
1680
+ // personalization gate to it (kywi-cms#91). The transparency panel's
1681
+ // hand-operated switcher sends source:'switcher' and is exempt.
1682
+ body: JSON.stringify({ audienceId: result.winningAudienceId, source: "boot" })
1589
1683
  }).catch(() => {
1590
1684
  });
1591
1685
  }
@@ -1694,13 +1788,21 @@ if (typeof document !== "undefined") {
1694
1788
  }
1695
1789
  }
1696
1790
  export {
1791
+ AUDIENCE_PIN_COOKIE,
1792
+ CONSENT_COOKIE,
1793
+ COOKIE_CONSENT_CATEGORY,
1697
1794
  Kywi,
1698
1795
  NAV_BREAKPOINT,
1796
+ SWITCHER_PIN_PREFIX,
1699
1797
  bootAudienceEngine,
1700
1798
  bootExperiments,
1799
+ clearDeniedCookies,
1701
1800
  enhanceNav,
1801
+ getConsentState,
1802
+ hasConsent,
1702
1803
  initNavMenus,
1703
1804
  openTransparencyPanel,
1704
- renderTransparencyPanel
1805
+ renderTransparencyPanel,
1806
+ setConsent
1705
1807
  };
1706
1808
  //# sourceMappingURL=kywi.esm.js.map