@ecomconsult/consentkit 0.5.13 → 0.5.15

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/src/ck-locales.js CHANGED
@@ -8,7 +8,11 @@
8
8
  + uk tr no nb is sr ca sq mk
9
9
 
10
10
  Key set mirrors the builtin en dictionary of ck-ui.js exactly:
11
- 19 flat strings + cat.{necessary,functional,analytics,marketing}.{title,desc}.
11
+ 19 flat strings + cat.{necessary,functional,analytics,marketing}.{title,desc},
12
+ plus `extraTitle` (SPEC V1.16 §1.2 — the heading of the «Дополнительно» card
13
+ in the settings panel). Its BODY, `extraText`, is deliberately not here: the
14
+ block renders only from what the site itself supplies, so a translated body
15
+ would put an empty operator card on every site that never configured one.
12
16
 
13
17
  NOTE: these translations are drafts produced for the prototype. They MUST be reviewed
14
18
  by native speakers before production use — legal wording (refusal, "always on") is the
@@ -40,6 +44,7 @@
40
44
  colPurpose: 'Цел',
41
45
  colExpiry: 'Изтича',
42
46
  floating: 'Настройки на бисквитките',
47
+ extraTitle: 'Допълнителна информация',
43
48
  cat: {
44
49
  necessary: { title: 'Необходими', desc: 'Нужни са, за да работи сайтът — вход, сигурност, запомняне на съгласието ви. Не могат да се изключат.' },
45
50
  functional: { title: 'Функционални', desc: 'Запомнят предпочитанията ви, например език или чат, за да не ги задавате отново.' },
@@ -68,6 +73,7 @@
68
73
  colPurpose: 'Účel',
69
74
  colExpiry: 'Platnost',
70
75
  floating: 'Nastavení cookies',
76
+ extraTitle: 'Doplňující informace',
71
77
  cat: {
72
78
  necessary: { title: 'Nezbytné', desc: 'Bez nich web nefunguje — přihlášení, bezpečnost, uložení vašeho souhlasu. Nelze je vypnout.' },
73
79
  functional: { title: 'Funkční', desc: 'Pamatují si vaše předvolby, třeba jazyk nebo chat, abyste je nenastavovali znovu.' },
@@ -96,6 +102,7 @@
96
102
  colPurpose: 'Formål',
97
103
  colExpiry: 'Udløber',
98
104
  floating: 'Cookieindstillinger',
105
+ extraTitle: 'Yderligere oplysninger',
99
106
  cat: {
100
107
  necessary: { title: 'Nødvendige', desc: 'Nødvendige for at siden virker — login, sikkerhed, hukommelse om dit samtykke. De kan ikke slås fra.' },
101
108
  functional: { title: 'Funktionelle', desc: 'Husker dine indstillinger, fx sprog eller chat, så du ikke skal vælge dem igen.' },
@@ -124,6 +131,7 @@
124
131
  colPurpose: 'Zweck',
125
132
  colExpiry: 'Laufzeit',
126
133
  floating: 'Cookie-Einstellungen',
134
+ extraTitle: 'Weitere Informationen',
127
135
  cat: {
128
136
  necessary: { title: 'Notwendig', desc: 'Nötig, damit die Website funktioniert — Anmeldung, Sicherheit, Speichern Ihrer Einwilligung. Sie lassen sich nicht abschalten.' },
129
137
  functional: { title: 'Funktional', desc: 'Merken sich Ihre Einstellungen, etwa Sprache oder Chat, damit Sie sie nicht erneut wählen müssen.' },
@@ -152,6 +160,7 @@
152
160
  colPurpose: 'Σκοπός',
153
161
  colExpiry: 'Λήξη',
154
162
  floating: 'Ρυθμίσεις cookies',
163
+ extraTitle: 'Πρόσθετες πληροφορίες',
155
164
  cat: {
156
165
  necessary: { title: 'Απαραίτητα', desc: 'Χρειάζονται για να λειτουργεί ο ιστότοπος — σύνδεση, ασφάλεια, διατήρηση της επιλογής σας. Δεν απενεργοποιούνται.' },
157
166
  functional: { title: 'Λειτουργικά', desc: 'Θυμούνται τις προτιμήσεις σας, όπως γλώσσα ή συνομιλία, ώστε να μην τις ορίζετε ξανά.' },
@@ -180,6 +189,7 @@
180
189
  colPurpose: 'Finalidad',
181
190
  colExpiry: 'Caduca',
182
191
  floating: 'Configuración de cookies',
192
+ extraTitle: 'Información adicional',
183
193
  cat: {
184
194
  necessary: { title: 'Necesarias', desc: 'Imprescindibles para que el sitio funcione: inicio de sesión, seguridad y memoria de su consentimiento. No se pueden desactivar.' },
185
195
  functional: { title: 'Funcionales', desc: 'Recuerdan sus preferencias, como el idioma o el chat, para que no tenga que configurarlas de nuevo.' },
@@ -208,6 +218,7 @@
208
218
  colPurpose: 'Eesmärk',
209
219
  colExpiry: 'Kehtivus',
210
220
  floating: 'Küpsiste seaded',
221
+ extraTitle: 'Lisateave',
211
222
  cat: {
212
223
  necessary: { title: 'Vajalikud', desc: 'Vajalikud saidi tööks — sisselogimine, turvalisus, teie nõusoleku meelespidamine. Neid ei saa välja lülitada.' },
213
224
  functional: { title: 'Funktsionaalsed', desc: 'Jätavad meelde teie eelistused, näiteks keele või vestluse, et te ei peaks neid uuesti valima.' },
@@ -236,6 +247,7 @@
236
247
  colPurpose: 'Tarkoitus',
237
248
  colExpiry: 'Voimassaolo',
238
249
  floating: 'Evästeasetukset',
250
+ extraTitle: 'Lisätiedot',
239
251
  cat: {
240
252
  necessary: { title: 'Välttämättömät', desc: 'Tarvitaan sivuston toimintaan: kirjautuminen, turvallisuus ja suostumuksenne muistaminen. Näitä ei voi poistaa käytöstä.' },
241
253
  functional: { title: 'Toiminnalliset', desc: 'Muistavat asetuksenne, kuten kielen tai chatin, jottei niitä tarvitse valita uudelleen.' },
@@ -264,6 +276,7 @@
264
276
  colPurpose: 'Finalité',
265
277
  colExpiry: 'Expiration',
266
278
  floating: 'Paramètres des cookies',
279
+ extraTitle: 'Informations complémentaires',
267
280
  cat: {
268
281
  necessary: { title: 'Nécessaires', desc: 'Indispensables au fonctionnement du site : connexion, sécurité, mémorisation de votre choix. Ils ne peuvent pas être désactivés.' },
269
282
  functional: { title: 'Fonctionnels', desc: 'Retiennent vos préférences, par exemple la langue ou le chat, pour ne pas avoir à les régler de nouveau.' },
@@ -292,6 +305,7 @@
292
305
  colPurpose: 'Cuspóir',
293
306
  colExpiry: 'Éagann',
294
307
  floating: 'Socruithe fianán',
308
+ extraTitle: 'Eolas breise',
295
309
  cat: {
296
310
  necessary: { title: 'Riachtanach', desc: 'Ag teastáil chun an suíomh a oibriú — logáil isteach, slándáil, do thoiliú a choinneáil. Ní féidir iad a mhúchadh.' },
297
311
  functional: { title: 'Feidhmiúil', desc: 'Coinníonn siad do roghanna, mar shampla an teanga nó an comhrá, ionas nach gá duit iad a shocrú arís.' },
@@ -320,6 +334,7 @@
320
334
  colPurpose: 'Svrha',
321
335
  colExpiry: 'Istječe',
322
336
  floating: 'Postavke kolačića',
337
+ extraTitle: 'Dodatne informacije',
323
338
  cat: {
324
339
  necessary: { title: 'Nužni', desc: 'Potrebni su za rad stranice — prijava, sigurnost, pamćenje vaše privole. Ne mogu se isključiti.' },
325
340
  functional: { title: 'Funkcionalni', desc: 'Pamte vaše postavke, primjerice jezik ili chat, da ih ne morate ponovno birati.' },
@@ -348,6 +363,7 @@
348
363
  colPurpose: 'Cél',
349
364
  colExpiry: 'Lejárat',
350
365
  floating: 'Sütibeállítások',
366
+ extraTitle: 'További információk',
351
367
  cat: {
352
368
  necessary: { title: 'Szükséges', desc: 'Az oldal működéséhez kellenek: belépés, biztonság, a hozzájárulása megjegyzése. Nem kapcsolhatók ki.' },
353
369
  functional: { title: 'Funkcionális', desc: 'Megjegyzik a beállításait, például a nyelvet vagy a chatet, hogy ne kelljen újra megadnia őket.' },
@@ -376,6 +392,7 @@
376
392
  colPurpose: 'Finalità',
377
393
  colExpiry: 'Scadenza',
378
394
  floating: 'Impostazioni dei cookie',
395
+ extraTitle: 'Informazioni aggiuntive',
379
396
  cat: {
380
397
  necessary: { title: 'Necessari', desc: 'Servono al funzionamento del sito: accesso, sicurezza, memoria del suo consenso. Non possono essere disattivati.' },
381
398
  functional: { title: 'Funzionali', desc: 'Ricordano le sue preferenze, come la lingua o la chat, così non deve impostarle di nuovo.' },
@@ -404,6 +421,7 @@
404
421
  colPurpose: 'Paskirtis',
405
422
  colExpiry: 'Galiojimas',
406
423
  floating: 'Slapukų nustatymai',
424
+ extraTitle: 'Papildoma informacija',
407
425
  cat: {
408
426
  necessary: { title: 'Būtini', desc: 'Reikalingi, kad svetainė veiktų: prisijungimas, saugumas, jūsų sutikimo įsiminimas. Jų išjungti negalima.' },
409
427
  functional: { title: 'Funkciniai', desc: 'Įsimena jūsų nustatymus, pavyzdžiui, kalbą ar pokalbius, kad jų nereikėtų nurodyti iš naujo.' },
@@ -432,6 +450,7 @@
432
450
  colPurpose: 'Nolūks',
433
451
  colExpiry: 'Derīgums',
434
452
  floating: 'Sīkdatņu iestatījumi',
453
+ extraTitle: 'Papildu informācija',
435
454
  cat: {
436
455
  necessary: { title: 'Nepieciešamās', desc: 'Vajadzīgas vietnes darbībai — pieteikšanās, drošība, jūsu piekrišanas saglabāšana. Tās nevar izslēgt.' },
437
456
  functional: { title: 'Funkcionālās', desc: 'Atceras jūsu iestatījumus, piemēram, valodu vai tērzēšanu, lai tie nebūtu jānorāda atkārtoti.' },
@@ -460,6 +479,7 @@
460
479
  colPurpose: 'Skop',
461
480
  colExpiry: 'Skadenza',
462
481
  floating: 'Issettjar tal-cookies',
482
+ extraTitle: 'Informazzjoni addizzjonali',
463
483
  cat: {
464
484
  necessary: { title: 'Meħtieġa', desc: 'Meħtieġa biex is-sit jaħdem — login, sigurtà, tifkira tal-kunsens tiegħek. Ma jistgħux jintfew.' },
465
485
  functional: { title: 'Funzjonali', desc: 'Jiftakru l-preferenzi tiegħek, bħal-lingwa jew iċ-chat, biex ma jkollokx terġa’ tissettjahom.' },
@@ -488,6 +508,7 @@
488
508
  colPurpose: 'Doel',
489
509
  colExpiry: 'Vervalt',
490
510
  floating: 'Cookie-instellingen',
511
+ extraTitle: 'Aanvullende informatie',
491
512
  cat: {
492
513
  necessary: { title: 'Noodzakelijk', desc: 'Nodig om de site te laten werken: inloggen, beveiliging en het onthouden van uw keuze. Ze kunnen niet uit.' },
493
514
  functional: { title: 'Functioneel', desc: 'Onthouden uw voorkeuren, zoals taal of chat, zodat u ze niet opnieuw hoeft in te stellen.' },
@@ -516,6 +537,7 @@
516
537
  colPurpose: 'Cel',
517
538
  colExpiry: 'Wygasa',
518
539
  floating: 'Ustawienia plików cookie',
540
+ extraTitle: 'Informacje dodatkowe',
519
541
  cat: {
520
542
  necessary: { title: 'Niezbędne', desc: 'Potrzebne, by strona działała: logowanie, bezpieczeństwo, zapamiętanie zgody. Nie można ich wyłączyć.' },
521
543
  functional: { title: 'Funkcjonalne', desc: 'Zapamiętują ustawienia, na przykład język lub czat, żeby nie trzeba było wybierać ich ponownie.' },
@@ -544,6 +566,7 @@
544
566
  colPurpose: 'Finalidade',
545
567
  colExpiry: 'Validade',
546
568
  floating: 'Definições de cookies',
569
+ extraTitle: 'Informações adicionais',
547
570
  cat: {
548
571
  necessary: { title: 'Necessários', desc: 'Precisos para o site funcionar: início de sessão, segurança e memória do seu consentimento. Não podem ser desativados.' },
549
572
  functional: { title: 'Funcionais', desc: 'Guardam as suas preferências, como o idioma ou o chat, para não ter de as definir outra vez.' },
@@ -572,6 +595,7 @@
572
595
  colPurpose: 'Pentru ce',
573
596
  colExpiry: 'Expiră',
574
597
  floating: 'Setări cookie-uri',
598
+ extraTitle: 'Informații suplimentare',
575
599
  // SPEC V1.10 §2 — заглушка встраивания. Only ro carries these here: en and
576
600
  // ru are builtin in ck-ui.js, and every other locale falls back to en.
577
601
  phText: 'Aici este conținut de la {host}. Se va încărca după acordul pentru «{cat}».',
@@ -616,6 +640,7 @@
616
640
  colPurpose: 'Účel',
617
641
  colExpiry: 'Platnosť',
618
642
  floating: 'Nastavenia cookies',
643
+ extraTitle: 'Doplňujúce informácie',
619
644
  cat: {
620
645
  necessary: { title: 'Nevyhnutné', desc: 'Bez nich stránka nefunguje — prihlásenie, bezpečnosť, zapamätanie vášho súhlasu. Nedajú sa vypnúť.' },
621
646
  functional: { title: 'Funkčné', desc: 'Pamätajú si vaše nastavenia, napríklad jazyk alebo chat, aby ste ich nezadávali znova.' },
@@ -644,6 +669,7 @@
644
669
  colPurpose: 'Namen',
645
670
  colExpiry: 'Poteče',
646
671
  floating: 'Nastavitve piškotkov',
672
+ extraTitle: 'Dodatne informacije',
647
673
  cat: {
648
674
  necessary: { title: 'Nujni', desc: 'Potrebni za delovanje spletnega mesta — prijava, varnost, pomnjenje vašega soglasja. Ni jih mogoče izklopiti.' },
649
675
  functional: { title: 'Funkcionalni', desc: 'Zapomnijo si vaše nastavitve, na primer jezik ali klepet, da jih ni treba določati znova.' },
@@ -672,6 +698,7 @@
672
698
  colPurpose: 'Syfte',
673
699
  colExpiry: 'Upphör',
674
700
  floating: 'Cookieinställningar',
701
+ extraTitle: 'Ytterligare information',
675
702
  cat: {
676
703
  necessary: { title: 'Nödvändiga', desc: 'Krävs för att webbplatsen ska fungera — inloggning, säkerhet, minne av ditt samtycke. De kan inte stängas av.' },
677
704
  functional: { title: 'Funktionella', desc: 'Kommer ihåg dina inställningar, till exempel språk eller chatt, så att du slipper välja igen.' },
@@ -700,6 +727,7 @@
700
727
  colPurpose: 'Мета',
701
728
  colExpiry: 'Термін',
702
729
  floating: 'Налаштування cookie',
730
+ extraTitle: 'Додаткова інформація',
703
731
  cat: {
704
732
  necessary: { title: 'Необхідні', desc: 'Без них сайт не працює: вхід, безпека, пам’ять про ваш вибір. Вимкнути неможливо.' },
705
733
  functional: { title: 'Функціональні', desc: 'Запам’ятовують ваші налаштування — наприклад мову або чат, — щоб ви не задавали їх знову.' },
@@ -728,6 +756,7 @@
728
756
  colPurpose: 'Amaç',
729
757
  colExpiry: 'Süre',
730
758
  floating: 'Çerez ayarları',
759
+ extraTitle: 'Ek bilgiler',
731
760
  cat: {
732
761
  necessary: { title: 'Zorunlu', desc: 'Sitenin çalışması için gerekli: oturum açma, güvenlik, onayınızın hatırlanması. Kapatılamazlar.' },
733
762
  functional: { title: 'İşlevsel', desc: 'Dil veya sohbet gibi tercihlerinizi hatırlar, böylece yeniden ayarlamanız gerekmez.' },
@@ -756,6 +785,7 @@
756
785
  colPurpose: 'Formål',
757
786
  colExpiry: 'Utløper',
758
787
  floating: 'Innstillinger for informasjonskapsler',
788
+ extraTitle: 'Tilleggsinformasjon',
759
789
  cat: {
760
790
  necessary: { title: 'Nødvendige', desc: 'Kreves for at nettstedet skal virke — innlogging, sikkerhet, minne om samtykket ditt. De kan ikke slås av.' },
761
791
  functional: { title: 'Funksjonelle', desc: 'Husker innstillingene dine, som språk eller chat, så du slipper å velge på nytt.' },
@@ -788,6 +818,7 @@
788
818
  colPurpose: 'Tilgangur',
789
819
  colExpiry: 'Gildistími',
790
820
  floating: 'Stillingar vefkaka',
821
+ extraTitle: 'Viðbótarupplýsingar',
791
822
  cat: {
792
823
  necessary: { title: 'Nauðsynlegar', desc: 'Nauðsynlegar til að vefurinn virki — innskráning, öryggi, að muna samþykki þitt. Ekki er hægt að slökkva á þeim.' },
793
824
  functional: { title: 'Virknivefkökur', desc: 'Muna stillingar þínar, til dæmis tungumál eða spjall, svo þú þurfir ekki að velja aftur.' },
@@ -817,6 +848,7 @@
817
848
  colPurpose: 'Сврха',
818
849
  colExpiry: 'Истиче',
819
850
  floating: 'Подешавања колачића',
851
+ extraTitle: 'Додатне информације',
820
852
  cat: {
821
853
  necessary: { title: 'Неопходни', desc: 'Потребни су да би сајт радио — пријава, безбедност, памћење ваше сагласности. Не могу се искључити.' },
822
854
  functional: { title: 'Функционални', desc: 'Памте ваша подешавања, на пример језик или ћаскање, да их не бисте бирали поново.' },
@@ -845,6 +877,7 @@
845
877
  colPurpose: 'Finalitat',
846
878
  colExpiry: 'Caduca',
847
879
  floating: 'Configuració de galetes',
880
+ extraTitle: 'Informació addicional',
848
881
  cat: {
849
882
  necessary: { title: 'Necessàries', desc: 'Calen perquè el lloc funcioni: inici de sessió, seguretat i memòria del vostre consentiment. No es poden desactivar.' },
850
883
  functional: { title: 'Funcionals', desc: 'Recorden les vostres preferències, com l’idioma o el xat, perquè no les hàgiu de tornar a configurar.' },
@@ -873,6 +906,7 @@
873
906
  colPurpose: 'Qëllimi',
874
907
  colExpiry: 'Skadon',
875
908
  floating: 'Cilësimet e cookie-ve',
909
+ extraTitle: 'Informacione shtesë',
876
910
  cat: {
877
911
  necessary: { title: 'Të nevojshme', desc: 'Nevojiten që faqja të funksionojë — hyrja, siguria, ruajtja e pëlqimit tuaj. Nuk mund të fiken.' },
878
912
  functional: { title: 'Funksionale', desc: 'Mbajnë mend preferencat tuaja, si gjuha ose biseda, që të mos i vendosni sërish.' },
@@ -901,6 +935,7 @@
901
935
  colPurpose: 'Цел',
902
936
  colExpiry: 'Истекува',
903
937
  floating: 'Поставки за колачиња',
938
+ extraTitle: 'Дополнителни информации',
904
939
  cat: {
905
940
  necessary: { title: 'Неопходни', desc: 'Потребни се за страницата да работи — најава, безбедност, помнење на вашата согласност. Не можат да се исклучат.' },
906
941
  functional: { title: 'Функционални', desc: 'Ги паметат вашите поставки, на пример јазикот или разговорот, за да не ги задавате повторно.' },
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') { onFail('fetch unsupported'); return; }
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
- if (r.notModified || !r.config) { initStrict('empty response without cache'); return; }
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
- }, function (reason) {
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
- // Minimal surface for the demo status panel; not a public API.
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
  };