Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
448f41d
A Fastly sandbox keeps an application for each app-config key
jwrosewell Oct 9, 2026
e802375
A `__KEY` selector with `{host}` names an app-config blob for each host
jwrosewell Oct 9, 2026
0e4f153
A harness serves two publishers from one local Viceroy instance
jwrosewell Oct 9, 2026
64750a4
The guides say how one service serves several publishers
jwrosewell Oct 9, 2026
f30aed1
Merge branch 'review/10-page-middleware' into review/11-config-by-host
Jamesr51d Oct 12, 2026
0166d02
A host name with a label that starts or ends in a hyphen is refused
Jamesr51d Oct 12, 2026
ba4008f
Merge the data page sandbox from the inspection endpoints into config…
Jamesr51d Oct 12, 2026
5ae3b03
Merge split/10-page-middleware into split/11-config-by-host
Jamesr51d Oct 13, 2026
b8314cb
Merge split/10-page-middleware into split/11-config-by-host
Jamesr51d Oct 13, 2026
1d0deeb
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 16, 2026
5c6729d
Read the Host header in the Fastly adapter without a panic on bytes t…
jwrosewell Oct 16, 2026
6d3c69a
Answer a host whose config key would pass 255 characters 421 without …
jwrosewell Oct 16, 2026
a630518
Read the Fastly selector from the TS__CONFIG__KEY entry of the store …
jwrosewell Oct 16, 2026
faf8026
Push a Fastly blob under a key other than the logical store ID with -…
jwrosewell Oct 16, 2026
3fd0d7d
Describe and test the TS__CONFIG__KEY selector in the guide and the l…
jwrosewell Oct 16, 2026
62306a4
Follow the selector design's revised tests and core comment for row 11
jwrosewell Oct 16, 2026
60fabb7
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 16, 2026
91e530d
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 16, 2026
3c511fe
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 16, 2026
22c7b12
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 16, 2026
1ee0bd2
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 16, 2026
2e961d4
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 18, 2026
5220bc5
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 18, 2026
42238f8
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 18, 2026
98ee3da
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 18, 2026
75f1fd6
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 19, 2026
4c2c275
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 19, 2026
427d9ce
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 19, 2026
08c3339
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 19, 2026
0eca6c2
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 28, 2026
ac6a47a
The keyed push writes its SHA-256 in hex one byte at a time
jwrosewell Oct 28, 2026
605f389
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 28, 2026
61e86bb
Core names the shared policy entry and reads the file it holds
jwrosewell Oct 28, 2026
30d3580
A host's blob reads its policy file from the shared entry
jwrosewell Oct 28, 2026
c365040
The keyed push keeps each policy file once, under its digest
jwrosewell Oct 28, 2026
953e2c3
The config by host guide, harness and changelog name the policy file
jwrosewell Oct 28, 2026
e297f44
The keyed push tests hand the temp folder to ts
jwrosewell Oct 28, 2026
3b2e5c1
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 28, 2026
0a1a523
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 28, 2026
f94224a
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 28, 2026
c58153a
The keyed push read-back tests pin a digest of their own
jwrosewell Oct 28, 2026
30a3896
Merge split/10-page-middleware into split/11-config-by-host
jwrosewell Oct 28, 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
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ jobs:
- name: Run cache purge harness
run: BID_DELAY=3 ./scripts/template-cache-local-test.sh purge

- name: Run config by host harness
run: ./scripts/config-by-host-local-test.sh

- name: Run config by host harness (reusable-sandbox feature)
run: ./scripts/config-by-host-local-test.sh reuse

