Description
Implement two independent upstream primitives in edgezero-core + edgezero-macros required by the trusted-server → EdgeZero migration. Full design and a maintainer-review appendix (verified against origin/main @ 42843b1) live in the spec:
Spec: docs/superpowers/specs/2026-07-02-edgezero-state-and-nested-secrets-design.md
The two workstreams are independent and can land in either order or in parallel.
Workstream A — State<T> extractor
RouterBuilder::with_state<T> registers a Clone + Send + Sync + 'static value (typically Arc<AppState>) that is cloned into each request's extensions before dispatch.
- New
State<T> extractor in extractor.rs resolving T by type from request.extensions(); works inside #[action] with no macro change.
- Router plumbing:
state_inserters: Vec<Arc<dyn Fn(&mut crate::http::Extensions) + Send + Sync>> threaded builder → RouterService::new → RouterInner, applied in dispatch before RequestContext::new.
- Naming: ship
State<T> + with_state (optionally Extension<T> alias) — open question A-1.
Workstream B — nested/array #[secret] support
- Reshape
SecretField from { name: &'static str } to a path-qualified { path: &'static [SecretPathSegment] } (Field(name) / ArrayEach).
- Derive (
edgezero-macros) recurses into #[app_config(nested)] fields, prefixing child paths; relax the scalar rule to accept Option<String>.
- Runtime
secret_walk becomes a path navigator; leaf errors report the dotted path via EdgeError::config_out_of_date.
- CLI path-aware secret reflection in
run_adapter_typed_checks / typed_secret_checks.
Open questions to resolve before/while implementing (from spec §6 + review §8)
- B-1 Are there secrets inside arrays in trusted-server
Settings, or only nested objects? Gate ArrayEach implementation on a confirmed need.
- B-2 Recurse via explicit
#[app_config(nested)] opt-in (recommended) vs type heuristic (rejected).
- B-3
AppConfigMeta::SECRET_FIELDS const → fn secret_fields() -> Cow<'static, [SecretField]>. Review found this is effectively forced (cross-crate const concat of prefixed &'static slices is not expressible); touches every impl incl. ~10 test impls.
- A-1 / A-2 extractor name; optional
ctx.state::<T>() accessor.
Must-not-miss (from review §8)
- Add
validate_excluding_secrets (app_config.rs:204) to the consumer list — for nested secrets it needs nested-ValidationErrors navigation (reuse the first_violating_field walk pattern), not a .name→.path rename, or the push/runtime split breaks for nested fields.
- Use the
crate::http facade (not bare http::Extensions) in the router plumbing.
- The
lib.rs "re-export State" step is unnecessary — edgezero_core::extractor::State is reachable once pub.
- Nested
KeyInNamedStore "innermost-parent" store_ref scoping is new behavior with no existing fixture (app-demo has only KeyInDefault + StoreRef).
Acceptance criteria
Description
Implement two independent upstream primitives in
edgezero-core+edgezero-macrosrequired by the trusted-server → EdgeZero migration. Full design and a maintainer-review appendix (verified againstorigin/main@42843b1) live in the spec:Spec:
docs/superpowers/specs/2026-07-02-edgezero-state-and-nested-secrets-design.mdThe two workstreams are independent and can land in either order or in parallel.
Workstream A —
State<T>extractorRouterBuilder::with_state<T>registers aClone + Send + Sync + 'staticvalue (typicallyArc<AppState>) that is cloned into each request's extensions before dispatch.State<T>extractor inextractor.rsresolvingTby type fromrequest.extensions(); works inside#[action]with no macro change.state_inserters: Vec<Arc<dyn Fn(&mut crate::http::Extensions) + Send + Sync>>threaded builder →RouterService::new→RouterInner, applied indispatchbeforeRequestContext::new.State<T>+with_state(optionallyExtension<T>alias) — open question A-1.Workstream B — nested/array
#[secret]supportSecretFieldfrom{ name: &'static str }to a path-qualified{ path: &'static [SecretPathSegment] }(Field(name)/ArrayEach).edgezero-macros) recurses into#[app_config(nested)]fields, prefixing child paths; relax the scalar rule to acceptOption<String>.secret_walkbecomes a path navigator; leaf errors report the dotted path viaEdgeError::config_out_of_date.run_adapter_typed_checks/typed_secret_checks.Open questions to resolve before/while implementing (from spec §6 + review §8)
Settings, or only nested objects? GateArrayEachimplementation on a confirmed need.#[app_config(nested)]opt-in (recommended) vs type heuristic (rejected).AppConfigMeta::SECRET_FIELDSconst →fn secret_fields() -> Cow<'static, [SecretField]>. Review found this is effectively forced (cross-crate const concat of prefixed&'staticslices is not expressible); touches every impl incl. ~10 test impls.ctx.state::<T>()accessor.Must-not-miss (from review §8)
validate_excluding_secrets(app_config.rs:204) to the consumer list — for nested secrets it needs nested-ValidationErrorsnavigation (reuse thefirst_violating_fieldwalk pattern), not a.name→.pathrename, or the push/runtime split breaks for nested fields.crate::httpfacade (not barehttp::Extensions) in the router plumbing.lib.rs"re-exportState" step is unnecessary —edgezero_core::extractor::Stateis reachable oncepub.KeyInNamedStore"innermost-parent"store_refscoping is new behavior with no existing fixture (app-demo has onlyKeyInDefault+StoreRef).Acceptance criteria
app-demostill builds/serves on all four adapters; its top-level#[secret] api_tokenstill resolves.edgezero-cli config validate/push/diffwork over a config with a nested secret.