AkurAI Build
Menu

AkurAI-Build

public

Latest change 01b73029cc9f18f675bad7fced75ac3c3fc10ded - Render README images by serving raw blobs and rewriting relative links by Ólafur Búi Ólafsson

use std::{
    io::Write,
    net::SocketAddr,
    path::{Path as FsPath, PathBuf},
    process::{Command, Stdio},
    sync::Arc,
    time::Duration,
};

use anyhow::{Context, Result, ensure};
use axum::{
    Json, Router,
    body::{Body, Bytes, to_bytes},
    extract::{DefaultBodyLimit, Path, Query, State},
    http::{HeaderMap, HeaderName, HeaderValue, Request, StatusCode, Uri, header},
    middleware::{self, Next},
    response::{Html, IntoResponse, Redirect, Response},
    routing::{any, get, post},
};
use hmac::{Hmac, Mac};
use minijinja::{AutoEscape, Environment, context};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::Sha256;
use subtle::ConstantTimeEq;
use tokio::io::AsyncWriteExt;
use tokio_util::io::ReaderStream;
use tower::limit::GlobalConcurrencyLimitLayer;
use tower_http::{compression::CompressionLayer, timeout::TimeoutLayer, trace::TraceLayer};
use tracing::{info, warn};

use crate::oidc;
use crate::{
    config,
    db::{Database, Session},
    git_process,
    runner::Runner,
    tree::RepoTree,
};

const MAX_BODY: usize = 1024 * 1024;
const MAX_GIT_BODY: usize = 512 * 1024 * 1024;
const MAX_CONNECTIONS: usize = 1024;
const MAX_BLOCKING_GIT: usize = 4;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
const LANDING: &str = include_str!("../app/pages/landing.html");
const DOCS: &str = include_str!("../app/pages/docs/index.html");
const DASHBOARD: &str = include_str!("../app/pages/index.html");
const REPOSITORIES: &str = include_str!("../app/pages/repositories.html");
const REPOSITORY_APP: &str = include_str!("../app/pages/repository-app.html");
const RUNS: &str = include_str!("../app/pages/runs.html");
const WORKERS: &str = include_str!("../app/pages/workers.html");
const CODE: &str = include_str!("../app/pages/repository-code.html");
const RUN: &str = include_str!("../app/pages/run.html");
const BASE: &str = include_str!("../app/templates/layouts/base.html");
const DOCS_LAYOUT: &str = include_str!("../app/templates/layouts/docs.html");
const DOCS_NAV: &str = include_str!("../app/templates/partials/docs-nav.html");
const PUBLIC_WORDMARK: &str = include_str!("../app/templates/partials/public-wordmark.html");
const PUBLIC_HEADER: &str = include_str!("../app/templates/partials/public-header.html");
const PUBLIC_FOOTER: &str = include_str!("../app/templates/partials/public-footer.html");
const CODE_CONTENT: &str = include_str!("../app/templates/partials/code-content.html");
const APP_ACCOUNT: &str = include_str!("../app/templates/partials/app-account.html");
const NOT_FOUND: &str = include_str!("../app/pages/_404.html");
const CSS: &str = include_str!("../public/app.css");
const JS: &str = include_str!("../public/app.js");

type HmacSha256 = Hmac<Sha256>;

#[derive(Clone)]
pub struct ServerOptions {
    pub address: SocketAddr,
    pub token: String,
    pub worker_count: usize,
    pub webhook_secret: String,
    pub public_url: String,
    pub data_root: PathBuf,
    pub allow_native: bool,
}

#[derive(Clone)]
struct AppState {
    database: Database,
    runner: Runner,
    tree: RepoTree,
    hosted_root: PathBuf,
    blocking_git: BlockingGit,
    token: Arc<str>,
    webhook_secret: Arc<str>,
    public_url: Arc<str>,
    templates: Arc<Environment<'static>>,
}

#[derive(Serialize)]
struct Envelope<T: Serialize> {
    ok: bool,
    data: T,
}

#[derive(Serialize)]
struct ErrorEnvelope {
    ok: bool,
    error: ErrorMessage,
}

#[derive(Serialize)]
struct ErrorMessage {
    code: &'static str,
    message: String,
    /// Where an unauthenticated browser or client should go to sign in.
    #[serde(skip_serializing_if = "Option::is_none")]
    login: Option<&'static str>,
}

struct ApiError {
    status: StatusCode,
    code: &'static str,
    message: String,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct TriggerRequest {
    git_ref: Option<String>,
    commit: Option<String>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepositoryRequest {
    name: String,
    url: String,
    branch: String,
}

#[derive(Serialize)]
struct RunProgress {
    run_id: i64,
    repository: String,
    git_ref: String,
    commit_sha: Option<String>,
    status: String,
    stage: String,
    progress: u8,
    jobs: Vec<ProgressJob>,
    deployments: Vec<ProgressDeployment>,
    error: Option<String>,
}

#[derive(Serialize)]
struct ProgressJob {
    id: i64,
    name: String,
    status: String,
    environment: Option<String>,
    approval_required: bool,
}

#[derive(Serialize)]
struct ProgressDeployment {
    id: i64,
    environment: String,
    status: String,
}

#[derive(Deserialize, Default)]
struct StateQuery {
    repo: Option<String>,
    status: Option<String>,
    git_ref: Option<String>,
    trigger: Option<String>,
    q: Option<String>,
    before: Option<i64>,
    limit: Option<usize>,
    offset: Option<usize>,
}

#[derive(Deserialize, Default)]
struct DeliveryMetricsQuery {
    repo: Option<String>,
    environment: Option<String>,
    window: Option<i64>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct VisibilityRequest {
    visibility: String,
}

#[derive(Serialize)]
struct RepositoryView {
    id: i64,
    name: String,
    url: String,
    default_branch: String,
    visibility: String,
    created_at: i64,
}

fn repository_client_url(
    repository_name: &str,
    repository_url: &str,
    data_root: &FsPath,
    public_url: &str,
) -> String {
    let repository_path = FsPath::new(repository_url);
    let hosted_root = data_root.join("hosted");
    if repository_path.starts_with(&hosted_root)
        && repository_path
            .parent()
            .and_then(FsPath::file_name)
            .is_some_and(|name| name == "hosted")
        && repository_path
            .extension()
            .is_some_and(|extension| extension == "git")
    {
        return format!(
            "{}/git/{repository_name}.git",
            public_url.trim_end_matches('/')
        );
    }
    repository_url.to_owned()
}

fn repository_view(
    repository: crate::db::Repository,
    data_root: &FsPath,
    public_url: &str,
) -> RepositoryView {
    let url = repository_client_url(&repository.name, &repository.url, data_root, public_url);
    RepositoryView {
        id: repository.id,
        name: repository.name,
        url,
        default_branch: repository.default_branch,
        visibility: repository.visibility,
        created_at: repository.created_at,
    }
}

#[derive(Serialize)]
struct StateResponse {
    repositories: Vec<RepositoryView>,
    runs: Vec<crate::db::Run>,
    workers: Vec<crate::db::Worker>,
    queued_runs: i64,
    run_cursor: Option<i64>,
    run_total: i64,
}

#[derive(Serialize)]
struct CodeEntryView {
    name: String,
    kind: String,
    size: u64,
    commit_message: Option<String>,
    href: String,
}

#[derive(Serialize)]
struct CodeBreadcrumb {
    label: String,
    href: String,
}

#[derive(Serialize)]
struct CommitView {
    sha: String,
    short_sha: String,
    message: String,
    author: String,
    date: String,
    href: String,
}

#[derive(Serialize)]
struct ContributorView {
    name: String,
    initial: String,
    commit_count: u64,
}

#[derive(Serialize)]
struct LanguageView {
    name: String,
    color: String,
    percentage: f64,
    percentage_label: String,
    offset: f64,
}

#[derive(Serialize)]
struct CodePage {
    repository: RepositoryView,
    reference: String,
    path: String,
    entries: Vec<CodeEntryView>,
    breadcrumbs: Vec<CodeBreadcrumb>,
    source: Option<String>,
    is_file: bool,
    parent_href: String,
    latest_commit: Option<crate::tree::LatestCommit>,
    commit_href: Option<String>,
    public: bool,
    description: Option<String>,
    counts: Option<crate::tree::RepoCounts>,
    readme_html: Option<String>,
    readme_name: Option<String>,
    recent_commits: Vec<CommitView>,
    contributors: Vec<ContributorView>,
    languages: Vec<LanguageView>,
}

#[derive(Serialize)]
struct IdResponse {
    id: i64,
}

#[derive(Serialize)]
struct LandingRepository {
    name: String,
    default_branch: String,
    owner: String,
    run_count: i64,
    successful_runs: i64,
}

#[derive(Serialize)]
struct PublicBuilder {
    name: String,
    initial: String,
    repository_count: usize,
}

#[derive(Deserialize)]
struct CallbackQuery {
    code: Option<String>,
    state: Option<String>,
    error: Option<String>,
}

#[derive(Deserialize)]
struct LoginQuery {
    return_to: Option<String>,
}

impl ApiError {
    fn unauthorized() -> Self {
        Self {
            status: StatusCode::UNAUTHORIZED,
            code: "unauthenticated",
            message: "sign in with AkurAI ID or present a valid bearer token".into(),
        }
    }
    fn bad(error: impl std::fmt::Display) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            code: "invalid_request",
            message: error.to_string(),
        }
    }
    fn not_found(message: impl Into<String>) -> Self {
        Self {
            status: StatusCode::NOT_FOUND,
            code: "not_found",
            message: message.into(),
        }
    }
    fn internal(error: impl std::fmt::Display) -> Self {
        tracing::error!(error = %error, "request failed");
        Self {
            status: StatusCode::INTERNAL_SERVER_ERROR,
            code: "internal",
            message: "request failed".into(),
        }
    }
}

#[derive(Clone)]
struct BlockingGit {
    permits: Arc<tokio::sync::Semaphore>,
}

impl BlockingGit {
    fn new(limit: usize) -> Self {
        Self {
            permits: Arc::new(tokio::sync::Semaphore::new(limit)),
        }
    }

    async fn run<T, F>(&self, operation: F) -> Result<T, ApiError>
    where
        T: Send + 'static,
        F: FnOnce() -> Result<T, ApiError> + Send + 'static,
    {
        let permit = self
            .permits
            .clone()
            .acquire_owned()
            .await
            .map_err(ApiError::internal)?;
        tokio::task::spawn_blocking(move || {
            let _permit = permit;
            operation()
        })
        .await
        .map_err(ApiError::internal)?
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        (
            self.status,
            Json(ErrorEnvelope {
                ok: false,
                error: ErrorMessage {
                    login: (self.status == StatusCode::UNAUTHORIZED).then_some(oidc::LOGIN_PATH),
                    code: self.code,
                    message: self.message,
                },
            }),
        )
            .into_response()
    }
}

fn render_markdown(source: &str, urls: Option<&ReadmeUrls>) -> String {
    let mut options = pulldown_cmark::Options::empty();
    options.insert(pulldown_cmark::Options::ENABLE_TABLES);
    options.insert(pulldown_cmark::Options::ENABLE_STRIKETHROUGH);
    options.insert(pulldown_cmark::Options::ENABLE_TASKLISTS);
    options.insert(pulldown_cmark::Options::ENABLE_FOOTNOTES);
    let parser = pulldown_cmark::Parser::new_ext(source, options);
    let mut html = String::with_capacity(source.len() * 2);
    pulldown_cmark::html::push_html(&mut html, parser);
    ammonia_clean(&html, urls)
}

/// Conservative allowlist sanitizer for rendered README HTML. Keeps the
/// output safe to inject with `|safe` without pulling in a full sanitizer
/// dependency: every tag not on the allowlist is escaped, attributes are
/// dropped except href/src on a/img with http(s), relative, or anchor URLs.
fn ammonia_clean(html: &str, urls: Option<&ReadmeUrls>) -> String {
    const ALLOWED: &[&str] = &[
        "a",
        "abbr",
        "b",
        "blockquote",
        "br",
        "code",
        "dd",
        "del",
        "details",
        "div",
        "dl",
        "dt",
        "em",
        "h1",
        "h2",
        "h3",
        "h4",
        "h5",
        "h6",
        "hr",
        "i",
        "img",
        "input",
        "kbd",
        "li",
        "ol",
        "p",
        "pre",
        "s",
        "small",
        "span",
        "strong",
        "sub",
        "summary",
        "sup",
        "table",
        "tbody",
        "td",
        "tfoot",
        "th",
        "thead",
        "tr",
        "ul",
    ];
    let mut output = String::with_capacity(html.len());
    let mut rest = html;
    while let Some(start) = rest.find('<') {
        output.push_str(&rest[..start]);
        let tail = &rest[start..];
        let Some(end) = tail.find('>') else {
            output.push_str("&lt;");
            rest = &tail[1..];
            continue;
        };
        let tag_body = &tail[1..end];
        let closing = tag_body.starts_with('/');
        let name_part = tag_body.trim_start_matches('/');
        let name: String = name_part
            .chars()
            .take_while(|character| character.is_ascii_alphanumeric())
            .collect::<String>()
            .to_ascii_lowercase();
        if ALLOWED.contains(&name.as_str()) {
            if closing {
                output.push_str(&format!("</{name}>"));
            } else {
                let self_closing = tag_body.trim_end().ends_with('/');
                let mut kept = String::new();
                for attribute in [
                    "href", "src", "alt", "title", "align", "checked", "disabled", "type", "open",
                    "start",
                ] {
                    if let Some(value) = extract_attribute(tag_body, attribute) {
                        if value.contains('"') {
                            continue;
                        }
                        let emit = if matches!(attribute, "href" | "src") {
                            match classify_url(&value) {
                                UrlKind::Keep => Some(value),
                                UrlKind::Drop => None,
                                UrlKind::Relative => match urls {
                                    Some(base) => base.resolve(attribute, &value),
                                    None => Some(value),
                                },
                            }
                        } else {
                            Some(value)
                        };
                        if let Some(value) = emit {
                            kept.push_str(&format!(" {attribute}=\"{value}\""));
                        }
                    }
                }
                output.push_str(&format!(
                    "<{name}{kept}{}>",
                    if self_closing { " /" } else { "" }
                ));
            }
        } else {
            output.push_str("&lt;");
            output.push_str(&html_escape(tag_body));
            output.push_str("&gt;");
        }
        rest = &tail[end + 1..];
    }
    output.push_str(rest);
    output
}

/// Base URLs for resolving relative links inside a rendered README. Relative
/// `img`/media `src` resolve to the raw-blob endpoint (so images load), while
/// relative `a` `href` resolve to the code browser (so links open the file
/// page, not raw bytes). Both resolve against the README's own directory.
struct ReadmeUrls {
    /// Directory containing the README, repo-relative (may be empty for root).
    dir: String,
    /// Encoded raw-blob prefix, e.g. `/repos/<repo>/raw/<ref>`.
    raw_prefix: String,
    /// Encoded code-browser prefix, e.g. `/repos/<repo>/tree/<ref>`.
    tree_prefix: String,
}

impl ReadmeUrls {
    /// Rewrite a relative URL to an absolute app URL, or drop it (`None`) when
    /// it escapes the repository root.
    fn resolve(&self, attribute: &str, value: &str) -> Option<String> {
        let (path_part, suffix) = split_url_suffix(value);
        let resolved = resolve_relative(&self.dir, path_part)?;
        let encoded = resolved
            .split('/')
            .filter(|segment| !segment.is_empty())
            .map(encode_code_segment)
            .collect::<Vec<_>>()
            .join("/");
        let prefix = if attribute == "src" {
            &self.raw_prefix
        } else {
            &self.tree_prefix
        };
        Some(format!("{prefix}/{encoded}{suffix}"))
    }
}

enum UrlKind {
    /// Absolute, root-relative, or in-page anchor: keep verbatim.
    Keep,
    /// Protocol-relative or a foreign scheme: drop.
    Drop,
    /// Repository-relative: rewrite against the README location.
    Relative,
}

fn classify_url(value: &str) -> UrlKind {
    if value.starts_with("https://") || value.starts_with("http://") || value.starts_with('#') {
        UrlKind::Keep
    } else if value.starts_with("//") {
        UrlKind::Drop
    } else if value.starts_with('/') {
        UrlKind::Keep
    } else if value.contains(':') {
        UrlKind::Drop
    } else {
        UrlKind::Relative
    }
}

/// Split a relative URL into its path and a trailing `?query`/`#fragment`.
fn split_url_suffix(value: &str) -> (&str, &str) {
    match value.find(['?', '#']) {
        Some(index) => value.split_at(index),
        None => (value, ""),
    }
}

/// Resolve a repository-relative reference against `dir`, normalising `.` and
/// `..`. Returns `None` if the path escapes the repository root or is empty.
fn resolve_relative(dir: &str, rel: &str) -> Option<String> {
    let mut segments: Vec<&str> = dir.split('/').filter(|s| !s.is_empty()).collect();
    for part in rel.split('/') {
        match part {
            "" | "." => {}
            ".." => {
                segments.pop()?;
            }
            other => segments.push(other),
        }
    }
    if segments.is_empty() {
        return None;
    }
    Some(segments.join("/"))
}

fn html_escape(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

fn extract_attribute(tag_body: &str, attribute: &str) -> Option<String> {
    let lower = tag_body.to_ascii_lowercase();
    let needle = format!("{attribute}=\"");
    let start = lower.find(&needle)? + needle.len();
    let end = tag_body[start..].find('"')? + start;
    Some(tag_body[start..end].to_string())
}

/// Format a Unix timestamp as `YYYY-MM-DD` (proleptic Gregorian, UTC).
fn format_epoch_date(timestamp: i64) -> String {
    let days = timestamp.div_euclid(86_400);
    // Howard Hinnant's civil_from_days algorithm.
    let z = days + 719_468;
    let era = z.div_euclid(146_097);
    let doe = z.rem_euclid(146_097);
    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
    let year = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let day = doy - (153 * mp + 2) / 5 + 1;
    let month = if mp < 10 { mp + 3 } else { mp - 9 };
    let year = if month <= 2 { year + 1 } else { year };
    format!("{year:04}-{month:02}-{day:02}")
}

fn contributor_initial(name: &str) -> String {
    name.split_whitespace()
        .take(2)
        .filter_map(|part| part.chars().next())
        .collect::<String>()
        .to_uppercase()
}

fn template_environment() -> Result<Environment<'static>> {
    let mut templates = Environment::new();
    templates.set_auto_escape_callback(|name| {
        if name.ends_with(".html") {
            AutoEscape::Html
        } else {
            AutoEscape::None
        }
    });
    templates.add_template("layouts/base.html", BASE)?;
    templates.add_template("layouts/docs.html", DOCS_LAYOUT)?;
    templates.add_template("partials/docs-nav.html", DOCS_NAV)?;
    templates.add_template("partials/public-wordmark.html", PUBLIC_WORDMARK)?;
    templates.add_template("partials/public-header.html", PUBLIC_HEADER)?;
    templates.add_template("partials/public-footer.html", PUBLIC_FOOTER)?;
    templates.add_template("partials/code-content.html", CODE_CONTENT)?;
    templates.add_template("partials/app-account.html", APP_ACCOUNT)?;
    templates.add_template("landing.html", LANDING)?;
    templates.add_template("docs.html", DOCS)?;
    templates.add_template("dashboard.html", DASHBOARD)?;
    templates.add_template("repositories.html", REPOSITORIES)?;
    templates.add_template("repository-app.html", REPOSITORY_APP)?;
    templates.add_template("runs.html", RUNS)?;
    templates.add_template("workers.html", WORKERS)?;
    templates.add_template("run.html", RUN)?;
    templates.add_template("repository-code.html", CODE)?;
    templates.add_template("not-found.html", NOT_FOUND)?;
    Ok(templates)
}

fn request_prefers_html(headers: &HeaderMap) -> bool {
    let Some(accept) = headers
        .get(header::ACCEPT)
        .and_then(|value| value.to_str().ok())
    else {
        return false;
    };
    let media_types = accept
        .split(',')
        .map(|value| value.split(';').next().unwrap_or_default().trim());
    let mut html = false;
    for media_type in media_types {
        if media_type == "application/json" || media_type.ends_with("+json") {
            return false;
        }
        html |= media_type == "text/html" || media_type == "application/xhtml+xml";
    }
    html
}

fn public_error_response(
    templates: &Environment<'_>,
    headers: &HeaderMap,
    error: ApiError,
    title: &str,
    heading: &str,
    description: &str,
) -> Result<Response, ApiError> {
    if !request_prefers_html(headers) {
        return Ok(error.into_response());
    }
    let status = error.status;
    let page = templates
        .get_template("not-found.html")
        .map_err(ApiError::internal)?
        .render(context! {
            title => title,
            heading => heading,
            description => description
        })
        .map_err(ApiError::internal)?;
    Ok((status, Html(page)).into_response())
}

async fn public_not_found(
    State(state): State<AppState>,
    headers: HeaderMap,
) -> Result<Response, ApiError> {
    public_error_response(
        &state.templates,
        &headers,
        ApiError::not_found("route not found"),
        "Page not found",
        "This page could not be found.",
        "The address may be outdated or mistyped. Return home or open the documentation.",
    )
}

pub async fn serve(database: Database, options: ServerOptions) -> Result<()> {
    ensure!(!options.token.is_empty(), "API token must not be empty");
    ensure!(
        !options.webhook_secret.is_empty(),
        "webhook secret must not be empty"
    );
    ensure!(
        options.worker_count <= 16,
        "worker count must be 0..=16 (0 = serve-only, no execution)"
    );
    database.validate_schema()?;
    if options.worker_count > 0 {
        // Recovery belongs to the executor role: only the worker instance may
        // mark orphaned runs interrupted and reset worker rows. A serve-only
        // restart must never clobber a live worker's in-flight run.
        let recovered = database.recover_interrupted()?;
        if recovered > 0 {
            warn!(recovered, "marked interrupted work after restart");
        }
        database.reset_workers()?;
        let configured: Vec<String> = (0..options.worker_count)
            .map(|index| format!("titan-{}", index + 1))
            .collect();
        let pruned = database.prune_workers_except(&configured)?;
        if pruned > 0 {
            warn!(
                pruned,
                "removed stale worker rows beyond the configured count"
            );
        }
    }
    let templates = template_environment()?;
    let hosted_root = options.data_root.clone();
    let tree = RepoTree::new(&options.data_root)?;
    let runner = Runner::new(database.clone(), options.data_root, options.allow_native)?;
    let state = AppState {
        database: database.clone(),
        runner: runner.clone(),
        tree,
        blocking_git: BlockingGit::new(MAX_BLOCKING_GIT),
        hosted_root,
        token: options.token.into(),
        webhook_secret: options.webhook_secret.into(),
        public_url: options.public_url.into(),
        templates: Arc::new(templates),
    };
    for index in 0..options.worker_count {
        let worker_id = format!("titan-{}", index + 1);
        database.register_worker(&worker_id, "titan", "docker,native", 1)?;
        tokio::spawn(worker(database.clone(), runner.clone(), worker_id));
    }

    let app = Router::new()
        .route("/", get(index))
        .route("/docs", get(documentation))
        .route("/docs/", get(documentation))
        .route("/repos/{repository}", get(public_repository))
        .route(
            "/repos/{repository}/tree/{reference}",
            get(public_code_root),
        )
        .route(
            "/repos/{repository}/tree/{reference}/{*path}",
            get(public_code_path),
        )
        .route(
            "/repos/{repository}/raw/{reference}/{*path}",
            get(public_raw_content),
        )
        .route(
            "/downloads/{repository}/{*path}",
            get(public_release_download),
        )
        .route(
            "/app/repositories/{repository}/tree/{reference}",
            get(app_code_root),
        )
        .route(
            "/app/repositories/{repository}/tree/{reference}/{*path}",
            get(app_code_path),
        )
        .route(
            "/app/repositories/{repository}/raw/{reference}/{*path}",
            get(app_raw_content),
        )
        .route("/app", get(dashboard))
        .route("/app/repositories", get(repositories_page))
        .route("/app/repositories/{repository}", get(repository_dashboard))
        .route("/app/run/{id}", get(run_page))
        .route("/app/runs", get(runs_page))
        .route("/app/workers", get(workers_page))
        .route("/auth/login", get(auth_login))
        .route("/auth/callback", get(auth_callback))
        .route("/auth/logout", get(auth_logout).post(auth_logout))
        .route("/assets/app.css", get(css))
        .route("/assets/app.js", get(js))
        .route("/git/{*path}", any(git_http))
        .route("/api/health", get(health))
        .route("/api/state", get(api_state))
        .route("/api/repos", post(create_repository))
        .route("/api/runs/{id}", get(run_detail))
        .route("/api/metrics/delivery", get(delivery_metrics))
        .route("/api/runs/{id}/events", get(run_events))
        .route("/api/runs/{id}/retry", post(retry))
        .route("/api/workers", get(api_workers))
        .route("/api/runs/{id}/promote/{environment}", post(promote))
        .route("/api/repos/{repository}/runs", post(trigger))
        .route(
            "/api/repos/{repository}/visibility",
            post(change_repository_visibility),
        )
        .route("/api/hooks/{repository}", post(webhook))
        .route("/api/artifacts/{id}", get(artifact))
        .route("/api/repos/{repository}/branches", get(branches))
        .route("/api/repos/{repository}/tree", get(repo_tree))
        .fallback(public_not_found)
        .with_state(state)
        .layer(DefaultBodyLimit::max(MAX_BODY))
        .layer(CompressionLayer::new())
        .layer(TimeoutLayer::with_status_code(
            StatusCode::REQUEST_TIMEOUT,
            REQUEST_TIMEOUT,
        ))
        .layer(TraceLayer::new_for_http())
        .layer(middleware::from_fn(security_headers))
        .layer(GlobalConcurrencyLimitLayer::new(MAX_CONNECTIONS));
    let listener = tokio::net::TcpListener::bind(options.address).await?;
    serve_bounded(listener, app).await
}

async fn worker(database: Database, runner: Runner, worker_id: String) {
    if let Err(error) = database.heartbeat_worker(&worker_id, "idle", None, false) {
        warn!(%worker_id, ?error, "worker registration heartbeat failed");
    }
    loop {
        match database.claim_next_queued_run() {
            Ok(Some(id)) => {
                if let Err(error) =
                    database.heartbeat_worker(&worker_id, "running", Some(id), false)
                {
                    warn!(%worker_id, ?error, "worker heartbeat failed");
                }
                let heartbeat_database = database.clone();
                let heartbeat_worker_id = worker_id.clone();
                let heartbeat = tokio::spawn(async move {
                    let mut ticker = tokio::time::interval(Duration::from_secs(5));
                    loop {
                        ticker.tick().await;
                        if let Err(error) = heartbeat_database.heartbeat_worker(
                            &heartbeat_worker_id,
                            "running",
                            Some(id),
                            false,
                        ) {
                            warn!(worker_id = %heartbeat_worker_id, ?error, "worker heartbeat failed");
                        }
                    }
                });
                let process_runner = runner.clone();
                let run =
                    tokio::task::spawn_blocking(move || process_runner.process_claimed_run(id))
                        .await;
                heartbeat.abort();
                let completed = match run {
                    Ok(Ok(detail)) => matches!(
                        detail.run.status.as_str(),
                        "waiting" | "succeeded" | "failed" | "canceled" | "interrupted"
                    ),
                    Ok(Err(error)) => {
                        tracing::error!(run_id = id, ?error, "run failed");
                        false
                    }
                    Err(error) => {
                        tracing::error!(run_id = id, ?error, "run worker panicked");
                        false
                    }
                };
                if let Err(error) = database.heartbeat_worker(&worker_id, "idle", None, completed) {
                    warn!(%worker_id, ?error, "worker idle heartbeat failed");
                }
            }
            Ok(None) => {
                if let Err(error) = database.heartbeat_worker(&worker_id, "idle", None, false) {
                    warn!(%worker_id, ?error, "worker heartbeat failed");
                }
                tokio::time::sleep(Duration::from_millis(500)).await;
            }
            Err(error) => {
                tracing::error!(?error, "queue poll failed");
                tokio::time::sleep(Duration::from_secs(2)).await;
            }
        }
    }
}

async fn index(State(state): State<AppState>) -> Result<Html<String>, ApiError> {
    let repositories = state
        .database
        .public_repositories()
        .map_err(ApiError::internal)?
        .into_iter()
        .map(|repository| LandingRepository {
            owner: repository_owner(&repository.url),
            name: repository.name,
            default_branch: repository.default_branch,
            run_count: repository.run_count,
            successful_runs: repository.successful_runs,
        })
        .collect::<Vec<_>>();
    let mut builder_counts = std::collections::BTreeMap::new();
    for repository in &repositories {
        *builder_counts.entry(repository.owner.clone()).or_insert(0) += 1;
    }
    let mut builders = builder_counts
        .into_iter()
        .map(|(name, repository_count)| PublicBuilder {
            initial: name.chars().next().unwrap_or('A').to_uppercase().collect(),
            name,
            repository_count,
        })
        .collect::<Vec<_>>();
    builders.sort_by(|left, right| {
        right
            .repository_count
            .cmp(&left.repository_count)
            .then_with(|| left.name.cmp(&right.name))
    });
    let page = state
        .templates
        .get_template("landing.html")
        .map_err(ApiError::internal)?
        .render(context! {
            version => env!("CARGO_PKG_VERSION"),
            repositories => repositories,
            builders => builders
        })
        .map_err(ApiError::internal)?;
    Ok(Html(page))
}

fn encode_code_segment(value: &str) -> String {
    value
        .bytes()
        .map(|byte| {
            if byte.is_ascii_alphanumeric() || b"-._~".contains(&byte) {
                (byte as char).to_string()
            } else {
                format!("%{byte:02X}")
            }
        })
        .collect()
}

fn code_base(public: bool, repository: &str, reference: &str) -> String {
    let prefix = if public {
        "/repos"
    } else {
        "/app/repositories"
    };
    format!(
        "{prefix}/{}/tree/{}",
        encode_code_segment(repository),
        encode_code_segment(reference),
    )
}

fn raw_base(public: bool, repository: &str, reference: &str) -> String {
    let prefix = if public {
        "/repos"
    } else {
        "/app/repositories"
    };
    format!(
        "{prefix}/{}/raw/{}",
        encode_code_segment(repository),
        encode_code_segment(reference),
    )
}

fn code_path_href(base: &str, path: &str) -> String {
    if path.is_empty() {
        return base.to_owned();
    }
    format!(
        "{base}/{}",
        path.split('/')
            .filter(|segment| !segment.is_empty())
            .map(encode_code_segment)
            .collect::<Vec<_>>()
            .join("/")
    )
}

fn code_breadcrumbs(
    public: bool,
    repository: &str,
    reference: &str,
    path: &str,
) -> Vec<CodeBreadcrumb> {
    let base = code_base(public, repository, reference);
    let mut breadcrumbs = vec![CodeBreadcrumb {
        label: repository.to_owned(),
        href: base.clone(),
    }];
    let mut current = String::new();
    for segment in path.split('/').filter(|segment| !segment.is_empty()) {
        if !current.is_empty() {
            current.push('/');
        }
        current.push_str(segment);
        breadcrumbs.push(CodeBreadcrumb {
            label: segment.to_owned(),
            href: code_path_href(&base, &current),
        });
    }
    breadcrumbs
}

async fn render_code(
    state: &AppState,
    repository_name: String,
    reference: String,
    path: String,
    public: bool,
    email: Option<String>,
) -> Result<Html<String>, ApiError> {
    let repository = state
        .database
        .repository(&repository_name)
        .map_err(|_| ApiError::not_found(format!("unknown repository {repository_name}")))?;
    if public && repository.visibility != "public" {
        return Err(ApiError::not_found("repository not found"));
    }
    let view = repository_view(repository.clone(), &state.hosted_root, &state.public_url);
    let base = code_base(public, &repository_name, &reference);
    let readme_urls = ReadmeUrls {
        dir: path.clone(),
        raw_prefix: raw_base(public, &repository_name, &reference),
        tree_prefix: base.clone(),
    };
    enum Lookup {
        Tree(crate::tree::TreeResponse),
        Blob(crate::tree::BlobResponse),
    }
    let tree = state.tree.clone();
    let name = repository.name.clone();
    let url = repository.url.clone();
    let lookup_reference = reference.clone();
    let lookup_path = path.clone();
    let lookup = state
        .blocking_git
        .run(
            move || match tree.tree(&name, &url, Some(&lookup_reference), &lookup_path) {
                Ok(response) => Ok(Lookup::Tree(response)),
                Err(_) => tree
                    .blob(&name, &url, &lookup_reference, &lookup_path)
                    .map(Lookup::Blob)
                    .map_err(|_| ApiError::not_found("repository ref or path not found")),
            },
        )
        .await?;
    struct TreeExtras {
        description: Option<String>,
        counts: Option<crate::tree::RepoCounts>,
        readme_html: Option<String>,
        readme_name: Option<String>,
        recent_commits: Vec<CommitView>,
        contributors: Vec<ContributorView>,
        languages: Vec<LanguageView>,
    }
    let (entries, source, is_file, latest_commit, extras) = match lookup {
        Lookup::Tree(tree) => {
            let mut offset = 0.0f64;
            let extras = TreeExtras {
                description: tree.description,
                counts: Some(tree.counts),
                readme_html: tree
                    .readme
                    .as_deref()
                    .map(|source| render_markdown(source, Some(&readme_urls))),
                readme_name: tree.readme_name,
                recent_commits: tree
                    .recent_commits
                    .into_iter()
                    .map(|commit| CommitView {
                        href: code_base(public, &repository_name, &commit.sha),
                        short_sha: commit.sha.chars().take(7).collect(),
                        sha: commit.sha,
                        message: commit.message,
                        author: commit.author,
                        date: format_epoch_date(commit.committed_at),
                    })
                    .collect(),
                contributors: tree
                    .contributors
                    .into_iter()
                    .map(|contributor| ContributorView {
                        initial: contributor_initial(&contributor.name),
                        name: contributor.name,
                        commit_count: contributor.commit_count,
                    })
                    .collect(),
                languages: tree
                    .languages
                    .into_iter()
                    .filter(|language| language.percentage > 0.0)
                    .map(|language| {
                        let view = LanguageView {
                            name: language.name,
                            color: language.color,
                            percentage: language.percentage,
                            percentage_label: format!("{:.1}", language.percentage),
                            offset,
                        };
                        offset += language.percentage;
                        view
                    })
                    .collect(),
            };
            (
                tree.entries
                    .into_iter()
                    .map(|entry| CodeEntryView {
                        href: code_path_href(
                            &base,
                            &if path.is_empty() {
                                entry.name.clone()
                            } else {
                                format!("{path}/{}", entry.name)
                            },
                        ),
                        name: entry.name,
                        kind: entry.kind,
                        size: entry.size,
                        commit_message: entry.commit_message,
                    })
                    .collect(),
                None,
                false,
                tree.latest_commit,
                extras,
            )
        }
        Lookup::Blob(blob) => (
            Vec::new(),
            Some(blob.content),
            true,
            blob.latest_commit,
            TreeExtras {
                description: None,
                counts: None,
                readme_html: None,
                readme_name: None,
                recent_commits: Vec::new(),
                contributors: Vec::new(),
                languages: Vec::new(),
            },
        ),
    };
    let commit_href = latest_commit
        .as_ref()
        .map(|commit| code_base(public, &repository_name, &commit.sha));
    let parent_href = if path.is_empty() {
        if public {
            format!("/repos/{}", encode_code_segment(&repository_name))
        } else {
            format!(
                "/app/repositories/{}",
                encode_code_segment(&repository_name)
            )
        }
    } else {
        let parent = path.rsplit_once('/').map_or("", |(parent, _)| parent);
        code_path_href(&base, parent)
    };
    let page = CodePage {
        repository: view,
        reference: reference.clone(),
        path: path.clone(),
        entries,
        breadcrumbs: code_breadcrumbs(public, &repository_name, &reference, &path),
        source,
        is_file,
        parent_href,
        latest_commit,
        commit_href,
        public,
        description: extras.description,
        counts: extras.counts,
        readme_html: extras.readme_html,
        readme_name: extras.readme_name,
        recent_commits: extras.recent_commits,
        contributors: extras.contributors,
        languages: extras.languages,
    };
    let rendered = state
        .templates
        .get_template("repository-code.html")
        .map_err(ApiError::internal)?
        .render(context! { page => page, email => email })
        .map_err(ApiError::internal)?;
    Ok(Html(rendered))
}

async fn public_code_root(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path((repository, reference)): Path<(String, String)>,
) -> Result<Response, ApiError> {
    match render_code(&state, repository, reference, String::new(), true, None).await {
        Ok(page) => Ok(page.into_response()),
        Err(error) if error.status == StatusCode::NOT_FOUND => public_error_response(
            &state.templates,
            &headers,
            error,
            "Repository content not found",
            "Repository content could not be found.",
            "The repository, ref, or path may have moved. Return to the site or open the documentation.",
        ),
        Err(error) => Err(error),
    }
}

async fn public_code_path(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path((repository, reference, path)): Path<(String, String, String)>,
) -> Result<Response, ApiError> {
    match render_code(&state, repository, reference, path, true, None).await {
        Ok(page) => Ok(page.into_response()),
        Err(error) if error.status == StatusCode::NOT_FOUND => public_error_response(
            &state.templates,
            &headers,
            error,
            "Repository content not found",
            "Repository content could not be found.",
            "The repository, ref, or path may have moved. Return to the site or open the documentation.",
        ),
        Err(error) => Err(error),
    }
}

async fn app_code_root(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
    Path((repository, reference)): Path<(String, String)>,
) -> Result<Response, ApiError> {
    let Some(session) = browser_session(&headers, &state) else {
        return Ok(login_redirect(&uri));
    };
    match render_code(
        &state,
        repository,
        reference,
        String::new(),
        false,
        Some(session.email),
    )
    .await
    {
        Ok(page) => Ok(page.into_response()),
        Err(error) if error.status == StatusCode::NOT_FOUND => public_error_response(
            &state.templates,
            &headers,
            error,
            "Repository content not found",
            "Repository content could not be found.",
            "The repository, ref, or path may have moved. Return to the site or open the documentation.",
        ),
        Err(error) => Err(error),
    }
}

async fn app_code_path(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
    Path((repository, reference, path)): Path<(String, String, String)>,
) -> Result<Response, ApiError> {
    let Some(session) = browser_session(&headers, &state) else {
        return Ok(login_redirect(&uri));
    };
    match render_code(
        &state,
        repository,
        reference,
        path,
        false,
        Some(session.email),
    )
    .await
    {
        Ok(page) => Ok(page.into_response()),
        Err(error) if error.status == StatusCode::NOT_FOUND => public_error_response(
            &state.templates,
            &headers,
            error,
            "Repository content not found",
            "Repository content could not be found.",
            "The repository, ref, or path may have moved. Return to the site or open the documentation.",
        ),
        Err(error) => Err(error),
    }
}

async fn public_raw_content(
    State(state): State<AppState>,
    Path((repository, reference, path)): Path<(String, String, String)>,
) -> Result<Response, ApiError> {
    let repo = state
        .database
        .repository(&repository)
        .map_err(|_| ApiError::not_found(format!("unknown repository {repository}")))?;
    if repo.visibility != "public" {
        return Err(ApiError::not_found("repository not found"));
    }
    raw_blob_response(&state, repo, reference, path).await
}

async fn app_raw_content(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
    Path((repository, reference, path)): Path<(String, String, String)>,
) -> Result<Response, ApiError> {
    if browser_session(&headers, &state).is_none() {
        return Ok(login_redirect(&uri));
    }
    let repo = state
        .database
        .repository(&repository)
        .map_err(|_| ApiError::not_found(format!("unknown repository {repository}")))?;
    raw_blob_response(&state, repo, reference, path).await
}

/// Serve a repository blob's raw bytes inline, so README-referenced images
/// resolve. The content type is inferred from the path extension and is
/// restricted to a static, non-executable allowlist; anything else is served
/// as an octet stream so the browser never treats repo content as active.
async fn raw_blob_response(
    state: &AppState,
    repository: crate::db::Repository,
    reference: String,
    path: String,
) -> Result<Response, ApiError> {
    let tree = state.tree.clone();
    let name = repository.name.clone();
    let url = repository.url.clone();
    let content_type = content_type_for(&path);
    let lookup_reference = reference.clone();
    let lookup_path = path.clone();
    let bytes = state
        .blocking_git
        .run(move || {
            tree.blob_bytes(&name, &url, &lookup_reference, &lookup_path)
                .map_err(|_| ApiError::not_found("file not found"))
        })
        .await?;
    let length = bytes.len();
    let mut response = Response::new(Body::from(bytes));
    response
        .headers_mut()
        .insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
    response.headers_mut().insert(
        header::CONTENT_LENGTH,
        HeaderValue::from_str(&length.to_string()).map_err(ApiError::internal)?,
    );
    response.headers_mut().insert(
        header::CACHE_CONTROL,
        HeaderValue::from_static("public, max-age=300"),
    );
    response.headers_mut().insert(
        HeaderName::from_static("x-content-type-options"),
        HeaderValue::from_static("nosniff"),
    );
    Ok(response)
}

/// Map a file extension to a safe, static content type for inline serving.
/// Only image/font/media types a README embeds are recognised; everything
/// else falls back to a downloadable octet stream.
fn content_type_for(path: &str) -> &'static str {
    let ext = path
        .rsplit('.')
        .next()
        .map(str::to_ascii_lowercase)
        .unwrap_or_default();
    match ext.as_str() {
        "png" => "image/png",
        "jpg" | "jpeg" => "image/jpeg",
        "gif" => "image/gif",
        "webp" => "image/webp",
        "svg" => "image/svg+xml",
        "ico" => "image/x-icon",
        "bmp" => "image/bmp",
        "avif" => "image/avif",
        "apng" => "image/apng",
        "mp4" => "video/mp4",
        "webm" => "video/webm",
        "woff" => "font/woff",
        "woff2" => "font/woff2",
        _ => "application/octet-stream",
    }
}

async fn documentation(State(state): State<AppState>) -> Result<Html<String>, ApiError> {
    let page = state
        .templates
        .get_template("docs.html")
        .map_err(ApiError::internal)?
        .render(context! { version => env!("CARGO_PKG_VERSION") })
        .map_err(ApiError::internal)?;
    Ok(Html(page))
}

fn repository_owner(url: &str) -> String {
    let (host, path) = if let Some((_, remote)) = url.split_once("://") {
        remote
            .split_once('/')
            .map(|(host, path)| (host.rsplit('@').next().unwrap_or(host), path))
            .unwrap_or((remote, ""))
    } else if let Some((_, remote)) = url.split_once('@') {
        remote.split_once(':').unwrap_or((remote, ""))
    } else {
        return "AkurAI Build".into();
    };
    let mut segments = path.split('/').filter(|segment| !segment.is_empty());
    match (segments.next(), segments.next()) {
        (Some("git"), Some(_)) => "AkurAI Build".into(),
        (Some(owner), Some(_)) => owner.into(),
        _ => host.into(),
    }
}

async fn public_repository(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(repository_name): Path<String>,
) -> Result<Response, ApiError> {
    let repository = match state.database.repository(&repository_name) {
        Ok(repository) if repository.visibility == "public" => repository,
        _ => {
            return public_error_response(
                &state.templates,
                &headers,
                ApiError::not_found(format!("unknown repository {repository_name}")),
                "Repository not found",
                "This repository could not be found.",
                "The repository may be private, renamed, or unavailable. Explore public work or open the documentation.",
            );
        }
    };
    let default_branch = repository.default_branch.clone();
    match render_code(
        &state,
        repository.name.clone(),
        default_branch,
        String::new(),
        true,
        None,
    )
    .await
    {
        Ok(page) => Ok(page.into_response()),
        Err(error) if error.status == StatusCode::NOT_FOUND => public_error_response(
            &state.templates,
            &headers,
            error,
            "Repository content not found",
            "Repository content could not be found.",
            "The repository may still be empty. Explore public work or open the documentation.",
        ),
        Err(error) => Err(error),
    }
}

async fn git_http(
    State(state): State<AppState>,
    Path(path): Path<String>,
    request: Request<Body>,
) -> Result<Response, ApiError> {
    authorize(request.headers(), &state)?;
    validate_git_path(&path).map_err(ApiError::bad)?;
    let is_push =
        request.method() == axum::http::Method::POST && path.ends_with("/git-receive-pack");
    let (parts, body) = request.into_parts();
    let body = to_bytes(body, MAX_GIT_BODY).await.map_err(ApiError::bad)?;
    let root = state.hosted_root.join("hosted");
    let git_path = path.clone();
    let response = tokio::task::spawn_blocking(move || run_git_http(root, git_path, parts, body))
        .await
        .map_err(ApiError::internal)?
        .map_err(ApiError::internal)?;
    // CI autonomy: a completed push to a hosted repository queues a pipeline
    // run for its default branch, exactly like an external webhook would.
    // Repositories without an .akurai.yml are skipped instead of producing
    // guaranteed-failed runs.
    if is_push
        && response.status().is_success()
        && let Some(repository) = path
            .split('/')
            .next()
            .and_then(|segment| segment.strip_suffix(".git"))
    {
        let bare = state
            .hosted_root
            .join("hosted")
            .join(format!("{repository}.git"));
        let has_pipeline =
            crate::git_process::output(&bare, ["cat-file", "-e", "HEAD:.akurai.yml"]).is_ok();
        if has_pipeline {
            match state.runner.queue(repository, None, None, "webhook") {
                Ok(run_id) => info!(repository, run_id, "queued run for pushed commit"),
                Err(error) => warn!(repository, ?error, "failed to queue run after push"),
            }
        } else {
            info!(
                repository,
                "push accepted; no pipeline config, run not queued"
            );
        }
    }
    Ok(response)
}

fn validate_git_path(path: &str) -> Result<()> {
    ensure!(!path.is_empty() && path.len() <= 512, "invalid Git path");
    let repository = path.split('/').next().unwrap_or_default();
    ensure!(repository.ends_with(".git"), "invalid Git repository path");
    ensure!(
        path.split('/').all(|component| {
            !component.is_empty()
                && component
                    .bytes()
                    .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
        }),
        "invalid Git path"
    );
    Ok(())
}

fn run_git_http(
    root: PathBuf,
    path: String,
    parts: axum::http::request::Parts,
    body: Bytes,
) -> Result<Response> {
    let mut command = Command::new("git");
    command
        .arg("http-backend")
        .env("GIT_PROJECT_ROOT", root)
        .env("GIT_HTTP_EXPORT_ALL", "1")
        .env("PATH_INFO", format!("/{path}"))
        .env("REQUEST_METHOD", parts.method.as_str())
        .env("QUERY_STRING", parts.uri.query().unwrap_or_default())
        .env("CONTENT_LENGTH", body.len().to_string())
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped());
    if let Some(value) = parts.headers.get(header::CONTENT_TYPE) {
        command.env("CONTENT_TYPE", value.to_str()?);
    }
    if let Some(value) = parts.headers.get(header::CONTENT_ENCODING) {
        command.env("HTTP_CONTENT_ENCODING", value.to_str()?);
    }
    if let Some(value) = parts.headers.get("git-protocol") {
        command.env("HTTP_GIT_PROTOCOL", value.to_str()?);
    }
    let mut child = command
        .spawn()
        .context("failed to start git http-backend")?;
    child
        .stdin
        .take()
        .context("git http-backend stdin is unavailable")?
        .write_all(&body)?;
    let output = child.wait_with_output()?;
    ensure!(
        output.status.success(),
        "git http-backend failed: {}",
        String::from_utf8_lossy(&output.stderr).trim()
    );
    git_cgi_response(&output.stdout)
}

fn git_cgi_response(output: &[u8]) -> Result<Response> {
    let (header_bytes, body) = output
        .windows(4)
        .position(|window| window == b"\r\n\r\n")
        .map(|index| (&output[..index], &output[index + 4..]))
        .or_else(|| {
            output
                .windows(2)
                .position(|window| window == b"\n\n")
                .map(|index| (&output[..index], &output[index + 2..]))
        })
        .context("git http-backend returned malformed CGI output")?;
    let mut status = StatusCode::OK;
    let mut response = Response::builder();
    for line in String::from_utf8_lossy(header_bytes).lines() {
        let (name, value) = line.split_once(':').context("invalid Git CGI header")?;
        if name.eq_ignore_ascii_case("status") {
            let code = value
                .split_whitespace()
                .next()
                .context("missing Git CGI status")?
                .parse::<u16>()?;
            status = StatusCode::from_u16(code)?;
        } else {
            response = response.header(
                HeaderName::from_bytes(name.trim().as_bytes())?,
                HeaderValue::from_str(value.trim())?,
            );
        }
    }
    Ok(response.status(status).body(Body::from(body.to_vec()))?)
}

async fn dashboard(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
) -> Result<Response, ApiError> {
    app_page(&state, &uri, &headers, "dashboard.html", context! {})
}

async fn repositories_page(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
) -> Result<Response, ApiError> {
    app_page(&state, &uri, &headers, "repositories.html", context! {})
}

async fn repository_dashboard(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
    Path(repository): Path<String>,
) -> Result<Response, ApiError> {
    let repository = state
        .database
        .repository(&repository)
        .map_err(ApiError::bad)?;
    let repository = repository_view(repository, &state.hosted_root, &state.public_url);
    app_page(
        &state,
        &uri,
        &headers,
        "repository-app.html",
        context! { repository => repository },
    )
}

async fn runs_page(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
) -> Result<Response, ApiError> {
    app_page(&state, &uri, &headers, "runs.html", context! {})
}

async fn run_page(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
    Path(id): Path<i64>,
) -> Result<Response, ApiError> {
    app_page(
        &state,
        &uri,
        &headers,
        "run.html",
        context! { run_id => id },
    )
}

async fn workers_page(
    State(state): State<AppState>,
    uri: Uri,
    headers: HeaderMap,
) -> Result<Response, ApiError> {
    app_page(&state, &uri, &headers, "workers.html", context! {})
}

/// Render an authenticated shell page, or bounce an anonymous browser to
/// `/auth/login` with a same-origin `return_to`.
fn app_page(
    state: &AppState,
    uri: &Uri,
    headers: &HeaderMap,
    template: &str,
    extra: minijinja::Value,
) -> Result<Response, ApiError> {
    let Some(session) = browser_session(headers, state) else {
        return Ok(login_redirect(uri));
    };
    let page = state
        .templates
        .get_template(template)
        .map_err(ApiError::internal)?
        .render(context! {
            version => env!("CARGO_PKG_VERSION"),
            email => session.email,
            ..extra
        })
        .map_err(ApiError::internal)?;
    Ok(Html(page).into_response())
}

/// `GET /auth/login` — start the AkurAI ID authorization-code flow.
async fn auth_login(
    State(state): State<AppState>,
    Query(query): Query<LoginQuery>,
) -> Result<Response, ApiError> {
    let config = idp_config(&state)?;
    let redirect = oidc::login_redirect(&state.database, &config, query.return_to.as_deref())
        .map_err(oidc_error)?;
    let mut response = Redirect::to(&redirect.location).into_response();
    response.headers_mut().insert(
        header::SET_COOKIE,
        HeaderValue::from_str(&redirect.set_cookie).map_err(ApiError::internal)?,
    );
    Ok(response)
}

/// `GET /auth/callback` — finish the flow: state, token, ID token, introspect,
/// userinfo, authorization, session. Any miss fails closed.
async fn auth_callback(
    State(state): State<AppState>,
    headers: HeaderMap,
    Query(query): Query<CallbackQuery>,
) -> Result<Response, ApiError> {
    let config = idp_config(&state)?;
    let database = state.database.clone();
    let state_cookie = request_cookie(&headers, oidc::STATE_COOKIE).map(str::to_owned);
    let outcome = tokio::task::spawn_blocking(move || {
        oidc::handle_callback(
            &database,
            &config,
            &oidc::CallbackParams {
                code: query.code.as_deref(),
                state: query.state.as_deref(),
                error: query.error.as_deref(),
                state_cookie: state_cookie.as_deref(),
            },
        )
    })
    .await
    .map_err(ApiError::internal)?
    .map_err(oidc_error)?;
    let mut response = Redirect::to(&outcome.location).into_response();
    for cookie in [&outcome.session_cookie, &outcome.clear_state_cookie] {
        response.headers_mut().append(
            header::SET_COOKIE,
            HeaderValue::from_str(cookie).map_err(ApiError::internal)?,
        );
    }
    Ok(response)
}

/// `GET|POST /auth/logout` — drop the session, then RP-initiated IDP logout.
async fn auth_logout(
    State(state): State<AppState>,
    headers: HeaderMap,
) -> Result<Response, ApiError> {
    let config = idp_config(&state)?;
    let redirect = oidc::logout_redirect(
        &state.database,
        &config,
        request_cookie(&headers, oidc::SESSION_COOKIE),
    );
    let mut response = Redirect::to(&redirect.location).into_response();
    for cookie in [&redirect.clear_session_cookie, &redirect.clear_state_cookie] {
        response.headers_mut().append(
            header::SET_COOKIE,
            HeaderValue::from_str(cookie).map_err(ApiError::internal)?,
        );
    }
    Ok(response)
}

fn idp_config(state: &AppState) -> Result<oidc::Config, ApiError> {
    oidc::config(&state.public_url).map_err(oidc_error)
}

fn oidc_error(error: oidc::Error) -> ApiError {
    match error {
        oidc::Error::Invalid(message) => ApiError::bad(message),
        oidc::Error::Forbidden(message) => ApiError {
            status: StatusCode::FORBIDDEN,
            code: "forbidden",
            message,
        },
        oidc::Error::Upstream(message) => {
            warn!(error = %message, "AkurAI ID back-channel call failed");
            ApiError {
                status: StatusCode::BAD_GATEWAY,
                code: "identity_provider_unavailable",
                message: "the identity provider could not be reached".into(),
            }
        }
        oidc::Error::Configuration(message) => {
            warn!(error = %message, "AkurAI ID is not configured; browser sign-in is unavailable");
            ApiError::internal(message)
        }
        oidc::Error::Internal(message) => ApiError::internal(message),
    }
}

async fn css() -> Response {
    asset(CSS, "text/css; charset=utf-8")
}
async fn js() -> Response {
    asset(JS, "text/javascript; charset=utf-8")
}
fn asset(body: &'static str, content_type: &'static str) -> Response {
    let mut response = Response::new(Body::from(body));
    response
        .headers_mut()
        .insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
    response.headers_mut().insert(
        header::CACHE_CONTROL,
        HeaderValue::from_static("public, max-age=300"),
    );
    response
}

async fn health(State(state): State<AppState>) -> Result<Json<Envelope<Value>>, ApiError> {
    state.database.check_ready().map_err(ApiError::internal)?;
    let workers = state.database.workers().map_err(ApiError::internal)?;
    let active_workers = workers
        .iter()
        .filter(|worker| worker.status == "running")
        .count();
    let queued_runs = state.database.queued_runs().map_err(ApiError::internal)?;
    Ok(ok(serde_json::json!({
        "service": "akurai-build",
        "status": "ok",
        "version": env!("CARGO_PKG_VERSION"),
        "workers": workers.len(),
        "active_workers": active_workers,
        "queued_runs": queued_runs,
    })))
}

async fn api_state(
    State(state): State<AppState>,
    headers: HeaderMap,
    Query(query): Query<StateQuery>,
) -> Result<Json<Envelope<StateResponse>>, ApiError> {
    authorize(&headers, &state)?;
    let admin = superuser_session(&headers, &state) || valid_bearer(&headers, &state);
    let all_repositories = state.database.repositories().map_err(ApiError::internal)?;
    let visible_repositories = all_repositories
        .iter()
        .filter(|repository| admin || repository.visibility == "public")
        .collect::<Vec<_>>();
    let repository_filter = if let Some(requested) = query.repo.as_deref() {
        let requested = requested
            .split(',')
            .map(str::trim)
            .filter(|name| !name.is_empty())
            .collect::<Vec<_>>();
        if requested.is_empty() {
            return Err(ApiError::bad("repository filter is empty"));
        }
        if !requested.iter().all(|name| {
            visible_repositories
                .iter()
                .any(|repository| repository.name == *name)
        }) {
            return Err(ApiError::not_found("repository not found"));
        }
        Some(requested.join(","))
    } else {
        None
    };
    let repositories = visible_repositories
        .into_iter()
        .cloned()
        .map(|repository| repository_view(repository, &state.hosted_root, &state.public_url))
        .collect();
    let mut run_query = crate::db::RunQuery {
        repository: repository_filter,
        statuses: list_filter(query.status),
        git_ref: query.git_ref,
        triggers: list_filter(query.trigger),
        search: query.q,
        before_id: query.before,
        limit: query.limit.unwrap_or(50),
        offset: query.offset.unwrap_or(0),
        public_only: !admin,
    };
    let run_cursor = match run_query.before_id {
        Some(cursor) => Some(cursor),
        None => state
            .database
            .max_run_id(&run_query)
            .map_err(ApiError::bad)?,
    };
    run_query.before_id = run_cursor;
    let run_total = state
        .database
        .count_runs(&run_query)
        .map_err(ApiError::bad)?;
    let runs = state
        .database
        .query_runs(&run_query)
        .map_err(ApiError::bad)?;
    let workers = state.database.workers().map_err(ApiError::internal)?;
    let queued_runs = state.database.queued_runs().map_err(ApiError::internal)?;
    Ok(ok(StateResponse {
        repositories,
        runs,
        workers,
        queued_runs,
        run_total,
        run_cursor,
    }))
}

async fn delivery_metrics(
    State(state): State<AppState>,
    headers: HeaderMap,
    Query(query): Query<DeliveryMetricsQuery>,
) -> Result<Json<Envelope<crate::db::DeliveryMetrics>>, ApiError> {
    authorize(&headers, &state)?;
    let repository_name = query
        .repo
        .as_deref()
        .ok_or_else(|| ApiError::bad("repo is required for repository-scoped metrics"))?;
    let repository = state
        .database
        .repository(repository_name)
        .map_err(|_| ApiError::not_found("repository not found"))?;
    if repository.visibility != "public"
        && !superuser_session(&headers, &state)
        && !valid_bearer(&headers, &state)
    {
        return Err(ApiError::not_found("repository not found"));
    }
    let window = query.window.unwrap_or(7 * 86_400);
    Ok(ok(state
        .database
        .delivery_metrics(Some(repository_name), query.environment.as_deref(), window)
        .map_err(ApiError::bad)?))
}

async fn api_workers(
    State(state): State<AppState>,
    headers: HeaderMap,
) -> Result<Json<Envelope<Vec<crate::db::Worker>>>, ApiError> {
    authorize(&headers, &state)?;
    Ok(ok(state.database.workers().map_err(ApiError::internal)?))
}

fn visible_run_detail(
    state: &AppState,
    headers: &HeaderMap,
    id: i64,
) -> Result<crate::db::RunDetail, ApiError> {
    let mut detail = state.database.detail(id).map_err(ApiError::bad)?;
    let repository = state
        .database
        .repository(&detail.run.repository)
        .map_err(ApiError::bad)?;
    let privileged = superuser_session(headers, state) || valid_bearer(headers, state);
    if repository.visibility != "public" && !privileged {
        return Err(ApiError::not_found("run not found"));
    }
    if !privileged {
        for deployment in &mut detail.deployments {
            deployment.organization_id = None;
            deployment.workspace_id = None;
            deployment.installation_id = None;
            deployment.app_id = None;
            deployment.installation_revision = None;
            deployment.commit_sha = None;
            deployment.artifact_digest = None;
            deployment.result = None;
            deployment.rollback_of_deployment_id = None;
            deployment.recovery_of_deployment_id = None;
        }
    }
    Ok(detail)
}
async fn run_detail(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(id): Path<i64>,
) -> Result<Json<Envelope<crate::db::RunDetail>>, ApiError> {
    authorize(&headers, &state)?;
    Ok(ok(visible_run_detail(&state, &headers, id)?))
}

async fn run_events(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(id): Path<i64>,
) -> Result<Response, ApiError> {
    authorize(&headers, &state)?;
    visible_run_detail(&state, &headers, id)?;

    let database = state.database.clone();
    let (reader, mut writer) = tokio::io::duplex(64 * 1024);
    tokio::spawn(async move {
        let mut previous = String::new();
        let mut ticker = tokio::time::interval(Duration::from_millis(500));
        loop {
            ticker.tick().await;
            let detail = match database.detail(id) {
                Ok(detail) => detail,
                Err(error) => {
                    let payload = serde_json::json!({"run_id": id, "status": "failed", "error": error.to_string()});
                    let _ = writer
                        .write_all(format!("event: error\ndata: {payload}\n\n").as_bytes())
                        .await;
                    break;
                }
            };
            let terminal = is_terminal_run(&detail.run.status);
            let payload = match serde_json::to_string(&run_progress(detail)) {
                Ok(payload) => payload,
                Err(_) => break,
            };
            if payload != previous {
                let frame = format!("event: progress\ndata: {payload}\n\n");
                if writer.write_all(frame.as_bytes()).await.is_err() {
                    break;
                }
                previous = payload;
            } else if writer.write_all(b": keepalive\n\n").await.is_err() {
                break;
            }
            if terminal {
                break;
            }
        }
    });

    let mut response = Response::new(Body::from_stream(ReaderStream::new(reader)));
    response.headers_mut().insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("text/event-stream"),
    );
    response.headers_mut().insert(
        header::CACHE_CONTROL,
        HeaderValue::from_static("no-cache, no-transform"),
    );
    response.headers_mut().insert(
        header::CONTENT_ENCODING,
        HeaderValue::from_static("identity"),
    );
    response.headers_mut().insert(
        HeaderName::from_static("x-accel-buffering"),
        HeaderValue::from_static("no"),
    );
    Ok(response)
}

fn run_progress(detail: crate::db::RunDetail) -> RunProgress {
    let job_count = detail.jobs.len() as u32;
    let job_units = detail
        .jobs
        .iter()
        .map(|job| match job.status.as_str() {
            "succeeded" | "failed" | "canceled" | "skipped" => 4,
            "waiting" => 3,
            "running" => 2,
            _ => 0,
        })
        .sum::<u32>();
    let progress = match detail.run.status.as_str() {
        "succeeded" => 100,
        "queued" => 2,
        "waiting" => 85,
        _ if job_count > 0 => (5 + (80 * job_units / (job_count * 4))).min(95) as u8,
        _ => 5,
    };
    let stage = if detail.run.status == "succeeded" {
        "succeeded".to_string()
    } else if is_terminal_run(&detail.run.status) {
        detail.run.status.clone()
    } else if detail.run.status == "waiting"
        || detail
            .jobs
            .iter()
            .any(|job| job.status == "waiting" && job.approval_required)
    {
        "waiting_for_approval".to_string()
    } else if let Some(deployment) = detail
        .deployments
        .iter()
        .rev()
        .find(|deployment| deployment.status != "succeeded")
    {
        format!("deploying:{}", deployment.environment)
    } else if let Some(job) = detail.jobs.iter().find(|job| job.status == "running") {
        format!("running:{}", job.name)
    } else {
        detail.run.status.clone()
    };

    RunProgress {
        run_id: detail.run.id,
        repository: detail.run.repository,
        git_ref: detail.run.git_ref,
        commit_sha: detail.run.commit_sha,
        status: detail.run.status,
        stage,
        progress,
        jobs: detail
            .jobs
            .into_iter()
            .map(|job| ProgressJob {
                id: job.id,
                name: job.name,
                status: job.status,
                environment: job.environment,
                approval_required: job.approval_required,
            })
            .collect(),
        deployments: detail
            .deployments
            .into_iter()
            .map(|deployment| ProgressDeployment {
                id: deployment.id,
                environment: deployment.environment,
                status: deployment.status,
            })
            .collect(),
        error: detail.run.error,
    }
}

fn is_terminal_run(status: &str) -> bool {
    matches!(status, "succeeded" | "failed" | "canceled" | "interrupted")
}

async fn create_repository(
    State(state): State<AppState>,
    headers: HeaderMap,
    Json(request): Json<RepositoryRequest>,
) -> Result<Json<Envelope<RepositoryView>>, ApiError> {
    authorize_mutation(&headers, &state)?;
    config::validate_repository(&request.name, &request.url, &request.branch)
        .map_err(ApiError::bad)?;
    let repository = state
        .database
        .add_repository(&request.name, &request.url, &request.branch)
        .map_err(ApiError::bad)?;
    Ok(ok(repository_view(
        repository,
        &state.hosted_root,
        &state.public_url,
    )))
}

async fn change_repository_visibility(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(repository): Path<String>,
    Json(request): Json<VisibilityRequest>,
) -> Result<Json<Envelope<RepositoryView>>, ApiError> {
    authorize_mutation(&headers, &state)?;
    let repository = state
        .database
        .set_repository_visibility(&repository, &request.visibility)
        .map_err(ApiError::bad)?;
    Ok(ok(repository_view(
        repository,
        &state.hosted_root,
        &state.public_url,
    )))
}

async fn trigger(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(repository): Path<String>,
    Json(request): Json<TriggerRequest>,
) -> Result<Json<Envelope<IdResponse>>, ApiError> {
    authorize_mutation(&headers, &state)?;
    let id = state
        .runner
        .queue(
            &repository,
            request.git_ref.as_deref(),
            request.commit.as_deref(),
            "manual",
        )
        .map_err(ApiError::bad)?;
    Ok(ok(IdResponse { id }))
}

async fn branches(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(repository): Path<String>,
) -> Result<Json<Envelope<Vec<git_process::RemoteBranch>>>, ApiError> {
    authorize(&headers, &state)?;
    let repo = state
        .database
        .repository(&repository)
        .map_err(|_| ApiError::bad("repository not found"))?;
    let default_branch = repo.default_branch;
    let url = repo.url;
    let branches = state
        .blocking_git
        .run(move || git_process::remote_branches(&url, &default_branch).map_err(ApiError::bad))
        .await?;
    Ok(ok(branches))
}

#[derive(Deserialize)]
struct TreeQuery {
    #[serde(rename = "ref")]
    reference: Option<String>,
    path: Option<String>,
}

async fn repo_tree(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(repository): Path<String>,
    Query(query): Query<TreeQuery>,
) -> Result<Json<Envelope<crate::tree::TreeResponse>>, ApiError> {
    authorize(&headers, &state)?;
    let repo = state
        .database
        .repository(&repository)
        .map_err(|_| ApiError::bad("repository not found"))?;
    let sub_path = query.path.unwrap_or_default();
    let tree = state.tree.clone();
    let name = repo.name;
    let url = repo.url;
    let reference = query.reference;
    let response = state
        .blocking_git
        .run(move || {
            tree.tree(&name, &url, reference.as_deref(), &sub_path)
                .map_err(ApiError::bad)
        })
        .await?;
    Ok(ok(response))
}

async fn retry(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(id): Path<i64>,
) -> Result<Json<Envelope<IdResponse>>, ApiError> {
    authorize_mutation(&headers, &state)?;
    let id = state.database.retry(id).map_err(ApiError::bad)?;
    Ok(ok(IdResponse { id }))
}

