Repository navigation
Description
Summary
Identity features pass on Spin, the same scope as T17.
Part of #1558, server-side identity on every adapter. Plan ID T18, planned in wave 18 (after the MVP: Spin); priority P3, size M. Design: spec; order and dependencies: plan.
Requirements
ID-4 Providers on every supported adapter
Lookups start at navigation alongside the origin fetch, share one deadline, and are awaited before finalization, so their IDs reach the record and the cookies of the same document response. A page load writes the record once.
ID-7 Abuse resistance
Where a trusted JA4 is available, record creation is refused when the JA4's TLS client family contradicts the User-Agent's browser family; a JA4 the rule does not recognize, or no trusted JA4 at all, does not block (DEC-18). The family rule and its data live in core with T12, which owns keeping them current; JA4 comes only from EdgeZero's ctx.ja4() (EZI-3).
ID-9 EC route
A same-origin route, /_ts/api/v1/ec by default, lets the script Trusted Server injects into proxied pages report consent and receive identity, so a reader who consents after the page loads gets an EC on that page view instead of the next navigation. A POST whose Origin matches the page mints the EC when the reader has none and consent allows, creates the record, runs the providers as a navigation does, and sets ts-ec, the SharedID cookie and the mirror cookie on its own response. It carries the consent strings the script reads from the CMP, the TCF string from __tcfapi and the GPP string from __gpp (Sourcepoint's capture from #1535 where present), which replace the consent cookies for that request; a withdrawal expires ts-ec, the SharedID cookie and the mirror cookie and tombstones the record in that response. The POST answers with JSON holding exactly what WB-4 would put in the mirror cookie for that request, and the SharedID only under SID-2 and SID-6; the script merges it into Prebid, and nothing else in the record reaches the page. A plain GET answers Prebid's SharedID pixelUrl with an empty 204: it never mints, calls a provider or changes consent, and only re-sets an existing SharedID cookie from the record (WB-3). The script sends consent changes in order with at most one call in flight. While a call is in flight, a newer change replaces a waiting one, except a waiting withdrawal, which is never replaced: later changes wait behind it. So every withdrawal reaches the server, after any grant in progress and before any later grant. The script discards a result that arrives while a newer consent change is waiting, and a withdrawal removes the IDs it merged into Prebid. Every response carries Cache-Control: no-store; a rate limit on the route waits with ID-6 (DEC-15). The route is served from the page's own host, which WebKit's cap on cookies from non-document responses exempts when the IP matches the page's (read from WebKit's source; the Accept tests it).
WB-3 SharedID cookie
When SID-2 and SID-6 allow it, the server sets the SharedID cookie from the record if the request has none or a different value. When neither the record nor the request has one, the server creates a SharedID in the format Prebid writes (a UUID v4), stores it in the record and sets it the same way, because a browser may block Prebid.js from loading at all (DEC-7). It creates and writes back a SharedID only when the configured storage.type is cookie (Prebid's cookie&html5 counts as cookie): with html5 Prebid never reads the cookie, so a server-made SharedID would split the reader's identity (SID-3). It uses Prebid's storage name and the Domain Prebid writes with, is not HttpOnly, and is Secure; SameSite=Lax for one year. It keeps Prebid's <name>_cst companion consistent, since a missing or different one makes Prebid rewrite the cookie from script, which Safari caps. Prebid's extend with a pixelUrl keeps working alongside it.
WB-4 Mirror cookie
__Host-ts-eids-mirror as in #1489: opt-in per partner (DEC-10), dropped per vendor when TCF consent is denied, needs bidstream_enabled and Purpose 4, 3,584-byte budget, host-only. Every mirrored ID expires within 30 days.
SID-1 Cookie name from config
The server reads SharedID from the cookie named by the publisher's Prebid storage.name, held in managed_user_ids[].storage.name. Prebid has no default; _sharedid and _pubcid are common.
SID-2 Opt-out
A _pubcid_optout cookie stops SharedID reads, storage, sending, creation and write-back. Prebid also reads the opt-out from localStorage, which the server cannot see, so the injected script copies a localStorage _pubcid_optout into the cookie (T20).
SID-6 EID consent
Reading SharedID into the record, storing it, sending it and writing it back each need EID permission (TCF Purposes 1 and 4, through the consent module's EID predicate in consent/mod.rs), in addition to EC permission and the SID-2 opt-out. EC permission alone (consent_allows_ec, Purpose 1) is not enough. Where consent is required but missing, permission is denied.
Current state
What the code does today, from the design's survey at the revisions it names:
- ID-7: Fastly checks only that a JA4 exists plus a browser User-Agent platform; main's known-browser table (
ec/device.rs) feeds analytics only and needs an HTTP/2 fingerprint that Axum behind CloudFront lacks; the RC's Axum checks only the User-Agent, with request filters, because it has no trusted JA4 (EZI-3); Cloudflare and Spin create no records yet. CloudFront sends JA4 to the origin only in theCloudFront-Viewer-JA4-Fingerprintheader, which an origin request policy must forward. Add JA4 request interface design spec edgezero#469's draft design reports Axum asNotExposedand leaves the trusted proxy header to its section 13.1. - ID-9: on main only a real-browser navigation through the publisher proxy mints an EC, and no such route exists. Extract existing Sourcepoint consent synchronization #1535 (RC line) posts Sourcepoint's GPP string to a separate, non-minting
/_ts/consentroute, wired into each adapter's router. - WB-3: no branch writes it.
- WB-4: in Mirror provider EIDs to a cookie and run pull sync as an identity provider #1489 such an ID never expires (
Expiry::Never); closed Add server-side identity providers for six vendors #1461 capped mirrored IDs at 24 hours. - SID-1: main reads only
sharedId(constants.rs:10), Prebid's module name, on Fastly for the record and on every adapter for/_ts/admin/eids.
Acceptance criteria
- ID-4: A mock provider that answers within the deadline has its ID in the record and in the mirror cookie of that navigation's response
- ID-4: One that misses the deadline is skipped and does not delay the response past it
- ID-7: On each supported adapter a scripted client with a browser User-Agent and a non-browser JA4 creates no record, a current Chrome, Safari and Firefox JA4 that no list names still creates one, and a request with no trusted JA4 is not refused for lacking one
- ID-9: On Fastly and Axum, a consented same-origin
POSTwith nots-eccreates the record and setsts-ecand the SharedID cookie in that response - ID-9: Its JSON omits a non-mirrored partner's ID, an ID whose vendor's TCF consent is denied and an expired ID
- ID-9: A withdrawal issued while a grant call with no EC and delayed providers is in flight leaves no identity cookie, no live record and no ID in Prebid
- ID-9: With grant A in flight, a withdrawal B and then a grant C reach the server as A, B, C, so B expires the identity cookies and tombstones A's record and C is handled as a first consent with no
ts-ec - ID-9: A cross-site
Origin, aGETand a client ID-7 refuses mint nothing and call no provider - ID-9: An unchanged returning reader gets no
Set-Cookie - ID-9: In Safari,
ts-ecand the SharedID cookie set by the route keep their one-yearMax-Ageon Fastly and on Axum behind CloudFront - WB-3: Record present and cookie absent gives both cookies with those attributes
- WB-3: A consented first visit with no SharedID anywhere gets one created, stored and set, and a reader with
_pubcid_optoutgets none - WB-3: With
storage.type = html5the server neither creates nor sets a SharedID - WB-3: An equal cookie with a consistent
<name>_cstgives noSet-Cookie - WB-3: An equal cookie with a missing or different
<name>_cstre-sets only the companion - WB-3: Purpose 1 denied or
_pubcid_optoutexpires both - WB-3: Purpose 4 denied with Purpose 1 granted gives no
Set-Cookieand leaves the browser's cookie alone - WB-4: An ID written with no stated expiry is mirrored with an expiry of at most 30 days
- SID-1: With
storage.name = "_pubcid"a_pubcidcookie is read on every supported adapter, and a cookie namedsharedIdis not - SID-2: With
_pubcid_optoutpresent nothing SharedID-related is read, stored, sent, created or written, and a localStorage-only opt-out stops creation from the next request on - SID-6: On each supported adapter, with a populated record, an absent or different browser cookie, Purpose 1 granted and Purpose 4 denied, no SharedID is read, sent or written back
Dependencies
Blocked by:
- Identity on Spin, the same scope as T8 #1589 Identity on Spin, the same scope as T8 (new in this plan)
- Integration-branch tag with the Spin parts edgezero#493 Integration-branch tag with the Spin parts (new in this plan)
- Identity features pass on Cloudflare from the shared app #1596 Identity features pass on Cloudflare from the shared app (new in this plan)
Notes from the plan
- The Cloudflare and Spin tickets (T8, T9, T17, T18, the EZI
candstickets, R3, R4, Cloudflare ClientInfo edgezero#91, As a developer I want to access client metadata in handlers edgezero#89 and "Blocked by" is set as a GitHub dependency between issues.
Activity