zuka
zuka/src/api/mod.rs

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
zuka/src/api/mod.rs
RSmod.rs2.6 KBDownload
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
6pub mod content;
7pub mod git_http;
8pub mod keys;
9pub mod openapi;
10pub mod repos;
11pub mod runs;
12
13use crate::error::{Error, Result};
14use crate::http::response::Body;
15use crate::http::{response, AppState};
16use hyper::body::Incoming;
17use hyper::{Request, Response};
18use std::sync::Arc;
19
20/// Dispatch a `/v1/...` request. `tail` excludes the `v1` segment.
21pub 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}