Skip to content

State<T> + nested/array #[secret] + Fastly dispatch fidelity (P0-C) + app! app-state (P0-D)#306

Draft
aram356 wants to merge 44 commits into
worktree-feature+introspection-routesfrom
worktree-state-nested-secrets-spec-review
Draft

State<T> + nested/array #[secret] + Fastly dispatch fidelity (P0-C) + app! app-state (P0-D)#306
aram356 wants to merge 44 commits into
worktree-feature+introspection-routesfrom
worktree-state-nested-secrets-spec-review

Conversation

@aram356

@aram356 aram356 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Four independent upstream primitives for the trusted-server -> EdgeZero migration, stacked on #300 (introspection routes; base worktree-feature+introspection-routes, auto-retargets to main when #300 merges).

Closes #304. Base depends on #300.

Note on #283 (JA4): C3 below provides the Fastly raw-request hook a JA4 interface could build on, but this PR does not deliver #283 (a standard cross-adapter JA4 API). There is no neutral JA4 type, no get_tls_ja4() wiring, and it's Fastly-only. #283 remains open as follow-up.

Specs: docs/superpowers/specs/2026-07-02-edgezero-state-and-nested-secrets-design.md (A+B), docs/superpowers/specs/2026-07-03-edgezero-p0cd-fastly-dispatch-and-appstate-design.md (P0-C/P0-D). Task-by-task plans under docs/superpowers/plans/ (all boxes checked).

Workstream A -- State<T> extractor

  • State<T> extractor (FromRequest by type from request extensions; Deref/DerefMut/into_inner; 500 when unregistered).
  • RouterBuilder::with_state<T> stores app state in a single Extensions bag; dispatch does request.extensions_mut().extend(state.clone()) after the introspection injects (last-write-wins by TypeId). Zero macro change -- #[action] composes State<T> via the generic FromRequest path.
  • Docs: docs/guide/handlers.md "Sharing app state".

Workstream B -- nested / array #[secret]

  • Owned, path-qualified SecretField { kind, path: Vec<SecretPathSegment>, optional }; AppConfigMeta::secret_fields() -> Vec<SecretField> (was const SECRET_FIELDS); dotted_path().
  • Derive: #[app_config(nested)] opt-in recurses into sub-struct / Vec<_> fields; accepts Option<String>; rejects Option<String> on #[secret(store_ref)]; empty/bare #[app_config] hard-errors; AppConfigRoot bound assertion; serde rename/rename_all guards extended along the path.
  • Runtime secret_walk is a path navigator (nested objects, arrays per-element, optional absent/null skip, KeyInNamedStore sibling resolved in the innermost parent, dotted [n] errors).
  • Push: nested/list-aware validate_excluding_secrets pruning.
  • CLI: path-aware TOML collector powers config validate/push/diff; TypedSecretEntry carries a dotted label.
  • CI: the nested-AppConfig audit guard is inverted -- nesting allowed iff the field opts in via #[app_config(nested)].
  • Docs: docs/guide/configuration.md "Nested and array secrets".

P0-C -- Fastly run_app dispatch fidelity

  • C1 multi-value response headers: set_header->append_header in from_core_response and the proxy response/request conversion (origin Set-Cookie no longer collapses).
  • C2 Hooks::owns_logging() -- platform-neutral opt-out gated in all four adapters' run_app; app!(owns_logging = <bool>) macro argument.
  • C3 run_app_with_request_extensions -- a generic pre-dispatch hook: an app closure reads raw-fastly::Request signals (e.g. JA4 / H2 / client IP -- the app supplies the closure and its own type) into a scratch Extensions before conversion, merged into the core request; visible to middleware and the State/extractor layer.

P0-D -- app! app-state injection

  • app!(state = <expr>) makes the macro-generated build_router() call .with_state(<expr>), reusing Workstream A's dispatch injection. Macro-only -- no adapter or Hooks change.
  • app-demo gains a state = crate::app_state() + State<Arc<DemoState>> handler with an end-to-end test.

