Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
bfeacda
Declare a loading cache in core
jwrosewell Oct 10, 2026
b51e896
Each robots.txt contributor's answer is held by the loading cache
jwrosewell Oct 10, 2026
e0e4c7b
Every adapter takes the loading cache its builders offer
jwrosewell Oct 10, 2026
3c5a37c
The 51Degrees loading cache is a module a stock build ships
jwrosewell Oct 10, 2026
80b3a93
A store that cannot give an entry a lifetime still holds it
jwrosewell Oct 10, 2026
2d7936e
The guides describe the loading cache
jwrosewell Oct 10, 2026
464f9a9
A Cloudflare load runs on after the response only inside scope_request
jwrosewell Oct 10, 2026
6948c5d
The crate's documentation names scope_request without linking it
jwrosewell Oct 10, 2026
b2069dd
Record the loading cache crate and its dependencies in Cargo.lock
jwrosewell Oct 10, 2026
bff016e
Each feature selects the loading cache that holds its copies
jwrosewell Oct 11, 2026
8b5451b
Merge branch 'review/23-prepare' into review/24-loading-cache
Jamesr51d Oct 12, 2026
6d022e6
An unreadable held robots.txt copy is removed and the module asked again
Jamesr51d Oct 12, 2026
f1ab013
Merge branch 'review/23-prepare' into review/24-loading-cache
Jamesr51d Oct 12, 2026
7098880
Merge 23-prepare into 24-loading-cache
Jamesr51d Oct 12, 2026
9dc219c
Merge 23-prepare into 24-loading-cache
Jamesr51d Oct 12, 2026
4bd093a
A cache table naming no module takes the one loading cache module the…
jwrosewell Oct 13, 2026
c0a048a
Merge 23-prepare into 24-loading-cache
jwrosewell Oct 13, 2026
1eae9e4
Merge 23-prepare into 24-loading-cache
jwrosewell Oct 13, 2026
92826a3
Merge split/23-prepare into split/24-loading-cache
Jamesr51d Oct 13, 2026
f4e81af
Merge split/23-prepare into split/24-loading-cache
Jamesr51d Oct 13, 2026
844b01f
Merge 23-prepare into 24-loading-cache
jwrosewell Oct 13, 2026
b5ba165
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 16, 2026
a1931fd
A shared load is given nothing of the reader whose request started it
jwrosewell Oct 16, 2026
146f955
Each application state gets a loading cache of its own, Cloudflare in…
jwrosewell Oct 16, 2026
674ba1a
CacheSpec loses detachable, which nothing read
jwrosewell Oct 16, 2026
6242303
The comments and guides say what a table naming no module takes
jwrosewell Oct 16, 2026
1533d51
The deploy check refuses a loading cache module nothing offers
jwrosewell Oct 16, 2026
0d49c46
Tests for the stock default, a zero memory lifetime, a dropped leader…
jwrosewell Oct 16, 2026
38485df
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 16, 2026
5dcb4bf
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 16, 2026
18b2bed
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 16, 2026
2d45cc2
Merge the review fixes into split/24-loading-cache
jwrosewell Oct 16, 2026
0e5a0aa
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 16, 2026
dd6f86e
Three test statements take the layout rustfmt gives them
jwrosewell Oct 16, 2026
215b4a7
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 16, 2026
5313f0e
The Fastly robots.txt tests expect the kept answer the loading cache …
jwrosewell Oct 16, 2026
3564019
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 16, 2026
4ffa62a
The Cloudflare and Spin adapters declare their key-value stores
jwrosewell Oct 17, 2026
3ebba58
The adapter comments say which stores run_app opens
jwrosewell Oct 17, 2026
72fb579
A Worker without its key-value binding fails every request in CI
jwrosewell Oct 17, 2026
4780e10
The changelog and guides say what Cloudflare and Spin keep
jwrosewell Oct 17, 2026
e536f8f
The adapter READMEs say each opens its key-value store for every request
jwrosewell Oct 17, 2026
7b0a714
The store tests move to a file of their own in each adapter
jwrosewell Oct 17, 2026
8c31abc
The robots.txt guide leaves its point on stores that cannot be opened
jwrosewell Oct 17, 2026
40ad339
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 18, 2026
8342ffb
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 18, 2026
67dc994
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 18, 2026
b5e40ae
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 18, 2026
ed09d90
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 19, 2026
5ceb738
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 19, 2026
0d0427b
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 19, 2026
e66b955
Merge split/23-prepare into split/24-loading-cache
jwrosewell Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ test-fastly-reuse = "test -p trusted-server-adapter-fastly --target wasm32-wasip
# run with the Axum adapter in CI.
build-axum = "build -p trusted-server-adapter-axum"
check-axum = "check -p trusted-server-adapter-axum"
clippy-axum = "clippy -p trusted-server-adapter-axum -p trusted-server-permission-signal-gpc -p trusted-server-permission-signal-gpp -p trusted-server-permission-signal-mtm -p trusted-server-permission-signal-tcf -p trusted-server-permission-signal-us-privacy -p trusted-server-cmp-osano -p trusted-server-modules -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-sellers -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-mock -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb --all-targets --all-features -- -D warnings"
test-axum = "test -p trusted-server-adapter-axum -p trusted-server-permission-signal-gpc -p trusted-server-permission-signal-gpp -p trusted-server-permission-signal-mtm -p trusted-server-permission-signal-tcf -p trusted-server-permission-signal-us-privacy -p trusted-server-cmp-osano -p trusted-server-modules -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-sellers -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-mock -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb"
clippy-axum = "clippy -p trusted-server-adapter-axum -p trusted-server-permission-signal-gpc -p trusted-server-permission-signal-gpp -p trusted-server-permission-signal-mtm -p trusted-server-permission-signal-tcf -p trusted-server-permission-signal-us-privacy -p trusted-server-cmp-osano -p trusted-server-modules -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-sellers -p trusted-server-loading-cache-51degrees -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-mock -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb --all-targets --all-features -- -D warnings"
test-axum = "test -p trusted-server-adapter-axum -p trusted-server-permission-signal-gpc -p trusted-server-permission-signal-gpp -p trusted-server-permission-signal-mtm -p trusted-server-permission-signal-tcf -p trusted-server-permission-signal-us-privacy -p trusted-server-cmp-osano -p trusted-server-modules -p trusted-server-identity-lockr -p trusted-server-audience-permutive -p trusted-server-cmp-sourcepoint -p trusted-server-cmp-didomi -p trusted-server-cmp-inmobi -p trusted-server-ads-txt-sellers -p trusted-server-loading-cache-51degrees -p trusted-server-tag-google-tag-manager -p trusted-server-testing-testlight -p trusted-server-framework-nextjs -p trusted-server-ad-tag-google -p trusted-server-bot-protection-datadome -p trusted-server-ad-server-mock -p trusted-server-auction-aps -p trusted-server-auction-prebid -p trusted-server-auction-prebid-server -p trusted-server-auction-protocol-openrtb"