async fn promote(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path((id, environment)): Path<(i64, String)>,
) -> Result<Json<Envelope<Value>>, ApiError> {
    authorize_mutation(&headers, &state)?;
    let jobs = state
        .database
        .approve_environment(id, &environment)
        .map_err(ApiError::bad)?;
    Ok(ok(
        serde_json::json!({"run_id":id,"environment":environment,"jobs":jobs}),
    ))
}

async fn artifact(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(id): Path<i64>,
) -> Result<Response, ApiError> {
    authorize(&headers, &state)?;
    let artifact = state.database.artifact(id).map_err(ApiError::bad)?;
    let path = state
        .runner
        .artifact_path(&artifact)
        .map_err(ApiError::internal)?;
    let file = tokio::fs::File::open(path)
        .await
        .map_err(ApiError::internal)?;
    let mut response = Response::new(Body::from_stream(ReaderStream::new(file)));
    response.headers_mut().insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("application/octet-stream"),
    );
    response.headers_mut().insert(
        header::CONTENT_LENGTH,
        HeaderValue::from_str(&artifact.bytes.to_string()).map_err(ApiError::internal)?,
    );
    let filename = artifact
        .name
        .rsplit('/')
        .next()
        .unwrap_or("artifact")
        .replace(['"', '\r', '\n'], "_");
    response.headers_mut().insert(
        header::CONTENT_DISPOSITION,
        HeaderValue::from_str(&format!("attachment; filename=\"{filename}\""))
            .map_err(ApiError::internal)?,
    );
    Ok(response)
}

async fn public_release_download(
    State(state): State<AppState>,
    Path((repository_name, download_path)): Path<(String, String)>,
) -> Result<Response, ApiError> {
    let download_path = download_path
        .strip_prefix('/')
        .unwrap_or(download_path.as_str());
    let repository = state
        .database
        .repository(&repository_name)
        .map_err(|_| ApiError::not_found(format!("unknown repository {repository_name}")))?;
    if repository.visibility != "public" || !valid_download_path(download_path) {
        return Err(ApiError::not_found("release download not found"));
    }
    let path = state
        .hosted_root
        .join("releases")
        .join(&repository_name)
        .join(download_path);
    let metadata = tokio::fs::metadata(&path)
        .await
        .map_err(|_| ApiError::not_found("release download not found"))?;
    if !metadata.is_file() {
        return Err(ApiError::not_found("release download not found"));
    }
    let file = tokio::fs::File::open(path)
        .await
        .map_err(ApiError::internal)?;
    let mut response = Response::new(Body::from_stream(ReaderStream::new(file)));
    response.headers_mut().insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("application/octet-stream"),
    );
    response.headers_mut().insert(
        header::CONTENT_LENGTH,
        HeaderValue::from_str(&metadata.len().to_string()).map_err(ApiError::internal)?,
    );
    let filename = download_path.rsplit('/').next().unwrap_or("download");
    response.headers_mut().insert(
        header::CONTENT_DISPOSITION,
        HeaderValue::from_str(&format!("attachment; filename=\"{filename}\""))
            .map_err(ApiError::internal)?,
    );
    Ok(response)
}

fn valid_download_path(path: &str) -> bool {
    !path.is_empty()
        && path.split('/').all(|component| {
            !component.is_empty()
                && component != "."
                && component != ".."
                && !component.starts_with('.')
                && component
                    .bytes()
                    .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_'))
        })
}

async fn webhook(
    State(state): State<AppState>,
    headers: HeaderMap,
    Path(repository): Path<String>,
    body: Bytes,
) -> Result<Json<Envelope<IdResponse>>, ApiError> {
    authorize_webhook(&headers, &state, &body)?;
    let payload: Value = serde_json::from_slice(&body).map_err(ApiError::bad)?;
    let git_ref = payload
        .get("ref")
        .and_then(Value::as_str)
        .context("webhook has no ref")
        .map_err(ApiError::bad)?;
    let commit = payload
        .get("after")
        .or_else(|| payload.get("checkout_sha"))
        .and_then(Value::as_str)
        .or_else(|| {
            payload
                .get("head_commit")
                .and_then(|value| value.get("id"))
                .and_then(Value::as_str)
        });
    let commit = commit.filter(|value| value.bytes().any(|byte| byte != b'0'));
    let id = state
        .runner
        .queue(
            &repository,
            Some(git_ref.strip_prefix("refs/heads/").unwrap_or(git_ref)),
            commit,
            "webhook",
        )
        .map_err(ApiError::bad)?;
    Ok(ok(IdResponse { id }))
}

fn list_filter(value: Option<String>) -> Vec<String> {
    value
        .into_iter()
        .flat_map(|value| {
            value
                .split(',')
                .map(str::trim)
                .filter(|item| !item.is_empty())
                .map(str::to_owned)
                .collect::<Vec<_>>()
        })
        .collect()
}

fn request_cookie<'a>(headers: &'a HeaderMap, name: &str) -> Option<&'a str> {
    headers
        .get(header::COOKIE)?
        .to_str()
        .ok()?
        .split(';')
        .map(str::trim)
        .find_map(|pair| pair.strip_prefix(name)?.strip_prefix('='))
}

fn valid_bearer(headers: &HeaderMap, state: &AppState) -> bool {
    headers
        .get(header::AUTHORIZATION)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer "))
        .is_some_and(|supplied| constant_time_equal(supplied.as_bytes(), state.token.as_bytes()))
}

/// Machine clients present the static API/Git bearer token; humans present the
/// `akurai_session` cookie minted by AkurAI ID. Nothing else authenticates.
fn authorize(headers: &HeaderMap, state: &AppState) -> Result<(), ApiError> {
    if let Some(supplied) = headers
        .get(header::AUTHORIZATION)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer "))
    {
        return constant_time_equal(supplied.as_bytes(), state.token.as_bytes())
            .then_some(())
            .ok_or_else(ApiError::unauthorized);
    }
    browser_session(headers, state)
        .map(|_| ())
        .ok_or_else(ApiError::unauthorized)
}

/// Resolve the durable session behind the `akurai_session` cookie, if any.
fn browser_session(headers: &HeaderMap, state: &AppState) -> Option<Session> {
    oidc::require_session(
        &state.database,
        request_cookie(headers, oidc::SESSION_COOKIE),
    )
}

/// `owner` and `admin` from `/userinfo` are platform superusers.
fn superuser_session(headers: &HeaderMap, state: &AppState) -> bool {
    browser_session(headers, state).is_some_and(|session| session.is_superuser())
}

/// Bounce an unauthenticated browser to the login route, preserving where it
/// was heading as a same-origin `return_to`.
fn login_redirect(uri: &Uri) -> Response {
    let target = uri
        .path_and_query()
        .map(|value| value.as_str())
        .unwrap_or("/");
    match oidc::same_origin_path(Some(target)).filter(|path| path != "/") {
        Some(path) => Redirect::to(&format!(
            "{}?return_to={}",
            oidc::LOGIN_PATH,
            oidc::encode(&path)
        )),
        None => Redirect::to(oidc::LOGIN_PATH),
    }
    .into_response()
}

fn authorize_mutation(headers: &HeaderMap, state: &AppState) -> Result<(), ApiError> {
    authorize(headers, state)?;
    if let Some(origin) = headers
        .get(header::ORIGIN)
        .and_then(|value| value.to_str().ok())
        && origin != state.public_url.as_ref()
    {
        return Err(ApiError::bad("origin is not allowed"));
    }
    Ok(())
}

fn authorize_webhook(headers: &HeaderMap, state: &AppState, body: &[u8]) -> Result<(), ApiError> {
    if let Some(token) = headers
        .get("x-gitlab-token")
        .and_then(|value| value.to_str().ok())
        && constant_time_equal(token.as_bytes(), state.webhook_secret.as_bytes())
    {
        return Ok(());
    }
    for name in ["x-hub-signature-256", "x-gitea-signature"] {
        if let Some(signature) = headers.get(name).and_then(|value| value.to_str().ok()) {
            let signature = signature.strip_prefix("sha256=").unwrap_or(signature);
            let supplied = decode_hex(signature).map_err(ApiError::bad)?;
            let mut mac = HmacSha256::new_from_slice(state.webhook_secret.as_bytes())
                .map_err(ApiError::internal)?;
            mac.update(body);
            if constant_time_equal(&supplied, &mac.finalize().into_bytes()) {
                return Ok(());
            }
        }
    }
    Err(ApiError::unauthorized())
}

fn decode_hex(value: &str) -> Result<Vec<u8>> {
    ensure!(
        value.len().is_multiple_of(2) && value.len() <= 128,
        "invalid signature"
    );
    value
        .as_bytes()
        .chunks_exact(2)
        .map(|pair| {
            let text = std::str::from_utf8(pair)?;
            Ok(u8::from_str_radix(text, 16)?)
        })
        .collect()
}

fn constant_time_equal(left: &[u8], right: &[u8]) -> bool {
    left.len() == right.len() && left.ct_eq(right).into()
}

fn ok<T: Serialize>(data: T) -> Json<Envelope<T>> {
    Json(Envelope { ok: true, data })
}

async fn security_headers(request: Request<Body>, next: Next) -> Response {
    let mut response = next.run(request).await;
    let is_json = response
        .headers()
        .get(header::CONTENT_TYPE)
        .and_then(|value| value.to_str().ok())
        .is_some_and(|value| value.contains("json"));
    let headers = response.headers_mut();
    headers.insert(
        header::X_CONTENT_TYPE_OPTIONS,
        HeaderValue::from_static("nosniff"),
    );
    headers.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
    headers.insert(
        header::REFERRER_POLICY,
        HeaderValue::from_static("no-referrer"),
    );
    headers.insert(header::CONTENT_SECURITY_POLICY, HeaderValue::from_static("default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; connect-src 'self'"));
    headers.insert(
        HeaderName::from_static("permissions-policy"),
        HeaderValue::from_static("camera=(), geolocation=(), microphone=()"),
    );
    headers.insert(
        HeaderName::from_static("cross-origin-opener-policy"),
        HeaderValue::from_static("same-origin"),
    );
    headers.insert(
        HeaderName::from_static("cross-origin-resource-policy"),
        HeaderValue::from_static("same-origin"),
    );
    if is_json {
        headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
    }
    response
}

async fn serve_bounded(listener: tokio::net::TcpListener, app: Router) -> Result<()> {
    use hyper_util::{
        rt::{TokioExecutor, TokioIo, TokioTimer},
        server::{conn::auto::Builder, graceful::GracefulShutdown},
        service::TowerToHyperService,
    };
    info!(address = %listener.local_addr()?, "AkurAI Build listening");
    let connections = Arc::new(tokio::sync::Semaphore::new(MAX_CONNECTIONS));
    let graceful = GracefulShutdown::new();
    let mut shutdown = std::pin::pin!(shutdown_signal());
    loop {
        let permit = tokio::select! { () = &mut shutdown => break, permit = connections.clone().acquire_owned() => permit? };
        let (socket, _) = tokio::select! { () = &mut shutdown => break, accepted = listener.accept() => accepted? };
        let mut builder = Builder::new(TokioExecutor::new());
        builder
            .http1()
            .timer(TokioTimer::new())
            .header_read_timeout(REQUEST_TIMEOUT);
        let connection = graceful.watch(
            builder
                .serve_connection_with_upgrades(
                    TokioIo::new(socket),
                    TowerToHyperService::new(app.clone()),
                )
                .into_owned(),
        );
        tokio::spawn(async move {
            if let Err(error) = connection.await {
                tracing::debug!(?error, "connection closed");
            }
            drop(permit);
        });
    }
    drop(listener);
    tokio::select! { () = graceful.shutdown() => {}, () = tokio::time::sleep(REQUEST_TIMEOUT) => warn!("graceful shutdown timed out") }
    Ok(())
}

