Repository navigation
Conversation
Google Tag Manager changed a page through two hooks, on every page of a deployment that selected it. An attribute hook pointed Google's tag addresses in `src` and `href` at the first-party path, and a script hook did the same in the text of inline scripts. Both are now the one middleware `tag.google-tag-manager`, which runs on the pages a `[[fetch]]` entry names it for and makes the same decisions. The script hook kept the text it was holding in the document's state, under its own id, so that one document's held text could not reach the next. The handler that replaces it is made for one document and holds the text itself. The test that an interrupted document leaves nothing for the next one now asks the registered middleware for a handler twice. The recorded page is unchanged. Its settings gain the entry that places the middleware.
DataDome changed a page through two hooks, on every page of a deployment that selected it. An attribute hook pointed a `src` or `href` that is a DataDome script's address at the first-party path, when `rewrite_sdk` is set. That is now the fetch middleware `bot-protection.datadome`. It makes the same decision under the same setting, on the pages a `[[fetch]]` entry names it for. A head injector wrote the client tag, and left it out for a request the request filter had marked. A change that depends on the request cannot be stored for every reader, so it is now the serve middleware `bot-protection.datadome.tag`, which runs on each reader's copy of the pages a `[[serve]]` entry names it for. It reads the same mark from the document's state and writes the same tag. The design text expected the tag to move to the start of the head. It does not move. A serve middleware's head markup goes straight before the script bundle, which is where the hook's markup went, so the recorded page is unchanged. Its settings gain the two entries.
Prebid changed a page through two hooks, on every page of a deployment that ran it. A head injector wrote the configuration Prebid.js reads and the tag that loads the bundle. An attribute hook removed an element whose `src` or `href` matches `script_patterns`, which is the publisher's own Prebid script. Both are now the one middleware `auction.prebid`, which runs on the pages a `[[fetch]]` entry names it for and makes the same two changes. Neither depends on the request, so both stay in the fetch phase and what they write is stored with a shared template, as it was. The recorded page is unchanged. Its settings gain the entry that places the middleware.
A browser module can read a setting from the tag the script bundle was loaded by. Two modules use that today, and each sets its attribute through a method of the head injector trait, which goes when the page hooks have converted. The attribute is not a change to a page that an entry places. It belongs to the module's browser code and is the same on every page the bundle is written into. A registration now states it with `.with_bundle_tag_attribute(name, value)`. The registry puts those after the attributes the head injectors still ask for, in registration order, and keeps the first value a name is given as it did. The tag is written as markup with no escaping, so a name or a value that could end the attribute refuses startup, where the tag's writer only asserted it in a debug build. Core's tagging stand-in states its attribute this way, so the test that a page's bundle tag carries it runs through the new path.
APS implemented the head injector trait, wrote no markup through it, and used it only to put `data-ts-aps-rendering-mode="publisher_native"` on the script bundle's tag where a demand source asks for native rendering. It now states that attribute on its registration and supplies no middleware, so no entry names it and no page is changed in its name. The attribute is on the tag in the same case as before, and the recorded page is unchanged.
GPT changed a page through two hooks, on every page of a deployment that selected it. A head injector wrote the `tsjs.adInit` bootstrap, and an attribute hook pointed the GPT script's address at the first-party path when `rewrite_script` is set. Both are now the one middleware `ad-tag.google`, which runs on the pages a `[[fetch]]` entry names it for and makes the same two changes. The head injector also put `data-ts-gam-attribution="true"` on the script bundle's tag when `gam_attribution_enabled` is set. The registration states that attribute now, so it is on the tag of every page the bundle is written into, as it was. The recorded page is unchanged. Its settings gain the entry that places the middleware.
GPT diagnostics wrote its bootstrap and the tag that loads its module through a head injector, which ran on every page of a deployment that selected it. What it writes depends on the request, because a reader turns diagnostics on with `?ts_console=1` and off again, so it has to run for each reader and never once for a page that is stored. It is now the serve middleware `ad-tag.google.diagnostics`, which runs on each reader's copy of the pages a `[[serve]]` entry names it for. The bootstrap still goes ahead of the script bundle and the module's tag straight after it, and a request that did not activate diagnostics still gets neither. The recorded page is unchanged. Its settings gain the entry that places the middleware, and so do the settings the browser tests run with, which select this module for the diagnostics test.
Next.js changed a page through three hooks, on every page of a deployment that selected it. Two script rewriters worked on the text of the `__NEXT_DATA__` script and of the React Server Components payload scripts, and a stream processor substituted the placeholders the second of them left. All three are now the one middleware `framework.nextjs`, which runs on the pages a `[[fetch]]` entry names it for. Its action carries two text handlers, in the order the rewriters were registered in, and the stream processor. They share the state the module keeps for a document as they did, because all three are made for the document together. The rewriters keep their logic. What each is told about a chunk of text is now a type of this crate with the same fields, less the selector, which neither rewriter read. Their tests change by that type's name alone. The recorded page is unchanged. Its settings gain the entry that places the middleware, and so do core's settings for the Next.js auction tests that the adapters and the parity suite share. The two Google Tag Manager tests that run Next.js alongside it name both.
Core's JavaScript asset proxy pointed a configured script at its first-party path, or removed one that is blocked, through an attribute hook. The hook ran on every page of a deployment that selected the proxy, and ahead of every module's own. It is now the middleware `js_asset_proxy`, which runs on the pages a `[[fetch]]` entry names it for. Its selector asks about the `src` of `script` elements alone, which the hook checked for itself. Its place among the page changes is the place the entry gives it. A middleware is asked about an address as the one named before it left it, so the proxy is named first where it has to see the address the page wrote. One test names it ahead of a module and another names the module ahead of it, and the guide says to name it first. The tag stand-in that core's tests run beside the proxy becomes a fetch middleware in the same change, because the two were tested together through the order of their hooks. The test that asked the hook about an element that is not a script is removed. The selector decides that now, and the page test that holds an image with a configured address covers it. A test is added for a script tag written in upper case. The recorded page is unchanged. Its settings gain the entry that places the middleware.
Core's tests stand two modules in for a vendor's. One carries a deferred browser module and writes its settings into the head, and the other rewrites script payloads in two passes. Both made their page change through the hooks. Each now supplies one fetch middleware under its own name, and a test that runs one on a page places it with an entry. The payload stand-in kept its captured payloads and its held script text in the document's state, because one rewriter served every document. Its text handler and its stream processor are now made for one document, and share what the handler captures between the two of them. Two tests checked that the payload stand-in had registered a stream processor before going on. They check that its middleware runs on the page. The test that two configurations of a module never share a template now also checks that each page carries the markup its own configuration writes. Nothing looked at what the deferred stand-in wrote before.
…ware Core's tests stand one module in for a vendor's that acts on one request. Its preparer or its filter marks the request, and its head injector read the mark and wrote a script ahead of the bundle and its module's tag after it. What it writes is one reader's, which the page path kept out of a shared template by handing a shared mode no request state. It now supplies one serve middleware under its own name, which reads the mark from the state the request left. A serve middleware runs on a reader's copy and nothing it writes is stored, so the same output is kept out of a shared template by where it runs. The tests of a request a module acts on place the middleware with a `[[serve]]` entry. The tests that render whole documents compose the serve entry as the production function does, which runs none on a template every reader shares. They also run a serve middleware that writes for every reader, marked or not, and refuse its output in a shared template. Without it a template that ran the serve entry would still pass, because a shared mode is handed no request state and the stand-in writes nothing without a mark.
Core moves the origin's address in `srcset` and `imagesrcset`, as it does in `href`, `src` and `action`, and no test said so. Taking either rewrite out left every test passing. This test runs both through the document processor, with a bare host among the candidates of the `srcset`, and a `srcset` that names another host, which is left as it is.
Every module that changed a page through a hook now supplies a middleware, core's JavaScript asset proxy and its test stand-ins included, so nothing implements the hooks any longer. Removed from `trusted-server-core`: - `IntegrationAttributeRewriter`, `IntegrationScriptRewriter`, `IntegrationHtmlStreamProcessorFactory` and `IntegrationHeadInjector` - their context types, `IntegrationAttributeContext`, `IntegrationScriptContext`, `IntegrationHtmlContext` and `IntegrationHtmlStreamContext` - the registration methods `with_attribute_rewriter`, `with_script_rewriter`, `with_html_stream_processor` and `with_head_injector`, and the registry's four lists behind them - `AttributeRewriteOutcome`, which was the registry's answer after asking every attribute rewriter - `ScriptTextAccumulator`, which kept a script's text in the document's state because one rewriter served every document. A text handler is made for one document and keeps its own. The document processor asks nothing of the registry about a page any more. It moves the origin's address in the five attributes it always has, and carries out the plan of the middleware an entry names. A request's state reached a document through the hooks, and a function handed a shared mode none of it, so that nothing made for one request could be stored. That function is removed with its reader. What a request left now reaches a document through the `[[serve]]` entry alone, which does not run on the pass that builds a template every reader shares, and the fetch chain is handed none of it. The tests that render whole documents fail if the serve entry is run on a shared template. Tests of a hook are removed where a middleware test already asserts the same thing. The test that a registration from the auction plan goes ahead of a section's module reads the order from the middleware names the registry lists. The guides describe a middleware as the one way a module changes a page, and the changelog lists what is removed.
A tool that writes a configuration, as `ts audit` does, selects the modules it finds on a page. A module's page changes run only where an entry names its middleware, and a tool had no way to learn those names without naming a module itself. `trusted-server-modules` now lists the middleware the stock modules supply, core's script proxy among them, each with the integration that supplies it and the phase it runs in, in the order the registry registers the modules. `middleware_for` gives the names for a set of integration ids and a phase. The list is held to what the modules register. One test builds a registry from the settings each module's recorded page is made with and compares what it registers for each phase, and another holds the order to the stock builders'. The registry gains `middleware_in`, which gives the names that run in a phase.
`ts audit generate` writes a draft configuration that selects the modules it can configure from what it finds on a page. A module changes a page only where an entry names its middleware, so a draft with no entry selected modules that changed nothing. The draft now ends with one `[[fetch]]` entry, and one `[[serve]]` entry where a selected module supplies a serve middleware, that run those modules' middleware on every HTML page. The names and their order come from the stock list, so the tool names no middleware itself. The script proxy is placed with the rest, so an asset an operator enables after review is rewritten as well as served. `trusted-server.example.toml` documents the two entries with every middleware the stock modules supply. The stock list's tests hold the template's entries and the configuration guide's table to the list, in order, so neither can fall behind a module. The guide's table is put in that order, and the changelog says how a deployment keeps a page as it was when it upgrades. The lockr guide and the getting started guide say a page change needs an entry, as the other module guides do.
The tests of a module's page change kept the names of the hooks they once called, such as `head_injector_emits_config_script` and `attribute_rewriter_rewrites_gpt_urls`, which name traits that no longer exist. They are renamed for the parts of a middleware they now test, `head_markup_`, `element_handler_` and `text_handler`. Thirty-nine names and section comments change in eight module crates, and no test's body does. Four comments in core still spoke of script rewriters. They speak of text handlers, and the description of the state a document's middleware share says what it holds in each phase. The test of the payload stand-in's placeholders is renamed, because the stand-in no longer keeps them in that state.
A sandbox that serves several requests kept the one application it built first and handed it to every request after. That is right while a service reads one app-config blob. The commit after this one lets a service read a blob for each host, which needs an application for each, and an application built from one publisher's blob must never answer a request for another's. The sandbox's payload is now `RetainedApps`, which keeps each application under the key of the blob it was built from and finds one by that key and by nothing else. It retains only a successful build and counts every attempt, as the framework's one slot did. The framework hands its payload out by shared reference and offers no way to replace it, so the holder is made once and its list sits behind a `RefCell`. It keeps the eight applications served most recently, because all of them share the sandbox's one memory bound. The build count a sandbox reports, in the counters probe, the response headers and the retirement line, is the holder's. A test now holds the count a response carries to the builds made, which nothing did before. Nothing changes for a service that reads one blob. It builds one application under one key.
One Fastly service can serve several publishers, each from an application
config of its own. A `__KEY` selector that contains `{host}` is a
template. The entry point puts the request's host in the placeholder's
place, lowercased and without a port or a trailing dot, before anything
reads settings, so the debug probes, the application build and the
finalizers all read the blob of the publisher the request is for.
A host with no blob is answered `421 Misdirected Request`, and the answer
is not to be stored. It is never answered from the logical store ID or
from another host's blob, because a shared fallback is how a host nobody
configured comes to be served another publisher's site. Only letters,
digits, hyphens and dots make a host name, so a `Host` header cannot be
made into the logical store ID or the key of a blob's chunk, each of
which carries an underscore. A lookup that fails is answered `500`,
because a request is not served while it is unknown which blob is its
own.
The placeholder goes in the selector that already chooses the key, and
not in a switch of its own, so that a staged version stays apart from
production. A staged deploy copies a service's runtime entries into a
store of its own and points only the key selector at
`<logical-store-id>_staging`. A separate switch would be copied with the
rest, and a staged version would then read the production blobs. The
selector is replaced, so a staged version reads the one staging blob.
A selector without the placeholder is the key as it stands and nothing is
looked up, so a service that reads one blob is unchanged.
`scripts/config-by-host-local-test.sh` pushes an app-config blob under each of two hosts and one at the logical store ID, each with a stub origin of its own, and runs the adapter under Viceroy. Without the placeholder every host reads the one blob. With it each host is answered from its own origin, in any letter case and with a port or a trailing dot, and a host with no blob, the logical store ID sent as a host and a longer name that ends in a publisher's host are each answered 421 with no publisher's page. Each response carries the sandbox's counters. In the default mode the fourth response is the first request of its sandbox, which has built one application. In `reuse` mode, built with the reusable sandbox, it is the fourth request of its sandbox, which has built two. Two publishers then shared a sandbox and never an application, which is the fault a unit test cannot show because it sits in the entry point. CI runs both modes in the Fastly job.
The configuration guide's section on the Fastly runtime config store gains "One blob for each host", with the push for each host, the selector entry, a table of what each request reads and is answered, and what the publishers on one service share. The Fastly guide points to it, and the changelog records the addition.
Core gains `build_info`, which holds the crate version, the time the build was made, the commit and the build run, each fixed when the build was compiled. A WebAssembly guest is given no custom environment variable when it runs on some hosts, so a fact read then would read as absent there. `SOURCE_DATE_EPOCH` gives the build time where it is set, so a reproducible build stays reproducible, and the time of the build stands in otherwise. `TRUSTED_SERVER_COMMIT` and `TRUSTED_SERVER_BUILD_RUN` give the other two, and each reads `unknown` without its input. The date arithmetic is written out in a file the build script and a test both compile, so the build script takes on no date dependency. Each is the builder's own report and nothing measures the binary. The attestation evidence in the next commit is what reads them.
With `[attestation]` in the settings, core answers with evidence in the sense of RFC 9334. It names the host the request arrived for, the publisher whose settings answered, the platform service, the build, the time and the caller's nonce, and it is signed with ECDSA P-256 over a configured context, one line feed and the evidence bytes. The context keeps a signature made here from passing as one the same key made for any other purpose. A module on a CDN cannot measure its own bytes, so the evidence shows who operates the deployment and not what its code is. A verifier holding the operator's public keys decides whether to trust it, and the page sends the reader there and claims nothing itself. - `[attestation]` holds `endpoint`, `operator`, `context` and `verify_url`, and refuses any other field. The endpoint defaults to `/_ts/attestation`, answers as JSON with `.json` added, and may not be an address the deployment already answers. - The host signed is the request's `Host` header. A forwarded host header is never read. - The signing keys are a schedule the build script compiles in from the file `TRUSTED_SERVER_ATTESTATION_KEYS` names, each in force from its start until the next one starts. No store the settings can reach holds one. A build given no schedule signs nothing and answers 503. - An endpoint under a prefix of its own owns that prefix. The prefix lists its pages and any other address beneath it is not found. Under the shared `/_ts` prefix only reads of the two addresses are taken. A settings validation error now carries the validator's message where it gives one, which is how a refused endpoint names the address it clashed with.
Each adapter registers the routes the settings ask for. Reads reach the handler, and other methods reach the publisher's origin unless the deployment owns the prefix. The Fastly adapter answers ahead of the Edge Cookie lifecycle and the request filters, because the pages are read only and a verifier fetching one from a server must not be turned away as a bot. The Fastly adapter reports the service, the version, the staging flag and the location from the environment variables Fastly gives every Compute service. The Axum, Cloudflare and Spin adapters report their own name. Each adapter's tests hold every fixed route it registers beneath an underscore prefix to the list an endpoint may not take, so a new fixed route cannot leave an endpoint free to register its address twice. The Fastly tests build a router for every endpoint the settings accept, and the parity tests hold the other three adapters to one answer for the owned prefix, the redirect from the default address and a deployment with no section.
`docs/guide/attestation.md` says what the evidence proves and what it does not, the addresses and answers, the settings, the key schedule, the formats, the checks a verifier makes, how replay and relay are met, and the limits. Every test it names is in the tree. The configuration guide gains the `[attestation]` section, the API reference the endpoint and its two rows in the route table, the example configuration a commented block, the docs sidebar the page, and the changelog the addition.
The build script read the key schedule itself, so nothing tested what it took and what it refused. The reading moves to `key_schedule.rs`, a file the build script and the attestation tests both compile, as the build time's arithmetic already is. A schedule's key id and key are written into Rust source as strings. The tests hold that nothing either holds can end the string it is written in, that a line of any other shape stops the build by its line number, and that the message never repeats the line, because the line holds a private key. A file saved with either line ending reads the same.
`scripts/attestation-local-test.sh` makes two signing keys, builds the Fastly adapter with their schedule compiled in and runs it under Viceroy. It then checks what the running adapter serves with OpenSSL and the public key alone, sharing no code with the adapter. The signature verifies over the context, a line feed and the payload. It fails with the other key of the schedule, under another context and once the host in the payload is changed. The key in force signs, and not the one that starts later. The evidence names the host the request named, lower case and without its port, and the publisher, the operator, the nonce, the platform, and the commit, run and build time the build was given. A forwarded host header never becomes the signed host, no nonce is signed unless one is asked for, a malformed nonce is refused, and another method is not answered with evidence. This is the one check of the schedule a build compiles in, because test builds carry no key. CI runs it last in the Fastly job, because it builds core with a schedule.
The inspector builds core from a lock file of its own, and core now uses ECDSA on P-256, so its resolution gains `ecdsa` and `rfc6979` at the versions the workspace lock holds.
`ts keys rotate` and `ts keys deactivate` do from an operator's machine what `POST /_ts/admin/keys/rotate` and `POST /_ts/admin/keys/deactivate` do from inside the running service. They drive the same rotation code in core, `KeyRotationManager`, over two store implementations that write through the `fastly` CLI with the operator's own token from `FASTLY_API_TOKEN`. The token reaches `fastly` in its environment and a private key reaches it on standard input, so neither is in an argument list another process can read. Every value is joined to its flag with `=`, so a key id beginning with `-` is never read as a flag. A key id is checked before anything is asked of `fastly`. A new key takes the rule the rotate route applies, `kid_is_creatable`. A key being retired takes the looser rule the deactivate route applies, exposed here as `kid_is_well_formed`, so that a key created under an earlier rule can still be removed. Neither command takes `current-kid` or `active-kids` for a key. Named as a key, either entry would be written over by a rotation or deleted with the key. Two things differ from the stores the service reads, and the command's stores allow for both. - A read is a call to the platform and can fail for a reason other than a missing entry. The rotation library takes any failed read for an entry that does not exist, which would write a key list without the keys already active, or let the current key leave the active set. So the stores tell a `404` from any other failure and write nothing once a read has failed. - The service's management client takes a `404` on a delete for success, which is how a delete tried again after a partial failure finishes. The stores do the same, after asking for the store, because the API answers `404` for a missing store too. `fastly_cli` holds the runner, so the next command that administers a service through the host uses the same one. Its report of a failed `fastly` names the command by its words and says what `fastly` printed, including when the process stopped before it read its input. The command comes before the removal of the two routes, so that no commit leaves an operator without a way to rotate a key.
The command called `POST /_ts/admin/cache/purge` on the publisher's domain with a shared password. It now runs `fastly service purge --key` with the operator's own token from `FASTLY_API_TOKEN`, so the credential that can empty the cache stays with the platform's access control and off the domain every reader reaches. A token with the `purge_select` scope is enough, which allows a purge by surrogate key and does not allow a purge of everything. `fastly service purge` is the Fastly CLI's name for the command since its version 14.0.0, where `fastly purge` became a deprecated alias. The repository pins 15.1.0. The surrogate key is derived in the command by `reader_url_surrogate_key`, the function the service tags a cached page with, and `--all` names `ts-template`, the key every cached object carries. Both scopes purge what they purged before. Neither runs `fastly service purge --all`, which would also flush objects the service cached without one of those keys. `--service <url>`, `--username` and `TRUSTED_SERVER_ADMIN_PASSWORD` go and `--service-id <id>` arrives. A service id that is not letters and digits is refused, because it becomes an argument of the `fastly` process. The tests that drove the command against a fixture service are replaced by tests against a fake `fastly`. The command changes before the route is removed, for the reason `ts keys` comes first. The guide's command table gains the two `ts cache` rows it lacked.
`GET /_ts/admin/ec` and `GET /_ts/admin/ec/{id}` read any reader's identity
graph record, and `GET /_ts/admin/eids` decoded the caller's EID cookies,
on the publisher's own domain behind one shared password. `GET /_ts/data`
shows a reader what is held against that reader's own Edge Cookie and asks
for no password, so the operator lookups go.
What goes with them: `ec/admin.rs`, `KvIdentityGraph::lookup_raw`, the
decoder's diagnostic view of an EID cookie, the two probes that established
handler coverage for the lookup by id, the adapters' routes and their
not-supported stand-ins, and the tests and guide sections for all of it.
What stays is the check that keeps administration paths away from the
origin. It moves to `closed_paths.rs` as `closed_path_response`, with the
matcher unchanged: every path beneath `/_ts/admin`, and the `/admin/keys`
alias beside it, as sent and after each of up to four percent-decoding
rounds. A request there that no route claims is answered `404` by the
server, so a caller's `Authorization` header and body are never forwarded.
The three removed paths are answered that way for every method, where a
method other than GET was answered `405`.
Core's version_header module is removed, with its call from the finalize step of the Axum, Cloudflare and Spin adapters and from the Fastly adapter's health probe and its settings fallback, so no response carries x-ts-version unless a [[serve]] entry names the service_version middleware. That middleware now writes the git version the build compiled in from TRUSTED_SERVER__GIT_VERSION or local git as X-TS-Version, when the build knows one, and the version the adapter reports for the service as X-TS-Service-Version, when it reports one. HEADER_X_TS_FASTLY_VERSION, which nothing wrote, goes with the module. The health tests of the three adapters prove the probe carries no version, the middleware's own tests prove each header is written when its version is known and nothing when neither is, and the publisher tests prove both headers reach a buffered page and neither reaches a page no entry names the middleware for. The configuration guide, the getting started guide, the EdgeZero guide and the changelog say the same.
# Conflicts: # docs/guide/configuration.md
# Conflicts: # docs/guide/configuration.md
jwrosewell
marked this pull request as ready for review
October 14, 2026 12:35
Collaborator
|
Moved into this repository as #1368, one branch of the stack, so the review continues there. This pull request is closed in its favor and its comments stay here for the record. |
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.
Stacks on a vendor's shared table (#1314). The additions that follow the seven pull requests open here are listed in #777.
Compare
split/26-shared-tableswithsplit/27-prefix-moduleto see only this pull request's change.
What this pull request does
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 givestsandts.<publisher.domain>and runs when[prefix] moduleselects nothing or namests, 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-ecfor a cookie,ts.example.comfor 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 leadingwww.folded, sowww.example.comandexample.comshare keys andclients.example.comandpublic.example.comdo not, and the domain comes from the publisher the request resolved to, never from the raw request, because aHostheader 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-templateandts-template-url-<digest>, and its store key,ts-template-cache-v<n>-<digest>, keeptswhatever the module gives, sots cache purgenames 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] moduleas 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 underts-was, the page's bundle tag tells the client script the prefix asdata-ts-public-prefixso 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
_probeandprobe.<publisher.domain>, and its report route answers the prefixes the request's services carry. The Axum adapter's tests select it with[prefix] moduleand 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
ts-ecis ignored under_ex, asthe_edge_cookie_is_read_and_written_under_the_requests_prefixproves.COOKIE_TS_ECtoCOOKIE_ECand so on, because their value is no longer a whole name. A crate outside core that read them writesservices.public_prefix().cookie_name(COOKIE_EC)instead.ServerKeyPrefix::for_settingsis removed. A caller takesservices.server_key_prefix().example.com., now gives the same server key prefix asexample.com, so its loading cache entries are made again under the new key.How it was verified
CI passed on
580cd27d8on the fork. 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.