Skip to content

Let a module name the browser's cookies and the shared store keys - #1368

Open
jwrosewell wants to merge 50 commits into
feature/26-shared-tablesfrom
feature/27-prefix-module
Open

jwrosewell wants to merge 50 commits into
feature/26-shared-tablesfrom
feature/27-prefix-module

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-core/src 25 files, 1582 lines added, 244 removed
crates/trusted-server-js/lib 7 files, 209 lines added, 30 removed
crates/trusted-server-adapter-fastly/src 3 files, 169 lines added, 30 removed
docs/guide 2 files, 58 lines added, 7 removed
crates/trusted-server-adapter-spin/src 1 file, 57 lines added, 5 removed
crates/trusted-server-adapter-axum/tests 1 file, 51 lines added, 0 removed
crates/trusted-server-adapter-axum/src 1 file, 50 lines added, 2 removed
crates/trusted-server-adapter-cloudflare/src 1 file, 49 lines added, 3 removed
crates/testing/seam-probe 1 file, 41 lines added, 11 removed
trusted-server.example.toml 1 file, 3 lines added, 1 removed
CHANGELOG.md 1 file, 2 lines added, 0 removed
crates/loading-cache/51degrees 1 file, 2 lines added, 6 removed
crates/trusted-server-core/benches 1 file, 1 line added, 0 removed

Closes

Closes #1285

Spec

Spec: prefix-module

Test plan

CI on this pull request at bb54e54 passed all 20 checks.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on this pull request at bb54e54

Checklist

  • Feature PRs: spec linked above, ending in a review log of two or more review rounds
  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

Stacks on a vendor's shared table (#1367).

What changed after the 51Degrees PR review

Agreed at the review on 23 October, point 1. No configuration or permission policy is compiled into the WASM, and a WASM without a policy does not start. Since #1345 a loaded domain carries its permission policy and every services graph needs one, so the tests below give theirs the sample policy.

Kept current

What this pull request does

[prefix]
module = "example"

A prefix module, PrefixModule, which gives two things for the publisher a request resolved to: a public prefix that starts every cookie name the browser holds, and a server key prefix that starts every key in a store publishers share. Core's own module gives ts and ts.<publisher.domain> and runs when [prefix] module selects nothing or names ts, so a stock build's names are byte for byte what they were, with a test.

A module gives the parts and each use writes its own form, because what is allowed differs by use: ts-ec for a cookie, ts.example.com for a store key. The public prefix carries no domain, because a browser already keeps each site's names apart from every other site's. The server key prefix carries the publisher's domain with a single leading www. folded, so www.example.com and example.com share keys and clients.example.com and public.example.com do not, and the domain comes from the publisher the request resolved to, never from the raw request, because a Host header can say anything and a prefix built from one would let any request start a key space of its own.

The template cache is the exception. Its surrogate keys, ts-template and ts-template-url-<digest>, and its store key, ts-template-cache-v<n>-<digest>, keep ts whatever the module gives, so ts cache purge names one key in every deployment. The store key's digest covers the request's host and the public prefix, so no two publishers share a template.

The registry resolves [prefix] module as it does [geo] and [device], refuses a name nothing this deployment runs supplies, and asks the module once when it is built, so a module naming a value its call never carries is refused at startup. Every adapter puts the resolved prefixes on the request's services, and the page handler reads them there. Every cookie core writes or reads is named through the public prefix, the Edge Cookie state carries the prefix it was read under, a module's response header naming a cookie under that prefix is refused as one under ts- was, the page's bundle tag tells the client script the prefix as data-ts-public-prefix so the script holds no copy, and the template fingerprint includes the prefix so a page stored under one is never served under another. A prefix's first part starts with a letter or a single _, so no cookie name starts with __, where browsers give names such as __Host- rules of their own. The client script refuses the same shapes.

The seam probe declares a prefix module giving _probe and probe.<publisher.domain>, and its report route answers the prefixes the request's services carry. The Axum adapter's tests select it with [prefix] module and read back both prefixes through the route.

Why

A deployment that is not Trusted Server, or one host serving several publishers, needs its own names in the browser and its own key space in a shared store, and until now both were fixed in core as ts. The names a reader's browser holds and the keys a store holds are a deployment's identity, so the module that gives them is an extension point rather than a setting, and the default keeps the stock build unchanged.

What breaks

  • Selecting a module other than core's orphans every reader's Edge Cookie, resolved marker, EIDs and tester cookie, and the identity-graph rows behind them. The Edge Cookie is read under the request's prefix alone, so a ts-ec is ignored under _ex, as the_edge_cookie_is_read_and_written_under_the_requests_prefix proves.
  • The cookie constants are renamed, COOKIE_TS_EC to COOKIE_EC and so on, because their value is no longer a whole name. A crate outside core that read them writes services.public_prefix().cookie_name(COOKIE_EC) instead.
  • ServerKeyPrefix::for_settings is removed. A caller takes services.server_key_prefix().
  • A publisher domain written with a trailing dot, example.com., now gives the same server key prefix as example.com, so its loading cache entries are made again under the new key.

How it was verified

