diff --git a/.superpowers/sdd/progress.md b/.superpowers/sdd/progress.md new file mode 100644 index 00000000..232a3e5a --- /dev/null +++ b/.superpowers/sdd/progress.md @@ -0,0 +1,9 @@ +# Phase 1 (EdgeZero store-registry migration) — progress ledger + +Plan: docs/superpowers/plans/2026-07-02-edgezero-store-registry-migration.md +Pin: edgezero @ d8f71a4a (--locked). D6-a confirmed (keep composite write path). +Branch: worktree-edgezero-migration-spec + +## Tasks +- Task 1: in progress (inventory + D5/D6-a decision record) +- Task 1: COMPLETE — preflight green, D6-a confirmed, D5 map recorded (commit pending) diff --git a/Cargo.lock b/Cargo.lock index c7716783..836ee365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1397,7 +1397,7 @@ dependencies = [ [[package]] name = "edgezero-adapter" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?branch=main#42843b1c3934fab32f99cc76ddd5881f421cccc7" +source = "git+https://github.com/stackpop/edgezero?branch=worktree-state-nested-secrets-spec-review#d8f71a4a8e201abf685f40c7cbd4276614bc11ee" dependencies = [ "toml", ] @@ -1405,7 +1405,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-axum" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?branch=main#42843b1c3934fab32f99cc76ddd5881f421cccc7" +source = "git+https://github.com/stackpop/edgezero?branch=worktree-state-nested-secrets-spec-review#d8f71a4a8e201abf685f40c7cbd4276614bc11ee" dependencies = [ "anyhow", "async-trait", @@ -1433,7 +1433,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-cloudflare" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?branch=main#42843b1c3934fab32f99cc76ddd5881f421cccc7" +source = "git+https://github.com/stackpop/edgezero?branch=worktree-state-nested-secrets-spec-review#d8f71a4a8e201abf685f40c7cbd4276614bc11ee" dependencies = [ "anyhow", "async-trait", @@ -1456,7 +1456,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-fastly" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?branch=main#42843b1c3934fab32f99cc76ddd5881f421cccc7" +source = "git+https://github.com/stackpop/edgezero?branch=worktree-state-nested-secrets-spec-review#d8f71a4a8e201abf685f40c7cbd4276614bc11ee" dependencies = [ "anyhow", "async-stream", @@ -1485,7 +1485,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-spin" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?branch=main#42843b1c3934fab32f99cc76ddd5881f421cccc7" +source = "git+https://github.com/stackpop/edgezero?branch=worktree-state-nested-secrets-spec-review#d8f71a4a8e201abf685f40c7cbd4276614bc11ee" dependencies = [ "anyhow", "async-trait", @@ -1512,7 +1512,7 @@ dependencies = [ [[package]] name = "edgezero-cli" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?branch=main#42843b1c3934fab32f99cc76ddd5881f421cccc7" +source = "git+https://github.com/stackpop/edgezero?branch=worktree-state-nested-secrets-spec-review#d8f71a4a8e201abf685f40c7cbd4276614bc11ee" dependencies = [ "chrono", "clap", @@ -1537,7 +1537,7 @@ dependencies = [ [[package]] name = "edgezero-core" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?branch=main#42843b1c3934fab32f99cc76ddd5881f421cccc7" +source = "git+https://github.com/stackpop/edgezero?branch=worktree-state-nested-secrets-spec-review#d8f71a4a8e201abf685f40c7cbd4276614bc11ee" dependencies = [ "anyhow", "async-compression", @@ -1568,12 +1568,13 @@ dependencies = [ [[package]] name = "edgezero-macros" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?branch=main#42843b1c3934fab32f99cc76ddd5881f421cccc7" +source = "git+https://github.com/stackpop/edgezero?branch=worktree-state-nested-secrets-spec-review#d8f71a4a8e201abf685f40c7cbd4276614bc11ee" dependencies = [ "log", "proc-macro2", "quote", "serde", + "serde_json", "syn 2.0.118", "toml", "validator", @@ -3564,7 +3565,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.118", @@ -3648,7 +3649,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5907,15 +5908,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -5949,30 +5941,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5985,12 +5960,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -6003,12 +5972,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -6021,24 +5984,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -6051,12 +6002,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -6069,12 +6014,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -6087,12 +6026,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -6105,12 +6038,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.7.15" diff --git a/Cargo.toml b/Cargo.toml index 3d139333..2faeee12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,12 +43,12 @@ criterion = { version = "0.5", default-features = false, features = ["cargo_benc derive_more = { version = "2.0", features = ["display", "error"] } directories = "5" ed25519-dalek = { version = "2.2", features = ["rand_core"] } -edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", branch = "main", default-features = false } -edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero", branch = "main", default-features = false } -edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero", branch = "main", default-features = false } -edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero", branch = "main", default-features = false } -edgezero-cli = { git = "https://github.com/stackpop/edgezero", branch = "main" } -edgezero-core = { git = "https://github.com/stackpop/edgezero", branch = "main", default-features = false } +edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", branch = "worktree-state-nested-secrets-spec-review", default-features = false } +edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero", branch = "worktree-state-nested-secrets-spec-review", default-features = false } +edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero", branch = "worktree-state-nested-secrets-spec-review", default-features = false } +edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero", branch = "worktree-state-nested-secrets-spec-review", default-features = false } +edgezero-cli = { git = "https://github.com/stackpop/edgezero", branch = "worktree-state-nested-secrets-spec-review" } +edgezero-core = { git = "https://github.com/stackpop/edgezero", branch = "worktree-state-nested-secrets-spec-review", default-features = false } env_logger = "0.11" error-stack = "0.6" fastly = "0.12" diff --git a/crates/trusted-server-core/src/config.rs b/crates/trusted-server-core/src/config.rs index 4499db7e..208b7f99 100644 --- a/crates/trusted-server-core/src/config.rs +++ b/crates/trusted-server-core/src/config.rs @@ -92,9 +92,11 @@ impl edgezero_core::app_config::AppConfigMeta for TrustedServerAppConfig { // Phase 1 intentionally preserves the existing inline-settings model: // `ts config push` publishes the validated Trusted Server config as one // app-config blob. Migrating app-level secrets to `EdgeZero` secret-store - // references needs nested/array extraction support and operator migration - // work tracked separately. - const SECRET_FIELDS: &'static [edgezero_core::app_config::SecretField] = &[]; + // references (via `#[secret]` on `Settings`) is Phase 3; until then no + // fields are externalized, so the secret-field set is empty. + fn secret_fields() -> Vec { + Vec::new() + } } /// Runs Trusted Server deploy-time validation for pushed app config. diff --git a/docs/superpowers/plans/2026-07-02-edgezero-store-registry-migration.md b/docs/superpowers/plans/2026-07-02-edgezero-store-registry-migration.md new file mode 100644 index 00000000..ddaebe92 --- /dev/null +++ b/docs/superpowers/plans/2026-07-02-edgezero-store-registry-migration.md @@ -0,0 +1,613 @@ +# EdgeZero Store-Registry Migration (Phase 1, D6-a) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Route trusted-server's runtime **and boot-time** config/secret **reads** through EdgeZero stores/registries, add **named/non-default KV** selection (default KV is already EdgeZero; named KV / `consent_store` selection is **not** — see Step 2c), reconcile every logical store id (kv/config/secrets) with `edgezero.toml`, and delete the duplicated Fastly chunk resolver — while **keeping** the runtime **write** path (key rotation) intact via a composite store (decision **D6-a**). + +**Architecture:** trusted-server core reads/writes stores through the bespoke `PlatformConfigStore`/`PlatformSecretStore` traits (each mixes read `get`/`get_string` + write `put`/`create`/`delete`), surfaced via `RuntimeServices` (one trait object per kind). EdgeZero's `ConfigStore`/`SecretStore` are **read-only**; per-request `ConfigRegistry`/`SecretRegistry` live in request extensions. This phase introduces a **composite store** whose *reads* resolve from EdgeZero and whose *writes* delegate to the existing management-API-backed impl, migrates the Fastly/Axum **boot** config read to EdgeZero, and adds **local** registry builders for Fastly's custom `oneshot` dispatch (EdgeZero's builders are `pub(crate)`). + +**Tech Stack:** Rust (mixed edition — core/fastly 2021, axum 2024; follow each crate), toolchain 1.95.0, `error-stack` `Report`, EdgeZero (`edgezero-core`/`edgezero-adapter-fastly` git dep, run `--locked`), Viceroy, `cargo test-{fastly,axum,cloudflare,spin}`. + +**Spec:** `docs/superpowers/specs/2026-07-02-edgezero-full-migration-design.md` §5 Phase 1, D5, D6, §4a. + +## Pinned dependency (verified) + +This plan targets the EdgeZero commit pinned in `Cargo.lock`: **`branch = worktree-state-nested-secrets-spec-review` @ `d8f71a4a`** (PR [stackpop/edgezero#306](https://github.com/stackpop/edgezero/pull/306), now including the merged P0 State + nested/array `#[secret]` work). That commit **has** every API this plan uses — re-verified against the pinned checkout: `store_registry.rs` (`StoreRegistry`/`ConfigRegistry`/`SecretRegistry`/`from_parts`), `StoresMetadata` + `Hooks::stores()` (`app.rs`), `dispatch_with_registries` + `build_*_registry` (adapter-fastly), `AxumDevServer::{with_config,with_kv,with_secret}_registry`, the `[stores.*]` `ids`/`default` manifest schema (`manifest.rs`, `deny_unknown_fields`), CloudflareConfigStore backed by **KV namespaces**, AxumConfigStore backed by **`.edgezero/local-config-.json`**. **P0 caveat:** the nested-secret work reshaped `AppConfigMeta` from a `const SECRET_FIELDS` to `fn secret_fields() -> Vec`; `TrustedServerAppConfig`'s impl was updated to match (empty until Phase 3) and `cargo check` is green on core + all four adapters + CLI at this pin. Older cargo-cache checkouts (`6ebc29a5`, `ce6bcf7`, `7ec2ad1`, …) predate this pin — **confirm any API check against `d8f71a4a`**, not an older checkout dir. + +> **Lockfile guard (execution):** `Cargo.toml` uses a **mutable branch** dependency (`branch = "worktree-state-nested-secrets-spec-review"`) and the upstream branch has **already advanced past `d8f71a4a`**. Execute every task with the committed `Cargo.lock` — build/test **`--locked`** and **do not `cargo update` the edgezero crates** unless the EdgeZero API is re-reviewed and this pin note re-verified. A silent bump could pull an unreviewed commit mid-migration. + +## Global Constraints + +- **Mixed Rust edition — follow each crate's `Cargo.toml`** (not global 2024): `trusted-server-core` and `trusted-server-adapter-fastly` are **2021**; `trusted-server-adapter-axum` is **2024**. Toolchain **1.95.0**; WASM target `wasm32-wasip1`. +- **Run every cargo command `--locked`.** The `Run:` snippets in the tasks below **omit `--locked` for brevity** — the executor MUST append it to **every** cargo invocation (each command in an `&&` chain too): `cargo test-fastly --locked`, `cargo check-cloudflare --locked`, etc. The edgezero dep is a **mutable branch** whose upstream head has advanced past the pinned `d8f71a4a`; `--locked` prevents a silent bump to an unreviewed commit (see the pin note). *(Safest: `export CARGO_NET_OFFLINE=true` for the session after the preflight fetch, so no command can update the lock.)* +- **Preflight before Task 1:** materialize + verify the pinned edgezero object locally so nobody re-checks APIs against a stale cache checkout — `cargo fetch --locked` then `cargo check -p trusted-server-core --locked`, and confirm the resolved rev is `d8f71a4a` (`grep 'edgezero-core' Cargo.lock`). The checkout dir is `…/git/checkouts/edgezero-efe7ff47d5367787//` — but **do not assume it is materialized**: on a given machine the cache may only hold an *older* checkout (e.g. `7ec2ad1`), so `cargo fetch --locked` first, then verify the resolved rev is `d8f71a4a` before checking APIs against any checkout dir. +- Errors: `error-stack` `Report` only (no `anyhow` outside the Spin entry point); `derive_more::Display`; import `Error` from `core::error::`. +- No `unwrap()` in production (`expect("should …")`); no `println!`/`eprintln!` (use `log`). +- No wildcard imports (except `use super::*` in `#[cfg(test)]`); no imports inside functions. +- Commits: sentence case, imperative, no semantic prefixes, no `Co-Authored-By`/AI footers. +- CI gate before PR: `cargo fmt --all -- --check`; `cargo clippy-{fastly,axum,cloudflare,spin-native,spin-wasm}`; **`cargo check-cloudflare` + `cargo check-spin`** (wasm-target surfaces — `test-cloudflare`/`test-spin` are native and do **not** compile the wasm runtime paths); `cargo test-{fastly,axum,cloudflare,spin}`; `cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity`. +- **Every task leaves all four adapters building and green.** +- **EdgeZero `ConfigStore`/`SecretStore` are read-only.** Runtime writes stay on the management path (D6-a). +- **Registry lookup is strict:** an unknown logical id yields `None`. Every id any config field names — in **any** kind (kv/config/secrets) — must be declared in `edgezero.toml`. +- **This plan is D6-a-locked.** If Task 1 selects D6-b (move key rotation to ops/CLI) or D6-c (upstream write API), **stop after Task 1** and write a separate plan — those change the admin API surface and are out of Phase 1 scope. + +--- + +## Task 1: Kind-aware store inventory + confirm D6-a (decision gate, no deletions) + +Deliverable: a **decision record** appended to "Task 1 Output" that Tasks 2+ consume. No code is deleted here. + +**Files:** +- Modify (append record): this plan file. +- Read-only inventory: `crates/trusted-server-core/src/**`, `crates/trusted-server-adapter-*/src/**`, `edgezero.toml`, `trusted-server.example.toml`, `crates/trusted-server-integration-tests/fixtures/**`. + +**Interfaces:** +- Produces: the **kind-partitioned store-id map** (`{kv, config, secrets}` → each logical id → platform name → declared?) and the **write-site list**, consumed by Tasks 2, 3, 8. + +- [ ] **Step 1: Enumerate store ids by kind** + +Run: +```bash +cd /Users/ag/projects/iab/trusted-server/.claude/worktrees/edgezero-migration-spec +# KV ids (logical ids referenced by Settings — NOT Fastly-only platform stores) +rg -n 'ec_store|consent_store|creative_store' crates/trusted-server-core/src/settings.rs crates/trusted-server-core/src/consent_config.rs crates/trusted-server-core/src/auction_config_types.rs trusted-server.example.toml +# config ids (incl. DataDome IP-CIDR config store) +rg -n 'config_store_id|jwks|JWKS_CONFIG_STORE_NAME|"app_config"|config_store\s*=|datadome-ip-bypass|default_ip_cidr_source_store' crates/trusted-server-core trusted-server.example.toml +# secret ids +rg -n 'secret_store_id|secret_store\s*=|"secrets"|ts_secrets|signing_keys|SIGNING_SECRET_STORE_NAME' crates/trusted-server-core trusted-server.example.toml +rg -n '\[stores\.' edgezero.toml +``` +Expected (verified): **KV** ids = `ec.ec_store` (`ec_identity_store`, `settings.rs:452`), `consent.consent_store` (`consent_config.rs:80`), and `auction.creative_store` (`auction_config_types.rs:28`, default `"creative_store"`, **deprecated** — creatives are delivered inline); **config** ids = the app-config blob store (**store id `trusted_server_config`**, see D5 rule below), `request_signing.config_store_id`, the JWKS store (`JWKS_CONFIG_STORE_NAME`), and **DataDome's IP-CIDR config store** (`ProtectionIpCidrSourceConfig.config_store`, default `datadome-ip-bypass`, `protection_scope.rs:165`); **secret** ids = `secrets` (`request_signing.secret_store_id`), DataDome `ts_secrets`, the S3 secret store, `signing_keys` (`SIGNING_SECRET_STORE_NAME`) — versus `edgezero.toml` declaring only one id per kind. NOTE: `counter_store` (`RATE_COUNTER_NAME` in the Fastly `rate_limiter.rs`) and `opid_store` are **Fastly-only** platform stores, not `Settings` logical ids — out of scope for D5. `creative_store` **is** a `Settings` id: declare it in `[stores.kv]` (deprecated) so strict lookup can't fail, and flag it for removal in a later phase. + + **D5 app-config store-id/key decision (record in Task 1 Output):** the app-config blob → config **store id `trusted_server_config`**, blob **key `trusted_server_config`** (key == store id, D7-consistent — see below). This changes `settings_data.rs::DEFAULT_CONFIG_STORE_ID` from `"app_config"` to `"trusted_server_config"` (`settings_data.rs:11`) **and** `config_payload.rs::CONFIG_BLOB_KEY` from `"app_config"` to `"trusted_server_config"`. Rationale: `default_config_key()` falls back to the id when no `…__KEY` env is set, and D7 forbids that runtime env — so the key must equal the id, or `ts config push` would need `--key`. The `app_config` name is fully retired. + + **Request-signing store ids (do NOT point at app-config):** request signing reads use hard-coded `JWKS_CONFIG_STORE_NAME = "jwks_store"` (config) + `SIGNING_SECRET_STORE_NAME = "signing_keys"` (secret); writes use `request_signing.config_store_id`/`secret_store_id`. Today the example sets these to `"app_config"`/`"secrets"` — which sends **writes to a different store than reads**. Fix: set `request_signing.config_store_id = "jwks_store"` and `secret_store_id = "signing_keys"` in `trusted-server.example.toml` + fixtures, and declare `jwks_store` (config) + `signing_keys` (secret) as logical ids in `edgezero.toml`. (Under the composite, reads resolve `registry.named("jwks_store")`; writes go to the same store via the writer/management id.) + +- [ ] **Step 2: Enumerate runtime WRITE sites** + +Run: +```bash +rg -n '\.config_store\(\)\.(put|delete)|\.secret_store\(\)\.(create|delete)' crates/trusted-server-core +``` +Expected: only `KeyRotationManager` in `crates/trusted-server-core/src/request_signing/rotation.rs` (`store_private_key`, `store_public_jwk`, `deactivate_key`, `delete_key`). Confirm no other runtime writers. + +- [ ] **Step 3: Record the kind-partitioned D5 map** + +Append a table to "Task 1 Output": for each `{kv|config|secrets}` id → resolution (declare in `edgezero.toml`, or collapse onto the kind's default) → the concrete platform resource per adapter. **Under D7 there is no `EDGEZERO__STORES__*__NAME` mapping** — the logical id opens the same-named platform store, so the table records the *platform resource per adapter* (Fastly local/prod store, CF KV namespace / flat secret keys, Spin KV label / variable), **not** an env var. Spec default: app-config blob → config id `trusted_server_config` key `trusted_server_config` (key == id); JWKS → its own config id; `ec_identity_store` → kv id; collapse `secrets`→`trusted_server_secrets` where identical; declare DataDome/S3/signing as distinct secret ids. + +- [ ] **Step 4: Confirm D6-a (or STOP)** + +Confirm this phase keeps the write-capable composite (D6-a). Record it. **If the team instead chooses D6-b/c, stop here** and open a separate plan (`…-key-rotation-ops-migration.md`); do not proceed to Task 2. + +- [ ] **Step 5: Commit the record** + +```bash +git add docs/superpowers/plans/2026-07-02-edgezero-store-registry-migration.md +git commit -m "Record Phase 1 kind-aware store-id map and confirm D6-a" +``` + +--- + +## Task 2: Declare all store ids (kv/config/secrets) in `edgezero.toml` + reconcile fields/fixtures + +**Files (exact):** +- Modify: `edgezero.toml` (`[stores.kv]`/`[stores.config]`/`[stores.secrets]` `ids`) +- Modify: `crates/trusted-server-core/src/settings_data.rs` (`DEFAULT_CONFIG_STORE_ID`) +- Modify: `crates/trusted-server-core/src/config_payload.rs` (`CONFIG_BLOB_KEY` → `"trusted_server_config"`) +- Modify: `trusted-server.example.toml`, `crates/trusted-server-integration-tests/fixtures/configs/trusted-server.integration.toml` +- Create: `crates/trusted-server-core/src/testdata/all-store-refs.toml` +- **Modify (integration test surfaces that hard-code `app_config` — break under the rename):** `crates/trusted-server-integration-tests/src/bin/generate-viceroy-config.rs` (`[local_server.config_stores.app_config]` + `app_config = '''…'''` + the generator test asserting them → `trusted_server_config`), `crates/trusted-server-integration-tests/tests/common/config.rs` (`{"app_config": envelope}` → `{"trusted_server_config": …}`), `crates/trusted-server-integration-tests/tests/environments/axum.rs` (env `TRUSTED_SERVER_CONFIG_APP_CONFIG_APP_CONFIG` → the trusted_server_config-keyed name) +- Modify (platform manifests): `fastly.toml`, `crates/trusted-server-adapter-cloudflare/wrangler.toml`, `crates/trusted-server-adapter-cloudflare/cloudflare.toml` (reconcile/delete stale schema, Step 6), `crates/trusted-server-adapter-spin/spin.toml` (labels + drop stale `v_…` comment); **Create** `crates/trusted-server-adapter-spin/runtime-config.toml`. The **serve command** (`spin up … --runtime-config-file …`) lives in `edgezero.toml` (Spin serve command, ~L95) and `CLAUDE.md` (Spin smoke line, ~L87) — update both, not `spin.toml`. +- **Create** `docs/internal/store-provisioning.md` (operator runbook: Fastly mgmt-id==logical-id + store create/link, Cloudflare `wrangler secret put`, Spin runtime-config) (KV-store backends for the declared Spin labels) and **Modify** `crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml` (declare the new local Fastly stores for parity). (Axum uses `.edgezero/local-config-*.json` local dev files — document but do not commit machine-local state.) +- Modify: `crates/trusted-server-adapter-cloudflare/src/app.rs` (`settings_from_cloudflare_config_json` side-channel key `app_config` → `CONFIG_BLOB_KEY`) +- Create: `crates/trusted-server-core/src/stores.rs` (shared `stores_metadata()`); Modify: each `crates/trusted-server-adapter-{fastly,axum,cloudflare,spin}/src/app.rs` (`impl Hooks` → add `fn stores()`) +- Test: `crates/trusted-server-core/src/settings.rs` (`#[cfg(test)]`) + +**Interfaces:** +- Consumes: Task 1 map. +- Produces: `Settings::referenced_store_ids_by_kind() -> ReferencedStoreIds { kv: BTreeSet, config: BTreeSet, secrets: BTreeSet }`; an `edgezero.toml` whose per-kind `ids` are supersets. + +- [ ] **Step 1: Write the failing test (parameterized over multiple configs)** + +Cover the example config, the integration fixture, AND a purpose-built config that exercises every store-backed field (DataDome IP-CIDR sources, S3 auth, request-signing) so optional/targeted settings can't escape coverage. Add to `settings.rs` under `#[cfg(test)]`: +```rust +fn assert_all_ids_declared(config_toml: &str, label: &str) { + let settings = Settings::from_toml(config_toml).unwrap_or_else(|e| panic!("{label} should parse: {e}")); + let referenced = settings.referenced_store_ids_by_kind(); + let declared = declared_store_ids_by_kind_from_manifest(); // reads edgezero.toml + for (kind, ids) in [("kv", &referenced.kv), ("config", &referenced.config), ("secrets", &referenced.secrets)] { + for id in ids { + assert!( + declared.for_kind(kind).contains(id), + "[{label}] {kind} store id `{id}` referenced by Settings is not declared in edgezero.toml", + ); + } + } +} + +#[test] +fn every_referenced_store_id_is_declared_by_kind() { + assert_all_ids_declared(include_str!("../../../trusted-server.example.toml"), "example"); + assert_all_ids_declared( + include_str!("../../trusted-server-integration-tests/fixtures/configs/trusted-server.integration.toml"), + "integration-fixture", + ); + // Purpose-built config exercising DataDome IP-CIDR, S3, and request-signing store refs. + assert_all_ids_declared(include_str!("testdata/all-store-refs.toml"), "all-store-refs"); +} +``` +(Create `crates/trusted-server-core/src/testdata/all-store-refs.toml` populating every store-id field with a declared id. `settings.rs` lives in `src/`, so the `include_str!` path relative to it is `testdata/all-store-refs.toml` — **not** `../testdata/…`.) + +- [ ] **Step 2: Run to verify it fails** + +Run: `cargo test-fastly every_referenced_store_id_is_declared_by_kind` +Expected: FAIL — `ec_identity_store`/`consent_store`/`creative_store` (kv), `jwks_store`/`datadome-ip-bypass`/`trusted_server_config` (config), `signing_keys`/`ts_secrets` (secrets) referenced but not declared. + +- [ ] **Step 3: Implement `referenced_store_ids_by_kind()` + manifest helper** + +Add the `ReferencedStoreIds` struct + method returning **KV** ids (`ec.ec_store`, `consent.consent_store`, `auction.creative_store`), **config** ids (`request_signing.config_store_id`, the app-config store id, and **every `ProtectionIpCidrSourceConfig.config_store`** from DataDome scopes — default `datadome-ip-bypass`), **secret** ids (`request_signing.secret_store_id`, DataDome `ts_secrets`, S3). Do **not** include `counter_store`/`opid_store`. Add test-only `declared_store_ids_by_kind_from_manifest()` parsing `edgezero.toml`. + +Apply the **D5 renames**: set `settings_data.rs::DEFAULT_CONFIG_STORE_ID = "trusted_server_config"`; set `request_signing.config_store_id = "jwks_store"` and `secret_store_id = "signing_keys"` in `trusted-server.example.toml` + fixtures (they must match the read constants — **not** `app_config`/`secrets`). + +- [ ] **Step 4: Declare every id in `edgezero.toml`** — `[stores.kv]` = `trusted_server_kv`, `ec_identity_store`, `consent_store`, `creative_store`; `[stores.config]` = `trusted_server_config`, `jwks_store`, `datadome-ip-bypass`; `[stores.secrets]` = `trusted_server_secrets`, `signing_keys`, `ts_secrets`, `s3-auth`. (Names double as the platform store names under D7.) Also set `config_payload.rs::CONFIG_BLOB_KEY = "trusted_server_config"` (blob key == store id, per the D5 rule) so `ts config push`'s default key and the boot read agree with no env/`--key`. + +- [ ] **Step 5: Wire `Hooks::stores()` on all four adapters (Blocker — metadata is not wired today).** Each `impl Hooks for TrustedServerApp` currently overrides only `routes()`; the default `stores()` returns **empty** `StoresMetadata`, so no registries can be built from it. Add `fn stores() -> StoresMetadata` returning the `[stores.*]` metadata, generated once from `edgezero.toml`. Prefer a single shared fn in `trusted-server-core` (`pub fn stores_metadata() -> StoresMetadata`) that all four adapters return, so the ids live in one place. Verify against `edgezero_core::app::StoresMetadata`/`StoreMetadata` shape. + +- [ ] **Step 5b: Anti-drift test — `stores_metadata()` and every adapter's `Hooks::stores()` must equal `edgezero.toml`.** Registries are built from `TrustedServerApp::stores()`, **not** from the `edgezero.toml` that Step 1's test validates — so a stale/incomplete `stores_metadata()` would pass Step 1 while runtime registries silently miss ids. Add a test that parses `edgezero.toml`'s `[stores.*]` ids/default and asserts they equal `trusted_server_core::stores_metadata()` **and** each `::TrustedServerApp::stores()` (per kind, ids as sets + default). Put the core half in `trusted-server-core` and one assertion in each adapter's test module (so a future adapter that forgets to return `stores_metadata()` fails). + +- [ ] **Step 6: Declare the stores in every PLATFORM manifest (Blocker — local resources missing), per each adapter's real mapping.** D7 requires each logical id to be openable as a real platform store. The adapters map kinds to concrete resources differently — declare exactly: + - **Fastly** — `fastly.toml` holds **only local Viceroy resources** under `[local_server]`: add each id as `[[local_server.kv_stores.]]` / `[local_server.config_stores.]` / `[local_server.secret_stores.]`. The **production** Fastly KV/config/secret stores + their service links are **not** in `fastly.toml` — they are an **operator/provisioning step** (create via `fastly kv-store`/`config-store`/`secret-store` + link to the service, or the EdgeZero provision path). Document those commands in the operator runbook; do not try to express them in `fastly.toml`. + - **Cloudflare manifest `cloudflare.toml` — reconcile the STALE schema (Medium blocker).** `crates/trusted-server-adapter-cloudflare/cloudflare.toml` still uses the pre-rewrite manifest schema (`[stores.kv].name = …`, `[stores.kv.adapters.cloudflare].name = …`), which the pinned EdgeZero manifest parser (`manifest.rs`, `deny_unknown_fields`) **rejects** in favor of `[stores.*]` `ids`/`default`. Either migrate it to the `ids`/`default` schema (matching `edgezero.toml`) or **delete it if `edgezero.toml` is the single source** and nothing loads `cloudflare.toml`. Do not leave a stale-schema manifest that a tool/test could load. + - **Cloudflare** (`wrangler.toml`): EdgeZero backs **config stores by a KV namespace binding** (`config_store.rs`) — so each **config** id (`trusted_server_config`, `jwks_store`, `datadome-ip-bypass`) gets a `[[kv_namespaces]]` binding (as does each KV id). **Secrets use a FLAT namespace — `CloudflareSecretStore::get_bytes` ignores `store_name` and reads `env.secret(key)`.** So do **not** `wrangler secret put signing_keys` (that provisions the wrong name). Provision the concrete secret **keys the code reads**: the signing KIDs written by `KeyRotationManager`, the DataDome `server_side_key_secret_name`, and the S3 `access_key_id` / `secret_access_key` / optional session-token keys. Document the exact `wrangler secret put ` commands in the operator runbook; `store_name`/store-id is irrelevant on Cloudflare. + - **Spin** (`spin.toml` **and** `runtime-config.toml`): config **and** KV ids open **KV-store labels** (`request.rs:282`). `spin.toml:41` currently declares only `key_value_stores = ["default"]` — extend it to list **every** kv+config logical id label (`trusted_server_kv`, `consent_store`, `creative_store`, `trusted_server_config`, `jwks_store`, `datadome-ip-bypass`). Each declared label **also needs a backend** in `runtime-config.toml` (`[key_value_store.