test-axum:
name: cargo test (axum native, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- 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 --local --key <host>`, and each request reads the blob under its own host. Each host's blob names its permission policy file and pins its SHA-256, and the push keeps each file `--permissions` supplies once, in a shared entry under `permissions_<sha256>`, written before the blob. 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.
- `ts audit generate` writes the `[[fetch]]` and `[[serve]]` entries that run the page changes of the modules it selects on every HTML page, and `trusted-server.example.toml` documents the entries with every middleware the stock modules supply. `trusted-server-modules` lists those middleware with `middleware()` and `middleware_for(...)`.
- Page changes as middleware. A module registers a change it makes to a page with `.with_middleware(...)`, and the ordered `[[fetch]]` and `[[serve]]` entries of the settings say which pages each one runs on and in what order. A fetch middleware runs on the page as the origin sent it and what it writes is stored with a shared template. A serve middleware runs on each reader's copy, whether the page came from the store or from the origin, and nothing it writes is stored. Where text a middleware writes into a script would end the script, or begin `<script` after `<!--`, its `<` is written as `\u003c`. An entry with no path after one with `path = "/"` is refused, because both cover every page.
- A builder registers its module from the compiled auction plan with `with_plan_registration` and checks its settings against the plan with `with_plan_validator`, so a module whose page support follows what the plan selects is not named in `trusted-server-core`. Prebid and APS register this way. A renderer descriptor carries its builder's statement of which payload key holds the identifier the renderer picks its bid by, made with `BidRenderer::picking_bid_by`, and the page's `hb_adid` is read through that statement.
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 41 additions & 13 deletions crates/trusted-server-adapter-fastly/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,19 @@ use trusted_server_device_fastly::FastlyHostSignals;
use crate::middleware::{AuthMiddleware, FinalizeResponseMiddleware};
use crate::platform::{
FastlyPlatformBackend, FastlyPlatformConfigStore, FastlyPlatformGeo, FastlyPlatformHttpClient,
FastlyPlatformSecretStore, UnavailableKvStore,
FastlyPlatformSecretStore, FastlyPolicyEntries, UnavailableKvStore,
};

// ---------------------------------------------------------------------------
// AppState
// ---------------------------------------------------------------------------

/// The stores a service reads and the key of its app-config blob.
///
/// [`Self::logical`] gives the logical stores and key. The entry point puts
/// the selector the config store holds in place of the key and resolves it
/// for the request before anything is built, with
/// `stores_with_stored_selector` and `stores_for_request`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RuntimeStoreConfig {
pub(crate) config_store_name: StoreName,
Expand All @@ -178,12 +184,11 @@ pub(crate) struct RuntimeStoreConfig {
impl RuntimeStoreConfig {
/// Store bindings for the Fastly runtime.
///
/// Store selection is a deploy-time input; no store selector reaches the
/// Fastly runtime. `EdgeZero` links each selected physical store to the
/// service version under its logical ID, so the runtime opens stores by
/// logical ID and reads the config entry under that same ID for every
/// publication target. Staging isolation comes from linking a different
/// physical store, never from a different key.
/// A deployment chooses each physical store, and `EdgeZero` links it to
/// the service version under its logical ID, so the runtime opens every
/// store by logical ID. The config key is the logical store ID until the
/// entry point puts the store's selector in its place. A staging version
/// is kept apart by the physical store it links.
pub(crate) fn logical() -> Self {
Self {
config_store_name: StoreName::from(DEFAULT_CONFIG_STORE_ID),
Expand Down Expand Up @@ -240,6 +245,12 @@ pub(crate) fn build_state(
pub(crate) fn load_settings_from_config_store(
stores: &RuntimeStoreConfig,
) -> Result<Settings, Report<TrustedServerError>> {
let entries = FastlyPolicyEntries::new(&FastlyPlatformConfigStore, &stores.config_store_name);
let policy_origin = if blob_carries_its_policy(&stores.config_key) {
PolicyOrigin::Blob
} else {
PolicyOrigin::Source(&entries)
};
// The settings are validated as they load, so the stock builders and the
// ones a deployment registered are supplied here as well as to the state
// build. Without them a `[demand]` or `[ad-server]` name one of them
Expand All @@ -250,11 +261,19 @@ pub(crate) fn load_settings_from_config_store(
&stores.config_store_name,
&stores.config_key,
&stores.secret_store_name,
PolicyOrigin::Blob,
policy_origin,
&trusted_server_modules::builders_with(registered_integrations()),
)
}

/// Whether the blob under `config_key` carries its policy file, which a blob
/// at the logical store ID does, as `EdgeZero`'s push writes it. The keyed
/// push writes every other key, and its blobs name a shared policy entry
/// instead.
fn blob_carries_its_policy(config_key: &str) -> bool {
config_key == DEFAULT_CONFIG_STORE_ID
}

/// Build the application state from explicit settings.
///
/// # Errors
Expand Down Expand Up @@ -1718,11 +1737,11 @@ mod tests {
use std::time::Duration;

use super::{
AppContext, AppState, AuctionDispatch, EcContext, EdgeCacheHeader, EidSyncSource,
HandlerFuture, NAMED_ROUTES, NamedRouteHandler, PAGE_BIDS_LEGACY_PATH, PAGE_BIDS_PATH,
RuntimeStoreConfig, TrustedServerApp, build_orchestrator_with_plan,
build_per_request_services, build_state_from_settings, handle_publisher_request,
publisher_response_into_streaming_response, startup_error_router,
AppContext, AppState, AuctionDispatch, DEFAULT_CONFIG_STORE_ID, EcContext, EdgeCacheHeader,
EidSyncSource, HandlerFuture, NAMED_ROUTES, NamedRouteHandler, PAGE_BIDS_LEGACY_PATH,
PAGE_BIDS_PATH, RuntimeStoreConfig, TrustedServerApp, blob_carries_its_policy,
build_orchestrator_with_plan, build_per_request_services, build_state_from_settings,
handle_publisher_request, publisher_response_into_streaming_response, startup_error_router,
};
use base64::Engine as _;
use bytes::Bytes;
Expand Down Expand Up @@ -1890,6 +1909,15 @@ mod tests {
TrustedServerApp::routes_for_state(&state)
}

#[test]
fn only_the_blob_at_the_logical_store_id_carries_its_policy_file() {
assert!(blob_carries_its_policy(DEFAULT_CONFIG_STORE_ID));
assert!(
!blob_carries_its_policy("a.publisher.example"),
"a blob the keyed push wrote for a host names a shared entry"
);
}

#[test]
fn each_request_is_given_the_policy_its_domain_loaded() {
let policy = trusted_server_core::test_support::policies::open();
Expand Down
Loading
Loading