Not exercised. No module outside the stock build gives a prefix. The probe's module is the one reader, through the Axum adapter.

References #1285.

jwrosewell and others added 18 commits October 11, 2026 17:24
Core gains a prefix module, `PrefixModule`, which gives two things for the
publisher a request resolved to: a public prefix that starts every name
the browser holds, being cookies and first-party storage keys, and a
server key prefix that starts every key in a store publishers share.
Core's own module gives `ts` and `ts.<publisher.domain>` and runs when
`[prefix] module` selects nothing, so a stock build's names are what they
were.

The registry resolves the selection as it does geo and device, refuses a
name nothing this deployment runs supplies, and asks the module once when
the registry is built, so a module naming a value its call never carries
is refused at startup. Every adapter puts the resolved prefixes on the
request's services, and the Fastly entry point keeps what its request
resolved to for the finishing steps that run after the request has been
handed to the router. The robots.txt holding and the loading cache take
the prefix from the request's services rather than from the settings.
The cookie constants lose their fixed `ts-` head and name the entry alone,
`ec`, `ecr`, `ec-pull-complete`, `eids` and `tester`, and every read and
write names the cookie through the request's public prefix, so a
deployment whose module gives `_ex` reads and writes `_ex-ec`. The Edge
Cookie state carries the prefix it was read under, and a module's response
header naming a cookie under that prefix is refused as a cookie under
`ts-` was.

The page's bundle tag tells the client script the prefix, as
`data-ts-public-prefix`, so the script names its own cookies after it and
holds no copy. The template fingerprint includes the prefix, so a page
stored under one is never served under another, and a key cookie naming
one of the request's own identity cookies shares no template.
A prefix's first part starts with a letter or a single `_`, so no cookie
name starts with `__`, where browsers give names such as `__Host-` rules
of their own and drop a cookie that breaks them. The Edge Cookie test
reads and writes under a prefix carrying a publisher's name as well, and
a test holds the client script to the attribute the server writes.
The probe declares a prefix module on its registration, giving `_probe`
and `probe.<publisher.domain>`, and its report route answers the prefixes
the request's services carry, so the Axum adapter proves from outside core
that `[prefix] module` resolves the module and that a request's services
carry what it gives.
The configuration guide lists `[prefix]` among the sections and describes
the selection and the names it changes. The integration guide's
registration table gains `.with_prefix_module(name, ...)` and a section on
giving the deployment its prefixes. The example configuration notes the
section, and the changelog records the addition and the renamed cookie
constants.
# Conflicts:
#	CHANGELOG.md
#	crates/trusted-server-adapter-fastly/src/app.rs
#	crates/trusted-server-adapter-fastly/src/middleware.rs
#	crates/trusted-server-core/src/integrations/registry.rs
#	crates/trusted-server-core/src/robots_txt/holding.rs
#	trusted-server.example.toml
The page handler and the HTML processor read the prefix the request's services carry instead of resolving the module again. Writing [prefix] module = "ts" selects core's own module, the per-request fallback to core's prefixes is gone, and the unused prefix forms are removed. The client refuses a leading __ as the server does, and the template cache keys keeping ts are named as the exception.
The Fastly test of the data page's sandbox called handle_data with the
three arguments it took before the prefix module, so the adapter's tests
did not compile. It now passes the default public prefix. The [prefix]
table in the configuration guide is formatted as Prettier writes it.
# Conflicts:
#	crates/trusted-server-core/src/publisher.rs
EntryRequest::of, which keeps the request's method, host, scheme, path and
query for the module calls that run after the request is handed to the
router, read Host with get_header_str, which panics on a value whose bytes
are not valid UTF-8. A client chooses those bytes, and a request addressed
by a hook can reach this read with such a Host. It now reads Host through
request_host, as entry_request and the selection by host do, so such a Host
is kept as no host.

Test: a_kept_entry_request_reads_a_host_that_is_not_utf8_as_no_host builds
the kept request from a Host of the bytes b"\xffa.example" and checks the
kept host and the module request it makes are both empty.
This branch's prefix module gives the start of every name the browser
holds, so COOKIE_TS_EC_RESOLVED became COOKIE_EC_RESOLVED, the name's
end. The three tests that the demo Edge Cookie module fix brought up from
split/4-client-resolve named the marker by the old constant, so core's
tests stopped compiling here. They now name the marker `ts-ecr`, its name
under the default public prefix, as this branch's own resolve tests do.
The default selection was built by SelectedPrefixModule::trusted_server,
which the registry called when nothing or ts is selected and which
Default called too. The code scanning rule for cleartext logging takes a
function whose name holds "trusted" to return a secret, so it followed
that module into the registry and the auction and reported a count of
planned providers and a skipped module's name as logged secrets. The
registry now asks for SelectedPrefixModule::default, as its other uses
already do, Default builds core's own module itself, and the
constructor, which nothing else calls, is gone.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:23
@jwrosewell
jwrosewell removed this pull request from stack #1422 October 28, 2026 11:47
@jwrosewell
jwrosewell added this pull request to stack #1465 October 28, 2026 18:28

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The names the browser holds and the keys a shared store holds are fixed as ts in core

2 participants