Skip to content

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

Closed

Description

@aram356

Status: implemented in #306 (stacked on #300)

Four independent upstream primitives for the trusted-server -> EdgeZero migration. Delivered in #306; base is the #300 branch and auto-retargets to main when #300 merges.

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/.

Workstream A -- State<T> extractor

  • State<T> extractor (FromRequest by type from request extensions; Deref/into_inner; 500 when unregistered); composes in #[action] with no macro change.
  • RouterBuilder::with_state<T> stores state in a single state_extensions: Extensions bag; dispatch extends it into every request. Shipped State<T> + with_state only (no Extension alias, no ctx.state() accessor).

Workstream B -- nested / array #[secret]

  • Owned path-qualified SecretField { kind, path: Vec<SecretPathSegment>, optional }; AppConfigMeta::secret_fields() -> Vec<SecretField> (const -> fn).
  • Derive recurses into #[app_config(nested)] object + Vec<_> fields; accepts Option<String>; path/rename guards; AppConfigRoot assertion.
  • Runtime secret_walk path navigator; nested/list-aware validate_excluding_secrets pruning; CLI path-aware TOML collector for validate/push/diff; inverted nested-AppConfig CI guard.

P0-C -- Fastly run_app dispatch fidelity

  • C1 multi-value Set-Cookie (response + proxy response/request); C2 platform-neutral Hooks::owns_logging() gated across all four adapters + app!(owns_logging = <bool>); C3 run_app_with_request_extensions, a generic pre-dispatch hook for raw-request signals.
  • Note: C3 is only the Fastly-side mechanism. A standard cross-adapter JA4 API (Expose JA4 through a standard request interface #283) is NOT delivered here and remains open.

P0-D -- app! app-state injection

  • app!(state = <expr>) makes the macro-generated router call .with_state(...), reusing A's dispatch injection. Macro-only; app-demo example + e2e test.

Open questions -- resolved during implementation

  • A-1 / A-2: ship State<T> + with_state only; no Extension alias, no ctx.state() accessor.
  • B-1 (arrays): implemented ArrayEach from day one (spec's own inventory has partners[*].api_key).
  • B-2 (recursion): explicit #[app_config(nested)] opt-in (type-heuristic rejected).
  • B-3 (const -> fn): AppConfigMeta::secret_fields() -> Vec<SecretField> -- cross-crate const concat was not expressible, so the fn form was taken; every impl (incl. test impls) flipped.
  • Router uses the crate::http::Extensions facade; no lib.rs re-export; validate_excluding_secrets got true nested/list-aware pruning; a purpose-built KeyInNamedStore fixture was added.

Acceptance criteria

  • All 5 CI gates green (fmt, clippy, workspace tests, feature check, spin wasm32) -- plus the nested-AppConfig audit, app-demo (own workspace) tests + clippy, and Fastly wasm32-wasip1 + Viceroy tests.
  • Unit + trybuild UI + integration tests per spec 3.4 / 4.7.
  • app-demo builds/serves on all four adapters; top-level #[secret] api_token still resolves.
  • edgezero-cli config validate/push/diff work over nested + array secrets.
  • Rustdoc + guide updates (docs/guide/handlers.md, docs/guide/configuration.md).

Activity

  1. added
    enhancementNew feature or request
    rustPull requests that update rust code
    and removed
    enhancementNew feature or request
    rustPull requests that update rust code
    on Jul 2, 2026
  2. linked a pull request that will close this issueadd State<T> + nested #[secret] design spec #305on Jul 2, 2026
  3. aram356 commented on Jul 2, 2026

    @aram356
    CollaboratorAuthor

    Second-pass review — additional blockers (verified against origin/main @ 42843b1)

    Folded into spec §8. Recommendation: split into two plans. Workstream A can start now; Workstream B waits on the items below.

    Workstream B — must fix before it is plan-ready

    • BLOCKER — invert the nested-AppConfig CI guard. crates/edgezero-cli/src/bin/check_no_nested_app_config.rs (run in CI as "Nested AppConfig audit", .github/workflows/test.yml:58) rejects any AppConfig struct nested inside another (through Option/Vec/Box/…). The #[app_config(nested)] design violates it today. Must become "nested AppConfig allowed iff field has #[app_config(nested)]" — changes the audit binary + its tests.
    • BLOCKER — optional-secret metadata. SecretField has only {kind, path}; add optional: bool so the walk/CLI can tell "required missing → error" from "optional absent → skip".
    • BLOCKER — commit to owned path segments. §4.2's &'static [SecretPathSegment] contradicts B-3's owned/Cow conclusion. Use secret_fields() -> Vec<SecretField> with owned path.
    • HIGH — register helper attribute: edgezero-macros/src/lib.rs:20 attributes(secret) → attributes(secret, app_config).
    • HIGH — TypedSecretEntry.field_name (edgezero-adapter/src/registry.rs:178) is borrowed &str; dotted/array paths are computed strings → make it owned/Cow and thread through the Spin collision check.
    • HIGH — nested-only rename_all: the guard at edgezero-macros/src/app_config.rs:75 only fires when direct #[secret] fields exist; a parent with only #[app_config(nested)] children escapes it. Extend to fire on any nested field.
    • HIGH — settle array scope now: examples include partners[*].api_key; don't ship object-only unless the trusted-server Settings audit confirms no array secret leaves — else include ArrayEach from the start.

    Workstream A — plan-ready

    No blockers. Router insertion before RequestContext::new (router.rs:267) and the generic #[action] FromRequest call (action.rs:87) fit as designed.

  4. changed the title [-]Implement State<T> extractor + nested/array #[secret] support (edgezero-core/macros)[/-] [+]Implement State<T> + nested/array #[secret] + Fastly dispatch fidelity (P0-C) + app! app-state (P0-D)[/+] on Jul 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions