Passa al contenuto principale

Internals — @avacy/core

Non è API pubblica. Questa pagina documenta come funziona @avacy/core sotto la superficie di Tier 1/Tier 2 — bootstrap, storage, framework di consenso. È materiale per il team, punto di partenza per decidere insieme al PO cosa (se qualcosa) promuovere a pubblico.

Bootstrap e ciclo di vita interno

CoreContext inizializza i provider in un ordine fisso: config → logger → store → language → framework. Ogni provider è un singleton risolto via initProvider(name), che a sua volta chiama uno dei cinque metodi privati (initConfig, initStore, initLoggers, initLanguage, initFrameworks). L'ordine non è arbitrario: il framework provider ha bisogno della config già caricata e dello store già pronto per decodificare un consenso persistito.

I framework di consenso (tcf/gcm/acm/custom) e i backend di storage (cookie/local-storage) vivono in due registri separati, entrambi estendibili a runtime:

  • FRAMEWORK_PROVIDER_REGISTRY — popolato staticamente dai 4 framework built-in
  • STORE_PROVIDER_REGISTRY — popolato da cookie/local-storage, estendibile via registerStoreProvider(type, ProviderClass) (Tier 2, per chi scrive uno storage custom)

Lettura sincrona dello stato: getContextData()

Core.getContextData() (esposto anche come funzione standalone in index.ts) ritorna lo snapshot corrente del contesto — core, config, storage, language, consent, customization, glossary, stacks, più decodedConsent/acmAddtlConsent opzionali (ContextData, context.types.ts) — in modo sincrono, oppure null se il boot non è ancora completo. Serve a sincronizzare una UI che monta dopo che gli eventi sono già stati emessi — tipicamente il banner al primo load, quando lo store non ha ancora un consenso e il flusso normale a eventi non basta perché non c'è nessun listener ancora agganciato quando l'evento parte.

triggerChangeLanguage non forza il download dei framework differiti

Un cambio lingua rietichetta solo le slice di customization già costruite (Object.keys(privacySettings)), mai tutti i framework configurati. Il motivo: un cambio lingua non deve mai far scattare il fetch di un framework differito (GVL TCF, lista ACM) prima che una superficie lo richieda davvero — altrimenti cambiare lingua alla prima visita, prima di aver mai aperto il second layer, scaricherebbe dati che l'utente potrebbe non vedere mai. Il chiamante può forzare un set esplicito di framework via options.frameworks (es. ['custom'] al boot, quando ancora nulla è costruito).

Listener del ciclo di vita del consenso

Quattro listener, uno per evento (onLoad al boot, onConsent a ogni scrittura, onCustomization quando si costruisce/ricostruisce la vista second-layer, onChangeLanguage a un cambio lingua). Orchestrati da Core, mai chiamati direttamente dall'esterno.

onLoad — bootstrap del consenso

Al boot: carica la lingua, poi legge il consenso grezzo dallo store.

  • Nessun consenso salvato (readConsent()null) → i framework vengono comunque notificati con un decoded consent vuoto (notifyConsentLoaded), non semplicemente lasciati "in attesa". Serve perché standard come TCF devono transitare cmpStatus da loading a loaded e rispondere subito a __tcfapi('getTCData', …) con eventStatus: 'cmpuishown' — senza questo passaggio, un partner che fa gate del proprio bootstrap su quell'evento (l'esempio storico: slot pubblicitari smartclip) vede il CMP come "in caricamento" per sempre e va in timeout.
  • Consenso scaduto (metadata.expiry nel passato) → stesso trattamento: notificato come vuoto, consent.valid: false.
  • Bump di policyVersion (config.policyVersion > metadata.policyVersion, cioè un consenso salvato sotto una policy più vecchia di quella attiva) → invalidato allo stesso modo. Un cambio di policy pubblicato lato config invalida silenziosamente tutti i consensi pre-esistenti, senza bisogno di una migrazione esplicita.
  • Altrimenti: decodifica per framework (decodeAllConsent), estrae le choices, consent.type viene dai metadata salvati (metadata.consentType, fallback 'Personalized' se assente — consenso da versioni precedenti che non salvavano il type).

onConsent — scrittura del consenso

  • Coalescenza Personalized → AcceptAll/RejectAll: solo i payload Personalized vengono ricontrollati contro un riferimento canonico (frameworkProvider.getConsent({type: 'AcceptAll'|'RejectAll', ...})) — se le choices finali combaciano esattamente, il type salvato viene promosso al canonico. Nessuna scorciatoia strutturale su AcceptAll ("tutti i flag true"): un AcceptAll legittimo produce comunque hasLegitimateInterest: false per purpose/vendor senza LI interattivo, quindi il confronto è sempre contro il riferimento vero, mai un check sui flag.
  • consentFilter (hook di config, adattamento a livello di build) gira PRIMA di encode/scrittura — riceve anche currentChoices (lo stato appena superato), così un filtro può preservare/derivare dal consenso che sta per essere sostituito.
  • La scadenza (decoded.expiry) si calcola da config.consentExpiryDays al momento della scrittura, non è fissa.
  • Scrive tramite store.writeConsent(serializeRawConsent(metadata, encodedMap)) — vedi formato sotto.