# --- Cloudflare adapter (native host + wasm32-unknown-unknown) ---
# Build/check target the WASM runtime (requires the `cloudflare` feature);
Expand Down
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- A robots.txt module's answer is held by the loading cache of the one loading cache module the build links, or of the module `[cache.robots-txt] module` names, briefly in memory and in the host's shared store outside it, so a module is asked again only when no copy young enough is held, rather than by every request. With no loading cache module linked, or several and none named, nothing is held and every request for the file asks each module, as every request did when no adapter gave the file a key-value store. The last answer is kept apart for 30 days and served when the module cannot answer, in the value cache on Fastly and in the key-value store on Cloudflare and Spin, and Axum, which has no key-value store, keeps none.
- **Breaking:** the Cloudflare and Spin adapters declare their key-value store, the Workers KV binding `TRUSTED_SERVER_KV` and Spin's `default` store, so EdgeZero opens it for every request and the loading cache keeps its copies there between requests, where every request was given a store whose every call failed. A declared store is required, so a Worker without the binding, or a Spin component that cannot open `default`, fails every request before any route runs.
- A consent management platform's middleware runs in the phase the publisher chooses. `cmp.inmobi`, `cmp.didomi` and `cmp.sourcepoint` may be named in a `[[fetch]]` or a `[[serve]]` entry, where each ran in the fetch phase alone. A generated configuration still places them in `[[fetch]]`. A `[[serve]]` entry naming `cmp.inmobi` for a page whose `[[fetch]]` entry names another middleware is refused, because the serve phase writes its head markup after what the fetch phase writes ahead of the script bundle, so a vendor's tag there would come ahead of the prompt. Naming `cmp.inmobi` for one page in both phases is refused, because the prompt would be written twice.
- **Breaking:** auction telemetry is selected the way every other module is. `[tinybird]` is now `[analytics.tinybird]`, and `[analytics] module = "tinybird"` selects it, which replaces the `enabled` line. A configuration that still carries `[tinybird]` is refused with directions. A stored configuration written by an earlier version carries the table even where telemetry was off, so push the configuration again with this version. The secret reference is `analytics.tinybird.auction_token_secret`, the table refuses a key it does not know, and a section that selects nothing, or a module this build does not have, is refused. A row is sent only for a reader whose permissions include `analytics.ad_reporting.measure_ad_performance`, so none is sent for a reader the consent gate refuses unless they permit it. The Axum, Cloudflare and Spin adapters send no telemetry, and log a warning at startup when the section selects a module.
- The DataDome module asks its Protection API about a publisher's pages beneath `/admin/` like any other page. It skipped every path beneath `/admin/` and `/_ts/admin/`, which left a publisher's own `/admin/` pages out of bot protection.
Expand Down Expand Up @@ -89,12 +91,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- A middleware may prepare, and may refuse. `Middleware::prepare` runs in the request's handler before the document is processed, as the middleware's module with the request's services, and what it answers is kept under the phase and the middleware's name for `create` in that phase, so a decision that needs a store or the network is made there while `create` stays synchronous. The fetch phase of a document that may be stored is prepared with no client IP, TLS details or client signals in the services. A refusal answers the request with its status and `Retry-After`, with no document, and nothing is stored. Core logs a refusal as a warning naming the middleware, and answers a status that is not a client or server error with `500`.
- A loading cache. `PlatformLoadingCache` in core loads a missing value once per key however many requests ask for it at the same moment, gives every waiting request the value or the error, and holds nothing that failed. A load is handed services with nothing of the reader in them, from `RuntimeServices::without_connection`, because its value serves every reader. A crate offers one through `IntegrationBuilder::with_loading_cache`, each feature that holds answers selects the module whose cache holds them in its own `[cache.<name>]` table, and an adapter takes the selections with `loading_caches_from`. A feature that selects none takes the one loading cache module the build links, and gets `NoLoadingCache`, which loads every time, when the build links none or several. The new `crates/loading-cache/51degrees`, the module `loading-cache.51degrees`, implements it with the published `fiftyone-caching` package as a brief copy in memory over the host's value cache on Fastly or its key-value store on Cloudflare and Spin, with the memory copy alone on Axum, and the stock list ships it, so every adapter takes it. Each application state keeps a memory copy of its own, which lasts one request where an adapter builds its state for each request, as Cloudflare and Spin do. Every load runs in its caller.
- `[cache.robots-txt]` selects, with `module`, the loading cache that holds each robots.txt module's answer, and sets the lifetimes of its copies, in seconds: `memory_seconds` (default 60), `store_seconds` (default 604800) and `store_idle_seconds` (default 2592000). A memory lifetime may be 0, the others must be at least 1, and none may be more than a year. A `module` no linked crate offers a cache from is refused by `ts config validate` and when the service starts.
- `ServerKeyPrefix`, the prefix every loading cache key starts with, being `ts.` and the publisher's domain with a leading `www.` dropped, so publishers sharing a store never share an entry.
- `PlatformValueCache`, the host's shared cache of small values with lifetimes, which `RuntimeServices` carries where the host supplies one. `lookup_or_reserve` obliges the caller that finds a key missing to fill it where the host can make other callers wait, and `remove` drops an entry. The Fastly adapter supplies one over Fastly's cache, answering the reservation with a transaction lookup and the removal with a purge of the entry's own surrogate key.
- A file the origin does not have is made by the middleware a `[[fetch]]` entry with a `path`, for a media type other than HTML, names for it. When the origin answers `404 Not Found` for that path itself, which names a file and not a folder, the middleware runs on an empty document of the entry's media type and its output is the file, so `ads.txt` is made from the sellers lines alone where the publisher has none. With no such entry, or for a request that names no host, the origin's answer stands.
- `[ads-txt] modules = ["sellers"]` adds the lines `[ads-txt.sellers] lines` gives to the publisher's `ads.txt`, from `crates/ads-txt/sellers`. 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, so the publisher's file stays authoritative, and a publisher whose origin has no file gets one made of the lines. Each line is checked as the settings load as a data record or a variable of the ads.txt grammar, and a selected module that no entry for `text/plain` on `/ads.txt` places is refused.
- A `[[fetch]]` or `[[serve]]` entry may cover a media type other than `text/html`: another `text/` type except `text/x-component`, `application/javascript`, `application/json` or `image/svg+xml`, written as a bare `type/subtype` in lower case. A response that declares a charset other than UTF-8, or whose status is not `200`, gets no middleware. A middleware says which media types it works on with `handles_media_type`, HTML alone unless it says otherwise, and an entry naming one under a media type it does not handle is refused when the registry is built. On a media type other than HTML a middleware's stream processor alone runs on the body, after core has moved the origin's addresses, in the fetch phase and then, for a response that is not stored, the serve phase, and an action that decides about HTML there is refused by name. A response an entry covers loses the origin's `ETag` and `Last-Modified`, and one a `[[serve]]` entry covers is sent `private, no-store` when a module left state on the request.
- The InMobi Choice consent prompt as a module, `[cmp] module = "inmobi"` with `[cmp.inmobi] script_url`, from `crates/cmp/inmobi`. Where a `[[fetch]]` entry names `cmp.inmobi`, its middleware writes the IAB TCF stub, InMobi's GPP stub and the deferred Choice loader into the head, in that order, and the two stubs' behavior is tested with Node's test runner, which CI now runs for every module's `tests/*.test.mjs`. The script URL is refused unless it is `https` on an InMobi Choice host with no user name or password, `cmp_id` unless it is a whole number from 1 to 4095, and entries that leave any page without the prompt are refused. The new `Middleware::template_key` keys a stored page by what a middleware writes that no setting decides, so a change to a stub stores pages again. The Trusted Server maintainers hold the crate until InMobi adopt it.
- The stock modules' fetch middleware give as their `template_key` the text compiled into their crates that they write into a page, so a change to it stores pages again. The Google ad tag gives its head scripts, the `tsjs.adInit` bootstrap among them, and the GPT script's first-party path, Prebid its configuration script and bundle tag, Didomi its settings script, and Sourcepoint its head scripts and its CDN's first-party path. Permutive, Lockr, DataDome and Google Tag Manager give the first-party path they point a vendor's address at.
- `[robots-txt]` serves a publisher's `/robots.txt` from the settings. `modules` names what makes the file, in order. `refuse_all` and `allow_all` are Trusted Server's own, and any other name is a module that contributes rules through the new `RobotsTxtContributor` extension point, declared with `with_robots_txt_contributor`. Trusted Server writes the file, so a contributor cannot produce a malformed one, a refusal cannot be loosened, and a path in `always_allow` stays open to every crawler refused the whole site. With `refuse_all` selected every response an adapter finalizes carries `X-Robots-Tag: noindex, nofollow`. A module is asked for the host the request was addressed to, never one a forwarded header names, and is given nothing of the crawler's connection. Its answer is held for that host in the request's key-value store until its refresh is due, and no adapter gives a request a working one, so every adapter asks each module on every request. A module that cannot answer has its last held answer served, and with none held, or with no rule in the whole file, the answer is `503` and never an empty file. On Fastly the file is answered before the Edge Cookie steps and the request filters. Without the section `/robots.txt` reaches the publisher's origin as before.
- `[robots-txt]` serves a publisher's `/robots.txt` from the settings. `modules` names what makes the file, in order. `refuse_all` and `allow_all` are Trusted Server's own, and any other name is a module that contributes rules through the new `RobotsTxtContributor` extension point, declared with `with_robots_txt_contributor`. Trusted Server writes the file, so a contributor cannot produce a malformed one, a refusal cannot be loosened, and a path in `always_allow` stays open to every crawler refused the whole site. With `refuse_all` selected every response an adapter finalizes carries `X-Robots-Tag: noindex, nofollow`. A module is asked for the host the request was addressed to, never one a forwarded header names, and is given nothing of the crawler's connection. Its answer is held for that host by the loading cache until its refresh is due. A module that cannot answer has its last held answer served, and with none held, or with no rule in the whole file, the answer is `503` and never an empty file. On Fastly the file is answered before the Edge Cookie steps and the request filters. Without the section `/robots.txt` reaches the publisher's origin as before.
- `ts keys rotate` and `ts keys deactivate` rotate and retire the request signing keys from an operator's machine. They write the two stores the service links as `jwks_store` and `signing_keys` through the `fastly` CLI, with the token from `FASTLY_API_TOKEN`. A private key reaches `fastly` on its standard input and never in its arguments, and a key id no platform can store is refused before anything is written. A command that cannot read the config store writes nothing, and a delete that failed part way can be run again.
- Attestation. With `[attestation]` in the settings a deployment serves evidence, in the sense of RFC 9334, of who operates it and which build it runs, as a page and as JSON at `[attestation] endpoint`, which defaults to `/_ts/attestation`. The evidence names the host the request arrived for, the publisher, the platform service and the build, with the time and the caller's nonce, and is signed with ECDSA P-256 under a configured context. The signing keys are a schedule compiled in from the build input `TRUSTED_SERVER_ATTESTATION_KEYS`, and a build given none answers `503` at the endpoint, as does a clock that cannot be read. A key in the schedule that is not a P-256 private key stops the build. A build also reports the commit, the build run, the tag it was made from and the build time it was given, from `TRUSTED_SERVER_COMMIT`, `TRUSTED_SERVER_BUILD_RUN`, `TRUSTED_SERVER_BUILD_TAG` and `SOURCE_DATE_EPOCH`. See `docs/guide/attestation.md`.
- One Fastly service can serve several publishers. A `TS__CONFIG__KEY` entry in the `trusted_server_config` store that contains `{host}` names an app-config blob for each host, pushed with `ts config push --key <host>`, and each request reads the blob under its own host. A host with no blob is answered `421 Misdirected Request`, never from the logical store ID or from another host's blob, a malformed selector is answered `500`, and a sandbox that serves several requests keeps an application for each blob, found only by that blob's key. Without the entry, or with a selector that has no placeholder, the service reads one blob. `scripts/config-by-host-local-test.sh` runs it under Viceroy.
Expand Down
Loading
Loading