Rename auth_token to run_token and add token-only API routes
Renames `ApiSession.auth_token` → `run_token` (and the `runs.auth_token`
DB column) throughout, updating the env var passed to quire-ci from
`QUIRE__AUTH_TOKEN` to `QUIRE__RUN_TOKEN`.
Adds new `/api/run/bootstrap` and `/api/run/secrets/{name}` endpoints
where the bearer token itself identifies the run — no `{run_id}` in the
path. The `verify_run_token` middleware looks up the run via
`SELECT id FROM runs WHERE run_token = ?1` and injects the resolved run
ID as a request extension. Updates `RunClient` in quire-ci to use the
new paths.
The original `/api/runs/{run_id}/…` routes are kept for now.
https://claude.ai/code/session_01XsLrdwUarxN9QHTfagwxJT
diff --git a/docs/config.md b/docs/config.md
index 24f923f..25e7a3d 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -94,7 +94,7 @@ file (mode `0600`) before `quire-ci` is spawned. The subprocess reads and
immediately unlinks the file.
**`"api"`:** `quire-ci` ignores the secrets map in the bootstrap file and
-fetches each value on demand from `GET /api/runs/{run_id}/secrets/{name}`
+fetches each value on demand from `GET /api/run/secrets/{name}`
using the per-run bearer token when a run-fn calls `(secret :name)`. The
bootstrap file is still written and used for run metadata; only the secret
values travel over the loopback HTTP channel instead of disk.
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index 9f8fbbf..9300965 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -49,7 +49,7 @@ struct Cli {
/// Transport credentials and telemetry settings for
/// orchestrator-dispatched runs, sourced from `QUIRE__*` env vars:
- /// `QUIRE__RUN_ID`, `QUIRE__SERVER_URL`, `QUIRE__AUTH_TOKEN`,
+ /// `QUIRE__RUN_ID`, `QUIRE__SERVER_URL`, `QUIRE__RUN_TOKEN`,
/// `QUIRE__TRANSPORT`, `QUIRE__SENTRY_DSN`.
#[facet(args::config, args::env_prefix = "QUIRE")]
quire: QuireConfig,
@@ -75,9 +75,9 @@ struct QuireConfig {
#[facet(default)]
server_url: String,
- /// Bearer token minted at run creation time (`QUIRE__AUTH_TOKEN`).
+ /// Bearer token minted at run creation time (`QUIRE__RUN_TOKEN`).
#[facet(sensitive, default)]
- auth_token: String,
+ run_token: String,
/// Transport mode (`QUIRE__TRANSPORT`).
#[facet(default)]
@@ -211,7 +211,7 @@ struct RunClient {
impl RunClient {
fn new(session: ApiSession) -> Self {
let mut headers = HeaderMap::new();
- let mut auth = HeaderValue::from_str(&format!("Bearer {}", session.auth_token))
+ let mut auth = HeaderValue::from_str(&format!("Bearer {}", session.run_token))
.expect("auth token contains only ASCII");
auth.set_sensitive(true);
headers.insert(AUTHORIZATION, auth);
@@ -220,7 +220,7 @@ impl RunClient {
.default_headers(headers)
.build()
.expect("failed to build HTTP client"),
- base_url: format!("{}/api/runs/{}", session.server_url, session.run_id),
+ base_url: format!("{}/api/run", session.server_url),
}
}
@@ -320,7 +320,7 @@ fn main() -> Result<()> {
let session = ApiSession {
run_id: cli.quire.run_id,
server_url: cli.quire.server_url,
- auth_token: cli.quire.auth_token,
+ run_token: cli.quire.run_token,
};
let transport = Transport {
session: session.clone(),
diff --git a/quire-core/src/ci/transport.rs b/quire-core/src/ci/transport.rs
index 416a0fc..4d9a0b1 100644
--- a/quire-core/src/ci/transport.rs
+++ b/quire-core/src/ci/transport.rs
@@ -17,8 +17,8 @@ pub struct ApiSession {
/// Base URL of quire-server (e.g. `http://127.0.0.1:3000`).
pub server_url: String,
/// Bearer token minted at run creation time. Matches
- /// `runs.auth_token` server-side.
- pub auth_token: String,
+ /// `runs.run_token` server-side.
+ pub run_token: String,
}
/// Transport mode for CI ↔ server communication.
diff --git a/quire-server/migrations/0005_rename_run_token.sql b/quire-server/migrations/0005_rename_run_token.sql
new file mode 100644
index 0000000..7d5608b
--- /dev/null
+++ b/quire-server/migrations/0005_rename_run_token.sql
@@ -0,0 +1 @@
+ALTER TABLE runs RENAME COLUMN auth_token TO run_token;
diff --git a/quire-server/src/ci/run.rs b/quire-server/src/ci/run.rs
index dbddcfd..d4c1c66 100644
--- a/quire-server/src/ci/run.rs
+++ b/quire-server/src/ci/run.rs
@@ -38,7 +38,7 @@ pub fn new_transport(mode: TransportMode, port: u16) -> Transport {
session: ApiSession {
run_id: uuid::Uuid::now_v7().to_string(),
server_url: format!("http://127.0.0.1:{port}"),
- auth_token: mint_auth_token(),
+ run_token: mint_run_token(),
},
mode,
}
@@ -118,12 +118,9 @@ impl Runs {
/// and the DB agree. Pass `None` for local runs; a fresh UUID is minted
/// and no auth token is stored.
pub fn create(&self, meta: &RunMeta, transport: Option<&Transport>) -> Result<Run> {
- let (id, auth_token_str) = match transport {
+ let (id, run_token_str) = match transport {
None => (uuid::Uuid::now_v7().to_string(), None),
- Some(t) => (
- t.session.run_id.clone(),
- Some(t.session.auth_token.as_str()),
- ),
+ Some(t) => (t.session.run_id.clone(), Some(t.session.run_token.as_str())),
};
let workspace_path = self.base_dir.join(&id).join("workspace");
@@ -135,7 +132,7 @@ impl Runs {
self.supersede_existing(&db, &meta.r#ref)?;
db.execute(
- "INSERT INTO runs (id, repo, ref_name, sha, pushed_at_ms, state, queued_at_ms, workspace_path, auth_token)
+ "INSERT INTO runs (id, repo, ref_name, sha, pushed_at_ms, state, queued_at_ms, workspace_path, run_token)
VALUES (?1, ?2, ?3, ?4, ?5, 'pending', ?6, ?7, ?8)",
rusqlite::params![
&id,
@@ -148,7 +145,7 @@ impl Runs {
std::io::ErrorKind::InvalidData,
"workspace path is not valid UTF-8",
))?,
- auth_token_str,
+ run_token_str,
],
)?;
@@ -345,7 +342,7 @@ impl Run {
Some(t) => {
cmd.env("QUIRE__RUN_ID", &t.session.run_id);
cmd.env("QUIRE__SERVER_URL", &t.session.server_url);
- cmd.env("QUIRE__AUTH_TOKEN", &t.session.auth_token);
+ cmd.env("QUIRE__RUN_TOKEN", &t.session.run_token);
if t.mode == TransportMode::Api {
self.store_bootstrap_data(git_dir, sentry_trace_id)?;
cmd.env("QUIRE__TRANSPORT", "api");
@@ -717,9 +714,9 @@ pub fn materialize_workspace(git_dir: &Path, sha: &str, workspace: &Path) -> Res
/// Mint a 32-character alphanumeric bearer token from the OS CSPRNG.
///
/// ~190 bits of entropy, opaque to the holder. Used as the per-run
-/// auth secret for the API transport; stored in the `runs.auth_token`
-/// column and passed to quire-ci via `QUIRE_CI_TOKEN`.
-fn mint_auth_token() -> String {
+/// auth secret for the API transport; stored in the `runs.run_token`
+/// column and passed to quire-ci via `QUIRE__RUN_TOKEN`.
+fn mint_run_token() -> String {
rand::rng()
.sample_iter(&Alphanumeric)
.take(32)
@@ -907,7 +904,7 @@ mod tests {
}
#[test]
- fn create_with_filesystem_persists_auth_token() {
+ fn create_with_filesystem_persists_run_token() {
let (_dir, quire) = tmp_quire();
let runs = test_runs(&quire);
let transport = new_transport(TransportMode::Filesystem, 3000);
@@ -918,20 +915,20 @@ mod tests {
let conn = crate::db::open(&quire.db_path()).expect("db");
let stored: Option<String> = conn
.query_row(
- "SELECT auth_token FROM runs WHERE id = ?1",
+ "SELECT run_token FROM runs WHERE id = ?1",
rusqlite::params![run.id()],
|row| row.get(0),
)
.expect("row");
assert_eq!(
stored.as_deref(),
- Some(transport.session.auth_token.as_str()),
- "filesystem transport should persist its minted auth token"
+ Some(transport.session.run_token.as_str()),
+ "filesystem transport should persist its minted run token"
);
}
#[test]
- fn create_with_api_persists_minted_auth_token() {
+ fn create_with_api_persists_minted_run_token() {
let (_dir, quire) = tmp_quire();
let runs = test_runs(&quire);
let transport = new_transport(TransportMode::Api, 3000);
@@ -942,14 +939,14 @@ mod tests {
let conn = crate::db::open(&quire.db_path()).expect("db");
let stored: Option<String> = conn
.query_row(
- "SELECT auth_token FROM runs WHERE id = ?1",
+ "SELECT run_token FROM runs WHERE id = ?1",
rusqlite::params![run.id()],
|row| row.get(0),
)
.expect("row");
assert_eq!(
stored.as_deref(),
- Some(transport.session.auth_token.as_str())
+ Some(transport.session.run_token.as_str())
);
}
@@ -966,15 +963,15 @@ mod tests {
),
] {
assert_eq!(transport.session.server_url, expected_url);
- assert_eq!(transport.session.auth_token.len(), 32);
+ assert_eq!(transport.session.run_token.len(), 32);
assert!(
transport
.session
- .auth_token
+ .run_token
.chars()
.all(|c| c.is_ascii_alphanumeric()),
"token should be alphanumeric, got {:?}",
- transport.session.auth_token
+ transport.session.run_token
);
assert!(
uuid::Uuid::parse_str(&transport.session.run_id).is_ok(),
@@ -985,9 +982,9 @@ mod tests {
}
#[test]
- fn mint_auth_token_returns_unique_values() {
- let a = mint_auth_token();
- let b = mint_auth_token();
+ fn mint_run_token_returns_unique_values() {
+ let a = mint_run_token();
+ let b = mint_run_token();
assert_ne!(a, b, "two mints should not collide");
}
diff --git a/quire-server/src/db.rs b/quire-server/src/db.rs
index 8bc3f9f..e699abc 100644
--- a/quire-server/src/db.rs
+++ b/quire-server/src/db.rs
@@ -16,6 +16,7 @@ static MIGRATIONS: std::sync::LazyLock<Migrations<'static>> = std::sync::LazyLoc
M::up(include_str!("../migrations/0002_sh_events.sql")),
M::up(include_str!("../migrations/0003_ci_api.sql")),
M::up(include_str!("../migrations/0004_bootstrap_api.sql")),
+ M::up(include_str!("../migrations/0005_rename_run_token.sql")),
])
});
diff --git a/quire-server/src/quire/web/api.rs b/quire-server/src/quire/web/api.rs
index a1da2f0..a75fe5d 100644
--- a/quire-server/src/quire/web/api.rs
+++ b/quire-server/src/quire/web/api.rs
@@ -2,7 +2,11 @@
//!
//! 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 scoped to that run's ID.
+//! 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.
use std::collections::HashMap;
use std::path::PathBuf;
@@ -21,11 +25,12 @@ use quire_core::ci::run::RunMeta;
use crate::Quire;
-/// Build the API router. Routes under `/runs/{run_id}` are wrapped in
-/// [`verify_bearer`] middleware which authenticates the bearer token against
-/// the run's stored token before any handler runs.
+/// Build the API router. Intended to be mounted at `/api` via `Router::nest`.
///
-/// 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.
pub fn router(quire: Quire) -> axum::Router {
let run_routes = axum::Router::new()
.route("/bootstrap", axum::routing::get(get_bootstrap))
@@ -35,8 +40,17 @@ pub fn router(quire: Quire) -> axum::Router {
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)
.with_state(quire)
}
@@ -89,10 +103,9 @@ impl IntoResponse for ApiError {
}
}
-/// Middleware that authenticates requests under `/runs/{run_id}` by verifying
-/// the `Authorization: Bearer <token>` header against `runs.auth_token` in the
-/// DB. Returns 401 if the header is absent or the token doesn't match, 404 if
-/// the run doesn't exist.
+/// 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,
@@ -129,7 +142,7 @@ async fn verify_bearer(
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 auth_token FROM runs WHERE id = ?1",
+ "SELECT run_token FROM runs WHERE id = ?1",
rusqlite::params![run_id],
|row| row.get(0),
)?;
@@ -144,21 +157,52 @@ async fn verify_bearer(
Ok(next.run(req).await)
}
-/// `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(
+/// Middleware for `/run/…`: 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(
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)?;
+ 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 = tokio::task::spawn_blocking(move || -> Result<String, ApiError> {
+ let db = quire.db_pool().lock().expect("db mutex poisoned");
+ let result: rusqlite::Result<String> = db.query_row(
+ "SELECT id FROM runs WHERE run_token = ?1",
+ rusqlite::params![token],
+ |row| row.get(0),
+ );
+ match result {
+ Ok(id) => Ok(id),
+ Err(rusqlite::Error::QueryReturnedNoRows) => Err(ApiError::Unauthorized),
+ Err(e) => Err(ApiError::Db(e)),
+ }
+ })
+ .await
+ .expect("blocking task panicked")?;
+
+ let mut req = axum::extract::Request::from_parts(parts, body);
+ req.extensions_mut().insert(run_id);
+ 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> {
let bootstrap =
tokio::task::spawn_blocking(move || -> std::result::Result<Bootstrap, ApiError> {
let db = crate::db::open(&quire.db_path())?;
@@ -213,6 +257,35 @@ async fn get_bootstrap(
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`
///
/// Returns the plain-text value of a named secret from the global config.
@@ -339,7 +412,7 @@ mod tests {
.expect("create");
let url = format!("/runs/{}/secrets/no_such_secret", transport.session.run_id);
- let resp = get(env.app(), &url, Some(&transport.session.auth_token)).await;
+ let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
@@ -396,7 +469,7 @@ mod tests {
.expect("create run");
let url = format!("/runs/{}/bootstrap", transport.session.run_id);
- let resp = get(env.app(), &url, Some(&transport.session.auth_token)).await;
+ let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
@@ -407,7 +480,7 @@ mod tests {
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.auth_token)).await;
+ let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
assert_eq!(resp.status(), StatusCode::OK);
use http_body_util::BodyExt;
@@ -432,7 +505,7 @@ mod tests {
.await;
let url = format!("/runs/{run_id}/bootstrap");
- let resp = get(env.app(), &url, Some(&transport.session.auth_token)).await;
+ let resp = get(env.app(), &url, Some(&transport.session.run_token)).await;
assert_eq!(resp.status(), StatusCode::OK);
use http_body_util::BodyExt;
@@ -450,7 +523,7 @@ mod tests {
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.auth_token;
+ let token = &transport.session.run_token;
let first = get(env.app(), &url, Some(token)).await;
assert_eq!(first.status(), StatusCode::OK);
@@ -466,7 +539,7 @@ mod tests {
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.auth_token)).await;
+ 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
@@ -495,7 +568,100 @@ mod tests {
.expect("create");
let url = format!("/runs/{}/secrets/my_token", transport.session.run_id);
- let resp = get(env.app(), &url, Some(&transport.session.auth_token)).await;
+ 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;
+
+ let resp = get(env.app(), "/run/bootstrap", None).await;
+ assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
+ }
+
+ #[tokio::test]
+ async fn token_route_bootstrap_returns_401_for_unknown_token() {
+ let env = TestEnv::new();
+
+ let resp = get(env.app(), "/run/bootstrap", Some("nosuchtoken")).await;
+ assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
+ }
+
+ #[tokio::test]
+ async fn token_route_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;
+
+ let resp = get(
+ env.app(),
+ "/run/bootstrap",
+ 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");
+ }
+
+ #[tokio::test]
+ async fn token_route_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;
+ let token = &transport.session.run_token;
+
+ let first = get(env.app(), "/run/bootstrap", Some(token)).await;
+ assert_eq!(first.status(), StatusCode::OK);
+
+ let second = get(env.app(), "/run/bootstrap", Some(token)).await;
+ assert_eq!(second.status(), StatusCode::GONE);
+ }
+
+ #[tokio::test]
+ async fn token_route_secret_returns_401_without_auth() {
+ let env = TestEnv::new();
+ let transport = new_transport(TransportMode::Api, 3000);
+ env.runs()
+ .create(&TestEnv::meta(), Some(&transport))
+ .expect("create");
+
+ let resp = get(env.app(), "/run/secrets/my_secret", None).await;
+ assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
+ }
+
+ #[tokio::test]
+ async fn token_route_secret_returns_plaintext_value() {
+ let env = TestEnv::new();
+ 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 resp = get(
+ env.app(),
+ "/run/secrets/my_token",
+ Some(&transport.session.run_token),
+ )
+ .await;
assert_eq!(resp.status(), StatusCode::OK);
use http_body_util::BodyExt;