Tests / CI gates (all green locally)

cargo fmt, cargo clippy --workspace --all-targets --all-features -D warnings, cargo test --workspace --all-targets, feature check (fastly cloudflare spin), spin wasm32-wasip2, the nested-AppConfig audit, app-demo (own workspace) tests + clippy, and the Fastly adapter's wasm32-wasip1 + Viceroy tests (C1/C3).

Notes

  • The Fastly adapter is wasm-only -- its tests run via wasm32-wasip1 + Viceroy from the crate dir, not host cargo test.
  • No workspace-wide lint allows added; the only new #[expect] are documented dead_code on #[derive(AppConfig)] test fixtures.

aram356 added 7 commits July 2, 2026 15:32
Design spec for two upstream edgezero-core/edgezero-macros primitives:
- State<T> extractor + RouterBuilder::with_state for app-owned shared state
- nested/array #[secret] support via path-qualified SecretField metadata

Filed under docs/superpowers/specs/. Includes a maintainer-review appendix
(§8) verifying every current-mechanics claim against origin/main @ 42843b1
and folding in the corrections found: http-facade use in the router plumbing,
the inaccurate lib.rs re-export step, the omitted validate_excluding_secrets
consumer (needs nested-ValidationErrors navigation, not a rename), the
per-struct guard-enforcement rewording, and B-3 being forced to the
secret_fields() fn lowering.
Adds the blockers a follow-up review found (verified against origin/main
@ 42843b1) and a Go/No-Go split:
- nested-AppConfig CI guard (check_no_nested_app_config.rs) must be inverted
- optional-secret metadata (optional: bool) is missing from SecretField
- path model must commit to owned segments (Vec/Cow), not &'static
- register the app_config helper attribute in the derive
- TypedSecretEntry.field_name must be owned for dotted/array paths
- enforce container rename_all on nested-only parents
- settle array scope before implementing
Workstream A is plan-ready now; B waits on the above.
@aram356 aram356 self-assigned this Jul 3, 2026
@aram356 aram356 marked this pull request as draft July 3, 2026 06:08
aram356 added a commit to IABTechLab/trusted-server that referenced this pull request Jul 3, 2026
Switch the six edgezero git deps from branch main to
worktree-state-nested-secrets-spec-review (stackpop/edgezero#306, stacked on
#300) to pick up the Phase 0 State<T> extractor work. cargo check-axum passes.
aram356 added 8 commits July 2, 2026 23:30
…erters

Replaces state_inserters: Vec<Arc<dyn Fn(&mut Extensions)>> with a single
state_extensions: Extensions on RouterBuilder/RouterInner. with_state inserts
by type; dispatch does extensions.extend(state_extensions.clone()). Drops the
StateInserter alias, one closure alloc per registered state, and one vtable
call per state per request. Identical behavior: Clone+Send+Sync+'static bound,
last-write-wins by TypeId.
@aram356 aram356 changed the title State<T> extractor + with_state (stacked on #300) State<T> extractor + nested/array #[secret] support (stacked on #300) Jul 3, 2026
aram356 added 8 commits July 3, 2026 09:58
…T_FIELDS naming

An empty #[app_config()] (or bare #[app_config]) previously returned Ok(false)
from nested_optin, silently NOT recursing the field and dropping the child's
#[secret] metadata. Now errors, matching the documented contract; adds an
app_config_empty trybuild fixture. Also renames stale SECRET_FIELDS references
in comments + the rename_all diagnostic to secret_fields(), and broadens that
diagnostic to mention #[app_config(nested)] children.
… path-aware secret check

Push: build_config_envelope_preserves_nested_and_array_secret_names asserts
nested + array secret key names survive verbatim into envelope.data.
Diff: diff_typed_rejects_empty_nested_secret proves the path-aware
typed_secret_checks catches an empty nested secret (naming integrations.
server_side_key) before the remote-read step.
…intainer-review revised

Adds the P0-C (Fastly run_app dispatch fidelity: Set-Cookie multi-value,
owns_logging opt-out, raw-request pre-dispatch hook) + P0-D (app! state=
injection) design, revised across three review rounds against 47a112c:
P0-D reduced to a macro-only change reusing the router Extensions bag;
C3 Extensions path + before-conversion scratch-bag ordering; C1 proxy
response fidelity; C2 cross-adapter owns_logging + missing_trait_methods
emission; full app! argument grammar.
aram356 added 13 commits July 4, 2026 20:41
…ts, crate-root app-demo state, real route TOML)
@aram356 aram356 changed the title State<T> extractor + nested/array #[secret] support (stacked on #300) State<T> + nested/array #[secret] + Fastly dispatch fidelity (P0-C) + app! app-state (P0-D) Jul 5, 2026
aram356 added 6 commits July 5, 2026 13:07
…sions; model cached app_state (OnceLock)

Finding 1: app!(state = <expr>) runs the state expression each time build_router()
is called (per request on Fastly), so the demo's app_state() now caches via
OnceLock and the guide warns to build heavy state once + hand out Arc clones.
Finding 2: handlers.md gains the app!(state = ...) macro path; fastly.md documents
run_app_with_request_extensions (JA4/H2/client-IP hook) and owns_logging opt-out.
…tate

- handlers.md: app! allows only one state=<expr> (macro rejects duplicates);
  use an aggregate app-state struct for multiple values. Fix stray '>' typo.
- fastly.md: get_tls_ja4() returns Option<&str>; Ja4 wraps String -> .to_owned().
- p0d plan: model the cached OnceLock<Arc<_>> app_state (was fresh Arc per call).
… model

- cli-reference.md / cli-walkthrough.md: bundled `edgezero config push` errors
  (no raw push); typed push writes ONE BlobEnvelope with every field verbatim,
  including #[secret] key NAMES — nothing is flattened or stripped. Correct the
  per-adapter mechanics to one (key, envelope_json) entry and fix the stale
  Fastly command (config-store-entry update --upsert --stdin).
- blob-app-config-migration.md: C::SECRET_FIELDS -> C::secret_fields().
- fastly.md: client IP is carried via FastlyRequestContext (only JA4/H2 need the
  raw hook); fix import path to context::FastlyRequestContext.
- axum.md: the local file is { "<store_id>": "<BlobEnvelope JSON>" }, not a
  flat per-leaf map; secret key names are preserved (never stripped/resolved).
  Show the AppConfig<C> extractor as the reader; drop the flat store.get example.
- cli-walkthrough.md: Spin secret note + env-overlay example — push preserves
  secret key NAMES and writes the nested blob, it does not strip secrets.
- axum config_store.rs: module + method doc comments describe the envelope
  entry (outer map still string->string; typed push writes one envelope string).
- spin.md / manifest-store-migration.md: use <your-cli> config push (bundled
  edgezero config push errors by design; typed push runs from the app CLI).
- axum.md + axum config_store.rs: the outer blob key is the *selected* config
  key — defaults to the logical store id, overridable with --key.
- axum config_store.rs: add generated_at to the BlobEnvelope example (required
  field), matching the fuller shape in the public Axum docs.
… selector

- cli-reference.md: add the shipped `config diff` command (typed-only; --format
  unified/json/structured, --exit-code, --key/--store/--local/--no-env/etc.), and
  document the config push flags that were missing: --key, --no-diff, --yes/-y.
- manifest-store-migration.md: EDGEZERO__STORES__CONFIG__<ID>__KEY is a concrete
  runtime blob selector (staging/canary), not free-form tuning — dedicated table
  row + note; disambiguate the free-form row's metavariable to <SUFFIX>.
- configuration.md: mention the __KEY selector next to __NAME so operators find it.
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