Repository navigation
Conversation
`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`.
`POST /_ts/admin/keys/rotate` and `POST /_ts/admin/keys/deactivate` let anyone holding one shared password replace the keys the service signs with, over the domain every reader reaches. `ts keys rotate` and `ts keys deactivate` do the same work from an operator's machine with the operator's own token, so the two routes and their handlers go. Only Fastly served them. Axum, Cloudflare and Spin registered them to answer `501`. The `/admin/keys/rotate` and `/admin/keys/deactivate` routes go with them. They existed to refuse those paths, so that a call to one was not forwarded to the origin with its credentials. `closed_path_response` already answers every path beneath `/admin/keys` and `/_ts/admin` before the publisher fallback, on each adapter, so the four paths are still answered `404` here for every fallback method. Each adapter's `key_administration_paths_are_answered_here_and_never_proxied` holds that with the credentials a caller would send. `kid_is_creatable` and the two validators behind it stay in `request_signing/endpoints.rs`, because `ts keys` refuses a key id with them before it writes. The tests of the password challenge ask at `/_ts/admin/anything`, since the path they asked at is not a route any more, and the two probe tests take `/first-party/proxy` as their named route for the same reason. The key rotation guide's diagram had `current-kid` written before `active-kids`. The code writes `active-kids` first, and the diagram now agrees with the text beside it. `[request_signing]` still requires `config_store_id` and `secret_store_id`, which only the removed handlers read. They go in the next commit.
`[request_signing]` required `config_store_id` and `secret_store_id`, the platform ids of the two stores the keys live in. Only the removed rotate and deactivate handlers read them, to write those stores. The service finds its keys by the names the stores are linked under, `jwks_store` and `signing_keys`. With the handlers gone, the two ids were settings an operator had to supply with real values for nothing to read. They go, with the placeholder and padding checks on them in startup and deploy validation, and the auction plan's rule that enabled signing needs both. `ts keys` takes the ids on its command line. A configuration that still carries either key is refused with directions, through the refusal the removed tables use, when it is validated and when the service starts. Serializing the settings writes `enabled` alone, so a configuration the service writes is one it reads back. Fixtures whose `[request_signing]` section held only the two ids now say `enabled = false`, which is what an absent `enabled` meant.
`management_api.rs` was a client for the Fastly management API. The deployed service read an API token from a secret store named `api-keys` and used it to write its own config store and secret store, which is how the key rotation routes stored a new key. Nothing calls it now that keys are rotated with `ts keys`, so the client goes, with the `api-keys` entries in `fastly.toml` and the Viceroy template. The Fastly platform's store writes, `put`, `delete` and `create`, refuse with a message saying the service does not write its stores. They remain on the traits because `ts keys` implements them over the `fastly` CLI. `BackendConfig::from_url` and its two variants built the backend the client reached the API through. Nothing else called them, so they go as well, and the one test that parsed a URL through them states the parsed parts directly. The adapter's `base64` and `url` dependencies go with their last users. A deployment that linked an `api-keys` secret store can unlink it and revoke the token it held.
`POST /_ts/admin/cache/purge` let anyone holding one shared password empty the service's cache over the publisher's own domain, with nothing limiting how often. `ts cache purge` asks the hosting platform to purge the same surrogate keys with the operator's own token, so the route goes, with its handler in core, the stand-ins that answered `501` on Axum, Cloudflare and Spin, the harness mode and workflow step that drove it, and the parity tests of it. `auth::authenticated_username`, which named the caller in the purge log, goes too. Nothing in the service purges by surrogate key or purges everything now, so `purge_url_surrogate_key` and `purge_all` leave the template cache interface and every implementation of it. `purge_url`, which core uses to drop an entry it finds invalid, stays. The harness's purge mode was the end to end check that purging a key empties the cache. Two Fastly tests keep that evidence where it can still be had without the route: purging `ts-template` through the platform clears a stored template, and purging one page's reader key clears that page and leaves another. No admin endpoint is left, so the rule that every one must be covered by a `[[handlers]]` entry goes: `ADMIN_ENDPOINTS`, the coverage check at startup and at deploy validation, and the test that held the list against the Fastly route table. A configuration with no handler starts. Handler passwords are still checked. A request to the removed path is still answered `404` by the server and never forwarded, as for any path beneath `/_ts/admin`.
HTTP Basic authentication existed to gate the endpoints beneath `/_ts/admin`. None is left, so the gate goes: `auth.rs` in core, the `AuthMiddleware` each of the four adapters registered, the `Handler` type with its regex and its placeholder and weak password checks, and `handlers[*].password` as a declared secret. A configuration that still carries `[[handlers]]` is refused with directions when it is validated and when the service starts, through the refusal the removed tables use. Loading it and ignoring the list would leave an operator who gated a path of their own, a staging site for instance, believing it was still gated. An empty list is refused too, so is a handler given by an environment variable, and the settings are never written with one. The upgrade has an order to it. A build before this series refuses a configuration without a handler covering `/_ts/admin`, and this build refuses one with any handler, so no one configuration starts both. The new configuration and the new build go live together. The publisher path treats an `Authorization` header as bound for the origin in every case. It shared a cached page across readers when the edge had checked the header itself, and with no check at the edge that case is gone, so a request carrying the header always bypasses the shared template. The closed path check is unchanged and still answers `/_ts/admin` and `/admin/keys` with `404` before the publisher fallback, so a caller that sends a password to either never has it forwarded. Each adapter's tests of that send the credentials a caller would. What goes with the gate: - the tests of the challenge in the four adapters and the parity suite, and the tests that proved other routes were not challenged; - `[[handlers]]` in every test fixture, 52 entries; - the `handler_password` secret in `fastly.toml`, the Spin manifest, the Viceroy template, the integration fixtures, the three harnesses and the four smoke scripts, whose missing-secret cases cover the two secrets that remain; - the handlers section of the configuration guide, the Basic Authentication section of the API reference, the `Handler.password` row of the configuration guide's table of exceptional fields, and "unless an operator adds a handler" in the contracts of `/first-party/sign` and `/_ts/debug/ja4`. The configuration page's tests take their example list from `proxy.asset_routes`, having taken it from `handlers`. The page-bids alias comments named a `[[handlers]]` pattern covering `/_ts` as one reason a browser falls back to the alias. They name a gate in front of the service, which is the case that remains.
The DataDome module skipped its protection API for every path beneath `/admin/` and `/_ts/admin/`, listing them with the server's own routes. Nothing of the server's is served beneath either. `/_ts/admin` and `/admin/keys` are answered by the closed path check before any request filter runs, and everything else beneath `/admin/` is the publisher's own site, which the list left out of protection. `only_the_servers_own_paths_are_internal` holds the list both ways.
`[robots-txt] modules` names, in order, what makes the file. `refuse_all` and `allow_all` are core's own and need nothing else. Any other name is a contributor a module declared with `with_robots_txt_contributor`, and core names none of them. A contributor returns groups of records and core writes every byte. That is what lets these hold whatever a contributor returns. - With `refuse_all` selected the file is the refusal and nothing else. - A path in `always_allow` stays open to every crawler a contribution refuses the whole site to, by a group of core's own below the contributions, which are not edited. - The `Sitemap` line and the publisher's own text go around the contributions. Each contributor's answer is held in the key-value store, under a key that is a hash of the publisher, the contributor's name and the contributor's fingerprint, and the contributor is asked again when its own refresh is due. A contributor that fails has its held answer served past its age. With nothing held the file fails closed with `503`, because leaving a contributor's rules out would allow by omission what those rules refused, and a `404` or an empty file reads to a crawler as permission. A store that cannot give an entry a lifetime is written without one, so the answer is still there to serve. A contributor is asked through the module context, with the request's method, path, host and scheme and without its query or anything about who asked, because its answer is served to every crawler. `[robots-txt]` is refused when the settings load if `modules` is absent, empty or names one thing twice, if a key is not a setting, or if a block belongs to a contributor `modules` does not name. A name nothing supplies is refused when the registry is built, so the file does not fail on the first request for it.
Declared on the probe's one registration, beside its geo, Edge Cookie and device modules, so an adapter's tests can select a contributor from a crate core does not know. Its one rule names the host, path and query it was asked for, which lets a test read back what a contributor is handed.
The route is registered only when the settings carry a `[robots-txt]` section, for `GET` and `HEAD`, beside each adapter's other fixed routes. On Fastly and Axum it is a row of the named-route table, gated by `named_route_is_served`, so a request for it passes what every named route's request passes there. Every other method on the path goes to the publisher fallback. Without the section the path reaches the publisher's origin like any other, so a publisher keeping their own file is left alone. With `refuse_all` selected every response an adapter finalizes carries `X-Robots-Tag: noindex, nofollow`. It is written last when a response is finalized, and replaces any tag already there, so that neither an origin's header nor an operator's `[response_headers]` can loosen it. The comment on the Axum adapter's one preparation point now names the attestation pages as the other route outside it.
The configuration guide gains the section, its settings, how the file is made and what is refused when. The API reference gains the route and its contract. The integration guide gains the contributor extension point, and the configuration rules list `robots-txt` among the types. The example configuration carries the section commented out.
`x-served-by` names the node that answered, and `x-cache` and `x-cache-hits` say how a cache treated the request. They arrive on a response from the publisher's origin or from a cache on the way to it. `strip_diagnostic_headers` removes the three from a response unless the request was for a path in the namespace it is given. `ROUTE_NAMESPACE` names `/_ts`, the prefix of every path this server answers itself. A path that only starts with the namespace's text, `/_tsx` for one, is not in it.
The finalize middleware of each adapter takes the request's path before the handler runs, and removes the three headers from the response unless the path is beneath `/_ts`. The removal comes before the finalize step, so a header of one of those names that an operator sets in `[response_headers]` is still sent. Each adapter has three tests of its own middleware. One sends a response naming its node for a publisher's page and finds the headers gone, one does the same for a path beneath `/_ts` and finds them kept, and one finds an operator's `X-Cache` on a publisher's page.
The response headers section of the configuration guide names the three headers and says where they are kept, and the changelog records the change.
`[tinybird]` is now `[analytics.tinybird]`, and `[analytics] module = "tinybird"` selects it, which replaces the table's `enabled` line. Every other thing a deployment can switch on is selected that way, and telemetry was the one section named after its vendor and switched on by a flag. - `Settings::telemetry()` returns the module's settings only when the section selects it, and the Fastly sink is built from that, so the sink has no enabled flag of its own. - A section that selects nothing, or a module this build does not have, is refused when the settings load, and the table refuses a key it does not know. - The secret reference is `analytics.tinybird.auction_token_secret`. The references of a table nothing selects are cleared before secrets are resolved, so the refusal is what an operator sees and not a failed lookup. - A configuration that still carries `[tinybird]` is refused with directions. An earlier version wrote the table into every stored configuration, on or off, so the message says to push the configuration again with this version. - The messages about the table name it through one constant. - An empty or absent token reference is refused as the settings load, where an empty one becomes absent as they normalize, and at deploy by the declared secret field, so the deploy validation's own check of the token is removed. Both refusals are tested. The example configuration carries the new block, which a test uncomments and loads.
The configuration guide's Tinybird section becomes the analytics section, with the selector, what is refused and where `[tinybird]` went. The telemetry guide, the configuration rules and the Tinybird project's README name the settings by their new table, and the changelog records the change as breaking.
`COOKIE_MTM_PREF` sat in core's constants, and the Model Terms for Marketing signal crate is the only code that reads the cookie. The constant moves to that crate, so core names one standard less. The Axum adapter's test of the signal takes the name from the crate.
`[cmp] module = "inmobi"` selects it, with the account's Choice loader in `[cmp.inmobi] script_url` and the IAB CMP id in `cmp_id`, which is 10 unless written. Where a `[[fetch]]` entry names `cmp.inmobi`, the module's middleware writes three things into the head, in this order: the IAB TCF v2 stub, which queues every `__tcfapi` call made before the prompt arrives and answers `ping` as a stub; InMobi's GPP stub, which answers the generic `__gpp` commands at once with the CMP id and queues the rest; and the Choice loader, deferred, so a slow consent vendor never holds up the page and the loader never runs ahead of a stub the publisher's own head carries. Each stub defines nothing where the page already has its interface. The module serves no browser module, because the stubs and the Choice script are the whole client side. The prompt writes the reader's answer to the cookie the TCF permission signal reads, so the answer reaches the permission model on the next request the way every signal does. The script URL becomes a `<script src>` on every page, and a settings document reaches the service from a config store rather than from a person reading it, so it is refused when it is empty, is not a URL, is not `https`, is on a host other than `cmp.inmobi.com` or `cdn.inmobi.com`, or carries a user name or password. A host that merely contains an allowed name is refused by what it is rather than by what it looks like, and the URL is escaped again when it is written, because a URL on the right host can still carry a quote in its query. A document that selects the prompt and names it in no entry is refused, because the prompt would never be written, and the table refuses a field it does not read. The two stubs are tested in a stand-in for the browser with Node's test runner, `node --test "crates/cmp/inmobi/tests/*.test.mjs"`, which runs the scripts as the page carries them. The module's tests cover each refusal, the tag's parts and their order, that a vendor's tag named after the prompt is written after it, and the page a reader receives, recorded in `src/fixtures/page-change.recorded.html`. The stock list offers the module after Didomi and lists its middleware in the fetch phase, the example configuration names it in its fetch entry and shows its table, and the cargo aliases build and test it with the other modules. 51Degrees maintain the crate until InMobi adopt it, which its manifest states.
A module that writes a script into pages keeps that script's tests beside it, as `tests/*.test.mjs`, run with Node's test runner and no packages. The vitest job runs every such file after the browser bundle's tests, from the repository root, so a stub that changes is tested the way a page runs it. The InMobi Choice module's two stubs are the first.
A guide page for the module, with its settings, where to place it, what is checked, and what a consent prompt module keeps to, in the sidebar beside the other consent management platforms. The configuration guide's section table, its list of the modules this repository ships, its placing table and its integration sections carry the module, the integrations overview lists it, the crate list in AGENTS.md names it, and the changelog records it.
An entry may cover any media type core streams as text: `text/html`, another `text/` type other than `text/x-component`, whose rows are length prefixed, `application/javascript`, `application/json` or `image/svg+xml`, written as a bare `type/subtype` in lower case. A response is matched by the type and subtype of its content type, without the parameters. A middleware says which media types it works on with `handles_media_type`, HTML alone unless it says otherwise, so every module keeps its behavior with no change to its code. An entry naming a middleware under a media type it does not handle is refused when the registry is built, naming the entry and the middleware. A chain carries its media type, and on one other than HTML its plan refuses an action that decides about HTML, being head markup or an element or text handler, by the middleware's name, because those are decisions about HTML and a stream processor is the one thing that applies to any body. The publisher runs the covering entries' stream processors on a body of such a media type after it has moved the origin's addresses in it: the fetch entry's on what is stored, and the serve entry's on each reader's copy, whether that copy is assembled from the store or fetched for the request, as on a page. Core's test fixture gains a middleware that works on `text/plain` and adds a line at the end of a body, and the tests cover the entries accepted and refused, the media type of a content type, a chain refusing an HTML decision on text and taking a stream, the registry refusing a middleware under a media type it does not handle in either direction, and a text body through the publisher with and without a covering entry.
The configuration guide says which media types an entry may cover, how a response is matched, what runs on a body that is not HTML and what is refused, with an entry for a text file as the example. The integration guide shows `handles_media_type` on a middleware that works on `text/plain`. The changelog records the change.
`[ads-txt] modules = ["sellers"]` selects it, with the lines to add in `[ads-txt.sellers] lines`. The module is a middleware for `text/plain`, named in a `[[fetch]]` entry on the file's path, which passes the origin's file through and writes the lines after its last one, on a line of their own, so a seller the deployment introduces is declared beside the publisher's own. The publisher's file stays authoritative, and a publisher whose origin has no file gets the origin's answer, because the module adds to the publisher's file and never invents one. Every buyer's crawler reads the file and a typo there costs the publisher revenue silently, so each line is checked as the settings load against the file's grammar: a data record is a domain, an account id, DIRECT or RESELLER and an optional certification authority id, separated by commas, and a variable is one of CONTACT, SUBDOMAIN, INVENTORYPARTNERDOMAIN, OWNERDOMAIN or MANAGERDOMAIN with a value. A comment, an empty line and an empty list are refused, naming the line and what is wrong with it, and so is a document that selects the module but names it in no entry, where the lines would never be written. The stock list offers the module after the page modules and lists its middleware in the fetch phase, the example configuration shows its tables and entry, the cargo aliases build and test it with the other modules, and the crate is the project's own, which its manifest states. The tests cover each refusal, the lines the grammar accepts, the file with and without a final line break, an empty file, a file read in pieces, and the module's place in a registry.
The configuration guide gains the section, its module's table, how the file is made and what is refused, with the entry that places the middleware, and lists the module in its key sections and placing tables. The rules guide names the section among the types and in its table. The changelog records the module.
`cmp.inmobi`, `cmp.didomi` and `cmp.sourcepoint` declare both phases, so an entry of either phase may name them. What a prompt writes is the same for every reader, so the fetch phase stores it in the page every reader is served from, which is what a generated configuration places, and the serve phase writes it on each reader's copy for a publisher who wants the stored page without it. Selecting the InMobi module with no entry of either phase naming it is still refused. The stock list keeps each prompt's row in the fetch phase, which is the phase `ts audit generate` writes, and its test allows a middleware that runs in several phases to be listed in one of them, checking that what is listed for a phase is registered for it and that everything registered is listed somewhere.
The InMobi guide places the prompt by an entry of either phase and says what each phase does with it, the configuration guide's middleware table says the same for every consent management platform, and the changelog records the change.
…ts path When the origin answers `404 Not Found` for a path a `[[fetch]]` entry with a path names middleware for, core runs that middleware on an empty document of the entry's media type and its output is the file, stored and served as a fetched one is. A GET and a HEAD are answered alike. With no such entry, on any other answer from the origin, or where only the entry covering every path applies, the origin's answer stands, because a page the origin does not have is the origin's answer. `PhaseEntries::creating` finds the entry. The sellers crate says its file is made from the lines alone where the publisher has none, which its middleware already does on an empty document.
The configuration guide says it with the entries for other media types and in the ads.txt section, the integration guide tells a middleware it may be run on an empty document, and the changelog records the rule.
`Middleware::prepare` answers a future for a document whose decision needs the request's services or has to wait, on the key-value store or the network. The request's handler runs it before the document is processed, once per phase the document goes through, as the middleware's module and with the permissions it declares, and keeps what it answers under the middleware's name in the context's `Prepared`, where `create` takes it as the type it prepared. `create` and every streaming path stay synchronous, so the Fastly adapter is as it was. A middleware that prepares nothing is asked `create` as before. A preparation may answer a `Refusal` instead, with a status, an optional `Retry-After` and a message. The handler answers the request with that status and the message as plain text, marks the response private and unstorable, and processes no document, so a middleware responsible for a file fails closed when it cannot make it. The first refusal ends the preparations of a document. A copy read from the store prepares its serve chain the same way before it is assembled. The middleware fixture gains one that prepares and one that refuses, and the tests cover what is prepared reaching `create` in either phase, the refusal's status, headers and body, the chain keeping each answer under its name, and the first refusal ending the preparation.
The section says when a middleware prepares, what it is called with, where its answer goes, how a refusal is answered, and what each phase's preparation must not depend on. The changelog records the addition.
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 #1370, 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 device signals (#1316). The additions that follow the seven pull requests open here are listed in #777.
Compare
split/28-device-signalswithsplit/29-viewport-sizesto see only this pull request's change.
What this pull request does
A format may name the viewport widths, in CSS pixels, at which the page asks for it, so a slot asks for a large size only where the page has room for one. Either bound may be left out, and a minimum above a maximum is refused when the settings load. When any format of a slot names a bound, the slot's page data carries
viewport_sizes, one entry for each band of widths, widest first. A slot whose formats name no bound asks for every format at every width, as before.The bands reach two places in the browser. The Google ad tag's head bootstrap and the bundle's slot definition apply them as a size mapping through
defineSizeMapping, so the ad server request carries only the band's sizes. The bundle's refresh auction offers a bidder only the sizes of the band the viewport is in, and leaves a slot whose band asks for no size out of the auction. The server-side auction does not filter its sizes and is still offered every format.A binary released before these fields refuses a format carrying either of them, which fails every request, so the binary that reads them is promoted before any configuration uses them. The configuration guide says so.
Why
A page that shows a billboard on a wide screen and a small banner on a phone has one slot and several formats, and without bounds the slot asks for every format at every width, so a bidder is offered a billboard the page cannot show. The bounds are the publisher's statement of what fits where. The ad tag passes them to the ad server as its size mapping, and the refresh auction holds a bidder to them.
How it was verified
CI passed on
fc048f625on the fork. Not exercised: the ad server's own handling of a size mapping. The tests prove the mapping is defined with the bands the page data carries, through the bootstrap and the bundle.References #1287.