Skip to content

Add ts config migrate for settings written for main - #1409

Open
jwrosewell wants to merge 30 commits into
feature/50-cookies-by-pathfrom
feature/51-config-migrate
Open

jwrosewell wants to merge 30 commits into
feature/50-cookies-by-pathfrom
feature/51-config-migrate

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-cli/src 8 files, 4706 lines added, 2 removed
crates/trusted-server-cli/tests 1 file, 680 lines added, 0 removed
docs/superpowers 1 file, 203 lines added, 0 removed
docs/guide 1 file, 79 lines added, 0 removed
CHANGELOG.md 1 file, 1 line added, 0 removed

Closes

Part of #777

Spec

Spec: config-migrate

Test plan

CI on this pull request at a71edd1 passed all 27 checks.

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

Checklist

  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

Stacks on cookies by path (#1400).

What this pull request does

The design is config-migrate. Its section 2.2 lists every breaking entry in the changelog that concerns the document and how the command handles it, and its section 6 gives the steps for moving a deployment.

ts config migrate --adapter <adapter> <file> names the host the document is pushed to, fastly, cloudflare, axum or spin, because the location lookup a document can select differs by host.

Rewritten without a question:

  • [integrations.<id>] becomes the module in the section for its type, with its settings at [<section>.<name>], so [integrations.prebid] becomes [auction.prebid] and [integrations.gpt] becomes [ad-tag.google]. A module that was not enabled is kept as a commented block under its new name.
  • [auction.providers.<id>] becomes [demand] modules and a [demand.<id>] table, with profile as implementation and the profile_config settings in the table, and a bidder route's provider becomes module.
  • The mediator and the mock ad server become [ad-server] module = "iabtl" and [ad-server.iabtl], and include_mediator_response becomes include_adserver_response.
  • The [publisher] origin settings become [source] module = "origin" and [source.origin], and each asset route's origin moves into its source table.
  • [tinybird] becomes [analytics] module = "tinybird" and [analytics.tinybird].
  • The Edge Cookie passphrase moves to [ec.hmac], the permission signal sources to [permission-signal] modules, and [device] module = "fastly" becomes "platform".
  • The [[fetch]] and [[serve]] entries a module's page change now needs are added for text/html, in the order trusted-server.example.toml shows, with the entries [robots-txt] and [ads-txt] need. Only a document written for main gains them, because a document already in the stack's shape loads without them.
  • For a document written for main, [geo] module = "platform" is added on Fastly and Cloudflare, where main ran the host's lookup. Axum and Spin have no lookup, so no geo module is added there.
  • No location header is selected on any adapter. A check: line says what main's responses carried on that adapter and, on Fastly, how to keep the x-geo-* headers.

Left for the operator, with the setting in place and the command exiting 1:

  • a [[handlers]] entry unless every path its pattern matches is one the stack closes, so a pattern with a quantifier, a class or a comment after ^/_ts/admin, or after the ^/admin/keys bound, stays
  • [consent.gdpr] and [consent.us_states], an unknown integration id, and settings that would clash after the move
  • on Axum and Spin, an Edge Cookie module with no geo module, which needs [geo] assume_single_jurisdiction = true or a geo module before the stack starts

Reported as check: lines, which leave the exit status alone: a host header override that moves, and on Cloudflare the region the lookup now reads, so US state rules can apply there.

Why

The stack's changelog lists 39 breaking entries under Unreleased, where main lists 10, and a settings document written for main does not load under the stack. The repository has no release and no tag, so no released version breaks, and with this command no operator rewrites a document by hand.

How it was verified

Locally on Windows, at b6a9c1e: the CLI's library suite passed, 706 tests with 9 ignored, 54 of them the command's, clippy on the CLI with warnings denied and cargo fmt --all -- --check passed, and Prettier passed on the changed Markdown. Each commit of the change passes clippy and the command's tests on its own. The golden test migrates main's own trusted-server.example.toml, copied from 5bc3886, and the result passes the stack's validation, and migrating it again changes nothing. The handler tests run every pattern through the regex crate main locks, on 16 request paths, against the paths core closes, so a removed handler guarded no path the stack forwards. Each new test fails with its change reverted. CI runs the CLI's full suite on Linux and macOS.

…nfig commands

validate_source_config parses a document as TrustedServerAppConfig and runs
the push-time validation, reporting only the byte offset where reading
failed or the phase that refused, because a refusal can quote a value.
ts config migrate checks the document it writes the same way, so the
function and its module are pub(crate). Nothing else changes.
…the shape the stack reads

The command reads one file, rewrites every shape the stack refuses or reads
differently, and prints the result on stdout. --write rewrites the file in
place, and --check changes nothing and exits 1 when the document needs
migrating. On stderr it prints a line for each change, for each item the
document cannot load without, which needs a decision or a value the
document does not hold, and for each change in behavior no setting
expresses, then whether the result passes the settings validation
ts config validate runs. No line prints a value from the document.

The rewrite uses toml_edit, so the comments, the order of the tables and
the formatting the operator wrote survive, a module that was not enabled is
commented out under its new name, and migrating a migrated document changes
nothing. The rules follow the table in
docs/superpowers/specs/2026-10-17-config-migrate-design.md.

The tests cover each rule and each item left to do, the order of the page
entries against trusted-server.example.toml, comments and table order,
Windows line endings, a document of marker values no line may print, and
the command's three modes. The golden test migrates main's own example,
copied into tests/fixtures with its commit recorded, and the result
validates under the stack's settings code and migrates again to itself.
The 43 migrate tests and the 7 audit validation tests pass on Windows, and
clippy with warnings denied and cargo fmt pass.
The command index in docs/guide/cli.md gains the command, and a section on
upgrading says to run it first, what it prints and exits with, what it
never does, and what to do after it. The changelog gains one line under
Added.
…w sections go

A profile_config setting named as one of its provider's own is left for
the operator, a module with no settings that was not enabled keeps only the
comments above its old table, and the sections that replace nothing follow
[publisher] and [ec], as the command does.
The check for the [integration] table ran only after [integrations] was
found, so a document with [integration] alone was not told it is not read.
It runs first now. An [auction] mediator left beside the mock ad server's
table, because [ad-server] is written too, is reported with that table and
no longer also as a mediator whose settings the document lacks.

New tests leave a profile_config setting named as one of its provider's
own, both forms of every setting the command refuses to guess between, and
[integration] alone, so each item the design lists as left to do has one.
The 46 migrate tests and the 7 audit validation tests pass on Windows, and
clippy with warnings denied and cargo fmt pass.
…h follow main

Three checks, about x-ts-version, Flight responses and the x-geo-*
headers, are printed for a document written for main, and the others with
the rewrite they follow from. A middleware name is among the values the
command renames.
A test parses the command with its file and --check, and refuses it with
both modes or with no file, which also runs clap's checks of the new
arguments. The 54 argument tests, the 46 migrate tests and the 7 audit
validation tests pass on Windows, and clippy with warnings denied and
cargo fmt pass.
@jwrosewell
jwrosewell added this pull request to stack #1408 October 17, 2026 09:20
…is closed

Main matched a handler's pattern anywhere in the path, so a handler on
^/admin/keys also guarded publisher paths such as /admin/keystore, which
this release forwards to the origin, and the rule removed it with the
password it put on them. A pattern that starts ^/admin/keys is now
removed only where /, $, (?:/|$) or (/|$) follows the prefix, because only
/admin/keys and the paths below /admin/keys/ are closed. Every path that
starts /_ts/admin is closed, so those patterns are removed as before. Any
other handler is left to do, and its line names the paths this release
closes.

The test a_handler_is_removed_only_where_every_path_it_matches_is_closed
removes the bounded forms and keeps ^/admin/keys, ^/admin/keys.* and the
other patterns that can match a path the stack forwards.
With host_header set, the page pipeline and every middleware take that
host as the page's origin host, so a page's addresses under it are
rewritten where main rewrote those under the host of origin_url. Moving
origin_host_header_override to [source.origin] host_header now prints a
check line that says so. The test a_host_header_override_is_reported_to_check
covers a document with the override and one without it.
Main ran every enabled integration's page changes on every page, so a
document written for main gets the [[fetch]] and [[serve]] entries that
keep its pages as they were. A document already in this release's shape
loads with no entry, with a warning naming each middleware no entry
names, so the rule no longer switches on page changes its operator left
off. The entries a module's file needs, on /ads.txt and /robots.txt, are
still added to any document, because the file's module refuses a document
without one.

The test a_document_in_the_stack_shape_with_no_page_entries_gets_none_added
selects a consent module in a document already in this release's shape.
The command takes --adapter, being fastly, cloudflare, axum or spin, as
ts config push and ts config diff do, because what main's responses
carried and what each adapter accepts differ by adapter.

A document written for main no longer gets [headers] modules = ["geo"].
headers.geo writes the reader's location in the x-geo-* headers, and main
wrote those on Fastly alone, with only x-geo-info-available on
Cloudflare, Axum and Spin, so selecting it started sending the reader's
location on Cloudflare, where main sent none. Whether pages carry the
reader's location is the operator's decision, so a check line now says
what main's responses carried on the adapter and, on Fastly, how to keep
the headers.

The test no_location_header_is_selected_for_the_operator runs on every
adapter, the test that no line carries a value runs on every adapter, and
the argument test needs --adapter and refuses an adapter the stack does
not have.
… one

The Axum dev server and Spin refuse [geo] module = "platform" at startup,
because neither host has a location lookup, and main's lookups there
located no request, so a document written for main gets no geo module on
those adapters. Its Edge Cookie module, made from main's passphrase, then
needs [geo] assume_single_jurisdiction = true or a geo module, which
startup checks and ts config validate does not, so that choice is left to
do. The command now reports, for any document and on every run until it
is fixed, an Edge Cookie module with no geo module and no
acknowledgment, and [geo] module = "platform" on an adapter whose host
has no lookup.

The test the_location_lookup_is_the_one_each_adapter_accepts runs core's
startup check of the acknowledgment on the result for each adapter, and
the_location_settings_startup_refuses_are_left_to_do_in_any_document
covers both items in documents already in this release's shape.
…w round 1

The design answers each finding of review round 1 and ends with a review
log. It gains sections on the adapters, consent and privacy, failure, and
compatibility and rollout, says what the command's validation leaves out
and which startup checks it reports as items left to do, defines a to do
item to cover a setting the stack ignores whose value the operator
carries elsewhere, gives the seam probe and the key-value store rows of
their own, and counts 39 breaking entries, as the changelog holds.

The CLI guide and the changelog entry name --adapter and the location
lookup each adapter gets, and say that no location header is selected.
The guide asks for a copy of the original before --write, and its steps
after migrating rename the overlay variables and keep the key-value store
on Cloudflare and Spin.
…or bound

Main matched a handler's pattern anywhere in the path, and a ?, * or {
straight after the ^/_ts/admin prefix or after the /admin/keys bound can
let the character or the bound before it match nothing. ^/admin/keys/?,
^/admin/keys/* and ^/admin/keys(?:/|$)? all matched /admin/keystore, and
^/_ts/admin? matched /_ts/admit, paths the stack forwards to the origin,
yet the command removed each such entry with a changed: line and no to
do: item. It now leaves such an entry to do. A + is still accepted,
because it matches what comes before it at least once.

The test a_handler_is_removed_only_where_every_path_it_matches_is_closed
runs each pattern through the regex crate main matched with and through
core's closed_path_response on a set of request paths, so each pattern
the command removes is shown to guard no path the stack forwards, and
each it keeps as unsafe is shown to guard one. It keeps the forms review
round 2 named, the other quantifier forms, and ^/admin/keys/?$, which
the command reads by its text and leaves to do although every path it
matches is closed.
…refix

The scan for a | outside a group ended a character class at the first ]
after a [, but the regex crate main matched with reads a ] at the start
of a class as a character, a [ inside a class as a nested class, and,
with the (?x) flag, a # as the start of a comment. A ( hidden that way
raised the scan's depth, so a later | looked as if it were inside a
group, and ^/_ts/admin[](]|^/members, which guarded /members on main,
was removed. The command now leaves to do any pattern with a [ or a #
after the prefix or the bound, and the scan reads only escapes and
groups.

The test a_handler_with_a_class_or_comment_after_its_prefix_is_left_to_do
shows through the regex crate that three such patterns guarded /members
on main, and that ^/_ts/admin/[a-z]+ is left to do although every path
it matches is closed. The test helper now writes each pattern as a TOML
string, so a pattern can hold a newline.
Main's Cloudflare lookup set no region, so its US state rules never
applied there. The stack's lookup reads the region from cf-region-code,
where Cloudflare sends it, so once a document written for main selects
[geo] module = "platform" on Cloudflare, a reader in a US state the
permissions.yaml rules list takes that state's us-state jurisdiction,
where the country gives non-regulated. The geo rewrite on Cloudflare now
prints a check line that says so.

The test the_region_the_cloudflare_lookup_now_reads_is_reported_to_check
runs on every adapter and finds the line on Cloudflare alone.
…ew round 2

The design answers each finding of review round 2 and adds its entry to
the review log, whose round 1 entry now names the public branch that
held that commit. It states the handler rule the code now follows, with
the quantifier, class and comment forms it leaves to do, names the
checks made only at startup, says the consent rules see the location
main's jurisdiction check saw, adds the region check line on Cloudflare,
says a document run on two adapters is migrated once for each, and no
longer calls this the top pull request of the stack.

The CLI guide adds the step on stored pages and stored assets being
fetched again, and says a configuration pushed to more than one adapter
is migrated once for each. The comment on the report's items left to do
gives the definition the design and the guide use.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@jwrosewell jwrosewell closed this Oct 18, 2026
@jwrosewell jwrosewell reopened this Oct 18, 2026
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell jwrosewell changed the title ts config migrate rewrites a settings document written for main Add ts config migrate for settings written for main Oct 19, 2026
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:24
@jwrosewell jwrosewell mentioned this pull request Oct 20, 2026
13 of 14 tasks

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant