Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| API and MCP server in Rust; a public index at the API root | 1 | //! The public API: REST at api.g1t.sh and the MCP server at mcp.g1t.sh. |
| 2 | //! | |
| 3 | //! One Worker, two hostnames. Both are thin adapters over the same | |
| 4 | //! operations (see [`operations::Op`]), which call the services that own | |
| 5 | //! the data. This Worker holds none. | |
| 6 | ||
| 7 | mod mcp; | |
| 8 | mod oauth; | |
| 9 | mod openapi; | |
| 10 | mod operations; | |
| 11 | mod rest; | |
| 12 | ||
| 13 | use g1t_contracts::identity::{ | |
| 14 | DeviceClaim, DeviceClaimArgs, DeviceStart, DeviceStartArgs, TokenArgs, | |
| 15 | }; | |
| Acceptance checks in sandboxes, line comments and review verdicts | 16 | use g1t_contracts::work::{CheckRun, ReportChecksArgs}; |
| API and MCP server in Rust; a public index at the API root | 17 | use g1t_contracts::{Failure, FailureCode, Outcome, Viewer}; |
| 18 | use serde_json::{Value, json}; | |
| 19 | use worker::{Context, Env, Method, Request, Response, Result, event}; | |
| 20 | ||
| 21 | use operations::Services; | |
| 22 | ||
| 23 | const API: &str = "https://api.g1t.sh"; | |
| 24 | ||
| 25 | fn method_name(method: Method) -> &'static str { | |
| 26 | match method { | |
| 27 | Method::Get => "GET", | |
| 28 | Method::Post => "POST", | |
| 29 | Method::Patch => "PATCH", | |
| 30 | Method::Put => "PUT", | |
| 31 | Method::Delete => "DELETE", | |
| 32 | Method::Options => "OPTIONS", | |
| 33 | Method::Head => "HEAD", | |
| 34 | _ => "OTHER", | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | /// An error in the shape every endpoint uses. | |
| 39 | fn failure(failure: &Failure) -> Result<Response> { | |
| 40 | Ok(Response::from_json(&json!({ "error": failure }))?.with_status(failure.code.http_status())) | |
| 41 | } | |
| 42 | ||
| 43 | fn fail(code: FailureCode, message: &str) -> Result<Response> { | |
| 44 | failure(&Failure { | |
| 45 | code, | |
| 46 | message: message.to_owned(), | |
| 47 | }) | |
| 48 | } | |
| 49 | ||
| 50 | /// A request body as JSON. An empty or malformed body is no input. | |
| 51 | async fn json_body(request: &mut Request) -> Value { | |
| 52 | request.json().await.unwrap_or(Value::Null) | |
| 53 | } | |
| 54 | ||
| 55 | /// Who a request's `Authorization: Bearer g1t_…` names. A missing token is | |
| 56 | /// an anonymous viewer; a wrong one is refused, so that a typo does not | |
| 57 | /// silently look signed out. | |
| 58 | async fn authenticate( | |
| 59 | request: &Request, | |
| 60 | services: &Services, | |
| 61 | ) -> Result<std::result::Result<Viewer, Response>> { | |
| 62 | let header = request.headers().get("authorization")?.unwrap_or_default(); | |
| 63 | let token = match header.split_once(' ') { | |
| 64 | Some((scheme, token)) if scheme.eq_ignore_ascii_case("bearer") && !token.is_empty() => { | |
| 65 | token.trim() | |
| 66 | } | |
| 67 | _ => return Ok(Ok(None)), | |
| 68 | }; | |
| 69 | let viewer: Viewer = g1t_kit::call( | |
| 70 | &services.identity, | |
| 71 | "user_for_access_token", | |
| 72 | &TokenArgs { | |
| 73 | token: token.to_owned(), | |
| 74 | }, | |
| 75 | ) | |
| 76 | .await?; | |
| 77 | if viewer.is_some() { | |
| 78 | return Ok(Ok(viewer)); | |
| 79 | } | |
| 80 | let mut response = fail(FailureCode::Unauthenticated, "Invalid access token.")?; | |
| 81 | // Tells an MCP client where to sign in again. | |
| 82 | response.headers_mut().set( | |
| 83 | "www-authenticate", | |
| 84 | &format!("{}, error=\"invalid_token\"", oauth::MCP_CHALLENGE), | |
| 85 | )?; | |
| 86 | Ok(Err(response)) | |
| 87 | } | |
| 88 | ||
| 89 | /// Where everything is, for someone or something exploring the API. | |
| 90 | fn index() -> Value { | |
| 91 | let repo = format!("{API}/v1/repos/{{owner}}/{{name}}"); | |
| 92 | json!({ | |
| 93 | "documentation_url": "https://docs.g1t.sh/api/reference/", | |
| 94 | "openapi_url": format!("{API}/openapi.json"), | |
| 95 | "mcp_url": "https://mcp.g1t.sh", | |
| 96 | "current_user_url": format!("{API}/v1/user"), | |
| 97 | "workspaces_url": format!("{API}/v1/workspaces"), | |
| 98 | "repositories_url": format!("{API}/v1/repos{{?q}}"), | |
| 99 | "repository_url": repo, | |
| 100 | "repository_events_url": format!("{repo}/events{{?before}}"), | |
| 101 | "labels_url": format!("{repo}/labels"), | |
| 102 | "issues_url": format!("{repo}/issues{{?state,label}}"), | |
| 103 | "issue_url": format!("{repo}/issues/{{number}}"), | |
| 104 | "issue_comments_url": format!("{repo}/issues/{{number}}/comments"), | |
| 105 | "pulls_url": format!("{repo}/pulls{{?state}}"), | |
| 106 | "pull_url": format!("{repo}/pulls/{{number}}"), | |
| 107 | "pull_changes_url": format!("{repo}/pulls/{{number}}/changes"), | |
| Acceptance checks in sandboxes, line comments and review verdicts | 108 | "pull_reviews_url": format!("{repo}/pulls/{{number}}/reviews"), |
| API and MCP server in Rust; a public index at the API root | 109 | "pull_session_url": format!("{repo}/pulls/{{number}}/session{{?after}}"), |
| 110 | "device_code_url": format!("{API}/v1/device/code"), | |
| 111 | "device_token_url": format!("{API}/v1/device/token"), | |
| 112 | "oauth_metadata_url": format!("{API}/.well-known/oauth-authorization-server"), | |
| 113 | "git_url": "https://g1t.sh/{owner}/{name}.git", | |
| 114 | }) | |
| 115 | } | |
| 116 | ||
| 117 | // Signing in from a tool. Accounts are created, and passwords typed, only | |
| 118 | // in a browser; a tool gets its token by having a person approve a code. | |
| 119 | ||
| 120 | async fn device_code(request: &mut Request, services: &Services) -> Result<Response> { | |
| 121 | let body = json_body(request).await; | |
| 122 | let started: DeviceStart = g1t_kit::call( | |
| 123 | &services.identity, | |
| 124 | "device_start", | |
| 125 | &DeviceStartArgs { | |
| 126 | client_name: body["client_name"].as_str().unwrap_or_default().to_owned(), | |
| 127 | }, | |
| 128 | ) | |
| 129 | .await?; | |
| 130 | Response::from_json(&json!({ | |
| 131 | "device_code": started.device_code, | |
| 132 | "user_code": started.user_code, | |
| 133 | "verification_uri": "https://g1t.sh/device", | |
| 134 | "verification_uri_complete": format!("https://g1t.sh/device?code={}", started.user_code), | |
| 135 | "expires_in": started.expires_in, | |
| 136 | "interval": started.interval, | |
| 137 | })) | |
| 138 | } | |
| 139 | ||
| 140 | async fn device_token(request: &mut Request, services: &Services) -> Result<Response> { | |
| 141 | let body = json_body(request).await; | |
| 142 | let claim: DeviceClaim = g1t_kit::call( | |
| 143 | &services.identity, | |
| 144 | "device_claim", | |
| 145 | &DeviceClaimArgs { | |
| 146 | device_code: body["device_code"].as_str().unwrap_or_default().to_owned(), | |
| 147 | }, | |
| 148 | ) | |
| 149 | .await?; | |
| 150 | Response::from_json(&match claim { | |
| 151 | DeviceClaim::Approved { token, user } => json!({ | |
| 152 | "status": "approved", | |
| 153 | "token": token, | |
| 154 | "username": user.username, | |
| 155 | "verified": user.verified, | |
| 156 | }), | |
| 157 | DeviceClaim::Pending => json!({ "status": "pending" }), | |
| 158 | DeviceClaim::Denied => json!({ "status": "denied" }), | |
| 159 | DeviceClaim::Expired => json!({ "status": "expired" }), | |
| 160 | }) | |
| 161 | } | |
| 162 | ||
| Acceptance checks in sandboxes, line comments and review verdicts | 163 | /// A sandbox reporting on its run of a pull request's acceptance checks. |
| 164 | /// The run's own token, in the body, is the credential: it was given to | |
| 165 | /// that sandbox and to nothing else. | |
| 166 | async fn report_checks( | |
| 167 | request: &mut Request, | |
| 168 | services: &Services, | |
| 169 | run_id: &str, | |
| 170 | ) -> Result<Response> { | |
| 171 | let body = json_body(request).await; | |
| 172 | let reported: Outcome<CheckRun> = g1t_kit::call( | |
| 173 | &services.work, | |
| 174 | "report_checks", | |
| 175 | &ReportChecksArgs { | |
| 176 | run_id: run_id.to_owned(), | |
| 177 | token: body["token"].as_str().unwrap_or_default().to_owned(), | |
| 178 | results: serde_json::from_value(body["results"].clone()).unwrap_or_default(), | |
| 179 | error: body["error"].as_str().map(str::to_owned), | |
| 180 | skip: false, | |
| 181 | }, | |
| 182 | ) | |
| 183 | .await?; | |
| 184 | match reported { | |
| 185 | Outcome::Ok(run) => Response::from_json(&json!({ "status": run.status })), | |
| 186 | Outcome::Fail(refused) => failure(&refused), | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| API and MCP server in Rust; a public index at the API root | 190 | async fn respond(mut request: Request, env: &Env) -> Result<Response> { |
| 191 | let method = method_name(request.method()); | |
| 192 | if method == "OPTIONS" { | |
| 193 | return Ok(Response::empty()?.with_status(204)); | |
| 194 | } | |
| 195 | let url = request.url()?; | |
| 196 | let path = url.path().to_owned(); | |
| 197 | let on_mcp = url.host_str().is_some_and(|host| host.starts_with("mcp.")); | |
| 198 | let services = Services::new(env)?; | |
| 199 | ||
| 200 | let viewer = match authenticate(&request, &services).await? { | |
| 201 | Ok(viewer) => viewer, | |
| 202 | Err(refused) => return Ok(refused), | |
| 203 | }; | |
| 204 | if let Some(response) = oauth::handle(&mut request, &services, method, &path).await? { | |
| 205 | return Ok(response); | |
| 206 | } | |
| 207 | if on_mcp { | |
| 208 | return mcp::handle(request, &services, &viewer).await; | |
| 209 | } | |
| 210 | ||
| 211 | match (method, path.trim_end_matches('/')) { | |
| 212 | ("GET", "" | "/v1") => return Response::from_json(&index()), | |
| 213 | ("GET", "/openapi.json") => return Response::from_json(&openapi::document()), | |
| 214 | ("POST", "/v1/device/code") => return device_code(&mut request, &services).await, | |
| 215 | ("POST", "/v1/device/token") => return device_token(&mut request, &services).await, | |
| Acceptance checks in sandboxes, line comments and review verdicts | 216 | ("POST", path) if path.starts_with("/v1/checks/") => { |
| 217 | let run_id = path.trim_start_matches("/v1/checks/").to_owned(); | |
| 218 | return report_checks(&mut request, &services, &run_id).await; | |
| 219 | } | |
| API and MCP server in Rust; a public index at the API root | 220 | _ => {} |
| 221 | } | |
| 222 | ||
| 223 | let query: Vec<(String, String)> = url | |
| 224 | .query_pairs() | |
| 225 | .map(|(name, value)| (name.into_owned(), value.into_owned())) | |
| 226 | .collect(); | |
| 227 | let body = if method == "GET" { | |
| 228 | Value::Null | |
| 229 | } else { | |
| 230 | json_body(&mut request).await | |
| 231 | }; | |
| 232 | let Some((route, input)) = rest::resolve(method, &path, &query, body) else { | |
| 233 | return fail(FailureCode::NotFound, "No such endpoint."); | |
| 234 | }; | |
| 235 | match route.op.run(&services, &viewer, &input).await? { | |
| 236 | Outcome::Ok(value) => Response::from_json(&value), | |
| 237 | Outcome::Fail(refused) => failure(&refused), | |
| 238 | } | |
| 239 | } | |
| 240 | ||
| 241 | // The API is called from browsers too: the reference's explorer, and apps | |
| 242 | // built on g1t. It carries no cookies, so any origin may call it. | |
| 243 | #[event(fetch)] | |
| 244 | async fn fetch(request: Request, env: Env, _ctx: Context) -> Result<Response> { | |
| 245 | let mut response = respond(request, &env).await?; | |
| 246 | let headers = response.headers_mut(); | |
| 247 | headers.set("access-control-allow-origin", "*")?; | |
| 248 | headers.set( | |
| 249 | "access-control-allow-headers", | |
| 250 | "authorization, content-type", | |
| 251 | )?; | |
| 252 | headers.set("access-control-allow-methods", "GET, POST, PATCH, OPTIONS")?; | |
| 253 | headers.set("access-control-expose-headers", "www-authenticate")?; | |
| 254 | Ok(response) | |
| 255 | } |