Repository navigation
Scope each publisher's cookies to its own path - #1400
Open
jwrosewell wants to merge 54 commits into
Open
jwrosewell wants to merge 54 commits into
jwrosewell wants to merge 54 commits into
Conversation
…efix and returning visitors Review round 1 found five blocking issues. The design said the adapters answer 421 for a removed prefix that is not a cookie path, when RequestAddress::new returns the error to the hook and only the Fastly adapter answers 421, for a missing blob. It said the first response writes each core cookie under the publisher's path, when a returning visitor's response writes no Edge Cookie, so the root cookie stays for up to a year and a withdrawal does not remove it. It said a request with no prefix reads as today, when an empty removed prefix writes Path=/ yet reads the first of two cookies of one name. Its adapter table, "so none changes", tests and acceptance did not cover the Fastly tester routes, and no test reaches the code that builds the prefix in production. The design now states what the code does in each case, says that only the Fastly adapter serves the tester routes, and adds the consent tools that write Path=/, signed origins, module cookies, nested prefixes and the browser ordering the reads rely on. The code changes the round asks for wait under Open questions, and this commit changes no code. The review log and the revision record carry the round, and the spec passes Prettier.
jwrosewell
added this pull request to stack #1401
October 15, 2026 17:56
Review round 2 found no blocking issues and asked for three corrections. The design said no test sends a request with a removed prefix through the readers of the Edge Cookie context, when one test in publisher.rs does, and the Fastly tester test does too, neither checking a cookie the context wrote. It named five integrations of the client script that use root-relative paths or write cookies from the page, and the script has twelve, so the design now names them all. The list of untrue statements under Open questions now also holds the claim in the request addressing spec that this design keeps the consent cookies, sharedId and the forwarded Cookie header with the publisher, the rustdoc of for_request saying that publishers never receive each other's cookies, and the doc of the cookie jar that states the longer-path-first order as a fact where RFC 6265 makes it a SHOULD. No code changed and the request addressing spec is untouched. The review log and the revision record carry the round, and the spec passes Prettier.
…efore RequestAddress::new no longer refuses a removed prefix that cannot be a cookie's Path. The refusal made the documented hook answer "by host" for a request such as /a;b/news, so on Fastly that request was served from the front host's own blob where it had been answered 421. The request is now addressed with the selector value a;b again, and PublicPrefix::under_path, which falls back to / with a warning, still keeps a `;` out of every Set-Cookie core writes. The InvalidPath error and the docs of new name the continuing path only, the doc of is_continuing_path is back above that function, and the module and RemovedPathPrefix docs say core writes the prefix as the Path of its own cookies.
handle_request_cookies kept the first cookie of each name whenever a request carried a RemovedPathPrefix, and core inserts one even when the hook removed nothing. A request whose cookies core writes under / then read the first of two cookies of one name, where a request with no prefix reads the last. The jar now keeps the first cookie only where core writes its cookies under the removed prefix. addressing::cookie_path_for holds that one rule, so an empty prefix, / and a prefix that cannot be a cookie's Path all read the last cookie and write under /, and under_path uses the same rule, so a prefix under / equals one with no path. The docs of handle_request_cookies and of the first-wins parser now say that RFC 6265 section 5.4 makes the longer-path-first order a SHOULD, and what happens in a browser that does not sort.
…ext and the resolve endpoint Two pieces of production code build the prefix the cookie writers use, being the for_request call in EcContext::read_from_request_with_geo_status and the resolve endpoint's use of the context's prefix, and no test failed when either was removed. finalize_writes_the_edge_cookie_under_the_removed_prefix reads the context from an addressed request, generates an identifier and runs ec_finalize_response. resolve_writes_under_the_removed_prefix_of_an_addressed_request reads the context from the resolve request itself, as an adapter does, and checks the Path of the Edge Cookie and its marker. Each test was run once with its piece of code reverted. With the context built from services.public_prefix().clone() the finalize test failed with the Edge Cookie at Path=/, and with the resolve endpoint writing under services.public_prefix() the resolve test failed with both cookies at Path=/.
On a request addressed by path a withdrawal expired the Edge Cookie and its resolved marker only under the removed prefix, so a copy of either under /, such as one written before core wrote under the publisher's path, stayed in the browser and was still sent on every path of the host. expire_ec_cookie, which core calls only on a withdrawal, now also expires both cookies under / when the prefix carries a path. With no removed prefix it writes the two expiries under / exactly as before. A cookie under / is one cookie for every publisher on the host that uses its name, so the root expiry reaches all of them. an_expiry_under_a_removed_prefix_also_expires_the_root_cookies checks the four Set-Cookie values byte for byte, and a_withdrawal_under_a_removed_prefix_also_expires_the_root_cookies runs a withdrawal through EcContext::read_from_request and ec_finalize_response, with and without a removed prefix.
The docs of PublicPrefix and EcContext::public_prefix described only the names a prefix gives, and the rustdoc of PublicPrefix::for_request said publishers served from one host by path never receive each other's cookies, which holds only for cookies written under a publisher's path. They now say a prefix carries the Path of the cookies it names, when that path is /, and that a cookie under / is still sent on every publisher's requests.
… to 4 Open questions 1 to 4 are now design text. RequestAddress::new accepts any removed prefix and under_path holds the one check, so /a;b/news is addressed as before and answered 421 on Fastly. A removed prefix that is empty, / or not a cookie path writes and reads its cookies as no prefix does, through cookie_path_for. A withdrawal also expires the Edge Cookie and the resolved marker under /, and the text says that cookie is shared by every publisher on the host that uses its name. Section 2 lists the new tests and the revert check of the two tests through the production wiring, section 3 and the acceptance follow the new behavior, and the revision record has a row for the code fixes. The code comments open question 5 listed are corrected with the code, so the one open question left is the pages this change makes untrue.
The series had no changelog entry. The new entry, beside the request addressing entry under Added, says which cookies core writes under the removed prefix, when a request still writes Path=/, which cookie of a name is read, what a withdrawal expires, and which cookies stay shared.
jwrosewell
removed this pull request from stack #1401
October 15, 2026 20:51
…text A returning visitor whose only Edge Cookie was written under / before core wrote under the publisher's path withdraws on a request addressed by path. a_withdrawal_reaches_a_returning_visitors_root_edge_cookie reads the context through EcContext::read_from_request with the built-in module selected, runs ec_finalize_response, and checks that the Edge Cookie and its resolved marker are expired under the prefix and under /, and that the visitor's row is tombstoned. The test passes on the code as it was, because expire_ec_cookie already expires both cookies under / when the prefix carries a path. With that root expiry removed it fails with only the two expiries under the prefix, so it keeps that fix in place.
… under the root On a request addressed by path a withdrawal expired the pull-sync marker only under the removed prefix. A marker under /, written before core wrote under the publisher's path or by a request addressed by host, is sent on that request too, and it stayed in the browser for up to an hour after the withdrawal. The marker is an HMAC tag over the Edge Cookie identifier, and docs/guide/edge-cookies.md says a withdrawal expires any marker the request carries. ec_finalize_response now also expires the marker under / when the prefix carries a path, as expire_ec_cookie does for the Edge Cookie. With no removed prefix it writes the one expiry under / as before. a_withdrawal_under_a_removed_prefix_also_expires_a_root_pull_sync_marker sends a marker on an addressed request and checks the paths of the marker's expiries. Without the fix it fails with the expiry under /publisher-a.example alone.
…ookie the request carried On a request addressed by path, a browser that holds an Edge Cookie under the publisher's path and one of another value under / sends both, for example when a request addressed by host created the second. The jar keeps one of them, so a withdrawal tombstoned only that identifier's row. The other row stayed live with consent granted, although the withdrawal expired its cookie in the browser. parse_ec_from_request now keeps the values of the other Edge Cookies when the prefix carries a path, read with the new cookies::cookie_values, and withdrawal_kv_keys adds their rows to the ones it tombstones. EcContext builds its prefix from the request once, before the parse, so the parse knows the path. A request with no removed prefix keeps no other values and withdraws as before. a_withdrawal_under_a_removed_prefix_tombstones_each_edge_cookies_row seeds a live row for each of two Edge Cookies, withdraws on an addressed request that carries both, and checks both rows. Without the withdrawal_kv_keys line it fails on the row of the cookie under /. cookie_values_gives_every_value_of_a_name_in_the_order_sent covers the helper.
13 of 14 tasks
jwrosewell
added this pull request to stack #1408
October 17, 2026 09:05
This was referenced Oct 17, 2026
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
jwrosewell
marked this pull request as ready for review
October 19, 2026 20:24
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-core/srcdocs/superpowerscrates/trusted-server-adapter-fastly/srcCHANGELOG.mdCloses
Follows #1328, whose design records the browser limit this pull request lifts.
Spec
Spec: cookies-by-path
Test plan
CI on this pull request at cdb0a2b 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!)On top of the Fastly names series (#1399). Follows #1328, whose design records the limit this lifts.
What this adds
What changes.
On a host that serves several publishers by path, every cookie core writes now carries the removed path prefix as its
Path, so the browser keeps each publisher's cookies under that publisher's path. A request with no removed prefix writesPath=/exactly as before. No setting is added.How it works.
PublicPrefixgainsunder_path,for_requestandcookie_path. Every core cookie writer already takes the publisher'sPublicPrefix, so each writes the name and thePathfrom one value. That covers the Edge Cookie and its expiry, the resolved marker and its expiry, the pull-sync marker and the tester cookie./where the removed prefix cannot be a cookie path.RequestAddress::newaccepts any removed prefix, as before this series, andPublicPrefix::under_pathwrites a prefix that cannot be a cookie'sPath, such as one with a;, as/with a warning, so noSet-Cookiecore writes carries it.cookiecrate's jar keeps the last of two same-named cookies, so where core writes its cookies under the removed prefix its jar now keeps the first of each name. An empty prefix,/and a prefix that cannot be a cookie path keep the last, as a request with no prefix does.sharedId. The permission signal crates read consent cookies throughRequestInfo::cookie, which already returns the first match, and core only checks whethersharedIdis present. A test holds the first-match behavior in place.Why
Request addressing lets one host serve publishers as
https://front.example/<publisher>/<path>, and its design records that publishers on one host are one browser origin, sharing consent cookies, thesharedIdcookie and the forwarded Cookie header. Browsers already keep cookies apart by path, so core uses that rule instead of a setting.What is left
/auction, the identity resolve endpoint, page bids), and some page integrations write cookies withPath=/from the page. The design describes the fix, which is for the server to tell the script the removed prefix in the page./from origins, from consent tools not set up for the publisher's path, or from core before this change stay shared and reach every origin./until itsMax-Ageends, and on that publisher's path the newer path-scoped cookie wins. A withdrawal expires both under/as well as under the prefix.Tests
The design page lists the tests this series adds, under "Tests" in its section 2.
Review
The design's first review round found five blocking issues, recorded in its review log. The three that needed code changes are fixed in this series, a removed prefix that cannot be a cookie path addressing the request as before (a0d1d00), an empty or root prefix reading the cookie jar as no prefix does (25c673f), and tests reaching the production wiring through the Edge Cookie context and the resolve endpoint (630068d). The second round found no blocking issue.
The design is
docs/superpowers/specs/2026-10-15-cookies-by-path-design.md, also in #1342 with every design document of the stack.