Remove /runs/{run_id}/… API routes
Server and quire-ci are deployed together, so the token-only /run/…
routes are sufficient. Drops verify_bearer, the path-param get_bootstrap
variant, and all tests that targeted the old routes.

https://claude.ai/code/session_01XsLrdwUarxN9QHTfagwxJT
change
commit f0226c8155a5d037b0c39f90510b956dbf43f952
author Claude <noreply@anthropic.com>
date
parent 29f8edab
diff --git a/quire-server/src/quire/web/api.rs b/quire-server/src/quire/web/api.rs
index a75fe5d..1d5f69e 100644
--- a/quire-server/src/quire/web/api.rs
+++ b/quire-server/src/quire/web/api.rs
@@ -2,13 +2,9 @@
 //!
 //! These routes use per-run bearer-token auth (not the Remote-User
 //! header auth used by the web UI). Each token is minted when the run
-//! is created and stored in `runs.run_token`.
-//!
-//! Two route families are provided:
-//! - `/runs/{run_id}/…` — legacy; path carries the run UUID, bearer token is verified against it.
-//! - `/run/…` — token-only; no run ID in the path, the bearer token itself identifies the run.
+//! is created and stored in `runs.run_token`. The bearer token itself
+//! identifies the run — no run ID appears in the path.
 
-use std::collections::HashMap;
 use std::path::PathBuf;
 
 use serde::Deserialize;
@@ -27,30 +23,19 @@ use crate::Quire;
 
 /// Build the API router. Intended to be mounted at `/api` via `Router::nest`.
 ///
-/// - `/runs/{run_id}/…` — [`verify_bearer`] checks the bearer token against the run's stored
-///   token before any handler runs.
-/// - `/run/…` — [`verify_run_token`] looks the run up by the bearer token itself (no run ID
-///   in the path) and injects the resolved run ID as a request extension.
+/// All routes are under `/run/…`. [`verify_run_token`] looks the run up by the bearer
+/// token and injects the resolved run ID as a request extension before any handler runs.
 pub fn router(quire: Quire) -> axum::Router {
     let run_routes = axum::Router::new()
         .route("/bootstrap", axum::routing::get(get_bootstrap))
         .route("/secrets/{name}", axum::routing::get(get_secret))
-        .layer(axum::middleware::from_fn_with_state(
-            quire.clone(),
-            verify_bearer,
-        ));
-
-    let token_routes = axum::Router::new()
-        .route("/bootstrap", axum::routing::get(get_bootstrap_by_token))
-        .route("/secrets/{name}", axum::routing::get(get_secret))
         .layer(axum::middleware::from_fn_with_state(
             quire.clone(),
             verify_run_token,
         ));
 
     axum::Router::new()
-        .nest("/runs/{run_id}", run_routes)
-        .nest("/run", token_routes)
+        .nest("/run", run_routes)
         .with_state(quire)
 }
 
@@ -103,61 +88,7 @@ impl IntoResponse for ApiError {
     }
 }
 
