g1t/apps/api/src/addresses.rs

136 lines5,490 bytesCodeBlame

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.

Merge branch 'worktree-agent-aaf03bdceac799c89'1//! Where this installation is reached: the site, the REST API and the MCP
2//! server. Hosted g1t sets none of the variables and gets g1t.sh's
3//! addresses; a self-hosted one sets them from its PUBLIC_URL
4//! (deploy/self-host/configs.mjs).
5//!
6//! The MCP server is on its own host hosted (`mcp.g1t.sh`). Self-hosted it
7//! can be a path on the API's host instead (`http://localhost:8789/mcp`):
8//! a request is for MCP when its host starts with `mcp.`, or when MCP_URL
9//! has a path and the request is under it.
10
11use worker::{Env, Url};
12
13pub const HOSTED_SITE: &str = "https://g1t.sh";
14pub const HOSTED_API: &str = "https://api.g1t.sh";
15pub const HOSTED_MCP: &str = "https://mcp.g1t.sh";
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct Addresses {
19 /// SITE_URL: where people sign in and approve, and where git remotes are.
20 pub site: String,
21 /// API_URL: the REST API, and the OAuth issuer.
22 pub api: String,
23 /// MCP_URL: the MCP server, and the OAuth protected resource.
24 pub mcp: String,
25}
26
27impl Default for Addresses {
28 fn default() -> Self {
29 Addresses {
30 site: HOSTED_SITE.to_owned(),
31 api: HOSTED_API.to_owned(),
32 mcp: HOSTED_MCP.to_owned(),
33 }
34 }
35}
36
37fn setting(value: Option<String>, default: &str) -> String {
38 let value = value.unwrap_or_default();
39 let value = value.trim().trim_end_matches('/');
40 if value.is_empty() { default.to_owned() } else { value.to_owned() }
41}
42
43impl Addresses {
44 pub fn from_settings(site: Option<String>, api: Option<String>, mcp: Option<String>) -> Addresses {
45 Addresses {
46 site: setting(site, HOSTED_SITE),
47 api: setting(api, HOSTED_API),
48 mcp: setting(mcp, HOSTED_MCP),
49 }
50 }
51
52 pub fn from_env(env: &Env) -> Addresses {
53 let var = |name: &str| env.var(name).ok().map(|value| value.to_string());
54 Addresses::from_settings(var("SITE_URL"), var("API_URL"), var("MCP_URL"))
55 }
56
57 /// What a client is told when it must sign in first (RFC 9728).
58 pub fn mcp_challenge(&self) -> String {
59 format!("Bearer resource_metadata=\"{}\"", self.protected_resource())
60 }
61
62 /// The protected resource metadata, at the MCP server's origin with
63 /// its path appended, as RFC 9728 places it.
64 pub fn protected_resource(&self) -> String {
65 match Url::parse(&self.mcp) {
66 Ok(url) if url.path() != "/" => {
67 let origin = url.origin().ascii_serialization();
68 format!("{origin}/.well-known/oauth-protected-resource{}", url.path().trim_end_matches('/'))
69 }
70 _ => format!("{}/.well-known/oauth-protected-resource", self.mcp),
71 }
72 }
73
74 /// A repository's git remote.
75 pub fn git_remote(&self, owner: &str, name: &str) -> String {
76 format!("{}/{owner}/{name}.git", self.site)
77 }
78
79 /// Whether a request to `url` is for the MCP server, and the path it
80 /// asks for there.
81 pub fn mcp_path(&self, url: &Url) -> Option<String> {
82 if url.host_str().is_some_and(|host| host.starts_with("mcp.")) {
83 return Some(url.path().to_owned());
84 }
85 // By path alone: behind a proxy the host a request names need not
86 // be the one people use, and no REST route starts with it.
87 let mcp = Url::parse(&self.mcp).ok()?;
88 let base = mcp.path().trim_end_matches('/');
89 if base.is_empty() {
90 return None;
91 }
92 let rest = url.path().strip_prefix(base)?;
93 (rest.is_empty() || rest.starts_with('/')).then(|| if rest.is_empty() { "/".to_owned() } else { rest.to_owned() })
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn hosted_is_the_default() {
103 let hosted = Addresses::from_settings(None, Some(String::new()), Some(" ".into()));
104 assert_eq!(hosted, Addresses::default());
105 assert_eq!(
106 hosted.mcp_challenge(),
107 "Bearer resource_metadata=\"https://mcp.g1t.sh/.well-known/oauth-protected-resource\""
108 );
109 assert_eq!(hosted.git_remote("acme", "rocket"), "https://g1t.sh/acme/rocket.git");
110 let on_mcp = Url::parse("https://mcp.g1t.sh/").unwrap();
111 assert_eq!(hosted.mcp_path(&on_mcp).as_deref(), Some("/"));
112 let on_api = Url::parse("https://api.g1t.sh/user").unwrap();
113 assert_eq!(hosted.mcp_path(&on_api), None);
114 }
115
116 #[test]
117 fn self_hosted_mcp_is_a_path_on_the_api() {
118 let own = Addresses::from_settings(
119 Some("http://localhost:8787/".into()),
120 Some("http://localhost:8789".into()),
121 Some("http://localhost:8789/mcp".into()),
122 );
123 assert_eq!(own.site, "http://localhost:8787");
124 assert_eq!(
125 own.protected_resource(),
126 "http://localhost:8789/.well-known/oauth-protected-resource/mcp"
127 );
128 let url = |text: &str| Url::parse(text).unwrap();
129 assert_eq!(own.mcp_path(&url("http://localhost:8789/mcp")).as_deref(), Some("/"));
130 assert_eq!(own.mcp_path(&url("http://localhost:8789/mcp/")).as_deref(), Some("/"));
131 assert_eq!(own.mcp_path(&url("http://localhost:8789/mcpx")), None);
132 assert_eq!(own.mcp_path(&url("http://localhost:8789/user")), None);
133 assert_eq!(own.mcp_path(&url("http://127.0.0.1:8789/mcp")).as_deref(), Some("/"), "by path, whatever the host");
134 assert_eq!(own.git_remote("acme", "rocket"), "http://localhost:8787/acme/rocket.git");
135 }
136}

This file's history is long; its oldest lines are credited to the oldest commit read.