Implementation Guide
Edition 3.1.1
Why Native Integration
Section titled “Why Native Integration”Section 01 makes the case for native identity management; this section makes it concrete. The advantages are real but bounded. State them honestly.
First-party code is not on default filter lists. Filter lists target known third-party tag and tracking hosts; a same-origin path on yourdomain.com is not one of them by default. This reduces blocking; it does not eliminate it. Determined blockers, privacy extensions, and future heuristics can still interfere, and a same-origin path can be added to a list. First-party delivery is harder to block, never impossible to block.
Server-set cookies receive a longer lifetime than script-set cookies on some browsers. A Set-Cookie from your own origin is treated as first-party. Exact lifetimes are browser- and version-dependent and change over time (see the dated Safari/ITP notes below); UIAF never promises a fixed cookie lifetime.
Application-priority execution. Your server processes the request before any client script loads, so middleware can read the existing cookie and derive request context from HTTP headers before the page renders.
Full HTTP access. Server-side code reads Referer, Cookie, and User-Agent, sets Set-Cookie, and reads URL parameters before client processing begins, subject to the pre-consent minimization rules below.
Fewer external runtime dependencies. UIAF runs in your application process rather than through a third-party script loaded at runtime, which removes one class of external failure. Your own infrastructure and the receiving endpoint remain dependencies: this is a reduction in external dependencies, not their elimination.
Capabilities, not tiers
Section titled “Capabilities, not tiers”Every decision in this guide keys off the effective consent vector, never a tier number. Tiers T0–T4 are derived shorthand only (section 07) and appear in no permission, send, purge, routing, or test condition here. BCP 14 requirement keywords are normative only inside the identified UIAF-09-* requirement lines; all other prose and pseudocode is illustrative.
The consent object has three layers (section 07, section 04):
- signals: four observed values, each
granted | denied | unknown | not_applicable. - status: lifecycle
pending | resolved | not_applicable(the data plane admits onlyresolved | not_applicable). - effective: four resolved values, each
allowed | denied; the only domain any behavior branches on.
// PSEUDOCODE — resolve the three-layer consent object (never a tier number)function parseConsent(source, config): var signals = readObservedSignals(source, config) // CMP-over-GCM authority; 4 × granted|denied|unknown|not_applicable var gpc = { detected: readGPC(source), applicable: config.gpc_applicable } var status = resolveStatus(signals, config) // pending until an authority resolves; // not_applicable ONLY under an explicitly configured T0 policy, else pending (fail-closed) var effective = deriveEffective(signals, gpc, status) // 16-row truth table; `unknown` fails closed to `denied`; // GPC detected+applicable forces the three ad purposes to denied (analytics untouched) // SOURCE is the SELECTED observed-signal authority AFTER CMP-over-GCM precedence — GPC is an overlay, never a source: // status == not_applicable => source is site_policy AND all four signals not_applicable (T0 policy) // status == resolved => source is one of cmp_cookiebot|cmp_onetrust|cmp_didomi|cmp_custom|gcm (never site_policy) var source = selectedSignalAuthority(config, status) // STATE_UPDATED_AT — the adapter contract of UIAF-07-CONSENT-015, exactly three permitted strategies // (a deployment never switches strategies silently between reads): // 1. SOURCE-SUPPLIED: the adapter supplies a TRUSTWORTHY last-material-change time for the selected // source record (T0 policy ACTIVATION time for site_policy) — stable across unchanged rereads, // NEVER the read/event/receipt/serialization time. // 2. FIRST-OBSERVATION FREEZE: where the source cannot supply one, the adapter freezes the FIRST-OBSERVATION // time of the current source record, persisted per record and keyed to the record identity the adapter // reads; stable across rereads of an unchanged record; replaced ONLY when the source record itself // materially changes (the replacement freezes the changed record's own first-observation time); // NEVER derived from read/hydration/serialization time. // 3. PENDING: an adapter that can neither supply a trustworthy time nor persist a freeze leaves the // integration in lifecycle pending (reason unavailable) — it NEVER invents a fresh timestamp per read. var state_updated_at = selectedRecordMaterialChangeTime(source, config) // strategy 1 or 2 above; strategy 3 never reaches here var out = { signals: signals, status: status, effective: effective, gpc: gpc, source: source, state_updated_at: state_updated_at } // consent_record_id is OMITTED when absent (never serialized as null); when present it is a non-null string // matching ^[A-Za-z0-9._:-]{1,128}$ (validated before inclusion) — an opaque local reference, never a raw CMP string. if hasReceiptRef(config) AND isValidRecordId(receiptRef(config)): out.consent_record_id = receiptRef(config) // The eventual data-plane consent object also carries a DERIVED tier for downstream shorthand, // but no behavior here ever branches on it (derived from effective; see section 07). return outServer/client record observability and the degraded mode (UIAF-07-CONSENT-016). parseConsent runs in two contexts: parseConsent(document, …) on the client and parseConsent(request, …) in middleware/edge. The deployment is configured so both observe the same consent record and version (a CMP record readable in both contexts, or a server-side mirror the client treats as authoritative).
How behavior branches on effective and status:
| Condition | Behavior |
|---|---|
status == "pending" | Hold. Only the minimized pending context (below) may live in memory. Zero send, persist, or storage write. |
all four effective == "denied" (dormant) | No payload of any kind. |
effective.analytics_storage == "allowed" | Establish persistent identity and the automatic session (base predicate holds). ad_storage decides click-ID capture. |
analytics_storage == "denied" but an explicit action is independently authorized | No persistent identity (uid: null), no automatic session; only an explicit conversion/identify under its own basis may emit. |
The automatic session needs the full predicate: analytics_storage: allowed alone is not enough. A deployment without the documented analytics/persistent-identity purpose does no automatic session and writes no session state:
// PSEUDOCODE — automatic-session authorization (checked before ownership/sequence/freeze/send)function automaticSessionAuthorized(consent): return (consent.status == "resolved" OR consent.status == "not_applicable") // lifecycle prerequisite AND consent.effective.analytics_storage == "allowed" // ad permissions alone never authorize it AND hasDocumentedAnalyticsPurpose(UIAF_CONFIG) // no purpose => no automatic session, no session-state write // maybeEmitSession then additionally requires a changed projection OR an unconsumed current-document trigger.UIAF-09-CONSENT-001 — Producers MUST branch on the
effectivevector (andstatus), never on a derived tier number, and MUST NOT emit any data-plane payload whilestatusispendingor while the foureffectivevalues are alldenied. An automaticsession— including any session-state write, ownership, sequence allocation, freeze, or send — MUST require the full predicate (lifecycleresolved/not_applicableandeffective.analytics_storage: allowedand a documented analytics/persistent-identity purpose and a changed projection or an unconsumed current-documentcreate/recoverytrigger); absent the documented purpose there is no automatic session and no session-state write.
Identity: server-minted, candidate-only, re-read before use
Section titled “Identity: server-minted, candidate-only, re-read before use”Persistent UIDs are server-minted only. There is no client-generated persistent-UID fallback. The client never places a UID in a request body; the cookie route rejects a body-supplied UID with 400 (see section 04). A response UID is candidate-only until the mandatory post-response, pre-persist/pre-send authoritative cookie re-read.
// PSEUDOCODE — shared authoritative barrier: re-read consent + reconcile (purges on reduced capability).// Returns the fresh consent. Used at EVERY await/lock boundary before any adopt / endpoint / allocate / send / write.function reauthorize(): var c = parseConsent(document, UIAF_CONFIG) // REAL API (document) reconcileOnInit(c) // capability may have decreased: purge before any use return c
var EPHEMERAL_NULL = { uid: null, resolution_method: "ephemeral", confidence: "low", is_new: false } // identity-only; session ownership is decided separately// NOTE: resolveIdentity NEVER manufactures session ownership — it returns identity only. Session ownership is// established separately by AWAITING the allocator (acquireSessionOwnership) after authorization; a predicate-loss// branch returns EPHEMERAL_NULL and no session ownership is attempted.
// PSEUDOCODE — resolve identity (client). ASYNC, coordinated by the short-lived uiaf-identity-issuance lock;// consent is re-read + reconciled at EVERY boundary, never carried stale across an await.async function resolveIdentity(): var consent = reauthorize() // fresh consent + reconcile if not basePredicate(consent): purgeGovernedPair(); return EPHEMERAL_NULL
// 1. Valid cookie is AUTHORITATIVE — but re-authorize before any adopt/allocate. var cookieUid = readValidUidCookie() // validated against the one canonical UID regex (section 04) if cookieUid != null: var c1 = reauthorize() // barrier before adopt/allocate if not basePredicate(c1): purgeGovernedPair(); return EPHEMERAL_NULL adoptPairFor(cookieUid) // pairs only a credential KNOWN-BOUND to cookieUid; else purge/no-op (UIAF-02-PAIR-001) return { uid: cookieUid, resolution_method: "cookie", confidence: "high", is_new: false }
// 2/3. No cookie => issuance boundary INSIDE the short-lived uiaf-identity-issuance lock. return await withIssuanceLock( /* WINNER (inside lock) */ async function() { var g = reauthorize() // AFTER grant: current consent + reconcile BEFORE any adopt/endpoint/allocate if not basePredicate(g): purgeGovernedPair(); return EPHEMERAL_NULL var raced = readValidUidCookie() // someone may have issued while we waited for the lock if raced != null: adoptPairFor(raced) return { uid: raced, resolution_method: "cookie", confidence: "high", is_new: false } var recovery = readRecoveryCredential() // {value, store} | null var recovering = (recovery != null AND continuityPolicyAllows(g)) // RETAIN the response: the create/recover response returns the (possibly rotated) credential bound to the issued UID. // DEGRADED PROFILE (UIAF-07-CONSENT-016): where the adapter declares the consent record unreadable to // the server, the cookie endpoint is fail-closed. Create/recovery attempts against it are BOUNDED by the // deployment-configured attempt budget; on exhaustion the client treats the deployment as the client-only // degraded profile (no server persistent-identity capability for the affected scope => uid:null ephemeral) // instead of retrying — a permanent create/recovery churn loop against a fail-closed server is non-conforming. var resp = recovering ? await postCookieRecover({ uiaf_recovery: recovery.value }) // closed body: ONLY uiaf_recovery : await postCookieCreate() // closed create body; no inbound UID var g2 = reauthorize() // AFTER endpoint await: re-read consent + reconcile AGAIN before pair/session write if not basePredicate(g2): purgeGovernedPair(); return EPHEMERAL_NULL // predicate lost mid-issuance => no pair, no session var authoritative = reReadUidCookie() // candidate UID stays candidate-only until this authoritative re-read if authoritative != null: // Persist the pair ONLY when the retained credential is known-bound to THIS authoritative UID. if resp.recovery != null AND credentialBoundTo(resp.recovery, authoritative): mirrorGovernedPair(authoritative, resp.recovery) // atomic {uid, retained-bound-recovery} pair else: purgeGovernedPair() // cookie still governs identity; never pair a mismatched credential // UIAF-04-ID-001 coherence: is_new:true REQUIRES resolution_method "cookie" — a confirmed first // issuance reports "cookie"; "new" is a receiver-side report value the client paths never mint. var method = recovering ? ((recovery.store == "localStorage") ? "localstorage_recovery" : "sessionstorage_recovery") : "cookie" return { uid: authoritative, resolution_method: method, confidence: "high", is_new: (not recovering) } purgeGovernedPair() // unreadable cookie => indeterminate return EPHEMERAL_NULL }, /* FALLBACK (lock unavailable or not acquired) */ async function() { requestIssuanceWakeAndReread() // wake + authoritative re-read ONLY — never calls the endpoint var f = reauthorize() // explicit re-read + reconcile + authorize before adopt/allocate if not basePredicate(f): purgeGovernedPair(); return EPHEMERAL_NULL var c = readValidUidCookie() if c != null: adoptPairFor(c) return { uid: c, resolution_method: "cookie", confidence: "high", is_new: false } return EPHEMERAL_NULL // eventual convergence } )uiaf_recovery is opaque, bounded, and lives in localStorage/sessionStorage only (never a cookie). It is server-bound to its UID, tenant, issuance version, expiry, and policy; returned over TLS with Cache-Control: no-store; never logged raw; never a DSR authenticator; and honors expiry/revocation/rotation. Script persistence is permitted only as the paired { uiaf_uid, uiaf_recovery } in the selected storage classes (UIAF-02-PAIR-001). mirrorGovernedPair(uid, recovery) writes the pair atomically and, when no recovery credential was returned or is available, purges/no-ops rather than leaving a lone script UID. The authoritative cookie continues to govern identity regardless.
Reconcile-on-init and middleware expiry precede use. Before any identity is used, two things run first: reconcile-on-init (re-check consent and purge what is no longer allowed) and middleware expiry (an invalid or expired cookie is treated as absent).
UIAF-09-ID-001 — The client MUST NOT generate a persistent UID or place any UID in a request body; UIDs are server-minted and a body UID is rejected with
400. A returned UID MUST be treated as candidate-only until the mandatory post-response cookie re-read, and a valid browser-held cookie MUST be adopted (not overwritten), whether it matches or diverges from server state.
UIAF-09-ID-002 — Purge scope MUST match the loss class and MUST NOT conflate credential invalidity with consent loss. (a) Credential-only invalidity — recovery-credential expiry/revocation/corruption or script-pair corruption MUST purge/no-op the governed
uiaf_uid+uiaf_recoveryscript pair (recovery becomes unavailable) but MUST NOT by itself purgeuiaf_session_state,uiaf_attribution, oruiaf_dirty_baseline; a still-valid authoritative cookie remains usable. (b) Permission/control-plane loss — analytics-permission/persistence loss, consent revocation, erasure, or a dormant/reconcile-prohibited state MUST atomically purge the pair anduiaf_session_stateand the wholeuiaf_attributionrecord anduiaf_dirty_baseline. (c) Ad-storage-only loss (analytics still allowed) MUST retain the pair and session state and strip only click-ID members fromuiaf_attribution. In all cases the implementation MUST NOT persist a bareuiaf_uidwithout its governed pair, and all key purges MUST defer to theuiaf-storage-keys.jsonregistry predicates rather than a hand-maintained list.
Consent re-read schedule and runtime IPC
Section titled “Consent re-read schedule and runtime IPC”Consent state and identity are re-read from authoritative sources on a fixed schedule. Browser IPC only wakes a re-read; it never carries state.
Identity issuance lock uiaf-identity-issuance. UID issuance/recovery is coordinated by a short-lived Web Lock. At most one coordinator calls the cookie endpoint per issuance boundary, and the authoritative cookie re-read is taken inside that boundary. Acquisition is best-effort: where Web Locks are unavailable, or the issuance lock is contended or not obtained, issuance degrades to a best-effort wake / authoritative re-read with eventual convergence (there is no server-enforced issuance correlation). This lock is separate from the session allocator lock uiaf-session-alloc:<session_id> and never provides the allocator’s fallback.
Consent wake-up channel uiaf-consent-wakeup. The registered binding is runtime_ipc.consent_wakeup_broadcast_channel = uiaf-consent-wakeup in uiaf-enums.json, distinct from the session-claim channel runtime_ipc.broadcast_channel = uiaf-session-claim. It is wake-only runtime IPC, not storage. A message carries no consent state and no UID or identity state, says only “consent may have changed,” and is never trusted as consent proof. Its receipt triggers only a re-read of the authoritative CMP/GPC/server-side consent state. Where BroadcastChannel is unavailable, the scheduled re-read points below are the fallback. There is no localStorage consent-epoch key.
Re-read schedule. Authoritative consent is re-read immediately before capture, every persistence write, retry queueing, retry drain, and every send, and on initialization, focus/visibility return, and pageshow/BFCache restore. Authoritative cookie is re-read immediately before every persist and send (and post-response during issuance). If capability decreased, pending payloads are purged, rebuilt, or dropped synchronously before any transmission. The server re-reads its own authoritative consent state on every relevant request, and browser IPC never updates server state.
UIAF-09-WAKE-001 — Authoritative consent MUST be re-read immediately before capture, every persistence write, retry queueing, retry drain, and every send, and on initialization, focus/visibility return, and
pageshowwithpersisted: true. Authoritative cookie state MUST be re-read immediately before every send and every persist: if the cookie no longer matches the identity candidate (diverged or disappeared) the payload MUST be rebuilt or dropped and MUST NOT be sent, and a baseline write MUST occur only when fresh consent and authoritative cookie state still match the committed projection (never a stale baseline write).uiaf-consent-wakeupMUST be treated as wake-only (no consent/UID/identity state) and its receipt MUST trigger only an authoritative re-read, never a state update. When capability has decreased, pending payloads MUST be purged, rebuilt, or dropped synchronously before any transmission.
UIAF-09-ID-003 — The entire issuance boundary MUST stay inside the short-lived
uiaf-identity-issuanceWeb Lock: after the grant, the winner re-reads the cookie, recovery credential, and current consent/policy, and only the winner calls the cookie endpoint (at most one call), awaits the response, and takes the post-response authoritative cookie re-read before releasing the lock. Where the lock is unavailable or not obtained, issuance MUST degrade to wake / authoritative re-read only, MUST NOT call the endpoint, converges eventually, and MUST NOT borrow the session allocator’s fallback.
Pre-consent: minimized context only
Section titled “Pre-consent: minimized context only”Before consent resolves there is no payload and no send. The only thing that may exist across the pre-resolution moment is a minimized, server-injected, current-request context (plus current-document memory) drawn from the closed pending_context_allowlist registry (section 04, uiaf-enums.json):
- The context is JSON
nullor a closed object of exactly{ utm_source, utm_medium, utm_campaign }, each member nullable. Referrer, referrer-domain, landing URL/path, click IDs, custom/query fields, identifiers, tokens, timestamps, and user-specific decoration are excluded. A referrer domain or landing path is attribution context but not a campaign-level field and cannot enter here. - Each admitted value has to exact-match a member of the finite, controller-configured campaign vocabulary for its key (normative in UIAF-09-CTX-001). Recognizing a UTM name or passing an
@/digit-run screen is necessary but never sufficient; free-form search text, identifiers, and sensitive values fail closed tonull.
Leakage controls. Server injection is not a confidentiality boundary. The initial request has already reached your infrastructure before any of this runs, so these controls limit further propagation, not that first arrival.
UIAF-09-CTX-001 — Each admitted pending-context value MUST exact-match a member of the finite controller-configured campaign vocabulary for its key (name recognition or a heuristic screen is never sufficient), and the injected context MUST be returned with
Cache-Control: private, no-storeandReferrer-Policy: strict-origin-when-cross-origin(or stricter) with URL/log minimization at the earliest controllable boundary, and excluded from shared/CDN caches, hydration serialization, service workers,history, error/CSP reports, analytics, and third-party-readable globals. The server-side context-injection helper used below (injectContextNoStore) is the Implementation-Guide equivalent of Spec 01’s{{ uiaf_context_json }}server-template injection (UIAF-01-CODE-001), and it MUST escape the injected value for its exact output context — HTML, and JavaScript-string/JSON context inside a<script>block — before rendering. Whilestatusispending, the implementation MUST NOT send, persist, or serialize anything beyond the minimized{ utm_source, utm_medium, utm_campaign }context in memory, and that context MUST NOT be written to any cookie, storage, payload, retry body, log, report, history, cache, service worker, hydration state, or third-party-readable global.
Session ownership and the allocator
Section titled “Session ownership and the allocator”Sessions are client-owned by default. uiaf_session_state ({ session_id, next_seq }) in sessionStorage is the sole live session representation; any prior session representations are the closed legacy_purge_only targets defined in uiaf-storage-keys.json (this guide keeps no partial legacy list). Exactly one allocator owns a given non-null session_id, coordinated by the Web Lock uiaf-session-alloc:<session_id> with the advisory channel uiaf-session-claim.
// PSEUDOCODE — duplicate-tab session allocator (section 04, §2.2 verbatim). ASYNC: ownership is// only established after the acquisition PROMISE resolves; nothing runs on a session before then.async function acquireSessionOwnership(session_id): // Absence fallback: no Web Locks API (including non-secure contexts where it does not exist) // => unconditional per-document rekey. A CSPRNG-distinct id is the ownership proof. if not hasWebLocks(): return unconditionalRekey()
var ac = new AbortController() // REAL API setTimeout(function() { ac.abort() }, 250) // REAL API — 250 ms acquisition deadline AS an AbortSignal
// SEPARATE GRANT SIGNALING: the lifetime request promise resolves only on RELEASE, so we never // await it for initialization. Instead the granted callback resolves a distinct `grant` promise // the instant ownership is established, and holds the lock for the document lifetime afterward. var grant = deferred() // resolves true on grant, rejects on abort/failure var release = deferred() // RESOLVING this ends heldForLifetime() and RELEASES the Web Lock var lifetime = navigator.locks.request( // REAL API — START and RETAIN — do NOT await this promise "uiaf-session-alloc:" + session_id, // exclusive, WITHOUT ifAvailable { signal: ac.signal }, function(lock) { // granted callback grant.resolve(true) // signal ownership immediately return release.promise // hold the lock until release() is called (document lifetime or invalidation) } ) lifetime.catch(function(){ grant.reject() }) // abort/rejection also fails the grant
try: await grant.promise // race grant vs AbortSignal/timeout/rejection // Return a RELEASE handle so invalidation can release this exact lifetime lock. return { session_id: session_id, owned: true, release: function(){ release.resolve() } } catch (abortedOrContended): return await conservativeRekey() // LOSER: timeout / abort / live competing owner => rekey
async function conservativeRekey(): // Overwrite only THIS tab's cloned session state with a fresh session, then AWAIT its lock BEFORE allocating. var fresh = uuidv4(); writeClonedSessionState(fresh, /*next_seq*/ 0) var r1 = await acquireFreshLockWithin250ms(fresh) // returns { owned, release } | { owned:false } if r1.owned: return { session_id: fresh, owned: true, release: r1.release } var fresh2 = uuidv4(); writeClonedSessionState(fresh2, /*next_seq*/ 0) // retry ONCE with another UUID var r2 = await acquireFreshLockWithin250ms(fresh2) if r2.owned: return { session_id: fresh2, owned: true, release: r2.release } return { session_id: null, owned: false, release: noop } // give up: session_id null, NO sequence
function unconditionalRekey(): var fresh = uuidv4(); writeClonedSessionState(fresh, /*next_seq*/ 0) // per-document session (no Web Lock to hold) return { session_id: fresh, owned: true, release: noop }
// ACQUIRE ONCE per document, then REUSE the proven result for all same-document transitions AND routes.// Re-acquiring on every consent change would self-contend on the lifetime lock and accumulate locks.async function ensureSessionOwnership(): if _session.owned AND _session.session_id != null: return _session // reuse the proven result; do NOT re-request the lifetime lock // First acquisition (or after invalidation): pick the lock target. An absent/corrupt/oversize // uiaf_session_state stages a FRESH UUID-v4 candidate (next_seq 0) — NEVER request uiaf-session-alloc:null. var candidate = validCurrentSessionId() OR stageFreshCandidate() // stageFreshCandidate mints+stages a fresh UUID _session = await acquireSessionOwnership(candidate) // acquires that exact lock; owned:true implies a valid non-null UUID return _session
function invalidateSessionOwnership(): // IDEMPOTENTLY RELEASE the retained lifetime lock/grant BEFORE clearing the result, so a later same-document // reauthorization can acquire the lock without self-contention. if _session.release: _session.release() // ends heldForLifetime() => releases the Web Lock (idempotent) _session = { session_id: null, owned: false, release: noop }
// Lifecycle hooks — releaseOwnership uses the SAME release/reset contract as invalidateSessionOwnership.function releaseOwnership(): invalidateSessionOwnership() // release the held lock + reset the retained resultwindow.addEventListener("pagehide", releaseOwnership) // REAL API — release ownership BEFORE navigation / BFCache suspensionwindow.addEventListener("pageshow", function(e) { // REAL API if (e.persisted) // BFCache restoration: ownership was released on pagehide serialize(runBfcacheRestore) // ENQUEUE the whole restore on the SAME chain as init/transition/route})
// The restore runs as one serialized unit so a concurrent transition/purge orders deterministically.async function runBfcacheRestore(): invalidateSessionOwnership() // the lifetime lock was released on pagehide; drop stale ownership var consent = reauthorize() // reread/reconcile authoritative CONSENT before any staging/reacquire if consent.status == "pending" OR allDenied(consent.effective) OR not automaticSessionAuthorized(consent): return // leave owned:false; no state use if not authoritativeCookieMatchesCurrentIdentity(): return // COOKIE barrier before staging/reacquire _session = await ensureSessionOwnership() // AWAIT bounded reacquire/rekey (stages a fresh candidate if needed) var c2 = reauthorize() // reread/reconcile CONSENT + cookie AGAIN before RETAINING the result if not automaticSessionAuthorized(c2) OR not authoritativeCookieMatchesCurrentIdentity(): invalidateSessionOwnership() // lost during reacquire => reset; never retain/use a stale result// BroadcastChannel(uiaf-session-claim) may announce conflicts and accelerate rekeying,// but silence NEVER proves ownership and cannot preserve an inherited session.Sequence allocation. After the event is authorized and immediately before body freeze, the owner reads the current next_seq (inclusive 0..4294967295) and persists the increment with no intervening async yield. session_start is true exactly when the allocated value is 0. Before an allocation would exceed the range, mint a new UUID-v4 session_id with next_seq: 0. Retries preserve the original event_id, session_seq, and session_start. Gaps are allowed and never reused; corrupt or oversized state mints a new session. The endpoint’s (session_id, session_seq) conflict rule is a safety net, not the allocator.
UIAF-09-SESS-001 — Identity resolution MUST NOT manufacture session ownership. Ownership MUST be acquired once per document after the first authorization and the proven result reused for all same-document transitions and routes; it MUST be re-acquired only after a lifecycle release/BFCache restore or after a purge/rekey invalidates it, and a full session purge (and
pagehide) MUST idempotently release the held lifetime Web Lock/grant before clearing the retained result — so a later same-document reauthorization acquires the lock without self-contention. The allocator MUST NOT requestuiaf-session-alloc:null: an absent/corrupt/oversizeuiaf_session_statefirst stages a fresh UUID-v4 candidate (a staged fresh candidate before its fresh-lock attempt is the only pre-ownership session-state write), andowned: trueMUST imply a valid non-null UUID. A payload MUST NOT use a non-null session/candidate without proven ownership, and the document MUST NOT allocate/use a sequence or treat any staged candidate as owned before it holds a proven-owned result; the sequence MUST be allocated synchronously immediately pre-freeze only from the proven result. Anowned: falseresult (the inherited lock and both fresh-UUID attempts failed) does not block an otherwise-authorized automatic session: it MUST degrade to a null-session emission (session_id: null,session_seq: null,session_start: false) — with no sequence allocation and no staged candidate used as owned — and the normal authorization/dirty-gate/freeze/send/commit-baseline logic still applies. Where the Web Locks API is available, acquisition MUST be exclusive (withoutifAvailable) under a 250 msAbortSignal, released onpagehideand re-acquired-or-rekeyed onpageshowwhenevent.persistedis true; on timeout/abort/contention it MUST rekey to a fresh UUID and retry the fresh-lock acquisition once. A BFCache restore MUST be enqueued on the same serialization chain as init/transition/route (so a concurrent transition/purge orders deterministically), MUST re-read/reconcile authoritative consent and cookie before any candidate staging/reacquire (leavingowned: falsewith no state use when unauthorized), and MUST re-read/reconcile consent and cookie again before retaining the reacquired result; stale pre-suspension ownership MUST never be used. Where the API is absent (including non-secure contexts), every new document MUST unconditionally rekey before its first authorized event.
Commit-gated baseline (the automatic-send gate)
Section titled “Commit-gated baseline (the automatic-send gate)”An automatic session transmits only when the canonical projection changes or an unconsumed current-document create/recovery one-shot trigger is pending. The baseline is the canonical RFC 8785 (JCS) projection string (not a hash) stored in uiaf_dirty_baseline. Build the projection over the exact closed tree defined in section 04 (no other key participates; absent nullables are explicit JSON null):
// exact closed projection tree (JCS-serialized to the baseline string; ≤ 8192 bytes){ "attribution": { "count": <int>, "last_touch": <touchpoint|null> }, "consent": { "effective": <4-permission vector>, "gpc": { "applicable": <bool>, "detected": <bool> }, "signals": <4-signal vector>, "source": <string>, "state_updated_at": <int>, "status": <string> }, "identity": { "session_id": <uuid|null>, "uid": <uid|null> }}// consent_record_id is ALWAYS excluded — rotating it alone yields byte-identical bytes and never triggers a session.// PSEUDOCODE — commit-gated emission (async send / ack / enqueue).// `owned` is the PROVEN allocator result ({session_id, owned}) awaited by the caller — this function never// manufactures ownership. It re-reads authoritative CONSENT and authoritative COOKIE immediately before// freeze/send and again before baseline write (UIAF-09-WAKE-001), and allocates the sequence ONLY once an event is due.async function maybeEmitSession(identity, owned, attribution, trigger): var consent = reauthorize() // IMMEDIATE pre-freeze/pre-send CONSENT barrier if not automaticSessionAuthorized(consent): return // reduced capability => drop (reconcile already purged) if not authoritativeCookieMatches(identity): return // COOKIE barrier: cookie diverged/disappeared => rebuild/drop, do not send
// OWNERSHIP (UIAF-09-SESS-001 + frozen allocator authority): a payload may use a NON-NULL session/candidate ONLY // with PROVEN ownership. Coordination exhaustion (owned:false — the inherited lock and BOTH fresh-UUID attempts // failed) does NOT block an otherwise-authorized automatic session: it DEGRADES to a NULL-session emission // (session_id:null, session_seq:null, session_start:false) — never using a staged candidate as owned, never // allocating/mutating a sequence, never claiming ownership. var hasOwn = (owned.owned AND owned.session_id != null AND isValidUuidV4(owned.session_id))
// DIRTY GATE — build the projection from the proven session_id (or null under exhaustion) WITHOUT allocating a sequence // (session_seq/start are transient and excluded from the projection tree). var idProj = { ...identity, session_id: (hasOwn ? owned.session_id : null) } var projection = jcs(closedProjectionTree(idProj, attribution, consent)) // exact JCS string, ≤ 8192 bytes var baseline = readDirtyBaseline() // the stored STRING, not a hash if projection == baseline AND not trigger.pending: return // nothing due => NO sequence mutation, no send
// Event IS due: finalize identity immediately pre-freeze, no async yield. Allocate the sequence synchronously // ONLY under proven ownership; under exhaustion session_seq stays null with NO allocateSeq call and NO candidate use. var idOut = { ...idProj, session_seq: null, session_start: false } if hasOwn: var seq = allocateSeqSync(owned.session_id) // reads uiaf_session_state.next_seq, persists the increment idOut.session_seq = seq; idOut.session_start = (seq == 0) var body = assembleAndFreeze("session", idOut, attribution, consent) // RFC 8785, ≤ 32768 bytes, frozen once
var result = await sendToEndpoint(body) // resolves to { acknowledged (2xx), durablyEnqueued } — never sendBeacon(true) alone
var committed = result.acknowledged OR result.durablyEnqueued if committed: trigger.consume() // committed => the trigger IS consumed here var c2 = reauthorize() // re-read CONSENT + reconcile AGAIN before baseline persistence // Baseline advances ONLY if fresh consent AND authoritative cookie STILL match the committed projection. if automaticSessionAuthorized(c2) AND storagePurposeAllowed(c2) AND authoritativeCookieMatches(idOut) AND stillProjects(idOut, attribution, c2, projection): writeDirtyBaseline(projection) // Split (S09-32): a PRE-SEND loss or a failed/non-committed attempt leaves BOTH baseline and trigger untouched. // A COMMITTED outcome consumes the trigger; a post-commit consent/cookie/projection mismatch then skips ONLY the baseline write.Corrupt or over-8192-byte baseline content is treated as absent (one bounded diagnostic), forcing a fresh emit rather than a false match; the baseline is purged on every applicable downgrade/revocation/erasure path. conversion and identify bypass this gate.
UIAF-09-BASE-001 —
uiaf_dirty_baselineMUST hold the canonical JCS projection string (not a hash), MUST advance only after a2xxacknowledgement or a permitted durable enqueue (never solely becausesendBeaconreturnedtrue), and MUST be purged independently on every applicable effective-permission downgrade — analytics,ad_storage, and routing-onlyad_user_data/ad_personalizationdecreases alike — before any new (stripped/restricted) projection is evaluated, and MUST additionally be purged unconditionally and idempotently on any revocation/erasure/dormant/reconcile-prohibited control-plane state regardless of whether a vector delta was observed, after which the field-specific actions apply (ad-storage loss strips click IDs; analytics/persistence loss performs the full identity/session/attribution purge). Retry-queue behavior follows the exact class split of UIAF-09-DLV-001: an ordinary permission downgrade drops only predicate-failing whole entries (unrelated permitted siblings retained), while an explicit revocation, verified erasure, or dormant transition clears the entireuiaf_retry_queue— this queue split does not extend a global clear to any other generic reconcile-prohibited case beyond those three named binding classes. Ad-purpose loss MUST NOT purge identity or session state.
Server-Rendered Applications (WordPress, Django, Rails, .NET, Laravel)
Section titled “Server-Rendered Applications (WordPress, Django, Rails, .NET, Laravel)”Every page request passes through application code. This is the most natural fit. Middleware derives request context and refreshes an existing valid cookie without overwriting it; the client resolves identity, allocates the session, and delivers.
Mock Code: Server Middleware
Section titled “Mock Code: Server Middleware”// PSEUDOCODE — server middlewarefunction uiafMiddleware(request, response, next): var consent = parseConsent(request, UIAF_CONFIG) // three-layer; never a tier number // Own classification (do NOT collapse invalid to absent): absent | valid | invalid. var cookie = classifyUidCookie(request) // { state: "absent"|"valid"|"invalid", uid } var cookieUid = (cookie.state == "valid") ? cookie.uid : null
// INVALID cookie => the registry requires EXPIRY: strip before downstream + emit an original-issuance-scope // Max-Age=0 deletion, then treat as absent (a fresh create may follow under the predicate). if cookie.state == "invalid": request = stripUidCookieFromRequest(request) // controller/downstream never sees the malformed cookie deleteCookieOriginalScope(response, "uiaf_uid") // Max-Age=0 at the exact issuance scope // now treated absent
// PREDICATE-LOSS DENY-BEFORE-USE: a VALID cookie whose base predicate no longer holds is stripped + expired too. if cookie.state == "valid" AND not basePredicate(consent): request = stripUidCookieFromRequest(request) // controller/downstream never sees the prior UID deleteCookieOriginalScope(response, "uiaf_uid") // Max-Age=0 at the exact issuance scope cookieUid = null
// Middleware expiry / no-overwrite: a VALID cookie (predicate holds) is authoritative and is NOT replaced or refreshed by the ROUTE. // A separate server-owned renewal lifecycle may re-issue the SAME UID (no fixed lifetime); it is not the route // and not a client write. Middleware never mints a UID and never writes a UID supplied in the request body.
// LIFECYCLE VARIANTS — exactly ONE injected context shape per request, selected by the shared helper: // (a) PENDING: the closed minimized 3-UTM context (no referrer/landing/click/identifiers); // (b) RESOLVED + analytics allowed: the trusted resolved-state request context (below); // (c) anything else: JSON null. Under the client-only adapter profile (UIAF-07-CONSENT-016) the // server cannot resolve consent, so (b) is never reached — the fail-closed behavior above applies. injectRequestContextNoStore(response, request, consent) // == Spec01 {{ uiaf_context_json }} injection; escapes for HTML + <script> JS-string/JSON (UIAF-09-CTX-001) next()// PSEUDOCODE — shared injection helper: lifecycle-variant selection, the trusted resolved-state request// context, and the permitted first-request server capture path. Used by the middleware above and by the// edge / SSR middleware below. The pending variant passes the RAW query string/bytes (never a// framework-materialized map) through the approved normalizer: strict form-urlencoded parse-once,// first-decoded-occurrence-wins, a malformed selected occurrence nulls THAT member (no later-duplicate fallback).function injectRequestContextNoStore(response, request, consent): // SAME leakage controls for EVERY variant: Cache-Control: private, no-store; Referrer-Policy: // strict-origin-when-cross-origin (or stricter); URL/log minimization at the earliest controllable // boundary; excluded from shared/CDN caches, hydration serialization, service workers, history, // error/CSP reports, analytics, and third-party-readable globals (UIAF-09-CTX-001). if consent.status == "pending": injectContextNoStore(response, minimizedCampaignContext(rawQueryString(request), UIAF_CONFIG)) // {utm_source,utm_medium,utm_campaign} | null ONLY return if consent.effective.analytics_storage != "allowed": injectContextNoStore(response, null) // resolved without analytics: the automatic path captures nothing return // RESOLVED variant — TRUSTED PROVENANCE: built ONLY from the request URL and headers observed by THIS // server on THIS request (never client script data, never stored state), gated FIELD-BY-FIELD by the // fresh effective vector (a field whose permission fails is null). It carries no UID, no identifiers, // and no session claims — identity resolution and session ownership remain client-side authorities. var ctx = { navigation_kind: "top_level", // this IS the document request (server-trusted) sec_fetch_site: requestHeaderOrNull(request, "Sec-Fetch-Site"), referrer: acceptedReferrerOrNull(request), // accepted absolute HTTP(S) referrer only landing_url: canonicalLandingOrNull(request), // host + serialized path; no query/fragment utms: extractUtms(parseQueryOnce(rawQueryString(request))), // frozen normalization pipeline click_ids: null } if consent.effective.ad_storage == "allowed": ctx.click_ids = extractClickIds(parseQueryOnce(rawQueryString(request))) // collection gated by ad_storage; registered spellings; {value,captured_at,expires_at} // PERMITTED FIRST-REQUEST SERVER CAPTURE: this construction IS the section-03 "capture on the first // HTTP request, server-side, when permitted" path — the server collects the trusted boundary evidence // (navigation kind, Sec-Fetch-Site, referrer, landing, UTMs, click IDs) on the landing request itself, // when permitted, instead of the client re-deriving it from blocker-exposed script surfaces. The client // consumes ctx as the trusted provenance of navigationDescriptor(): it still re-reads authoritative // consent, runs the boundary/fingerprint acceptance and dedup of captureAttribution, persists under // analytics_storage, and emits under its own gates. The server never writes client storage, never // manufactures identity or session ownership, and never resurrects cleared attribution. injectContextNoStore(response, ctx)Mock Code: Client-Side Template Block
Section titled “Mock Code: Client-Side Template Block”// PSEUDOCODE — inline in page template (self-contained IIFE)(function() { // TRUSTED-FIRST: the shared reference validator and producer runtime are initialized before any // untrusted page code runs (UIAF-04-INIT-001); the same-realm hardening is relied upon only under that order. initTrustedRuntime();
// ONE serialized chain (mutex) for the init run and every consent transition — no concurrency with a purge. var _txn = Promise.resolve(); var _session = { session_id: null, owned: false }; // document-scoped proven ownership, acquired once and reused function serialize(fn) { _txn = _txn.then(fn, fn); return _txn; }
async function runSession() { var consent = reauthorize(); // authoritative re-read + reconcile BEFORE any early return if (consent.status === "pending") return; // hold: nothing sent or persisted if (allDenied(consent.effective)) return; // dormant: no payload if (!automaticSessionAuthorized(consent)) return; // no documented purpose => no automatic session / no session-state write var identity = await resolveIdentity(); // async: internally re-reads + reconciles; identity ONLY (no session) consent = reauthorize(); // AFTER identity await: barrier before ownership/capture if (!automaticSessionAuthorized(consent)) return; // reduced capability during issuance => stop, no ownership/capture/send var owned = await ensureSessionOwnership(); // acquire ONCE per document; reuse the proven result on later transitions consent = reauthorize(); // AFTER the (up-to-250ms/rekey) allocator await: barrier before capture if (!automaticSessionAuthorized(consent)) { invalidateSessionOwnership(); return; } // loss => release/reset, no capture/seq/send // Spec03 signature: captureAttribution(url, navigation) — it rereads authoritative consent INTERNALLY. // navigationDescriptor() carries TRUSTED metadata (navigation kind, reload/BFCache status, trusted // Sec-Fetch-Site/referrer/landing/new-allowlisted-signal evidence) — sourced from the injected // RESOLVED request context when the server supplied one, else from trusted client navigation APIs; // it does NOT fabricate a reliable external boundary and is never a consent object or bare URL used as proof. var attribution = captureAttribution(location.href, navigationDescriptor()); // REAL API (location.href) — click IDs only if effective.ad_storage allowed await maybeEmitSession(identity, owned, attribution, currentTrigger()); // re-reads consent + cookie again before freeze/send + baseline }
onConsentChange(function() { serialize(runSession); }); // every change enqueued on the same chain (payload never trusted) serialize(runSession); // initial run also enqueued})();Platform-Specific Hooks
Section titled “Platform-Specific Hooks”| Platform | Middleware mechanism | Where to register |
|---|---|---|
| PHP / Laravel | Middleware class | app/Http/Kernel.php |
| WordPress | Must-use plugin (loaded before ordinary plugins and the theme) | a single file in wp-content/mu-plugins/ |
| Django | Middleware class | MIDDLEWARE in settings.py |
| Rails | before_action | ApplicationController |
| .NET Core | Middleware pipeline | Program.cs |
WordPress trusted-first boundary. The enforceable WordPress integration is a must-use plugin. WordPress loads mu-plugins before ordinary plugins and the active theme, so the middleware (context injection, deny-before-use cookie handling) runs before plugin/theme output, and the trusted browser bootstrap (initTrustedRuntime) can be emitted as the first controlled head script, ahead of third-party/plugin scripts. functions.php plus an init callback does not by itself satisfy this: the theme loads after active plugins and init establishes no script-output order before arbitrary plugin/theme output. functions.php is nonconforming unless the deployment proves equivalent ordering.
Single Page Applications (React, Vue, Angular)
Section titled “Single Page Applications (React, Vue, Angular)”SPAs navigate client-side after the initial load. Initialize once on mount; hook the router so subsequent navigations are gated by the commit-gated baseline, not re-initialized.
// PSEUDOCODE — runs once on app mountvar _identity = null, _consent = nullvar _session = { session_id: null, owned: false } // the PROVEN allocator result; only a proven-owned result is reused for route eventsvar _txn = Promise.resolve() // transition serialization chain
// ONE serialized chain (mutex) for EVERY operation — init, transition, route, capture, persist, send.// A newly queued transition cannot run concurrently with an in-flight op; each op re-reads authoritative// consent + reconciles immediately before use, and re-checks after any endpoint/network await.function serialize(fn): _txn = _txn.then(fn, fn) // run after prior work regardless of prior outcome return _txn
async function initializeUIAF(): initTrustedRuntime() // UIAF-04-INIT-001: trusted runtime/validator before untrusted code onConsentChange(function() { serialize(runSession) }) // every change enqueued on the same chain (payload never trusted) serialize(runSession) // the initial run is ALSO enqueued so it cannot race a transition
// Single serialized worker for init and every transition.async function runSession(): if not await ensureAuthorizedRuntimeState(): return // shared prerequisite path; bails safely to the next cycle // Spec03 captureAttribution(url, navigation): rereads authoritative consent internally before collection/persistence. var attribution = captureAttribution(location.href, navigationDescriptor()) // REAL API (location.href) — trusted navigation metadata; not consent, not a bare URL await maybeEmitSession(_identity, _session, attribution, currentTrigger()) // re-reads consent + cookie again before freeze/send + baseline
// SHARED PREREQUISITE PATH — called by EVERY authorized operation (initial run, consent// transition, AND route change) BEFORE any capture or maybeEmitSession. It reauthorizes,// resolves/adopts identity when absent or stale, and AWAITS proven session ownership when// not yet established. Returning false BAILS SAFELY to the next cycle: the pending/dormant// case waits for a consent transition, and a mid-operation capability loss is picked up by// the next serialized run — no path emits without these prerequisites.async function ensureAuthorizedRuntimeState(): var consent = reauthorize() // authoritative re-read + reconcile BEFORE any early return installRouteHookIfNeeded() // install ONCE, even on the pending early-return path (callbacks are gated) _consent = consent // update shared state only inside the barrier if consent.status == "pending" OR allDenied(consent.effective): return false if not automaticSessionAuthorized(consent): return false // no purpose => no automatic session / no session-state write // IDENTITY: resolve/adopt when ABSENT (never resolved — e.g. a route that first observes resolved // consent before the CMP callback ran) or STALE (authoritative cookie diverged/disappeared, or a // cookie has appeared since an EPHEMERAL_NULL result). A retained EPHEMERAL_NULL without a readable // cookie is a RESOLVED indeterminate state — re-resolution waits for a consent transition, so routes // never hammer the issuance boundary. if _identity == null OR identityStale(_identity): _identity = await resolveIdentity() // async: internally re-reads + reconciles; identity ONLY (no session) consent = reauthorize(); _consent = consent // AFTER identity await: barrier; update shared consent only after it if not automaticSessionAuthorized(consent): return false // reduced capability during issuance => bail to the next cycle // OWNERSHIP: acquire ONCE per document; reuse the proven result across transitions/routes. Re-await // when not currently proven (first authorized pass, BFCache restore, purge invalidation, or a prior // coordination-exhaustion result — a later attempt may succeed once a competing tab releases). if not (_session.owned AND _session.session_id != null): _session = await ensureSessionOwnership() // AWAIT the proven allocator result (grant OR bounded exhaustion) consent = reauthorize(); _consent = consent // AFTER the (up-to-250ms/rekey) allocator await: barrier before capture if not automaticSessionAuthorized(consent): invalidateSessionOwnership(); return false // loss => release/reset, bail return true
function identityStale(identity): if identity.uid != null: return not authoritativeCookieMatches(identity) // cookie is authoritative; divergence => re-resolve/adopt return readValidUidCookie() != null // a cookie appeared since EPHEMERAL_NULL => adopt it
// Router hook — every navigation is ENQUEUED on the same chain (not merely awaited), so it cannot race a purge.var _routeHookInstalled = falsefunction installRouteHookIfNeeded(): if _routeHookInstalled: return _routeHookInstalled = true router.onRouteChange(function(newUrl, previousUrl) { serialize(function() { return onRouteChange(newUrl, previousUrl) }) })
async function onRouteChange(newUrl, previousUrl): // EVERY authorized route crosses the SAME shared path first: a route that observes newly allowed // analytics while _identity is still null and _session still unowned (pending initialization, a // missed/delayed CMP callback, a route callback queued before the consent callback) reauthorizes, // establishes identity + proven ownership, and only then proceeds — otherwise it bails safely and // the consent callback / next route re-enters on the same chain. if not await ensureAuthorizedRuntimeState(): return // SPA navigation is a CONTINUATION and not a reliable external boundary unless a new allowlisted signal is present; // previousUrl is context, NEVER proof. captureAttribution rereads authoritative consent internally. var attribution = captureAttribution(newUrl, spaNavigationDescriptor(newUrl, previousUrl)) // Route events REUSE the retained proven owned result (_session) — BFCache restore updates it via pageshow. await maybeEmitSession(_identity, _session, attribution, currentTrigger()) // re-reads consent again immediately before send/writeEvery transition re-reads authoritatively and re-establishes identity and proven ownership through the shared path as the new effective vector requires (re-resolution when identity is absent/stale, purge via reconcile). It emits at most a normal session. There is no dedicated consent-change event type (consent transitions are control-plane; see section 07). Because the route hook is installed on the first serialized run (even when it holds for pending), a pending → resolved → route-change sequence resolves identity on the resolving transition, and the hook is already in place for the subsequent navigation. A route that first observes resolved consent (before any CMP callback ran) establishes the same prerequisites through ensureAuthorizedRuntimeState before capture. So no route can emit with _identity null or ownership unestablished.
Capture takes navigation, not consent. The approved Attribution Capture signature is captureAttribution(url, navigation). Capture reads authoritative consent internally immediately before collection/persistence; a consent object is never passed in as proof. The navigation argument is trusted navigation metadata: navigation kind (top-level vs sub-resource), reload/BFCache status, and trusted Sec-Fetch-Site/referrer/landing/new-allowlisted-signal evidence as applicable.
// PSEUDOCODE — reconcile-on-init (mechanism-aware; runs before any early return).// Behavior is DRIVEN BY the storage-key registry (uiaf-storage-keys.json) — no competing hand-maintained key list.// Each key's purge is governed by its OWN registered predicate; capability-field transitions (never tier numbers) drive it.function reconcileOnInit(consent): purgeLegacyMechanismsOnSight() // exact live + legacy mechanisms deferred SOLELY to uiaf-storage-keys.json (no list here)
// (S09-19) On ANY applicable effective-permission downgrade — analytics OR ad_storage OR ad_user_data/ad_personalization // (routing-only) — purge the baseline FIRST, before any stripped/restricted projection is evaluated. if anyEffectivePermissionDowngrade(consent): purgeByRegistryPredicate("uiaf_dirty_baseline")
// (S09-18) CREDENTIAL-ONLY invalidity is NOT consent loss: recovery expiry/revocation/corruption or script-pair // corruption purges/no-ops ONLY the governed script pair and makes recovery unavailable; a still-valid authoritative // cookie remains usable, and uiaf_session_state / uiaf_attribution are NOT purged for this alone. if recoveryCredentialInvalidOrCorrupt() OR scriptPairCorrupt(): purgeGovernedPair() // pair-only / no-op; recovery unavailable; cookie still governs
var eff = consent.effective // PERMISSION / control-plane loss: analytics-permission/persistence loss, consent revocation, erasure, dormant, // reconcile-prohibited => the BROADER registry purge (do NOT conflate credential revocation with consent revocation). if analyticsPersistenceLostOrRevokedOrErasedOrDormant(consent): atomicPurgeGovernedPair() // uiaf_uid + uiaf_recovery together — never a lone uid purgeByRegistryPredicate("uiaf_session_state") // session state purged on analytics/persistence loss invalidateSessionOwnership() // a full session purge RELEASES/RESETS the retained proven ownership purgeByRegistryPredicate("uiaf_attribution") // delete the WHOLE attribution record purgeByRegistryPredicate("uiaf_dirty_baseline") // UNCONDITIONAL + idempotent here: revocation/erasure/dormant/ // reconcile-prohibited can occur with NO vector delta, so do NOT // rely on the earlier downgrade-delta limb to purge the baseline else if eff.ad_storage != "allowed": // AD-PURPOSE-ONLY loss: RETAIN the identity pair and session state; strip ONLY click-ID members. stripClickIdMembers("uiaf_attribution")
// Retry-queue behavior SPLIT (S09-30): the control-plane full-purge classes (explicit revocation, verified // erasure, dormant) clear the ENTIRE uiaf_retry_queue — including otherwise-permitted entries — because the // subject withdrew / was erased / went dormant. Ordinary permission downgrades keep the narrower behavior: // whole-entry predicate-selective drops with unrelated permitted entries retained. if controlPlaneFullPurgeClass(consent): // explicit revocation | verified erasure | dormant clearEntireRetryQueue() else: dropRetryEntriesWhosePredicateFails(consent) // selective whole-entry drops; unrelated permitted entries retained // Server middleware complements this: it treats a prior UID as ABSENT before use and emits an // original-scope deletion cookie (Path/Domain matching the issuance scope) when the predicate is lost.UIAF-09-ID-004 — Reconcile-on-init MUST execute before any
pending/all-denied early return so that staleuiaf_uid/uiaf_recovery/uiaf_session_state/uiaf_attribution/uiaf_retry_queue/uiaf_dirty_baselinenever survive a downgrade first observed at initialization; the purge MUST be mechanism-aware over client script-clearable storage only, defer the exact live/legacy key set to the registries, and (server-side) treat a prior UID as absent before use and emit an original-scope deletion cookie on predicate loss. Consent-change handling MUST NOT trust anonConsentChangecallback payload as proof; it MUST re-read authoritative consent at every re-read boundary before acting.
UIAF-09-CONSENT-002 — Consent transitions MUST be serialized so that no route change, capture, persistence write, queue, drain, or send runs concurrently with a purge/re-resolution; every such path MUST cross one authoritative-re-read + reconcile/purge barrier before any early return or use, and shared consent state MUST be updated only after that barrier completes.
Static Sites (Hugo, Jekyll, Astro Static, GitHub Pages)
Section titled “Static Sites (Hugo, Jekyll, Astro Static, GitHub Pages)”No server-side code. If edge compute is available (Cloudflare Workers, Vercel/Netlify Edge, CloudFront Functions), an edge function leaves an existing valid cookie untouched (no-overwrite) and injects the minimized context. It does not mint or renew a UID; new UIDs come only from /api/uiaf/cookie.
Without any server or edge, the persistent-identity profile is nonconforming: the site runs in ephemeral mode: uid: null, session-only, no persistent cookie. There is no client-minted persistent UID; identity.resolution_method reports ephemeral and confidence: "low".
// PSEUDOCODE — edge functionasync function handleRequest(request): // DENY-BEFORE-USE: classify + evaluate consent BEFORE forwarding to origin. var consent = parseConsent(request, UIAF_CONFIG) var cookie = classifyUidCookie(request) // { state: "absent"|"valid"|"invalid", uid } // INVALID or valid-but-predicate-lost => strip before forwarding and expire (Max-Age=0, original scope). var expire = (cookie.state == "invalid") OR (cookie.state == "valid" AND not basePredicate(consent)) if expire: request = stripUidCookieFromRequest(request) // do NOT forward the prior/malformed UID to origin var response = await fetch(request) // REAL API — forward AFTER deny/strip if expire: deleteCookieOriginalScope(response, "uiaf_uid") // original-issuance-scope deletion cookie on the response // A valid cookie whose predicate holds is authoritative and is NOT overwritten; the edge never mints/renews a UID. injectRequestContextNoStore(response, request, consent) // shared helper (Server-Rendered section): PENDING 3-UTM shape | RESOLVED trusted request context incl. the permitted first-request capture path | null; escapes for HTML + <script> JS-string/JSON (UIAF-09-CTX-001); RAW query in; no uid return responseHybrid / SSR Frameworks (Next.js, Nuxt, SvelteKit, Remix)
Section titled “Hybrid / SSR Frameworks (Next.js, Nuxt, SvelteKit, Remix)”Server middleware provides context and leaves an existing valid cookie untouched (no-overwrite); a separate, server-owned renewal lifecycle (not shown here and not this route) may re-issue the same UID. Client hydration resolves identity, allocates the session, and installs the shared route hook (identical to the SPA module above).
Server automatic-session ownership is the exception, not the default. The browser owns automatic session events. A server may emit an automatic session only when it authoritatively holds all four authorities: (1) the prior committed canonical projection string, (2) the current consent/policy state, (3) the session_id and its next sequence, and (4) the committed delivery state (a 2xx or durable server-side enqueue).
// PSEUDOCODE — SSR server middlewarefunction middleware(request): // DENY-BEFORE-USE: classify + evaluate consent BEFORE invoking downstream. var consent = parseConsent(request, UIAF_CONFIG) var cookie = classifyUidCookie(request) // { state: "absent"|"valid"|"invalid", uid } var expire = (cookie.state == "invalid") OR (cookie.state == "valid" AND not basePredicate(consent)) if expire: request = stripUidCookieFromRequest(request) // prior/malformed UID not visible to downstream handlers var response = next() // downstream AFTER deny/strip if expire: deleteCookieOriginalScope(response, "uiaf_uid") // original-issuance-scope deletion cookie (Max-Age=0) injectRequestContextNoStore(response, request, consent) // shared helper (Server-Rendered section): PENDING 3-UTM shape | RESOLVED trusted request context incl. the permitted first-request capture path | null; escapes for HTML + <script> JS-string/JSON (UIAF-09-CTX-001); RAW query in; no uid return response
// Client hydration reuses the SPA module: initTrustedRuntime, parseConsent, reconcileOnInit,// resolveIdentity (async, issuance lock), acquireSessionOwnership (awaited), ensureAuthorizedRuntimeState// (the shared init/transition/route prerequisite path), installRouteHookIfNeeded, maybeEmitSession.UIAF-09-OWN-001 — A server MUST NOT emit an automatic
sessionunless it authoritatively holds all four of the prior committed projection string, the current consent/policy state, thesession_id/next-sequence, and the committed delivery state; otherwise it MUST restrict server emission to explicit server-knownconversion/identify. Hydration MUST seeduiaf_dirty_baselineonly from a committed server outcome and MUST NOT suppress a client event on an unacknowledged or indeterminate server attempt.
Delivery: same-origin relay first
Section titled “Delivery: same-origin relay first”A same-origin relay is the recommended primary path. Its URL is deployment-chosen and deliberately not tracker-shaped. Avoid names that match ad-blocker heuristics (collect, _ga, _fbp, _gcl, track, analytics); pick a neutral first-party path. After Origin/Sec-Fetch-Site, CSRF, and rate-limit checks, the approved artifact pipeline runs in exactly this order:
- Total raw-byte preconditions first: fatal UTF-8 decode, byte size ≤ 32768, duplicate-member-name rejection, JSON parse/structure, schema/enums, and RFC 8785 canonical-JCS equality. These are byte/structure gates on the raw body before any object is trusted.
- Own-only relay canonicalization: force
_meta.emittertoclientfor browser submissions (a public/browser assertion ofserveris rejected; only an authenticated server-to-server channel may preserveserver), producing the canonical object. - Semantic/reference validation of that canonical object: the prohibited-content and semantic screens run on the canonicalized object.
Only then is the payload forwarded with server-added downstream authentication; the browser never holds secrets. (This is not schema-only validation, and step 2 never precedes step 1.)
Direct public ingest is an explicitly untrusted surface. A schema-valid payload cannot prove its own authenticity, so a direct endpoint enforces bounds, quota, and anomaly limits with zero privileged/control-plane effects. It does not attempt “forgery rejection.”
Transport and retry follow section 04: 2xx success; 4xx and an unexpected final 3xx are terminal drop/observe; network failures and 5xx retry the byte-identical frozen body (never re-serialized). Each uiaf_retry_queue entry is keyed by immutable event_id and carries immutable required_permissions/purpose/destinations. It is persisted only when the event-purpose storage permission allows it, with bounded exponential backoff + jitter, a 72-hour TTL, and an attempt cap. On a capability downgrade an entry whose predicate fails is dropped whole before send (never redacted or resent under the same event_id), while unrelated still-permitted entries are retained. If any click-ID occurrence in an already-frozen retry body has reached its expires_at, the entire entry is terminally dropped (body/route/permissions/event_id never edited); a sanitized emission is a new logical event with a new event_id. sendBeacon(true) is UA-queue acceptance, not an endpoint ACK; the keepalive/beacon budget is shared headroom, not a guarantee; and baseline advancement / trigger consumption never rest on sendBeacon(true) alone. On the unload path, when durable retry storage is permitted and available, the byte-identical body is durably enqueued before/alongside sendBeacon and retained until a 2xx-acknowledged drain.
Control plane (revocation, DSR). These reuse no data-plane envelope and emit no data-plane event; see the exact contracts in section 04. Browser revocation is a same-origin, CSRF-hardened, idempotent, non-enumerating, tombstone-only request. The prior UID is used solely to honor the withdrawal before local purge. The local purge and dormant transition proceed regardless of the call’s outcome; the request is not queued and writes no persistent opt-out marker. Privileged erasure/access/portability is gated on authenticated server-side requester verification with a downstream cascade and asynchronous status; a uiaf_uid, any derived hash, or uiaf_recovery never authenticates it.
UIAF-09-DLV-001 — Retry entries MUST carry the immutable frozen body keyed by
event_idwith immutablerequired_permissions/purpose/destinations, honor a 72-hour TTL and attempt cap, and be stored only when the event-purpose storage permission allows. On an ordinary permission downgrade an entry MUST be dropped whole (never redacted or resent under the sameevent_id) on predicate loss or on any frozen click-ID reachingexpires_at, and unrelated still-permitted entries MUST be retained. On an explicit revocation, verified erasure, or dormant transition the implementation MUST clear the entireuiaf_retry_queue, including otherwise-permitted entries.
UIAF-09-CTRL-001 — Browser revocation MUST be same-origin, CSRF-hardened, idempotent, non-enumerating, tombstone-only, not queued, and write no persistent marker, with local purge/dormancy proceeding regardless of call outcome; privileged DSR operations MUST require authenticated server-side requester verification and MUST NOT be authenticated by a
uiaf_uid, derived hash, oruiaf_recovery.
Cookie lifetime and ITP (dated)
Section titled “Cookie lifetime and ITP (dated)”Cookie lifetimes are browser- and version-dependent and change; verify against current WebKit behavior rather than treating any number as permanent. These forms mirror Identity Management and Browser Landscape, which carry the dated primary sources.
- A1. ITP does not cap an aligned server-
Set-Cookiefirst-party cookie the way it caps script-writable storage. Safari’s maximum here is undocumented (the draft RFC 6265bis request ceiling is ~400 days: what the server requests, not a lifetime any browser guarantees). Server-set cookies are generally more resilient, not immune, not guaranteed. - A2. Script-writable storage (including
document.cookiecookies) is purged after 7 days of Safari use without recognized first-party user interaction. A recognized interaction is a tap, click, or keyboard input; passive or full-page navigation does not reset the clock. UIAF sets no script cookie for the persistent UID, so the identity is never in this class. - A3. The decorated-navigation 24-hour cap applies to a cookie set via
document.cookieon a page whose referrer is an ITP-classified tracker and whose landing URL carries any query string or fragment. ITP does not inspect cookie contents, and named click IDs are industry examples of decoration, not WebKit’s predicate. UIAF sets no script cookie, so this class never holds the identity. - A4. WebKit’s CNAME/“cloaking” heuristic compares address prefixes. The first half of the address differs: /16 for IPv4, /64 for IPv6, per current WebKit source; the tracking-prevention documentation describes the CNAME/IP cloaking defense but does not state these prefix lengths. “Same IP” / an exact-IP or version-numbered guarantee is not the rule. Same-infrastructure hosting is the safe path.
- A5. These reduce, not eliminate, identity loss. Recovery via
uiaf_recovery(under the base predicate and continuity policy) is the durable path. A browser cannot distinguish manual cookie clearing from expiry, andidentity.confidencehonestly reports reduced reliability (_metacarries no confidence field). UIAF makes no fixed-lifetime cookie promise.
Two supporting first-party ITP-resilience patterns: a reverse-proxy path (yourdomain.com/<neutral-path> → your sGTM endpoint, same origin) and a CNAME subdomain (subject to the A4 prefix heuristic). The application-level /api/uiaf/cookie route is the default.
Testing and Validation
Section titled “Testing and Validation”Capability-based Test Matrix
Section titled “Capability-based Test Matrix”Each row is expressed from the effective-capability model and the v3 flows, not tiers.
| Scenario | What to verify | Expected outcome |
|---|---|---|
New visitor, analytics_storage: allowed (create) | /api/uiaf/cookie create → server-minted UID adopted via the mandatory re-read | is_new: true, resolution_method: "cookie" (a confirmed first issuance reports cookie, never the receiver-side report value "new" — UIAF-04-ID-001), cookie authoritative; the {uid, recovery} pair is mirrored only when a bound credential was returned and the predicate holds — otherwise no lone script UID |
| Return visitor, valid cookie present | Cookie adopted, no Set-Cookie overwrite/refresh | is_new: false, resolution_method: "cookie", same UID |
| Divergent valid server cookie + old credential | Server cookie diverges; stored credential is bound to a different UID | Server cookie adopted (authoritative); pair not persisted (credential not bound to the authoritative UID); no mismatched pair written |
| Rotated recovery credential | create/recover returns a rotated credential bound to the issued UID | Retained response credential mirrored atomically as the {uid, recovery} pair; stale store value not reused |
Recovery (cookie absent, uiaf_recovery present, continuity allows) | recover body carries only uiaf_recovery; candidate UID re-read | resolution_method: "localstorage_recovery" or "sessionstorage_recovery", is_new: false |
| Body-UID smuggling | POST /api/uiaf/cookie with a UID in the body | 400, no cookie set (identity-fixation fixture) |
| Consent metadata mismatch/stability | site_policy with resolved; cmp_*/gcm with not_applicable; GPC asserted as source; state_updated_at on unchanged reread; consent_record_id rotation | Mismatches rejected/never produced; state_updated_at stable across unchanged rereads; record-ID rotation leaves projection byte-identical (no session) |
Consent adapter state_updated_at strategies (UIAF-07-CONSENT-015) | Source supplies a trustworthy material-change time; source cannot; adapter can neither supply one nor persist a freeze | Source-supplied time used verbatim; else the registered first-observation freeze (persisted per source record, stable across unchanged rereads, replaced only on the record’s own material change); else lifecycle pending (reason unavailable) — never an invented per-read timestamp, never a silent strategy switch |
| Server-unreadable client consent record (UIAF-07-CONSENT-016 degraded mode) | CMP record resolvable in the browser but unreadable to middleware (client-only adapter profile) | Server fails closed: consent unresolved for the request, deny-before-use, no issuance/refresh against the unobservable record; client bounds create/recovery attempts (deployment-configured budget) then runs the client-only degraded profile (uid: null, no server persistent-identity capability) — no permanent churn loop |
| No documented analytics purpose | analytics_storage: allowed but no documented purpose | No automatic session, no ownership/sequence/freeze/send, no uiaf_session_state write |
| Allocator actually awaited (first load / contention / route) | ensureSessionOwnership is invoked and awaited after authorization; the proven result is retained and reused | A non-null session is used only after a proven-owned grant; owned: false degrades to a null-session emission (no seq allocation, no candidate used as owned); identity never manufactures ownership |
| Ownership acquired once, reused across transitions | Multiple onConsentChange runs in one document | Lock acquired once; later transitions/routes reuse the proven _session; no self-contention or lock accumulation |
| First-load absent/corrupt/oversize state | No/invalid uiaf_session_state | A fresh UUID candidate is staged and its exact lock acquired; uiaf-session-alloc:null is never requested; owned:true implies a non-null UUID |
| Both fresh-lock attempts fail (authorized + event due) | Inherited + both fresh-UUID lock attempts fail; session otherwise authorized and an event is due | Emits one schema-valid/reference-valid session with session_id: null, session_seq: null, session_start: false; zero allocateSeq calls, zero candidate-as-owned use, no guessed seq; normal trigger/baseline commit semantics apply |
| Full purge resets ownership | Analytics/persistence loss purge | Retained _session released/reset; next authorized run re-acquires |
| Consent loss during allocation (SSR + SPA) | Capability drops during the up-to-250 ms allocator await | Post-allocator reauthorize fires; on loss the just-acquired ownership is reset and there is no capture/seq/send |
| Dirty gate before sequence | Same projection + no trigger vs changed/trigger | Same/no-trigger => next_seq unchanged (no allocation); changed/trigger => exactly one synchronous pre-freeze allocation |
| Authoritative cookie at send/baseline | Cookie diverges/disappears after identity & before send; or changes after send & before baseline | Pre-send cookie barrier rebuilds/drops (no send); post-commit baseline write skipped when cookie/consent no longer match the committed projection — no stale baseline |
| Capture navigation authority | Reliable cross-site boundary; identical-UTM arrival across a real boundary; SPA continuation/reload/BFCache | captureAttribution(url, navigation) rereads consent internally; distinct touch_id on the real boundary even with identical UTMs; continuation/reload/BFCache is not a boundary (no new touch) |
| Consent loss during/after allocation (capture) | Capability drops during/after the allocator await, before capture/persist | Post-allocator + pre-persist barriers drop before any click-ID persistence; no stale click IDs written or sent |
| BFCache restore ordering | Restore vs a concurrent consent change/purge; pending/dormant restore; cookie divergence; acquisition failure | Restore enqueued on the one chain; consent+cookie barriers before staging and before retaining; pending/dormant/divergent/failed => owned:false, no state use; stale pre-suspension ownership never used |
Allocator race (two tabs, one session_id) | Only one owner; the loser rekeys to a fresh UUID or emits session_id: null | One allocator per non-null session_id; no shared inherited session |
| No Web Locks / non-secure context | Unconditional per-document rekey before first event | CSPRNG-distinct session_id; silence never proves ownership |
| BFCache restore | pageshow.persisted re-acquire-or-rekey; pagehide released ownership | Bounded reacquire under 250 ms AbortSignal, else rekey |
| Unreadable cookie after issuance | Post-response re-read returns nothing | Identity indeterminate; an authorized anonymous uid: null event still permitted; no lone UID persisted |
| Issuance race / issuance failure | Two tabs issue concurrently; lock contended or unavailable | Only the winner calls the endpoint; loser degrades to wake / authoritative re-read with eventual convergence |
| Consent wake + re-reads | uiaf-consent-wakeup fires; focus/visibility/pageshow | Wake carries no state; authoritative consent re-read before capture/write/queue/drain/send; decreased capability purges pending before transmit |
| Invalid cookie expiry (route/middleware/edge) | Non-canonical uiaf_uid cookie present | Classified invalid: stripped before downstream + original-issuance-scope Max-Age=0 deletion emitted, then treated absent; a fresh create may follow under the predicate |
| Raw pending query (duplicate + malformed) | Duplicate utm_* keys; percent-decoding error on the selected occurrence | Raw query parsed once: first decoded occurrence wins; a malformed selected occurrence fails that member with no later-duplicate fallback and valid siblings survive; zero admitted => top-level null; never a framework-materialized map |
| Credential-only invalidity (pair-only purge) | uiaf_recovery expiry/revocation/corruption or script-pair corruption; cookie still valid | Governed script pair purged/no-op (recovery unavailable); uiaf_session_state/uiaf_attribution/baseline retained; authoritative cookie still usable |
| Analytics/persistence permission loss (full purge, ordinary) | Ordinary analytics/persistence permission loss | Governed pair + uiaf_session_state + whole attribution + baseline purged; original-scope deletion cookie emitted; retry is selective — only predicate-failing whole entries dropped, unrelated permitted siblings retained (queue not fully cleared) |
| Control-plane purge (revocation / verified erasure / dormant / reconcile-prohibited) | Explicit revocation, verified erasure, dormant, or reconcile-prohibited — even with no vector delta | Governed pair + uiaf_session_state + whole attribution purged; baseline purged unconditionally/idempotently (not reliant on a downgrade delta); for the three named binding classes (revocation/erasure/dormant) the entire uiaf_retry_queue is cleared (see row below) |
| Retry outcomes | 2xx drains; 4xx/unexpected 3xx terminal drop; 5xx/network requeue byte-identical | Same event_id; no re-serialization |
| Retry TTL / click-expiry | Entry past 72 h TTL, or a frozen click-ID past expires_at | Whole entry terminally dropped; a sanitized emission uses a new event_id |
| Capability downgrade with queued entries (ordinary) | Predicate-failing entries dropped before send; unrelated permitted entries retained | Dropped entries never redacted/resent under the same event_id; siblings retained |
| Revocation / verified erasure / dormant (queue) | Explicit revocation, verified erasure, or dormant transition with queued entries | Entire uiaf_retry_queue cleared, including otherwise-permitted entries; ordinary-downgrade sibling-retention control still holds |
| Full-lock release on invalidation | Authorization loss / full purge while owned, then later reauthorization | Invalidation releases the held lifetime lock; later same-document reacquire succeeds without self-contention; BFCache restore stays serialized |
| Server automatic-session ownership | Server emits a session without all four authorities | Rejected: server limited to explicit conversion/identify; hydration seeds baseline only from a committed outcome; no client-event suppression |
| Revocation with network failure | Local purge + dormancy proceed regardless of call outcome | Tombstone-only, idempotent, not queued, no persistent marker |
ad_storage: denied, analytics_storage: allowed (ad-purpose-only loss) | Only click-ID members stripped; identity pair + session state RETAINED | click_ids: {}, _meta.data_quality: "stripped"; uiaf_uid/uiaf_recovery/uiaf_session_state intact |
ad_storage downgrade — baseline first | ad_storage drops while analytics stays allowed | uiaf_dirty_baseline purged before the new stripped projection is evaluated; identity/session NOT purged; click IDs stripped |
ad_user_data/ad_personalization routing-only downgrade | Only a routing permission decreases | Baseline purged before projection evaluation; identity/session/attribution retained; routing suppressed downstream |
| Deny-before-use ordering (edge/SSR) | Predicate lost; request forwarded to origin/downstream | Prior UID stripped from the request before fetch/next(); deletion cookie attached to the response; downstream never sees the UID |
| Relay ingress ×3 | Public browser asserts server; relay browser submission; authenticated S2S asserts server | Public server rejected; relay forces client; authenticated S2S preserves server — each only after raw-byte preconditions pass, then canonicalization, then semantic validation |
| Serialized barrier race | Purge queued while a route/capture/send is pending | The pending op completes or is superseded on one chain; a stale-consent op never sends/writes; purge completes before subsequent use |
| Transition/consent-loss during issuance lock | Capability drops after lock grant / after the endpoint await | Post-grant and post-endpoint reauthorize barriers fire; predicate-loss returns uid:null, session_id:null; no pair mirror, no allocateSession/session-state write, no event |
| Consent loss during delivery / before baseline write | Capability drops after identity resolves (pre-send) vs after a committed send (pre-baseline) | Pre-send loss (or non-committed attempt) leaves BOTH baseline and trigger untouched; a committed outcome consumes the trigger, and a post-commit consent/cookie/projection mismatch skips ONLY the baseline write (no stale baseline) |
| pending → resolved → route change | Init holds for pending; later transition resolves; then navigate | Route hook installed on the first serialized run; resolving transition resolves identity + emits; subsequent navigation gated and sends on baseline change |
| Route first observes resolved consent (CMP callback missed/delayed) | Init was pending; a route callback queued before the consent callback observes newly allowed analytics with _identity null and _session unowned | ensureAuthorizedRuntimeState reauthorizes, resolves/adopts identity, and awaits proven ownership BEFORE capture/maybeEmitSession; on any prerequisite failure it bails safely to the next cycle — no invalid/ephemeral emission, no lost create trigger |
All four effective: denied | Dormant | No payload emitted at all |
status: pending | Hold | Zero send/persist; only minimized {utm_source,utm_medium,utm_campaign} in memory |
Browser Developer Tools Validation
Section titled “Browser Developer Tools Validation”The exact live and legacy storage keys are defined by the registries (uiaf-storage-keys.json, uiaf-enums.json); the names below are illustrative pointers, not a competing list.
- Application → Cookies:
uiaf_uidpresent in canonical{uuid_v4}.{unix_seconds}form,Secure,SameSite=Lax,HttpOnly=false; treat lifetime as browser-dependent (see the dated notes), not a fixed number. - Application → Session/Local Storage:
uiaf_session_stateis{ session_id, next_seq };uiaf_dirty_baselineholds a canonical projection string (not a hash);uiaf_attributionholds touchpoint data; when present, theuiaf_uid/uiaf_recoverypair mirror matches the cookie. A loneuiaf_uidwithout its recovery credential is never written. - Network: the relay/endpoint POST body matches the schema (section 04); the
consentobject reflects resolved consent andidentity.confidencethe reliability, while_metacarries onlyuiaf_version/data_quality/attribution_completeness/emitter; response is2xx. - Console: no errors from UIAF code.
Common Mistakes
Section titled “Common Mistakes”- Minting a UID on the client or putting one in a request body. UIDs are server-minted; a body UID is
400. Use create/recover and re-read the authoritative cookie. - Overwriting a valid cookie. A valid browser-held cookie is unconditional authority. Adopt it, and never refresh or replace it from the route or from script. Mirror to localStorage/sessionStorage only as the atomic
{uid, recovery}pair, and only when a bound recovery credential is available and the base predicate holds; otherwise write no script UID (the cookie still governs identity). - Branching on tier numbers. Behavior keys off the
effectivevector andstatus; tiers are display shorthand only. - Buffering more than the allowlisted campaign context while pending. Only
{ utm_source, utm_medium, utm_campaign }from the configured vocabulary may live in memory pre-consent, never referrer, landing, click IDs, or identifiers. - Advancing the baseline on
sendBeacon(true). The baseline advances only on a2xxack or a permitted durable enqueue;sendBeacon(true)is UA-queue acceptance only. - Storing a hash instead of the projection string.
uiaf_dirty_baselineis the canonical JCS string; a non-JCS serialization never matches by parsing equal. - Exposing a
dataLayer-style replay getter. UIAF does not expose a getter that re-emits historical events on read; each logical event is authorized and emitted once with its ownevent_id. - Tracker-shaped delivery paths or cookie names. Names/paths matching
_ga,_fbp,_gcl,collect,analyticsare targeted by filter lists (section 05); choose neutral first-party names. - Firing before consent resolves. Hold while
statusispending; emit only after resolution under the effective-capability branches.