-/// Middleware for `/runs/{run_id}/…`: verifies the `Authorization: Bearer <token>` header
-/// against `runs.run_token` in the DB. Returns 401 if the header is absent or the token
-/// doesn't match, 404 if the run doesn't exist.
-async fn verify_bearer(
-    State(quire): State<Quire>,
-    req: axum::extract::Request,
-    next: Next,
-) -> Result<Response, ApiError> {
-    let (mut parts, body) = req.into_parts();
-
-    let Some(TypedHeader(Authorization(bearer))) =
-        <TypedHeader<Authorization<Bearer>> as FromRequestParts<()>>::from_request_parts(
-            &mut parts,
-            &(),
-        )
-        .await
-        .ok()
-    else {
-        return Err(ApiError::Unauthorized);
-    };
-    let token = bearer.token().to_string();
-
-    let run_id = <AxumPath<HashMap<String, String>> as FromRequestParts<()>>::from_request_parts(
-        &mut parts,
-        &(),
-    )
-    .await
-    .ok()
-    .and_then(|mut p| p.0.remove("run_id"));
-
-    let req = axum::extract::Request::from_parts(parts, body);
-
-    let Some(run_id) = run_id else {
-        return Err(ApiError::NotFound);
-    };
-
-    tokio::task::spawn_blocking(move || -> Result<(), ApiError> {
-        let db = quire.db_pool().lock().expect("db mutex poisoned");
-        let stored: Option<String> = db.query_row(
-            "SELECT run_token FROM runs WHERE id = ?1",
-            rusqlite::params![run_id],
-            |row| row.get(0),
-        )?;
-        match stored {
-            Some(ref t) if t == &token => Ok(()),
-            _ => Err(ApiError::Unauthorized),
-        }
-    })
-    .await
-    .expect("blocking task panicked")?;
-
-    Ok(next.run(req).await)
-}
-
-/// Middleware for `/run/…`: looks up the run by the `Authorization: Bearer <token>` header
+/// Middleware that looks up the run by the `Authorization: Bearer <token>` header
 /// value against `runs.run_token`. Returns 401 if the header is absent or no run matches.
 /// On success, injects the resolved run ID as a request extension so handlers can use it.
 async fn verify_run_token(
@@ -200,9 +131,19 @@ async fn verify_run_token(
     Ok(next.run(req).await)
 }
 
-/// Fetch and activate bootstrap data for `run_id`. One-shot: transitions the run from
-/// `pending` → `active` and returns 410 on any subsequent call.
-async fn fetch_bootstrap(quire: Quire, run_id: String) -> Result<axum::Json<Bootstrap>, ApiError> {
+/// `GET /api/run/bootstrap`
+///
+/// Returns the bootstrap payload for a run. One-shot: the server marks
+/// bootstrap as fetched on the first successful read and returns 410 on
+/// any subsequent call. Auth is handled by [`verify_run_token`] middleware.
+///
+/// Returns 404 if the run does not have API bootstrap data (e.g. the run
+/// was created with filesystem transport and `store_bootstrap_data` was
+/// never called).
+async fn get_bootstrap(
+    State(quire): State<Quire>,
+    axum::Extension(run_id): axum::Extension<String>,
+) -> Result<axum::Json<Bootstrap>, ApiError> {
     let bootstrap =
         tokio::task::spawn_blocking(move || -> std::result::Result<Bootstrap, ApiError> {
             let db = crate::db::open(&quire.db_path())?;
@@ -257,39 +198,10 @@ async fn fetch_bootstrap(quire: Quire, run_id: String) -> Result<axum::Json<Boot
     Ok(axum::Json(bootstrap))
 }
 
-/// `GET /api/runs/:run_id/bootstrap`
-///
-/// Returns the bootstrap payload for a run. One-shot: the server marks
-/// bootstrap as fetched on the first successful read and returns 410 on
-/// any subsequent call. Auth is handled by [`verify_bearer`] middleware.
-///
-/// Returns 404 if the run does not have API bootstrap data (e.g. the run
-/// was created with filesystem transport and `store_bootstrap_data` was
-/// never called).
-async fn get_bootstrap(
-    State(quire): State<Quire>,
-    AxumPath(params): AxumPath<HashMap<String, String>>,
-) -> Result<axum::Json<Bootstrap>, ApiError> {
-    let run_id = params.get("run_id").cloned().ok_or(ApiError::NotFound)?;
-    fetch_bootstrap(quire, run_id).await
-}
-
-/// `GET /api/run/bootstrap`
-///
-/// Token-only variant of [`get_bootstrap`]: no run ID in the path; the bearer token
-/// itself identifies the run. Auth and run-ID injection are handled by
-/// [`verify_run_token`] middleware.
-async fn get_bootstrap_by_token(
-    State(quire): State<Quire>,
-    axum::Extension(run_id): axum::Extension<String>,
-) -> Result<axum::Json<Bootstrap>, ApiError> {
-    fetch_bootstrap(quire, run_id).await
-}
-
-/// `GET /api/runs/:run_id/secrets/:name`
+/// `GET /api/run/secrets/:name`
 ///
 /// Returns the plain-text value of a named secret from the global config.
-/// Auth is handled by [`verify_bearer`] middleware before this handler runs.
+/// Auth is handled by [`verify_run_token`] middleware before this handler runs.
 /// Returns 404 if the secret is not declared in config.
 #[derive(serde::Deserialize)]
 struct SecretPath {
@@ -365,57 +277,6 @@ mod tests {
         app.oneshot(req).await.unwrap()
     }
 
-    #[tokio::test]
-    async fn secret_returns_401_without_auth_header() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        env.runs()
-            .create(&TestEnv::meta(), Some(&transport))
-            .expect("create");
-        let url = format!("/runs/{}/secrets/my_secret", transport.session.run_id);
-
-        let resp = get(env.app(), &url, None).await;
-        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
-    }
-
-    #[tokio::test]
-    async fn secret_returns_401_for_wrong_token() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        env.runs()
-            .create(&TestEnv::meta(), Some(&transport))
-            .expect("create");
-        let url = format!("/runs/{}/secrets/my_secret", transport.session.run_id);
-
-        let resp = get(env.app(), &url, Some("wrongtoken")).await;
-        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
-    }
-
-    #[tokio::test]
-    async fn secret_returns_404_for_unknown_run() {
-        let env = TestEnv::new();
-        let resp = get(
-            env.app(),
-            "/runs/00000000-0000-0000-0000-000000000001/secrets/my_secret",
-            Some("token"),
-        )
-        .await;
-        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
-    }
-
-    #[tokio::test]
-    async fn secret_returns_404_for_unknown_secret_name() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        env.runs()
-            .create(&TestEnv::meta(), Some(&transport))
-            .expect("create");
-        let url = format!("/runs/{}/secrets/no_such_secret", transport.session.run_id);
-
-        let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
-        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
-    }
-
     async fn create_run_with_bootstrap(
         env: &TestEnv,
         transport: &crate::ci::Transport,
@@ -439,148 +300,6 @@ mod tests {
 
     #[tokio::test]
     async fn bootstrap_returns_401_without_auth() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        let run_id = create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
-        let url = format!("/runs/{run_id}/bootstrap");
-
-        let resp = get(env.app(), &url, None).await;
-        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
-    }
-
-    #[tokio::test]
-    async fn bootstrap_returns_404_for_unknown_run() {
-        let env = TestEnv::new();
-        let resp = get(
-            env.app(),
-            "/runs/00000000-0000-0000-0000-000000000001/bootstrap",
-            Some("token"),
-        )
-        .await;
-        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
-    }
-
-    #[tokio::test]
-    async fn bootstrap_returns_404_when_git_dir_not_stored() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        env.runs()
-            .create(&TestEnv::meta(), Some(&transport))
-            .expect("create run");
-        let url = format!("/runs/{}/bootstrap", transport.session.run_id);
-
-        let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
-        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
-    }
-
-    #[tokio::test]
-    async fn bootstrap_returns_payload_on_first_fetch() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        let run_id = create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
-        let url = format!("/runs/{run_id}/bootstrap");
-
-        let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
-        assert_eq!(resp.status(), StatusCode::OK);
-
-        use http_body_util::BodyExt;
-        let body = resp.into_body().collect().await.unwrap().to_bytes();
-        let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
-        assert_eq!(parsed["git_dir"], "/repos/test.git");
-        assert_eq!(parsed["meta"]["sha"], "abc1".repeat(10));
-        assert_eq!(parsed["meta"]["ref"], "refs/heads/main");
-        assert!(parsed["sentry_trace_id"].is_null());
-    }
-
-    #[tokio::test]
-    async fn bootstrap_returns_sentry_trace_id_when_stored() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        let run_id = create_run_with_bootstrap(
-            &env,
-            &transport,
-            "/repos/test.git",
-            Some("aaaabbbbccccdddd0000111122223333"),
-        )
-        .await;
-        let url = format!("/runs/{run_id}/bootstrap");
-
-        let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
-        assert_eq!(resp.status(), StatusCode::OK);
-
-        use http_body_util::BodyExt;
-        let body = resp.into_body().collect().await.unwrap().to_bytes();
-        let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
-        assert_eq!(
-            parsed["sentry_trace_id"],
-            "aaaabbbbccccdddd0000111122223333"
-        );
-    }
-
-    #[tokio::test]
-    async fn bootstrap_returns_410_on_second_fetch() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        let run_id = create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
-        let url = format!("/runs/{run_id}/bootstrap");
-        let token = &transport.session.run_token;
-
-        let first = get(env.app(), &url, Some(token)).await;
-        assert_eq!(first.status(), StatusCode::OK);
-
-        let second = get(env.app(), &url, Some(token)).await;
-        assert_eq!(second.status(), StatusCode::GONE);
-    }
-
-    #[tokio::test]
-    async fn bootstrap_transitions_run_to_active() {
-        let env = TestEnv::new();
-        let transport = new_transport(TransportMode::Api, 3000);
-        let run_id = create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
-        let url = format!("/runs/{run_id}/bootstrap");
-
-        get(env.app(), &url, Some(&transport.session.run_token)).await;
-
-        let db = crate::db::open(&env.quire.db_path()).expect("db open");
-        let (state, started_at_ms): (String, Option<i64>) = db
-            .query_row(
-                "SELECT state, started_at_ms FROM runs WHERE id = ?1",
-                rusqlite::params![run_id],
-                |row| Ok((row.get(0)?, row.get(1)?)),
-            )
-            .expect("query");
-        assert_eq!(state, "active");
-        assert!(started_at_ms.is_some());
-    }
-
-    #[tokio::test]
-    async fn secret_returns_plaintext_value() {
-        let env = TestEnv::new();
-        // Write config with a secret.
-        fs_err::write(
-            env.quire.config_path(),
-            r#"{:secrets {:my_token "hunter2"}}"#,
-        )
-        .expect("write config");
-        let transport = new_transport(TransportMode::Api, 3000);
-        env.runs()
-            .create(&TestEnv::meta(), Some(&transport))
-            .expect("create");
-        let url = format!("/runs/{}/secrets/my_token", transport.session.run_id);
-
-        let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
-        assert_eq!(resp.status(), StatusCode::OK);
-
-        use http_body_util::BodyExt;
-        let body = resp.into_body().collect().await.unwrap().to_bytes();
-        let parsed: serde_json::Value = serde_json::from_slice(&body).expect("json body");
-        assert_eq!(parsed["value"], "hunter2");
-    }
-
-    // Token-only routes (/run/…)
-
-    #[tokio::test]
-    async fn token_route_bootstrap_returns_401_without_auth() {
         let env = TestEnv::new();
         let transport = new_transport(TransportMode::Api, 3000);
         create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
@@ -590,7 +309,7 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn token_route_bootstrap_returns_401_for_unknown_token() {
+    async fn bootstrap_returns_401_for_unknown_token() {
         let env = TestEnv::new();
 
         let resp = get(env.app(), "/run/bootstrap", Some("nosuchtoken")).await;
@@ -598,7 +317,7 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn token_route_bootstrap_returns_payload_on_first_fetch() {
+    async fn bootstrap_returns_payload_on_first_fetch() {
         let env = TestEnv::new();
         let transport = new_transport(TransportMode::Api, 3000);
         create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
@@ -618,7 +337,7 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn token_route_bootstrap_returns_410_on_second_fetch() {
+    async fn bootstrap_returns_410_on_second_fetch() {
         let env = TestEnv::new();
         let transport = new_transport(TransportMode::Api, 3000);
         create_run_with_bootstrap(&env, &transport, "/repos/test.git", None).await;
@@ -632,7 +351,7 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn token_route_secret_returns_401_without_auth() {
+    async fn secret_returns_401_without_auth() {
         let env = TestEnv::new();
         let transport = new_transport(TransportMode::Api, 3000);
         env.runs()
@@ -644,7 +363,7 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn token_route_secret_returns_plaintext_value() {
+    async fn secret_returns_plaintext_value() {
         let env = TestEnv::new();
         fs_err::write(
             env.quire.config_path(),