@ecomconsult/consentkit 0.5.12 → 0.5.14
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/README.md +30 -9
- package/npm/internal-stub.mjs +1 -1
- package/package.json +1 -1
- package/src/ck-core.js +29 -1
- package/src/ck-debug.js +20 -5
- package/src/ck-saas.js +154 -6
- package/src/ck-ui.js +17 -0
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Vanilla ES2020, zero dependencies, no build step.
|
|
|
29
29
|
- **Equal-weight buttons, no pre-ticked boxes** — the consent invariants are
|
|
30
30
|
fixed by design, see [CONTRIBUTING.md](https://github.com/vermoh/ConsentKit/blob/main/CONTRIBUTING.md)
|
|
31
31
|
|
|
32
|
-
> **Status: prototype (v0.5.
|
|
32
|
+
> **Status: prototype (v0.5.14).** The core, the UI and the demo are verified in
|
|
33
33
|
> a browser and covered by an automated suite (`npm test`); several distribution
|
|
34
34
|
> paths are not yet tested against live systems. See
|
|
35
35
|
> [Project status](#project-status) before shipping this to production.
|
|
@@ -819,19 +819,19 @@ external requests. Rebuild them with `tools/build-inline.mjs` (see
|
|
|
819
819
|
[`tools/README.md`](https://github.com/vermoh/ConsentKit/blob/main/tools/README.md)); each block's header records the exact
|
|
820
820
|
command that produced it.
|
|
821
821
|
|
|
822
|
-
ConsentKit 0.5.
|
|
822
|
+
ConsentKit 0.5.14, rebuilt 2026-09-07, uncompressed — gzip on the server cuts
|
|
823
823
|
this roughly threefold. Every block includes the branding extension and the
|
|
824
824
|
attribution line; `--no-branding` drops both the code and the config and takes
|
|
825
825
|
**~26 KB** back off:
|
|
826
826
|
|
|
827
827
|
| Block | Languages | Bytes | gzip | `--no-branding` |
|
|
828
828
|
|---|---|---|---|---|
|
|
829
|
-
| `ready/en-bar.txt` | en |
|
|
830
|
-
| `ready/ru-bar.txt` | ru, ro, en |
|
|
831
|
-
| `ready/ru-box.txt` | ru, ro, en |
|
|
832
|
-
| `ready/ru-box-right.txt` | ru, ro, en |
|
|
833
|
-
| `ready/ru-modal.txt` | ru, ro, en |
|
|
834
|
-
| `ready/eu-bar.txt` | 34 languages |
|
|
829
|
+
| `ready/en-bar.txt` | en | 275,509 | 86,249 | 249,096 |
|
|
830
|
+
| `ready/ru-bar.txt` | ru, ro, en | 277,578 | 87,146 | 250,979 |
|
|
831
|
+
| `ready/ru-box.txt` | ru, ro, en | 277,593 | 87,156 | 250,994 |
|
|
832
|
+
| `ready/ru-box-right.txt` | ru, ro, en | 277,602 | 87,161 | 250,997 |
|
|
833
|
+
| `ready/ru-modal.txt` | ru, ro, en | 277,586 | 87,153 | 250,985 |
|
|
834
|
+
| `ready/eu-bar.txt` | 34 languages | 326,232 | 105,534 | 299,653 |
|
|
835
835
|
|
|
836
836
|
The blocks are dominated by the core and the UI (roughly 97 KB and 129 KB of
|
|
837
837
|
source respectively, comments included — the builder concatenates the sources
|
|
@@ -1023,6 +1023,27 @@ Client versions. The WordPress plugin tracks the same numbers and keeps its own
|
|
|
1023
1023
|
notes in
|
|
1024
1024
|
[`plugins/wordpress/consentkit/readme.txt`](https://github.com/vermoh/ConsentKit/blob/main/plugins/wordpress/consentkit/readme.txt).
|
|
1025
1025
|
|
|
1026
|
+
### 0.5.14
|
|
1027
|
+
|
|
1028
|
+
- **The loader tolerates a second snippet with a dead site id.** A page that
|
|
1029
|
+
carries two `data-ck-id` blocks — a migration that left the old one in place —
|
|
1030
|
+
is driven by the snippet whose config loads; the failing one only warns. The
|
|
1031
|
+
strict fallback is raised solely when *every* snippet fails, never while
|
|
1032
|
+
another is still in flight. The same id twice (Tilda duplicates the head
|
|
1033
|
+
block) initialises once and warns about nothing.
|
|
1034
|
+
- **A second copy of the script on the same page no longer replaces the first.**
|
|
1035
|
+
Each snippet loads the whole bundle, so a two-snippet page ran the client
|
|
1036
|
+
twice: the second core published a fresh, uninitialised engine over
|
|
1037
|
+
`window.ConsentKit` and the second UI layer mounted its own banner, leaving
|
|
1038
|
+
the real config applied to an engine nothing pointed at any more — a banner
|
|
1039
|
+
drawn from the built-in defaults. The second copy now stands down: the first
|
|
1040
|
+
engine keeps its state, its DOM patches and its observer, and the page mounts
|
|
1041
|
+
exactly one banner.
|
|
1042
|
+
|
|
1043
|
+
### 0.5.13
|
|
1044
|
+
|
|
1045
|
+
- Host database: `fbcdn.net` → marketing (Facebook SDK chunks and plugin images); `aichat.md`, `bubble.aichat.md` → functional (chat widget); `challenges.cloudflare.com` (Cloudflare challenge / Turnstile) and `i.imgur.com` → infrastructure, never held.
|
|
1046
|
+
|
|
1026
1047
|
### 0.5.12
|
|
1027
1048
|
- **Services in the preferences panel are collapsed by default and compact.**
|
|
1028
1049
|
The group's «N services · M cookies» line became a disclosure button; opening
|
|
@@ -1139,7 +1160,7 @@ notes in
|
|
|
1139
1160
|
|
|
1140
1161
|
## Project status
|
|
1141
1162
|
|
|
1142
|
-
**This is a prototype (v0.5.
|
|
1163
|
+
**This is a prototype (v0.5.14), not a released product.** It is honest about
|
|
1143
1164
|
what has been verified and what has not.
|
|
1144
1165
|
|
|
1145
1166
|
### Verified
|
package/npm/internal-stub.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export function undecidedState() {
|
|
|
28
28
|
*/
|
|
29
29
|
export function createStub() {
|
|
30
30
|
const stub = {
|
|
31
|
-
version: '0.5.
|
|
31
|
+
version: '0.5.14',
|
|
32
32
|
config: {},
|
|
33
33
|
init: function () { return undecidedState(); },
|
|
34
34
|
allowed: function (cat) { return cat === 'necessary'; },
|
package/package.json
CHANGED
package/src/ck-core.js
CHANGED
|
@@ -9,6 +9,29 @@
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
11
|
if (!global) { return; }
|
|
12
|
+
|
|
13
|
+
/* SPEC §1.9 — a second copy of ck.js on the page stands down.
|
|
14
|
+
A page can carry two snippets: an old page-level block with a dead site id
|
|
15
|
+
that nobody ever removed, plus the site-wide one. Each <script> loads the
|
|
16
|
+
WHOLE bundle, so this file runs twice — and without this guard the second
|
|
17
|
+
run would publish a fresh, uninitialised engine over `window.ConsentKit`.
|
|
18
|
+
Everything the first engine did stays behind on an object nothing points at
|
|
19
|
+
any more: its consent state, and the loader that is mid-fetch holding a
|
|
20
|
+
`CK` reference to it. The page would then end on the default config with a
|
|
21
|
+
banner drawn from default texts, while the real config was applied to the
|
|
22
|
+
orphan.
|
|
23
|
+
Standing down here is also what the two loaders' arbitration in ck-saas.js
|
|
24
|
+
assumes: `__ckSaas` coordinates them on the premise that both are talking
|
|
25
|
+
to the SAME core, so `shared.done` genuinely means "this page is
|
|
26
|
+
initialised" rather than "some object was initialised".
|
|
27
|
+
The patches, the MutationObserver and the initial scan are already live
|
|
28
|
+
from the first run; re-installing them would double every interception.
|
|
29
|
+
`init` is the test rather than mere presence, because ck-ui-branding.js
|
|
30
|
+
pre-creates a bare `window.ConsentKit = {}` when it happens to load first.
|
|
31
|
+
That object has no `init`, so we fall through and publish over it exactly
|
|
32
|
+
as before. */
|
|
33
|
+
if (global.ConsentKit && typeof global.ConsentKit.init === 'function') { return; }
|
|
34
|
+
|
|
12
35
|
var doc = global.document;
|
|
13
36
|
|
|
14
37
|
// ---------------------------------------------------------------------------
|
|
@@ -92,6 +115,7 @@
|
|
|
92
115
|
|
|
93
116
|
// --- marketing -----------------------------------------------------
|
|
94
117
|
'connect.facebook.net': 'marketing',
|
|
118
|
+
'fbcdn.net': 'marketing', // Facebook CDN: SDK chunks (static.xx) and plugin images (scontent.*)
|
|
95
119
|
'facebook.net': 'marketing', // connect.facebook.net covered above
|
|
96
120
|
'analytics.tiktok.com': 'marketing',
|
|
97
121
|
'googleadservices.com': 'marketing',
|
|
@@ -205,6 +229,7 @@
|
|
|
205
229
|
// Booking and form embeds — the visitor came to the page to use them.
|
|
206
230
|
'calendly.com': 'functional',
|
|
207
231
|
'typeform.com': 'functional',
|
|
232
|
+
'aichat.md': 'functional', // aichat.md chat widget (bubble.aichat.md loader)
|
|
208
233
|
// 999.md — the Moldovan classifieds platform. Shops embed its listing
|
|
209
234
|
// widgets; simpalsmedia.com is the group's asset host that serves them.
|
|
210
235
|
'999.md': 'functional',
|
|
@@ -432,6 +457,9 @@
|
|
|
432
457
|
// places is classified, not waved through — so a bare `cloudflare.com` here
|
|
433
458
|
// would be a bug.
|
|
434
459
|
var INFRA_DB = [
|
|
460
|
+
// --- bot checks and image hosts: not a consent decision -------------
|
|
461
|
+
'challenges.cloudflare.com', // Cloudflare challenge / Turnstile (incl. *.challenges.cloudflare.com)
|
|
462
|
+
'i.imgur.com', // hot-linked images
|
|
435
463
|
// --- site builders and hosting platforms ---------------------------
|
|
436
464
|
'tildacdn.com',
|
|
437
465
|
'tildacdn.net',
|
|
@@ -1973,7 +2001,7 @@
|
|
|
1973
2001
|
// Public API
|
|
1974
2002
|
// ---------------------------------------------------------------------------
|
|
1975
2003
|
var ConsentKit = {
|
|
1976
|
-
version: '0.5.
|
|
2004
|
+
version: '0.5.14',
|
|
1977
2005
|
config: config,
|
|
1978
2006
|
|
|
1979
2007
|
init: function (userConfig) {
|
package/src/ck-debug.js
CHANGED
|
@@ -272,6 +272,7 @@
|
|
|
272
272
|
source: 'источник',
|
|
273
273
|
srcSaas: 'SaaS',
|
|
274
274
|
srcInline: 'инлайн',
|
|
275
|
+
twoSnippets: 'два снипета',
|
|
275
276
|
secConsent: 'Согласие',
|
|
276
277
|
decidedAt: 'решение',
|
|
277
278
|
method: 'способ',
|
|
@@ -348,6 +349,7 @@
|
|
|
348
349
|
source: 'source',
|
|
349
350
|
srcSaas: 'SaaS',
|
|
350
351
|
srcInline: 'inline',
|
|
352
|
+
twoSnippets: 'two snippets',
|
|
351
353
|
secConsent: 'Consent',
|
|
352
354
|
decidedAt: 'decided',
|
|
353
355
|
method: 'method',
|
|
@@ -423,6 +425,7 @@
|
|
|
423
425
|
source: 'sursă',
|
|
424
426
|
srcSaas: 'SaaS',
|
|
425
427
|
srcInline: 'inline',
|
|
428
|
+
twoSnippets: 'două fragmente',
|
|
426
429
|
secConsent: 'Consimțământ',
|
|
427
430
|
decidedAt: 'decizie',
|
|
428
431
|
method: 'mod',
|
|
@@ -910,14 +913,26 @@
|
|
|
910
913
|
body.textContent = '';
|
|
911
914
|
|
|
912
915
|
// 1. Client
|
|
916
|
+
/* Read straight off _saas rather than through buildReport(): more than one
|
|
917
|
+
snippet on a page is a fact about the page, not about the report, and it
|
|
918
|
+
is worth surfacing because the symptom it explains — a banner configured
|
|
919
|
+
by an id the owner did not expect — is otherwise invisible. */
|
|
920
|
+
var siteIds = null;
|
|
921
|
+
try {
|
|
922
|
+
var ids = CK && CK._saas && CK._saas.siteIds;
|
|
923
|
+
if (ids && ids.length > 1) { siteIds = ids.join(', '); }
|
|
924
|
+
} catch (e) { siteIds = null; }
|
|
925
|
+
|
|
913
926
|
var s1 = section(T.secClient);
|
|
914
|
-
|
|
927
|
+
var rows1 = [
|
|
915
928
|
[T.version, r.client.version],
|
|
916
929
|
[T.source, r.client.source === 'saas' ? T.srcSaas : T.srcInline],
|
|
917
|
-
['siteId', r.client.siteId]
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
])
|
|
930
|
+
['siteId', r.client.siteId]
|
|
931
|
+
];
|
|
932
|
+
if (siteIds) { rows1.push([T.twoSnippets, siteIds]); }
|
|
933
|
+
rows1.push(['policyVersion', r.client.policyVersion]);
|
|
934
|
+
rows1.push(['ETag', r.client.etag]);
|
|
935
|
+
s1.appendChild(defs(rows1));
|
|
921
936
|
body.appendChild(s1);
|
|
922
937
|
|
|
923
938
|
// 2. Consent
|
package/src/ck-saas.js
CHANGED
|
@@ -64,11 +64,35 @@
|
|
|
64
64
|
// document.currentScript is correct while this file is executing.
|
|
65
65
|
var cur = doc.currentScript;
|
|
66
66
|
if (cur && cur.getAttribute && cur.getAttribute('data-ck-id')) { return cur; }
|
|
67
|
+
// Fallback for the exotic case where currentScript is unavailable (an
|
|
68
|
+
// async injection, an old browser). With two snippets on the page both
|
|
69
|
+
// loaders would pick the same last tag — which the §1.2 stand-down below
|
|
70
|
+
// turns into "the second one goes quiet" rather than a double init. Not
|
|
71
|
+
// ideal, but it degrades in the safe direction, and every real snippet is
|
|
72
|
+
// a plain synchronous <script> where currentScript is set.
|
|
67
73
|
var all = doc.querySelectorAll('script[data-ck-id]');
|
|
68
74
|
return all && all.length ? all[all.length - 1] : null;
|
|
69
75
|
} catch (e) { return null; }
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
// Every distinct data-ck-id on the page, in document order. Read from the DOM
|
|
79
|
+
// rather than accumulated across loaders, because on a real page the second
|
|
80
|
+
// snippet's tag may not be parsed yet when the first loader runs — the last
|
|
81
|
+
// loader to execute sees them all, and it is the one that matters.
|
|
82
|
+
function tagIds() {
|
|
83
|
+
var out = [];
|
|
84
|
+
try {
|
|
85
|
+
var all = doc.querySelectorAll('script[data-ck-id]');
|
|
86
|
+
if (!all) { return out; }
|
|
87
|
+
for (var i = 0; i < all.length; i++) {
|
|
88
|
+
var id = '';
|
|
89
|
+
try { id = String(all[i].getAttribute('data-ck-id') || '').trim(); } catch (e2) { id = ''; }
|
|
90
|
+
if (id && out.indexOf(id) === -1) { out.push(id); }
|
|
91
|
+
}
|
|
92
|
+
} catch (e) { /* noop */ }
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
var tag = findOwnTag();
|
|
73
97
|
if (!tag) { return; } // no data-ck-id -> standalone page, stay inert
|
|
74
98
|
|
|
@@ -87,6 +111,54 @@
|
|
|
87
111
|
return;
|
|
88
112
|
}
|
|
89
113
|
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Shared state across every loader on the page (SPEC §1.1)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/* A page can carry more than one snippet: a site migrated between two ids and
|
|
118
|
+
the old block was never removed, an agency pasted its own on top of the
|
|
119
|
+
client's. Each copy of this file is a separate IIFE with its own closure, so
|
|
120
|
+
the only thing they can agree on is a global.
|
|
121
|
+
|
|
122
|
+
`pending` here counts CONFIG FETCHES still in flight — deliberately not the
|
|
123
|
+
same thing as the beacon queue further down, which is also called `pending`
|
|
124
|
+
but lives in this file's closure and is what _saas.pending() reports.
|
|
125
|
+
|
|
126
|
+
`warnedDup` and `activeId` are not in the spec's field list, but the once-
|
|
127
|
+
only duplicate warning and the "driven by snippet X" message have nowhere
|
|
128
|
+
else to live: whichever loader speaks last would otherwise repeat the
|
|
129
|
+
warning, and the loser needs the winner's id. */
|
|
130
|
+
var shared = global.__ckSaas;
|
|
131
|
+
if (!shared || typeof shared !== 'object') {
|
|
132
|
+
shared = global.__ckSaas = { pending: 0, done: false, ids: [], failures: [] };
|
|
133
|
+
}
|
|
134
|
+
if (!shared.ids || typeof shared.ids.length !== 'number') { shared.ids = []; }
|
|
135
|
+
if (!shared.failures || typeof shared.failures.length !== 'number') { shared.failures = []; }
|
|
136
|
+
|
|
137
|
+
/* SPEC §1.2 — the same id twice is Tilda duplicating the head block, not two
|
|
138
|
+
snippets. It is not a misconfiguration, so it must not warn; but it must
|
|
139
|
+
also not fetch or initialise a second time, or the page would pay for two
|
|
140
|
+
config requests and CK.init() would run twice. Stand down silently, and
|
|
141
|
+
BEFORE pending++ so the vanished loader cannot hold the strict fallback
|
|
142
|
+
hostage in §1.4. */
|
|
143
|
+
if (shared.ids.indexOf(siteId) !== -1) { return; }
|
|
144
|
+
shared.ids.push(siteId);
|
|
145
|
+
shared.pending++;
|
|
146
|
+
|
|
147
|
+
var allIds = tagIds();
|
|
148
|
+
// A tag whose loader has not executed yet is still a snippet on this page.
|
|
149
|
+
for (var ai = 0; ai < shared.ids.length; ai++) {
|
|
150
|
+
if (allIds.indexOf(shared.ids[ai]) === -1) { allIds.push(shared.ids[ai]); }
|
|
151
|
+
}
|
|
152
|
+
if (allIds.length > 1 && !shared.warnedDup) {
|
|
153
|
+
shared.warnedDup = true;
|
|
154
|
+
var others = [];
|
|
155
|
+
for (var oi = 0; oi < allIds.length; oi++) {
|
|
156
|
+
if (allIds[oi] !== siteId) { others.push(allIds[oi]); }
|
|
157
|
+
}
|
|
158
|
+
warn('another ConsentKit snippet on this page uses site id ' + others.join(', ') +
|
|
159
|
+
'; keep one snippet per site');
|
|
160
|
+
}
|
|
161
|
+
|
|
90
162
|
var cacheKey = CACHE_PREFIX + siteId;
|
|
91
163
|
var activeConfig = null; // config currently driving this page load
|
|
92
164
|
|
|
@@ -142,13 +214,64 @@
|
|
|
142
214
|
} catch (e) { return 0; }
|
|
143
215
|
}
|
|
144
216
|
|
|
217
|
+
/* This loader's own config attempt resolves exactly once. Both the cold path
|
|
218
|
+
and the cached path run through here, and the cached path is the reason the
|
|
219
|
+
guard exists at all: it settles SUCCESSFULLY the moment the cache is read,
|
|
220
|
+
and then starts a background revalidation whose failure must not decrement
|
|
221
|
+
`pending` a second time or push a failure the page never suffered. */
|
|
222
|
+
var settled = false;
|
|
223
|
+
function settleOk() {
|
|
224
|
+
if (settled) { return; }
|
|
225
|
+
settled = true;
|
|
226
|
+
shared.done = true;
|
|
227
|
+
shared.activeId = shared.activeId || siteId;
|
|
228
|
+
shared.pending--;
|
|
229
|
+
}
|
|
230
|
+
|
|
145
231
|
function initWith(config, why) {
|
|
232
|
+
settleOk();
|
|
146
233
|
activeConfig = config;
|
|
147
234
|
applyHostDb(config);
|
|
148
235
|
try { CK.init(config); } catch (e) { error('init() failed: ' + (e && e.message)); }
|
|
149
236
|
if (why) { /* reserved for diagnostics */ }
|
|
150
237
|
}
|
|
151
238
|
|
|
239
|
+
/* SPEC §1.4 — one loader's config failed. What follows depends entirely on
|
|
240
|
+
what the OTHER loaders are doing, which is why none of this can be decided
|
|
241
|
+
locally.
|
|
242
|
+
|
|
243
|
+
The case that motivates the whole section: a site has a live snippet and a
|
|
244
|
+
dead one (an id that was deleted from the account). Before 0.5.14 the dead
|
|
245
|
+
one's 404 raised the strict fallback and re-initialised the core with
|
|
246
|
+
everything denied — a working banner replaced by a broken one because of a
|
|
247
|
+
block nobody had noticed in years. Now the dead snippet only fails; the
|
|
248
|
+
live snippet still drives the page. */
|
|
249
|
+
function settleFail(reason) {
|
|
250
|
+
if (settled) { return; }
|
|
251
|
+
settled = true;
|
|
252
|
+
shared.pending--;
|
|
253
|
+
shared.failures.push(siteId + ': ' + reason);
|
|
254
|
+
|
|
255
|
+
if (shared.done) {
|
|
256
|
+
// Someone else already initialised the page. Touching CK.init() now would
|
|
257
|
+
// swap ConsentKit.config identity out from under a banner the visitor may
|
|
258
|
+
// already be looking at.
|
|
259
|
+
warn('config for ' + siteId + ' unavailable (' + reason + '); the page is driven by snippet ' +
|
|
260
|
+
(shared.activeId || 'unknown'));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (shared.pending > 0) {
|
|
264
|
+
// SPEC §1.5: the strict fallback is a last resort, and it is not the last
|
|
265
|
+
// resort while another snippet may still succeed. CFG_TIMEOUT_MS bounds
|
|
266
|
+
// that wait — every in-flight fetch aborts by then, so this cannot hang.
|
|
267
|
+
warn('config for ' + siteId + ' unavailable (' + reason + '); waiting for the other snippet');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Every snippet on the page failed. This is the 0.5.13 behaviour, and for a
|
|
271
|
+
// single tag the joined list is exactly one entry.
|
|
272
|
+
initStrict(shared.failures.join('; '));
|
|
273
|
+
}
|
|
274
|
+
|
|
152
275
|
// Strict FALLBACK: banner shows, every opt-in category stays off, no journal.
|
|
153
276
|
// policyVersion 'strict-fallback' deliberately mismatches any stored consent,
|
|
154
277
|
// so a previous decision is not silently reused when the server is unreachable.
|
|
@@ -161,13 +284,28 @@
|
|
|
161
284
|
// Known trackers are still blocked, as always.
|
|
162
285
|
function initStrict(reason) {
|
|
163
286
|
warn('config unavailable (' + reason + ') — strict fallback: banner shown, all opt-in categories denied, journal disabled.');
|
|
287
|
+
shared.strictFallback = true;
|
|
288
|
+
// A _saas already published by another loader must learn this too: the
|
|
289
|
+
// object below is a plain assignment and the last loader to run wins, so a
|
|
290
|
+
// late publisher would otherwise report strictFallback: false.
|
|
291
|
+
try { if (CK._saas) { CK._saas.strictFallback = true; } } catch (e) { /* noop */ }
|
|
164
292
|
initWith({ policyVersion: 'strict-fallback' });
|
|
165
293
|
}
|
|
166
294
|
|
|
167
295
|
// cacheMode: 'default' lets the HTTP cache answer (cold load); 'no-cache'
|
|
168
296
|
// forces a conditional request to the origin (background revalidation).
|
|
169
297
|
function fetchConfig(etag, cacheMode, onOk, onFail) {
|
|
170
|
-
if (typeof global.fetch !== 'function') {
|
|
298
|
+
if (typeof global.fetch !== 'function') {
|
|
299
|
+
/* Deferred to a microtask rather than called inline. Every other failure
|
|
300
|
+
path here is already asynchronous, so a synchronous one would be the
|
|
301
|
+
single case where this loader could reach the strict fallback before a
|
|
302
|
+
later snippet on the page had a chance to register itself in
|
|
303
|
+
__ckSaas.pending. A microtask, not a timer: it still resolves before
|
|
304
|
+
any network could. */
|
|
305
|
+
try { global.Promise.resolve().then(function () { onFail('fetch unsupported'); }); }
|
|
306
|
+
catch (e) { onFail('fetch unsupported'); }
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
171
309
|
var ctrl = null, timer = null;
|
|
172
310
|
try { ctrl = new global.AbortController(); } catch (e) { ctrl = null; }
|
|
173
311
|
var opts = { method: 'GET', credentials: 'omit', mode: 'cors' };
|
|
@@ -236,12 +374,13 @@
|
|
|
236
374
|
// Cold path: no cached config exists, so the HTTP cache cannot serve a
|
|
237
375
|
// stale one. Default caching is the right economy here.
|
|
238
376
|
fetchConfig(null, null, function (r) {
|
|
239
|
-
|
|
377
|
+
// An empty 304 without a cache to satisfy it is a failure like any other,
|
|
378
|
+
// and now goes through the same arbitration: another snippet may still be
|
|
379
|
+
// holding a usable config.
|
|
380
|
+
if (r.notModified || !r.config) { settleFail('empty response without cache'); return; }
|
|
240
381
|
writeCache(r.etag, r.config);
|
|
241
382
|
initWith(r.config, 'network');
|
|
242
|
-
},
|
|
243
|
-
initStrict(reason);
|
|
244
|
-
});
|
|
383
|
+
}, settleFail);
|
|
245
384
|
}
|
|
246
385
|
|
|
247
386
|
// ---------------------------------------------------------------------------
|
|
@@ -421,10 +560,19 @@
|
|
|
421
560
|
global.addEventListener && global.addEventListener('pagehide', flush, false);
|
|
422
561
|
} catch (e) { /* noop */ }
|
|
423
562
|
|
|
424
|
-
|
|
563
|
+
/* Minimal surface for the demo status panel; not a public API.
|
|
564
|
+
|
|
565
|
+
With two loaders this is assigned twice and the last one wins, so nothing
|
|
566
|
+
that must survive may be read out of this closure: `siteIds` comes from the
|
|
567
|
+
DOM and `strictFallback` from the shared object, both of which every loader
|
|
568
|
+
agrees on. `pending()` still reports the BEACON queue — a different counter
|
|
569
|
+
from __ckSaas.pending, and the one the demo page and the panel already
|
|
570
|
+
read. */
|
|
425
571
|
CK._saas = {
|
|
426
572
|
siteId: siteId,
|
|
427
573
|
api: apiBase,
|
|
574
|
+
siteIds: allIds.slice(),
|
|
575
|
+
strictFallback: shared.strictFallback === true,
|
|
428
576
|
pending: function () { return pending.length; },
|
|
429
577
|
config: function () { return activeConfig; }
|
|
430
578
|
};
|
package/src/ck-ui.js
CHANGED
|
@@ -5,6 +5,18 @@
|
|
|
5
5
|
(function () {
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
/* SPEC §1.9 — the second copy of ck.js on the page draws nothing.
|
|
9
|
+
Two snippets each load the whole bundle, so this file runs twice. Every
|
|
10
|
+
copy is its own IIFE with its own `mounted` flag, so a second copy would
|
|
11
|
+
register a second set of ck:* listeners and mount a second banner into the
|
|
12
|
+
same #ck-root — the first copy's, which it cannot see. ck-core.js stands
|
|
13
|
+
down for the same reason; this is the UI half of it.
|
|
14
|
+
SSR-safe: with no window we behave exactly as before and fall through. The
|
|
15
|
+
flag is claimed further down, after the `typeof document` guard, so a copy
|
|
16
|
+
that only ever published the pure `_contrast` helpers into a DOM-less
|
|
17
|
+
context does not lock out a later copy that could actually render. */
|
|
18
|
+
if (typeof window !== 'undefined' && window.__ckUiLoaded) { return; }
|
|
19
|
+
|
|
8
20
|
var OPT_IN = ['functional', 'analytics', 'marketing'];
|
|
9
21
|
var ALL_CATS = ['necessary'].concat(OPT_IN);
|
|
10
22
|
|
|
@@ -2971,6 +2983,11 @@
|
|
|
2971
2983
|
// this file in Node is a no-op rather than a throw (mirrors the core).
|
|
2972
2984
|
if (typeof document === 'undefined') return;
|
|
2973
2985
|
|
|
2986
|
+
// Claim the page for this copy (SPEC §1.9, guard at the top of the file).
|
|
2987
|
+
// Here rather than at the top: only now does this copy take ownership of the
|
|
2988
|
+
// listeners and the mount, and only that ownership is worth locking.
|
|
2989
|
+
try { if (typeof window !== 'undefined') { window.__ckUiLoaded = true; } } catch (e) { /* noop */ }
|
|
2990
|
+
|
|
2974
2991
|
document.addEventListener('ck:init', function (e) {
|
|
2975
2992
|
var d = (e && e.detail) || {};
|
|
2976
2993
|
var cfg = d.config || safeConfig();
|