Repository navigation
Create and write the Edge Cookie on every adapter - #1411
Open
jwrosewell wants to merge 27 commits into
Open
jwrosewell wants to merge 27 commits into
jwrosewell wants to merge 27 commits into
Conversation
Each of the three adapters now runs the Fastly adapter's Edge Cookie steps. build_ec_request_state keeps the bot gate's answer, taken from the device classification, beside the Edge Cookie context, and dispatch_fallback creates an Edge Cookie for a real browser's page navigation before the publisher path runs. Every response of the request cycle carries an EcFinalizeState, and FinalizeResponseMiddleware runs ec_finalize_response on it once the finalize headers are written, before the request filters' effects. POST /_ts/api/v1/ec/resolve is a named route in the request cycle, answered by handle_ec_resolve. These adapters open no identity graph, because the graph needs strongly consistent reads and conditional writes the key-value store they can open does not offer. Core writes a created Edge Cookie only with its graph row, so the middleware writes it with set_ec_cookie_on_response for a request that passed the bot gate and whose permissions allow the Edge Cookie. The cookie takes the request's public prefix and, on a request addressed by path, the removed prefix as its Path. The graph's syncs are skipped, the resolve route creates no cookie, and where [ec] ec_store names a store each adapter logs once at startup that it opens none. src/app/request_cycle_tests.rs in each adapter checks, with a reader in the US and a stub origin, that a first page view addressed by path gets ts-ec under that path, that a returning page view keeps its cookie, that a crawler's page view gets none, that the resolve route answers 403 to a foreign Origin and 204 to the publisher's without reaching the origin, and that startup warns when ec_store is set. cargo test-axum (1,362 passed), cargo test-cloudflare (66) and cargo test-spin (100) pass, and so do the cross-adapter parity tests (17), the Cloudflare wasm32 check, the Spin native and wasm32-wasip1 checks and its release build, clippy with warnings denied on the three adapters and cargo fmt.
docs/superpowers/specs/2026-10-17-edge-cookie-on-every-adapter-design.md sets out the Fastly adapter's Edge Cookie steps, why the Axum, Cloudflare and Spin adapters open no identity graph, how they write a created Edge Cookie without one behind the same gates, what the resolve route answers there, the startup warning, and the two open questions, being whether the resolve route should write a client-side module's cookie without a graph and whether a Fastly service with no ec_store should write one.
…adapter The changelog gains a line under Changed. The route table of the API reference lists the resolve route on every adapter, and its section says what the route answers on an adapter with no identity graph. The Edge Cookie guide and the ec_store row of the configuration guide say the Axum, Cloudflare and Spin adapters open no graph, and their own guides say the Edge Cookie is created and written there.
ec_finalize_response wrote a newly created Edge Cookie only once its identity-graph row existed, so with no graph it wrote none, and handle_ec_resolve answered 204 with no cookie when it had no graph. The identity graph is one optional store of the data kept with the Edge Cookie, such as partner IDs, so a missing graph no longer stops the cookie. With a graph both record the row first, as before. Without one both skip the graph steps and write the cookie, so the resolve route answers as it does with a graph. The bot gate held the cookie back from a request that does not look like a browser only because the adapter gave that request no graph. Core now reads the gate itself, from the device signals the adapter sets on the context (EcContext::passed_bot_gate, which a context with no device signals does not pass), in both places and at the step where the missing graph stopped the cookie, so a crawler's answers are unchanged. finalize_generated_ec_without_kv_skips_cookie_and_header becomes finalize_generated_ec_without_kv_writes_the_cookie_and_skips_the_graph, which shows that a response finalized with no graph gets the headers one with a graph gets and that no graph step runs. New tests cover the bot gate in the finalize step, in the resolve route and on the context, and the resolve route with no graph. The tests that expect a created cookie now set a browser's device signals, as every adapter sets them on every request.
… core Core now writes a created Edge Cookie with no identity graph, so the FinalizeResponseMiddleware of each adapter no longer writes it after ec_finalize_response, which would write it twice. EcFinalizeState loses the bot gate's answer, which only that write read, and the request state keeps it for creating an Edge Cookie on a page navigation. The resolve route's comment says a created cookie is set with no graph row. The startup warning, logged once where [ec] ec_store names a store, now says the Edge Cookie is still written but the data the graph would keep with it, such as partner IDs, is not. The edge_cookie helper of each adapter's request cycle tests fails a response that writes the Edge Cookie more than once, so the first page view shows core alone writes it, and the resolve route's 204 is put down to the server-side module, which creates nothing from a posted value.
with_no_ec_store_the_entry_point_writes_a_created_edge_cookie, run under Viceroy, builds a browser's page view state with build_ec_request_state from settings that name no [ec] ec_store, so no identity graph is opened, creates the Edge Cookie as the publisher fallback does, and finalizes it with the entry point's apply_edgezero_ec_finalize, which now writes the cookie once with the created identifier. The resolve route's comment says the bot-gated graph keeps an unrecognized client's row out of the graph and core's bot gate keeps the cookie from it.
docs/superpowers/specs/2026-10-17-edge-cookie-on-every-adapter-design.md now says core writes a created Edge Cookie with or without an identity graph, behind the bot gate it reads from the device signals, so the cookie is written on every host and the graph is optional, and the resolve route answers without a graph as it does with one. Its two open questions, whether the resolve route should write a client-side module's cookie on an adapter that opens no graph and whether a Fastly service with no ec_store should write a created Edge Cookie, are both answered yes, because the Edge Cookie is the identifier and the identity graph is one store of the data associated with it. The startup warning, the rollout, the tests and the acceptance follow, and the revision record gains a row.
… graph The changelog gains a line under Changed saying the Edge Cookie is written without an identity graph as it is with one, so a Fastly service with no [ec] ec_store now writes a created cookie and the resolve route answers there as it does with a graph, and that a crate outside core sets the device signals the bot gate reads. The line for the Axum, Cloudflare and Spin adapters no longer says the resolve route creates no cookie there. The Edge Cookie guide says the store is optional and what is written without it, the Fastly guide says the cookie is written without the store, the API reference says the resolve route answers 204 to a request the bot gate stops and sets the cookie with no row where there is no graph, and the Axum, Cloudflare and Spin guides say the resolve route sets the cookie there.
# Conflicts: # crates/trusted-server-adapter-axum/src/app.rs
Core now reads the bot gate from the device signals on the Edge Cookie context, and a context with none gets no created cookie. The navigation test that checks a new identifier keeps its cookie built its context with no device signals, so the gate stopped the cookie and the test failed. It now sets a browser's signals, as the other tests of a created cookie do.
jwrosewell
added this pull request to stack #1408
October 18, 2026 01:31
…at a graph replaces it The changelog line for writing the Edge Cookie without a graph says the identifier auction providers get as user.id on a Fastly service with no ec_store is now one value for the life of the cookie, where it was a new value on each page view, that naming an ec_store later gives each browser holding such a cookie a new identifier, and that reading the bot gate from the device signals is breaking for crates outside core. The line for the Axum, Cloudflare and Spin adapters says the bot gate there passes any client that sends a browser's User-Agent, and that a real browser's identifier reaches auction providers as user.id from its first page view where storage and personalized-ad selection are both set. The Edge Cookie guide says the identifier is used and shared without the store as it is with it, and that adding the store later gives each browser holding a cookie written without it a new identifier, because the identity graph replaces an identifier it has no row for. The Fastly guide says the same of adding the store to a service that ran without it.
…warning with no ec_store The request cycle tests of the Axum, Cloudflare and Spin adapters send a page view carrying ts-ec and a TC String that refuses storage, from a reader with no location, so the permissions resolve at the top of the sample rules, where storage needs a signal. The response expires the Edge Cookie. The reader's services carry the permission signal modules the adapter links, as the adapter's own services do, so the TCF module reads the string, and the settings turn its expiry check off, because the string is fixed. The warning that an adapter opens no identity graph takes the flag it is logged under, as the analytics warning does, so the startup test also checks that the warning sets no flag when [ec] ec_store names no store. On each adapter the withdrawal test fails when a response the permissions do not allow is given no finalize state, and the warning test fails when the warning does not need an ec_store.
…a graph replaces it docs/superpowers/specs/2026-10-17-edge-cookie-on-every-adapter-design.md answers the first review round. Consent and privacy no longer says the Edge Cookie stays a first-party identifier without a graph. It says core sends the identifier to the auction providers as user.id and in the auction request's id, and lets module calls read it, behind the storage and personalized-ad selection permissions, names the two cases where this change discloses more, being the first page views on these adapters and on a Fastly service with no ec_store, and says the disclosure is intended. Rollout says a graph configured later replaces every cookie written without one, through confirm_then_recover_orphaned_ec, and why the graph keeps that rather than adopt such a cookie, and Decisions says the same. It also says the three adapters already ran the integration proxy's creation, what the bot gate is on these adapters, where the resolve route checks it, what the store offers, what a deployment with no module and a crate outside core now see, what the Fastly test does, and which answer no adapter test covers. The new withdrawal test and the startup warning's case with no ec_store are listed, three items join What is not in this change, the revision record gains a row and a review log records round 1.
…module's cookie A client-side module creates nothing at the edge, so orphan recovery has no replacement for its cookie and leaves it, and its page script posts to the resolve route only while the resolved marker is absent. So once a graph is configured, such a cookie written without one keeps its identifier with no row, and each page view pays for an extra read and a strongly consistent existence check. The changelog, the Edge Cookie guide and the Fastly guide now limit the replacement to a server-side module and state that case. The changelog also says a client-side module gave no identifier before this change, and that its identifier reaches auction providers from the requests after its resolve. The API reference says the resolve route asks the module before it checks the bot gate, and the page script says the row is persisted only where there is an identity graph.
…client-side orphan Each adapter's request cycle tests register a test Edge Cookie module that creates nothing at the edge and takes its identifier from the value the page posts, as a client-side module does, and select it with [ec] module. The publisher's post of that value gets 200 through the router with the Edge Cookie, its resolved marker and Cache-Control no-store, and nothing reaches the origin. With the resolve route put back to answering 204 when there is no identity graph, the test fails on all three adapters. Core's test of an orphan recovery that cannot rotate gains a client-side module's case. A client_fixed cookie whose row is missing keeps its identifier, no cookie is written and the miss is recorded as failed, because the module creates nothing at the edge to rotate to. Without the failed snapshot on that exit the case fails, and no other test reaches that exit.
…e's cookie Fixes from the second review round. Once a graph is configured, a client-side module's cookie written without one keeps its identifier with no row, because the module creates nothing at the edge for recovery to rotate to and its page script posts again only once the resolved marker is gone, and the design now says what each page navigation then pays. The replacement is limited to a server-side module's cookie, and a third case where the disclosure grows is added, a client-side module on a host with no graph, whose readers now get an identifier from the resolve route. The design also says the 2026-07-10 invariant that a cookie follows its row now holds only with a graph, that orphan recovery reads the recovery flag rather than the bot gate for a crate outside core, and which tests cover the resolve route's 200 through each adapter and a client-side orphan in core. Giving such a cookie its row is listed beside a graph that adopts a cookie. The review log gains the round 2 entry and names the public branch round 1 was judged against.
# Conflicts: # CHANGELOG.md # crates/trusted-server-adapter-axum/src/app/request_cycle_tests.rs # crates/trusted-server-adapter-cloudflare/src/app/request_cycle_tests.rs # crates/trusted-server-adapter-spin/src/app/request_cycle_tests.rs
jwrosewell
added this pull request to stack #1418
October 19, 2026 12:18
jwrosewell
removed this pull request from stack #1418
October 19, 2026 16:30
jwrosewell
added this pull request to stack #1421
October 19, 2026 17:38
jwrosewell
removed this pull request from stack #1421
October 19, 2026 18:29
jwrosewell
added this pull request to stack #1422
October 19, 2026 19:20
This was referenced Oct 19, 2026
jwrosewell
marked this pull request as ready for review
October 19, 2026 20:24
13 of 14 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Changes
crates/trusted-server-adapter-spin/srccrates/trusted-server-adapter-axum/srccrates/trusted-server-adapter-cloudflare/srccrates/trusted-server-core/srcdocs/superpowerscrates/trusted-server-adapter-fastly/srcdocs/guideCHANGELOG.mdcrates/trusted-server-js/libCloses
Part of #777
Spec
Spec: edge-cookie-on-every-adapter
Test plan
CI on this pull request at e28df81 passed all 27 checks.
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code (clippy deniesunwrap_usedin CI)logmacros as AGENTS.md asks (notprintln!)Stacks on the request filters on every adapter (#1410).
What this pull request does
The design is edge-cookie-on-every-adapter. The Edge Cookie request state, the finalize step and the resolve route ran on the Fastly adapter alone. They now run on the Axum, Cloudflare and Spin adapters too, so a selected Edge Cookie module creates and writes its cookie on every adapter, under the public prefix and the path the cookies by path change gives it.
Core writes a created Edge Cookie with or without an identity graph. With a graph it records the row as before, and without one it skips the graph steps and still writes the cookie, because the Edge Cookie is the identifier, the identity graph is one store of the data associated with it, and the permission state comes from the permission model whatever its sources. So a Fastly service with no
ec_storenow writes a created cookie too, and the resolve route sets a client-side module's cookie as it does with a graph.EcContext::passed_bot_gate(), so a crawler gets no created cookie on any path, where the gate on Fastly used to come from giving a crawler no graph.user.idand in the auction request's id, behind the storage and personalized-ad selection permissions, so this change discloses it in three cases where nothing was sent before, which the design lists.How it was verified
Locally on Windows at 0a94453:
cargo test-axum(1,366),cargo test-cloudflare(70),cargo test-spin(104) and the Fastly adapter tests under Viceroy (300) passed, with clippy refusing warnings on core and the three adapters andcargo fmt --all -- --check. On each of the three adapters the resolve route sets a client-side module's cookie with no graph, a withdrawal sent through the router expires the cookie, and the warning for a missingec_storeis checked. Core tests an orphan that a client-side module cannot replace. Each new test fails with its change reverted, and a crawler gets a cookie with the gate removed. With the rows below merged in, all 2,988 of core's tests passed at 23c5bf1.