async fn shutdown_signal() {
    let ctrl_c = async {
        if let Err(error) = tokio::signal::ctrl_c().await {
            warn!(?error, "failed to install Ctrl-C handler");
        }
    };
    #[cfg(unix)]
    let terminate = async {
        match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
            Ok(mut signal) => {
                signal.recv().await;
            }
            Err(error) => warn!(?error, "failed to install terminate handler"),
        }
    };
    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();
    tokio::select! { () = ctrl_c => {}, () = terminate => {} }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tower::Service;
    #[test]
    fn templates_compile_and_render_without_inline_code() -> Result<()> {
        let environment = template_environment()?;
        let page = environment.get_template("landing.html")?.render(context! {
            version => "test",
            repositories => vec![LandingRepository {
                name: "AkurAI-Build".into(),
                default_branch: "main".into(),
                owner: "olibuijr".into(),
                run_count: 4,
                successful_runs: 3,
            }],
            builders => vec![PublicBuilder {
                name: "olibuijr".into(),
                initial: "O".into(),
                repository_count: 1,
            }]
        })?;
        let docs = environment
            .get_template("docs.html")?
            .render(context! { version => "test" })?;
        assert!(docs.contains("AkurAI Build documentation"));
        assert!(docs.contains("akurai_run_queue"));
        assert!(!docs.contains("Bunfork"));
        assert!(docs.contains("AkurAI <b>Build</b>"));
        assert!(!docs.contains("AkurAI <b>/Build</b>"));
        assert!(page.contains("olibuijr /</span> AkurAI-Build"));
        assert!(page.contains("Trending repositories"));
        assert!(page.contains("Public builders"));
        assert!(!page.contains("<script>"));
        Ok(())
    }

    #[tokio::test]
    async fn public_errors_negotiate_branded_html_and_json() -> Result<()> {
        let environment = template_environment()?;
        let mut html_headers = HeaderMap::new();
        html_headers.insert(
            header::ACCEPT,
            HeaderValue::from_static("text/html,application/xhtml+xml"),
        );
        let html = public_error_response(
            &environment,
            &html_headers,
            ApiError::not_found("missing"),
            "Page not found",
            "This page could not be found.",
            "Return home or open the documentation.",
        )
        .map_err(|error| anyhow::anyhow!(error.message))?;
        assert_eq!(html.status(), StatusCode::NOT_FOUND);
        assert_eq!(
            html.headers().get(header::CONTENT_TYPE),
            Some(&HeaderValue::from_static("text/html; charset=utf-8"))
        );
        let html_body = to_bytes(html.into_body(), MAX_BODY).await?;
        let html_body = String::from_utf8(html_body.to_vec())?;
        assert!(html_body.contains("AkurAI <b>Build</b>"));
        assert!(html_body.contains("Go home"));
        assert!(!html_body.contains("{\"ok\":false"));

        let mut json_headers = HeaderMap::new();
        json_headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
        let json = public_error_response(
            &environment,
            &json_headers,
            ApiError::not_found("missing"),
            "Page not found",
            "This page could not be found.",
            "Return home or open the documentation.",
        )
        .map_err(|error| anyhow::anyhow!(error.message))?;
        assert_eq!(json.status(), StatusCode::NOT_FOUND);
        assert_eq!(
            json.headers().get(header::CONTENT_TYPE),
            Some(&HeaderValue::from_static("application/json"))
        );
        let json_body = to_bytes(json.into_body(), MAX_BODY).await?;
        let json_body: Value = serde_json::from_slice(&json_body)?;
        assert_eq!(json_body["error"]["code"], "not_found");
        assert_eq!(json_body["error"]["message"], "missing");
        Ok(())
    }

    /// Insert a live session into the state's database and return its cookie.
    fn authenticated_session_cookie(state: &AppState) -> Result<String> {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_secs() as i64;
        let session = Session {
            id: "1".repeat(64),
            sub: "test-user".into(),
            email: "test@example.com".into(),
            role: "member".into(),
            organization_id: None,
            tenant_id: None,
            groups: Vec::new(),
            id_token: String::new(),
            created_at: now,
            expires_at: now + oidc::SESSION_TTL,
        };
        state.database.create_session(&session)?;
        Ok(format!("{}={}", oidc::SESSION_COOKIE, session.id))
    }

    #[tokio::test]
    async fn not_found_handlers_negotiate_branded_html_and_json_end_to_end() -> Result<()> {
        let state = test_state("api-token-123", "webhook-secret");
        let session = authenticated_session_cookie(&state)?;
        let cases = [
            ("/missing", "This page could not be found.", None),
            (
                "/repos/missing",
                "This repository could not be found.",
                None,
            ),
            (
                "/repos/missing/tree/main",
                "Repository content could not be found.",
                None,
            ),
            (
                "/repos/missing/tree/main/src/missing.rs",
                "Repository content could not be found.",
                None,
            ),
            (
                "/app/repositories/missing/tree/main",
                "Repository content could not be found.",
                Some(session.as_str()),
            ),
            (
                "/app/repositories/missing/tree/main/src/missing.rs",
                "Repository content could not be found.",
                Some(session.as_str()),
            ),
        ];

        for (uri, heading, cookie) in cases {
            let mut request = Request::builder()
                .uri(uri)
                .header(header::ACCEPT, "text/html,application/xhtml+xml");
            if let Some(cookie) = cookie {
                request = request.header(header::COOKIE, cookie);
            }
            let mut app = test_router(state.clone());
            let response = app.call(request.body(Body::empty())?).await?;
            assert_eq!(response.status(), StatusCode::NOT_FOUND, "{uri}");
            assert_eq!(
                response.headers().get(header::CONTENT_TYPE),
                Some(&HeaderValue::from_static("text/html; charset=utf-8")),
                "{uri}"
            );
            let body = to_bytes(response.into_body(), MAX_BODY).await?;
            let body = String::from_utf8(body.to_vec())?;
            assert!(body.contains("AkurAI <b>Build</b>"), "{uri}");
            assert!(body.contains(heading), "{uri}");
            assert!(!body.contains("{\"ok\":false"), "{uri}");

            let mut request = Request::builder()
                .uri(uri)
                .header(header::ACCEPT, "application/json");
            if let Some(cookie) = cookie {
                request = request.header(header::COOKIE, cookie);
            }
            let mut app = test_router(state.clone());
            let response = app.call(request.body(Body::empty())?).await?;
            assert_eq!(response.status(), StatusCode::NOT_FOUND, "{uri}");
            assert_eq!(
                response.headers().get(header::CONTENT_TYPE),
                Some(&HeaderValue::from_static("application/json")),
                "{uri}"
            );
            let body = json_body(response).await;
            assert_eq!(body["ok"], false, "{uri}");
            assert_eq!(body["error"]["code"], "not_found", "{uri}");
        }
        Ok(())
    }

    #[test]
    fn directory_sizes_render_as_not_applicable() -> Result<()> {
        let environment = template_environment()?;
        let entries = || {
            vec![
                CodeEntryView {
                    name: "src".into(),
                    kind: "tree".into(),
                    size: 0,
                    commit_message: None,
                    href: "/repos/AkurAI-Build/tree/main/src".into(),
                },
                CodeEntryView {
                    name: "Cargo.toml".into(),
                    kind: "blob".into(),
                    size: 42,
                    commit_message: None,
                    href: "/repos/AkurAI-Build/tree/main/Cargo.toml".into(),
                },
            ]
        };
        let page = |public: bool| CodePage {
            repository: RepositoryView {
                id: 1,
                name: "AkurAI-Build".into(),
                url: "https://example.com/AkurAI-Build.git".into(),
                default_branch: "main".into(),
                visibility: "public".into(),
                created_at: 0,
            },
            reference: "main".into(),
            path: String::new(),
            entries: entries(),
            breadcrumbs: vec![CodeBreadcrumb {
                label: "AkurAI-Build".into(),
                href: "/repos/AkurAI-Build/tree/main".into(),
            }],
            source: None,
            is_file: false,
            parent_href: "/repos/AkurAI-Build".into(),
            latest_commit: None,
            commit_href: None,
            public,
            description: None,
            counts: None,
            readme_html: None,
            readme_name: None,
            recent_commits: Vec::new(),
            contributors: Vec::new(),
            languages: Vec::new(),
        };

        // Public branch: GitHub-style file rows; trees show a dash, blobs a size.
        let rendered = environment
            .get_template("repository-code.html")?
            .render(context! { page => page(true) })?;
        assert!(rendered.contains("<span class=\"pt-repo-file-age\">&mdash;</span>"));
        assert!(rendered.contains("<span class=\"pt-repo-file-age\">42</span>"));
        assert!(!rendered.contains("<span class=\"pt-repo-file-age\">0</span>"));

        // App fallback branch keeps the attached-table markup.
        let rendered = environment
            .get_template("repository-code.html")?
            .render(context! { page => page(false) })?;
        assert!(rendered.contains(
            "<td data-label=\"Size\"><span aria-label=\"Not applicable\">&mdash;</span></td>"
        ));
        assert!(rendered.contains("<td data-label=\"Size\">42</td>"));
        assert!(!rendered.contains("<td data-label=\"Size\">0</td>"));
        Ok(())
    }

    #[test]
    fn code_path_segments_encode_special_characters_and_separators() {
        assert_eq!(encode_code_segment("!*'()"), "%21%2A%27%28%29");
        assert_eq!(
            code_path_href(
                "/app/repositories/repo/tree/main",
                "bang!/star*/quote'/left(/right)"
            ),
            "/app/repositories/repo/tree/main/bang%21/star%2A/quote%27/left%28/right%29"
        );
        assert_eq!(encode_code_segment("../secret\\file"), "..%2Fsecret%5Cfile");
    }

    #[test]
    fn public_repository_owner_uses_remote_namespace() {
        assert_eq!(
            repository_owner("https://github.com/olibuijr/AkurAI-Build.git"),
            "olibuijr"
        );
        assert_eq!(
            repository_owner("git@gitlab.example.com:platform/service.git"),
            "platform"
        );
        assert_eq!(
            repository_owner("https://akurai-build.example.com/git/service.git"),
            "AkurAI Build"
        );
    }

    #[test]
    fn hosted_repository_url_uses_public_clone_route() {
        let root = FsPath::new("/home/olibuijr/.local/share/akurai-build");
        assert_eq!(
            repository_client_url(
                "AkurAI-Build",
                "/home/olibuijr/.local/share/akurai-build/hosted/AkurAI-Build.git",
                root,
                "https://akurai-build.olibuijr.com",
            ),
            "https://akurai-build.olibuijr.com/git/AkurAI-Build.git"
        );
        assert_eq!(
            repository_client_url(
                "AkurAI-Build",
                "https://github.com/olibuijr/AkurAI-Build.git",
                root,
                "https://akurai-build.olibuijr.com",
            ),
            "https://github.com/olibuijr/AkurAI-Build.git"
        );
    }

    #[test]
    fn authentication_helpers_fail_closed() {
        assert!(constant_time_equal(b"same", b"same"));
        assert!(!constant_time_equal(b"same", b"different"));
        assert!(decode_hex("not-hex").is_err());
    }

    #[test]
    fn durable_session_cookie_authorizes_without_a_bearer_token() -> Result<()> {
        let state = test_state("api-token-123", "webhook-secret");
        let cookie = authenticated_session_cookie(&state)?;
        let mut headers = HeaderMap::new();
        headers.insert(header::COOKIE, HeaderValue::from_str(&cookie)?);
        assert!(authorize(&headers, &state).is_ok());
        assert!(browser_session(&headers, &state).is_some());
        // A member session is not a platform superuser.
        assert!(!superuser_session(&headers, &state));

        // An unknown cookie value is refused; nothing is derivable client-side.
        let mut forged = HeaderMap::new();
        forged.insert(
            header::COOKIE,
            HeaderValue::from_str(&format!("{}={}", oidc::SESSION_COOKIE, "9".repeat(64)))?,
        );
        assert!(authorize(&forged, &state).is_err());
        Ok(())
    }

    #[test]
    fn expired_session_cookies_are_rejected() -> Result<()> {
        let state = test_state("api-token-123", "webhook-secret");
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)?
            .as_secs() as i64;
        let session = Session {
            id: "2".repeat(64),
            sub: "test-user".into(),
            email: "test@example.com".into(),
            role: "owner".into(),
            organization_id: None,
            tenant_id: None,
            groups: Vec::new(),
            id_token: String::new(),
            created_at: now - oidc::SESSION_TTL - 60,
            expires_at: now - 1,
        };
        state.database.create_session(&session)?;
        let mut headers = HeaderMap::new();
        headers.insert(
            header::COOKIE,
            HeaderValue::from_str(&format!("{}={}", oidc::SESSION_COOKIE, session.id))?,
        );
        assert!(browser_session(&headers, &state).is_none());
        assert!(!superuser_session(&headers, &state));
        assert_eq!(
            authorize(&headers, &state)
                .expect_err("expired session must not authorize")
                .status,
            StatusCode::UNAUTHORIZED
        );
        Ok(())
    }

    #[test]
    fn git_smart_http_paths_and_cgi_responses_are_bounded() -> Result<()> {
        validate_git_path("tasks.git/info/refs")?;
        assert!(validate_git_path("../tasks.git/info/refs").is_err());
        assert!(validate_git_path("tasks/info/refs").is_err());

        let response = git_cgi_response(
            b"Status: 200 OK\r\nContent-Type: application/x-git-upload-pack-result\r\n\r\npack",
        )?;
        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(
            response.headers().get(header::CONTENT_TYPE),
            Some(&HeaderValue::from_static(
                "application/x-git-upload-pack-result"
            ))
        );
        Ok(())
    }

    // ── constant_time_equal boundaries ──

    #[test]
    fn constant_time_equal_rejects_empty_input() {
        assert!(!constant_time_equal(b"", b"token"));
        assert!(!constant_time_equal(b"token", b""));
    }

    #[test]
    fn release_download_path_rejects_traversal_and_hidden_files() {
        assert!(valid_download_path("bifrost-0.2.2-x86_64.iso"));
        assert!(valid_download_path(
            "alpm/x86_64/bifrost-system-0.2.2-1-any.pkg.tar.zst"
        ));
        assert!(!valid_download_path("../secret"));
        assert!(!valid_download_path("nested/../file.iso"));
        assert!(!valid_download_path("nested/.hidden"));
        assert!(!valid_download_path(""));
    }

    #[test]
    fn constant_time_equal_rejects_wrong_token() {
        assert!(!constant_time_equal(b"correct", b"wrong-token"));
        assert!(!constant_time_equal(b"xyz", b"abc"));
    }

    #[test]
    fn constant_time_equal_rejects_too_short_token() {
        assert!(!constant_time_equal(b"secret", b"sec"));
    }

    #[test]
    fn constant_time_equal_rejects_correct_prefix_but_longer_token() {
        // "secret" is a prefix of "secret-extended" — must be rejected
        assert!(!constant_time_equal(b"secret", b"secret-extended"));
        assert!(!constant_time_equal(b"secret-extended", b"secret"));
    }

    #[test]
    fn constant_time_equal_accepts_exact_match_only() {
        assert!(constant_time_equal(
            b"exact-token-value",
            b"exact-token-value"
        ));
        assert!(constant_time_equal(b"a", b"a"));
    }

    // ── decode_hex boundaries ──

    #[test]
    fn decode_hex_rejects_odd_length() {
        assert!(decode_hex("abc").is_err());
    }

    #[test]
    fn decode_hex_rejects_non_hex_characters() {
        assert!(decode_hex("zz").is_err());
        assert!(decode_hex("gg12").is_err());
    }

    #[test]
    fn decode_hex_accepts_valid_hex() -> Result<()> {
        assert_eq!(decode_hex("48656c6c6f")?, b"Hello");
        assert_eq!(decode_hex("00ff")?, vec![0x00, 0xff]);
        assert_eq!(decode_hex("")?, Vec::<u8>::new());
        Ok(())
    }

    // ── authorize header parsing ──

    fn test_state(token: &str, webhook_secret: &str) -> AppState {
        const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
        let temp = tempfile::tempdir().expect("tempdir");
        let database = Database::memory(KEY).expect("database");
        let runner = Runner::new(database.clone(), temp.path().to_owned(), false).expect("runner");
        let tree = RepoTree::new(temp.path()).expect("tree");
        AppState {
            database,
            runner,
            tree,
            blocking_git: BlockingGit::new(MAX_BLOCKING_GIT),
            hosted_root: temp.path().to_owned(),
            token: Arc::from(token),
            webhook_secret: Arc::from(webhook_secret),
            public_url: Arc::from("https://build.example"),
            templates: Arc::new(template_environment().expect("templates")),
        }
    }

    #[test]
    fn authorize_rejects_missing_authorization_header() {
        let state = test_state("api-token-123", "wh-secret");
        let headers = HeaderMap::new();
        let result = authorize(&headers, &state);
        assert!(result.is_err());
        assert_eq!(
            result.expect_err("should be unauthorized").status,
            StatusCode::UNAUTHORIZED
        );
    }

    #[test]
    fn authorize_rejects_malformed_scheme_not_bearer() {
        let state = test_state("api-token-123", "wh-secret");
        let mut headers = HeaderMap::new();
        headers.insert(
            header::AUTHORIZATION,
            HeaderValue::from_static("Basic some-value"),
        );
        let result = authorize(&headers, &state);
        assert!(result.is_err());
    }

    #[test]
    fn authorize_rejects_bearer_with_wrong_token() {
        let state = test_state("api-token-123", "wh-secret");
        let mut headers = HeaderMap::new();
        headers.insert(
            header::AUTHORIZATION,
            HeaderValue::from_static("Bearer wrong-value"),
        );
        let result = authorize(&headers, &state);
        assert!(result.is_err());
    }

    #[test]
    fn authorize_accepts_correct_bearer_token() {
        let state = test_state("api-token-123", "wh-secret");
        let mut headers = HeaderMap::new();
        headers.insert(
            header::AUTHORIZATION,
            HeaderValue::from_static("Bearer api-token-123"),
        );
        assert!(authorize(&headers, &state).is_ok());
    }

    #[test]
    fn authorize_webhook_rejects_missing_header() {
        let state = test_state("api-token", "webhook-secret");
        let headers = HeaderMap::new();
        let result = authorize_webhook(&headers, &state, b"{}");
        assert!(result.is_err());
        assert_eq!(
            result.expect_err("should be unauthorized").status,
            StatusCode::UNAUTHORIZED
        );
    }

    #[test]
    fn authorize_webhook_rejects_wrong_token() {
        let state = test_state("api-token", "webhook-secret");
        let mut headers = HeaderMap::new();
        headers.insert("x-gitlab-token", HeaderValue::from_static("wrong-token"));
        let result = authorize_webhook(&headers, &state, b"{}");
        assert!(result.is_err());
    }

    #[test]
    fn authorize_webhook_accepts_correct_x_gitlab_token() {
        let state = test_state("api-token", "webhook-secret");
        let mut headers = HeaderMap::new();
        headers.insert("x-gitlab-token", HeaderValue::from_static("webhook-secret"));
        assert!(authorize_webhook(&headers, &state, b"{}").is_ok());
    }

    // ── ApiError envelope shape ──

    #[tokio::test]
    async fn unauthorized_error_response_has_correct_envelope_shape() {
        let error = ApiError::unauthorized();
        let response = error.into_response();
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
        let bytes = axum::body::to_bytes(response.into_body(), 4096)
            .await
            .expect("read body");
        let body = String::from_utf8(bytes.to_vec()).expect("utf-8");
        let parsed: serde_json::Value = serde_json::from_str(&body).expect("json");
        assert_eq!(parsed["ok"], false);
        assert_eq!(parsed["error"]["code"], "unauthenticated");
        assert_eq!(parsed["error"]["login"], "/auth/login");
        assert!(
            parsed["error"]["message"]
                .as_str()
                .is_some_and(|m| !m.is_empty())
        );
    }

    #[test]
    fn readme_rewrites_relative_urls_by_kind_and_leaves_others() {
        let urls = ReadmeUrls {
            dir: "docs".into(),
            raw_prefix: "/repos/Demo/raw/main".into(),
            tree_prefix: "/repos/Demo/tree/main".into(),
        };
        let source = "\
![shot](assets/shot.webp)\n\n\
<img src=\"../logo.svg\" alt=\"logo\">\n\n\
[guide](guide.md) [site](https://example.com/x) [top](#intro)\n\n\
![escape](../../etc/passwd) ![abs](/already/abs.png)\n";
        let html = render_markdown(source, Some(&urls));
        // Relative image src -> raw endpoint, resolved against the README dir.
        assert!(html.contains("src=\"/repos/Demo/raw/main/docs/assets/shot.webp\""));
        // `..` climbs out of docs/ to the repo root.
        assert!(html.contains("src=\"/repos/Demo/raw/main/logo.svg\""));
        // Relative anchor href -> code browser, not raw.
        assert!(html.contains("href=\"/repos/Demo/tree/main/docs/guide.md\""));
        // Absolute and anchor URLs are left untouched.
        assert!(html.contains("href=\"https://example.com/x\""));
        assert!(html.contains("href=\"#intro\""));
        assert!(html.contains("src=\"/already/abs.png\""));
        // A reference escaping the repo root is dropped, not emitted.
        assert!(!html.contains("etc/passwd"));
    }

    // ── In-process app / router tests ──

    fn test_router(state: AppState) -> Router {
        Router::new()
            .route("/repos/{repository}", get(public_repository))
            .route(
                "/repos/{repository}/tree/{reference}",
                get(public_code_root),
            )
            .route(
                "/repos/{repository}/tree/{reference}/{*path}",
                get(public_code_path),
            )
            .route(
                "/repos/{repository}/raw/{reference}/{*path}",
                get(public_raw_content),
            )
            .route(
                "/app/repositories/{repository}/raw/{reference}/{*path}",
                get(app_raw_content),
            )
            .route(
                "/app/repositories/{repository}/tree/{reference}",
                get(app_code_root),
            )
            .route(
                "/app/repositories/{repository}/tree/{reference}/{*path}",
                get(app_code_path),
            )
            .route("/api/health", get(health))
            .route("/api/state", get(api_state))
            .route("/api/runs/{id}/promote/{environment}", post(promote))
            .route("/api/hooks/{repository}", post(webhook))
            .route("/api/repos", post(create_repository))
            .fallback(public_not_found)
            .with_state(state)
            .layer(DefaultBodyLimit::max(MAX_BODY))
            .layer(middleware::from_fn(security_headers))
    }

    async fn json_body(response: Response) -> serde_json::Value {
        let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
            .await
            .expect("read body")
            .to_vec();
        serde_json::from_slice(&bytes).expect("json")
    }

    #[tokio::test]
    async fn app_unauthenticated_api_returns_401() {
        let mut app = test_router(test_state("api-token-123", "webhook-secret"));
        let request = Request::builder()
            .uri("/api/state")
            .body(Body::empty())
            .expect("build request");
        let response = app.call(request).await.expect("call");
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
        let body = json_body(response).await;
        assert_eq!(body["ok"], false);
        assert_eq!(body["error"]["code"], "unauthenticated");
        assert_eq!(body["error"]["login"], "/auth/login");
    }

    #[tokio::test]
    async fn app_health_is_public_200() {
        let mut app = test_router(test_state("api-token-123", "webhook-secret"));
        let request = Request::builder()
            .uri("/api/health")
            .body(Body::empty())
            .expect("build request");
        let response = app.call(request).await.expect("call");
        assert_eq!(response.status(), StatusCode::OK);
        let body = json_body(response).await;
        assert_eq!(body["ok"], true);
    }

    #[tokio::test]
    async fn app_security_headers_are_present() {
        let mut app = test_router(test_state("api-token-123", "webhook-secret"));
        let request = Request::builder()
            .uri("/api/health")
            .body(Body::empty())
            .expect("build request");
        let response = app.call(request).await.expect("call");
        let headers = response.headers();
        assert_eq!(
            headers
                .get(header::X_CONTENT_TYPE_OPTIONS)
                .and_then(|v| v.to_str().ok()),
            Some("nosniff")
        );
        assert_eq!(
            headers
                .get(header::X_FRAME_OPTIONS)
                .and_then(|v| v.to_str().ok()),
            Some("DENY")
        );
        let csp = headers
            .get(header::CONTENT_SECURITY_POLICY)
            .and_then(|v| v.to_str().ok())
            .expect("CSP header");
        assert!(
            csp.contains("script-src 'self'"),
            "CSP must include script-src 'self': {csp}"
        );
    }

    #[tokio::test]
    async fn app_webhook_rejects_bad_token() {
        let mut app = test_router(test_state("api-token-123", "webhook-secret"));
        let request = Request::builder()
            .uri("/api/hooks/my-repo")
            .method("POST")
            .header("x-gitlab-token", "wrong-value")
            .header(header::CONTENT_TYPE, "application/json")
            .body(Body::from(r#"{"ref":"refs/heads/main","after":"abc123"}"#))
            .expect("build request");
        let response = app.call(request).await.expect("call");
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
    }

    #[tokio::test]
    async fn app_promote_requires_authorization() {
        let mut app = test_router(test_state("api-token-123", "webhook-secret"));
        let request = Request::builder()
            .uri("/api/runs/1/promote/staging")
            .method("POST")
            .header(header::CONTENT_TYPE, "application/json")
            .body(Body::empty())
            .expect("build request");
        let response = app.call(request).await.expect("call");
        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
    }

    #[tokio::test]
    async fn app_body_limit_rejects_oversized_request() {
        let state = test_state("api-token-123", "webhook-secret");
        let mut app = Router::new()
            .route("/api/repos", post(create_repository))
            .with_state(state)
            .layer(DefaultBodyLimit::max(8))
            .layer(middleware::from_fn(security_headers));
        let oversized = vec![b'a'; 64];
        let request = Request::builder()
            .uri("/api/repos")
            .method("POST")
            .header(header::CONTENT_TYPE, "application/json")
            .header(
                header::AUTHORIZATION,
                HeaderValue::from_static("Bearer api-token-123"),
            )
            .body(Body::from(oversized))
            .expect("build request");
        let response = app.call(request).await.expect("call");
        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
    }

    #[tokio::test]
    async fn app_unknown_route_returns_404() {
        let mut app = test_router(test_state("api-token-123", "webhook-secret"));
        let request = Request::builder()
            .uri("/nonexistent")
            .body(Body::empty())
            .expect("build request");
        let response = app.call(request).await.expect("call");
        assert_eq!(response.status(), StatusCode::NOT_FOUND);
    }

    #[tokio::test]
    async fn app_wrong_method_returns_405() {
        let mut app = test_router(test_state("api-token-123", "webhook-secret"));
        // GET /api/repos is not defined, only POST
        let request = Request::builder()
            .uri("/api/repos")
            .body(Body::empty())
            .expect("build request");
        let response = app.call(request).await.expect("call");
        assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
    }

    #[tokio::test]
    async fn app_authorized_api_state_accepts_valid_token() {
        let mut app = test_router(test_state("api-token-123", "webhook-secret"));
        let request = Request::builder()
            .uri("/api/state")
            .header(
                header::AUTHORIZATION,
                HeaderValue::from_static("Bearer api-token-123"),
            )
            .body(Body::empty())
            .expect("build request");
        let response = app.call(request).await.expect("call");
        // Should be OK even though DB queries might fail on empty tables;
        // the authorize gate passes so we either get 200 or a DB error,
        // but never 401.
        assert_ne!(response.status(), StatusCode::UNAUTHORIZED);
    }
    #[tokio::test]
    async fn blocking_git_limits_concurrency_and_preserves_api_errors() {
        use std::sync::atomic::{AtomicUsize, Ordering};

        let gate = BlockingGit::new(1);
        let active = Arc::new(AtomicUsize::new(0));
        let maximum = Arc::new(AtomicUsize::new(0));
        let operation = |active: Arc<AtomicUsize>, maximum: Arc<AtomicUsize>| {
            move || {
                let current = active.fetch_add(1, Ordering::SeqCst) + 1;
                maximum.fetch_max(current, Ordering::SeqCst);
                std::thread::sleep(Duration::from_millis(25));
                active.fetch_sub(1, Ordering::SeqCst);
                Ok(())
            }
        };
        let first = gate.run(operation(active.clone(), maximum.clone()));
        let second = gate.run(operation(active, maximum.clone()));
        let (first, second) = tokio::join!(first, second);
        assert!(first.is_ok());
        assert!(second.is_ok());
        assert_eq!(maximum.load(Ordering::SeqCst), 1);

        let error = gate
            .run(|| Err::<(), _>(ApiError::bad("specific git failure")))
            .await
            .expect_err("operation error");
        assert_eq!(error.status, StatusCode::BAD_REQUEST);
        assert_eq!(error.message, "specific git failure");
    }
}