onCustomization — costruzione della vista second-layer

Choices, glossario e stack si costruiscono in parallelo (Promise.all), sono indipendenti fra loro.

  • Un rebuild scoped (payload.frameworks esplicito, es. ['custom'] al boot) fa merge sopra le slice già costruite — gli altri framework mantengono il proprio valore precedente. Un rebuild non-scoped sostituisce tutto.
  • Stack: null da getAllStacks significa "nessun framework in scope ha il concetto di stack" → la slice esistente resta intatta (un rebuild scoped su custom non deve azzerare gli stack che TCF aveva già risolto). Un array vuoto [] è invece una risposta vera ("nessuna whitelist configurata") e sovrascrive.
  • pruneUnpresentableStacks gira prima che il renderer calcoli i covered-ids — l'ordine conta: uno stack si presenta solo se TUTTI i suoi membri sono sopravvissuti al filtro vendor, e la potatura deve avvenire prima o i membri di uno stack scartato spariscono invece di tornare voci individuali.

onChangeLanguage

Il più semplice: languageProvider.setLanguage(lang)mainFramework.onLanguageChange(lang) (se implementato) → aggiorna language.activeLanguage/words nel context. Nessuna logica di coalescenza o guardia — l'orchestrazione più delicata (non forzare il fetch di framework differiti) vive un livello sopra, in Core.triggerChangeLanguage (vedi sezione Bootstrap).

consent.utils.ts — formato di RawConsent

RawConsent è documentato come "stringa opaca" per chi implementa IStoreProvider (Tier 2) — nella pratica è JSON, JSON.stringify({ meta: RawConsentMetadata, data: FrameworkRawConsentMap }):

{
"meta": { "expiry": 1234567890000, "policyVersion": 2, "cmpVersion": "3.0.0", "consentType": "AcceptAll" },
"data": { "tcf": "CPz...", "gcm": "...", "acm": "2~1.2.3~dv." }
}

parseRawConsent/serializeRawConsent fanno il round-trip; un JSON malformato in lettura ritorna { metadata: null, data: {} } invece di lanciare — un consenso corrotto nello storage si comporta come "nessun consenso", non come un errore di boot.

preferences-order.ts — ordine di default della nav

DEFAULT_PREFERENCES_ORDER (usato quando ui.behavior.preferencesOrder è omesso): gcm::purpose, tcf::purpose, tcf::specialPurpose, tcf::feature, tcf::specialFeature, tcf::vendor, custom::vendor, acm::vendor, custom::customPurpose, glossary, retention.

