worklyn / zukapublic
Agent-first git hosting. One Rust binary: git over HTTP and SSH, a REST API, MCP, CI, and multi-tenant isolation.
Get a copy:
git clone https://zuka.worklyn.com/worklyn/zuka.git
| 1 | // REST route table for `/v1`. |
| 2 | // |
| 3 | // Handlers translate wire shape and nothing else. Access checks live on `Identity` |
| 4 | // and ref rules live in `git::store`, so REST and MCP cannot drift (SPEC §1.1). |
| 5 | |
| 6 | pub mod content; |
| 7 | pub mod git_http; |
| 8 | pub mod keys; |
| 9 | pub mod openapi; |
| 10 | pub mod repos; |
| 11 | pub mod runs; |
| 12 | |
| 13 | use crate::error::{Error, Result}; |
| 14 | use crate::http::response::Body; |
| 15 | use crate::http::{response, AppState}; |
| 16 | use hyper::body::Incoming; |
| 17 | use hyper::{Request, Response}; |
| 18 | use std::sync::Arc; |
| 19 | |
| 20 | /// Dispatch a `/v1/...` request. `tail` excludes the `v1` segment. |
| 21 | pub async fn route( |
| 22 | request: Request<Incoming>, |
| 23 | state: Arc<AppState>, |
| 24 | tail: &[&str], |
| 25 | ) -> Result<(Response<Body>, Option<String>)> { |
| 26 | // In tenant mode this verifies the control plane's assertion instead of a |
| 27 | // token. `/v1` bodies are small and already bounded, so buffering to check the |
| 28 | // body hash costs nothing meaningful here. |
| 29 | let identity = { |
| 30 | let method = request.method().as_str().to_string(); |
| 31 | let path = request.uri().path().to_string(); |
| 32 | let headers = request.headers().clone(); |
| 33 | state.identify(&headers, &method, &path)? |
| 34 | }; |
| 35 | let account = format!("{}#{}", identity.account, identity.token_id); |
| 36 | state.check_rate(&account)?; |
| 37 | |
| 38 | let response = match tail { |
| 39 | ["account"] => { |
| 40 | let account_name = identity.account.clone(); |
| 41 | let for_usage = Arc::clone(&state); |
| 42 | // One `git count-objects` per repository; not on a runtime worker. |
| 43 | let usage = |
| 44 | crate::git::exec::blocking(move || Ok(for_usage.usage(&account_name))).await?; |
| 45 | let limits = state.quota_limits(); |
| 46 | response::json( |
| 47 | hyper::StatusCode::OK, |
| 48 | &serde_json::json!({ |
| 49 | "account": identity.account.as_str(), |
| 50 | "scopes": identity.scopes, |
| 51 | "repos": identity.repos.iter().map(|r| r.as_str()).collect::<Vec<_>>(), |
| 52 | "usage": usage, |
| 53 | "limits": { |
| 54 | "max_repos": limits.max_repos, |
| 55 | "max_repo_bytes": limits.max_repo_bytes, |
| 56 | "max_account_bytes": limits.max_account_bytes, |
| 57 | "rate_per_minute": state.config.rate_per_minute, |
| 58 | }, |
| 59 | }), |
| 60 | ) |
| 61 | } |
| 62 | ["keys", rest @ ..] => keys::route(request, state, identity, rest).await?, |
| 63 | ["repos", rest @ ..] => repos::route(request, state, identity, rest).await?, |
| 64 | _ => return Err(Error::NotFound("route")), |
| 65 | }; |
| 66 | Ok((response, Some(account))) |
| 67 | } |