applyPreferencesOrder è una funzione pura: gli id presenti in order vengono portati in testa (ordinati secondo la loro posizione nell'array), il resto resta nell'ordine relativo originale in coda. Id in order senza una voce nav a runtime sono ignorati silenziosamente — la visibilità resta guidata dai dati, non dalla config dell'ordine.

Il tipo di storage è una scelta statica, decisa una volta al boot da config.store.type ('cookie' | 'local-storage', estendibile con un tipo custom via registerStoreProvider). initStore() istanzia una singola classe e non la cambia mai più durante la sessione — non esiste un meccanismo che passi da uno storage all'altro a runtime.

⚠️ Nessun fallback automatico cookie → local-storage. CookieStoreProvider.writeConsent() scrive il cookie senza mai controllare la dimensione del valore, senza try/catch attorno alla scrittura. Se il consenso codificato (es. TCF + ACM combinati, con molte finalità/fornitori) supera il limite pratico di un cookie (circa 4KB, limite del browser, non di Avacy), il browser tronca o rifiuta silenziosamente la scrittura — nessun errore, nessun avviso, il consenso va perso senza segnalazione a nessun livello. Verificato anche nel legacy avacy_banner: lo stesso comportamento, nessun controllo di dimensione da nessuna parte, in nessuna delle due generazioni del CMP. Se questo è un requisito reale (segnalato da Fabio, 2026-09-03) è una feature da costruire da zero, non una configurazione da attivare.

LocalStorageStoreProvider, dal lato opposto, propaga un QuotaExceededError del browser come eccezione (non lo assorbe, non prova un fallback) — coerente col fatto che oggi lo storage è una scelta singola e non switchabile.

Framework di consenso — meccaniche interne

TCF

Stack IAB (tcf.stacks)resolveStacks() applica una whitelist disgiunta, non una lista di preferenze: se due stack whitelistati condividono una finalità o una special feature già "reclamata" da uno stack precedente, il secondo viene scartato per intero (non tagliato alle sole finalità libere) e un warning riporta l'elenco delle finalità/feature contese. Le finalità dello stack scartato tornano a comparire come voci individuali. collectCoveredIds() calcola l'unione delle finalità/feature coperte dagli stack risolti, usata a valle per escluderle dalle liste individuali.

Allineamento lingua della GVLgetGvl({ alignLanguage: true }) mantiene una singola promise condivisa (gvlAlign) per allineare la libreria @iabtechlabtcf/core alla lingua attiva. Un codice ISO malformato (es. "cz" invece di "cs") fa fallire l'allineamento senza far rigettare la promise condivisa: tutti i chiamanti in attesa la vedono comunque risolversi, con un warning loggato e la GVL che resta nella sua ultima lingua valida (di default EN) invece di un banner bloccato. Un allineamento fallito non resta memoizzato — la chiamata successiva ritenta.

⚠️ Trappola della libreria sottostante (non un bug Avacy, ma da conoscere): GVL.LANGUAGE_CACHE di @iabtechlabtcf/core è una cache statica per-processo, chiave sulla sola lingua — due istanze GVL costruite da fixture/vendor-list diverse che si allineano alla stessa lingua, nello stesso processo, si contaminano a vicenda. GVL.emptyCache() (il pattern oggi in uso) pulisce solo il fetch-cache per versione, non questa cache linguistica — serve GVL.emptyLanguageCache(). Rilevante soprattutto per chi scrive test contro una GVL reale, non per la libreria in produzione (un solo processo browser, una sola vendor-list attiva).

Vendors ristretti (tcf.vendors) — quando la lista fornitori è ristretta a un sottoinsieme, MainFrameworkProvider.applyCustomVendorPurposeOverride filtra automaticamente anche finalità/feature/stack che restano "orfani" (nessun fornitore attivo li offre più) — non serve nasconderli a mano in config.

Il provider GCM mappa segnali di consenso a categorie GCM (ad_storage, analytics_storage, ecc.) via mapToPurpose/gcmPurpose, con supporto per una GVL remota alternativa (remoteGVLName) quando la mappatura non è quella di default. Il provider è deliberatamente semplice: nessun proprio storage, nessuna UI propria — proietta lo stato deciso altrove (TCF/Custom) sui signal GCM.

ensureConfig() risolve la lista fornitori in ordine di priorità: config inline (acmProvidersConfig) → URL esplicito (acmProvidersUrl) → nome file + assetPath (acmProvidersConfigName, pattern legacy). Il fetch è deferred: non parte al boot, solo alla prima richiesta reale (apertura CPC, o una mutazione di consenso che deve codificare l'AC string). fetchConfig() normalizza il formato risposta, che può arrivare come array (mock interni) o come oggetto keyed-by-id (formato reale Google ATP/RAI, es. {"39": {id: "39", ...}}) — un id assente sia dal payload sia dalla chiave viene scartato per evitare un fornitore fantasma con id 0.

getVendorsCount() ha 3 fonti in ordine di autorevolezza: lista già caricata (se presente, autoritativa — è già stata intersecata con vendors configurato) → array vendors esplicito (locale, nessun fetch) → fetch della lista (solo quando vendors: 'all', l'unico caso in cui il conteggio esatto richiede la lista remota).

Custom (vendor list non-IAB)

Stessa forma di ensureConfig/fetchConfig di ACM (config inline / URL / config-name+assetPath), applicata a una vendor list completamente custom (non TCF, non Google). Include un glossario opzionale (getGlossaryEntries) con titolo/descrizione localizzati per lingua, fallback a cascata: lingua richiesta → lingua di default → primo valore disponibile nell'oggetto localizzato.

Nota di prodotto: il core non fornisce contenuto di default per un glossario non-TCF — è tutto a carico della config del cliente. Come popolarlo in fase di wizard/migrazione resta una decisione di prodotto aperta.

Disclosure (divulgazione storage fornitori)

fetchDisclosures(url) scarica e normalizza le disclosure di storage per fornitore (identificatore, tipo, durata massima, domini, finalità) — usato dal pannello disclosure nella CPC. Normalizza domain (singolare, legacy) o domains (plurale), ciascuno stringa singola o array, a un array uniforme. Degrada in modo pulito su qualunque fallimento (rete giù, risposta non-ok, JSON malformato, disclosures assente) a { ok: false, error: <chiave i18n> } — mai un'eccezione che risale al chiamante.

Logger

Due implementazioni selezionabili via loggers: LoggerType[] (LoggerType = 'console' | 'event'): 'console' (passthrough diretto a console.info/warn/error), 'event' (DOMEventLoggerProvider — dispatcha un CustomEvent('avacy:log', { detail: { level, message, topic }, bubbles: true }) su document, un contratto pubblico de facto per chi integra e vuole intercettare i log senza aprire la console). Sopra queste, MainLoggerProvider è sempre istanziato (non selezionabile) — orchestra e smista ai subloggers attivi in loggers.