grove
at @10
1//! The HTTP API: the store's operations, with the actor taken from the request's token.
2//!
3//! `Authorization: Bearer <token>`. A person's token can do anything that person can; an
4//! agent's token acts as that agent, so the model refuses it the decisions a person must make
5//! in person (landing, declining, groups, tokens). Public projects can be read without a token.
6
7use crate::http::{Request, Response};
8use crate::json::{self, obj, Value};
9use crate::model::{Actor, ChangeState, Step};
10use crate::objects::{Id, Objects};
11use crate::store::{self, Content, Store};
12use crate::{hex, wire};
13use std::sync::Mutex;
14
15pub struct Api {
16 store: Mutex<Store>,
17 /// Object reads and writes don't need the store's lock.
18 objects: Objects,
19}
20
21type Out = Result<Response, Response>;
22
23fn err(status: u16, msg: impl AsRef<str>) -> Response {
24 Response::error(status, msg.as_ref())
25}
26
27fn from_store(e: store::Error) -> Response {
28 match e {
29 store::Error::Refused(m) => err(400, m),
30 store::Error::Behind { .. } => err(409, e.to_string()),
31 other => err(500, other.to_string()),
32 }
33}
34
35fn body(req: &Request) -> Result<Value, Response> {
36 if req.body.is_empty() {
37 return Ok(Value::Object(Vec::new()));
38 }
39 let text = std::str::from_utf8(&req.body).map_err(|_| err(400, "body isn't UTF-8"))?;
40 json::parse(text).map_err(|e| err(400, e.to_string()))
41}
42
43fn field<'a>(v: &'a Value, k: &str) -> Result<&'a str, Response> {
44 v.get(k).and_then(Value::as_str).ok_or_else(|| err(400, format!("missing {k}")))
45}
46
47fn num(v: &Value, k: &str) -> Result<u64, Response> {
48 v.get(k).and_then(Value::as_i64).filter(|n| *n >= 0).map(|n| n as u64).ok_or_else(|| err(400, format!("missing {k}")))
49}
50
51fn parse_id(h: &str) -> Result<Id, Response> {
52 hex::decode(h).and_then(|b| b.try_into().ok()).ok_or_else(|| err(400, format!("{h:?} isn't an id")))
53}
54
55fn ids(v: &Value, k: &str) -> Result<Vec<Id>, Response> {
56 v.get(k).and_then(Value::as_array).ok_or_else(|| err(400, format!("missing {k}")))?.iter().map(|x| parse_id(x.as_str().unwrap_or(""))).collect()
57}
58
59fn ok(v: Value) -> Out {
60 Ok(Response::json(200, &v))
61}
62
63impl Api {
64 pub fn new(store: Store) -> std::io::Result<Api> {
65 let objects = Objects::open(&store.dir().join("objects")).map_err(|e| std::io::Error::other(e.to_string()))?;
66 Ok(Api { store: Mutex::new(store), objects })
67 }
68
69 /// The store, even if an earlier request panicked while holding it. Store writes are
70 /// all-or-nothing, so a panic can't leave it half-changed.
71 fn store(&self) -> std::sync::MutexGuard<'_, Store> {
72 self.store.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
73 }
74
75 pub fn handle(&self, req: Request) -> Response {
76 match self.route(&req) {
77 Ok(r) | Err(r) => r,
78 }
79 }
80
81 fn actor(&self, req: &Request) -> Result<Option<Actor>, Response> {
82 let Some(h) = req.header("authorization") else { return Ok(None) };
83 let token = h.strip_prefix("Bearer ").ok_or_else(|| err(401, "use Authorization: Bearer <token>"))?;
84 let store = self.store();
85 store.authenticate(token.trim()).map(Some).ok_or_else(|| err(401, "unknown or revoked token"))
86 }
87
88 fn required(&self, req: &Request) -> Result<Actor, Response> {
89 self.actor(req)?.ok_or_else(|| err(401, "sign in: send a token"))
90 }
91
92 fn route(&self, req: &Request) -> Out {
93 let segs: Vec<&str> = req.path.trim_matches('/').split('/').collect();
94 let m = req.method.as_str();
95 match (m, segs.as_slice()) {
96 ("GET", ["api", "health"]) => ok(obj([("ok", true.into())])),
97 ("GET", ["api", "whoami"]) => {
98 let a = self.required(req)?;
99 let store = self.store();
100 let agent_name = a.agent.as_ref().and_then(|id| store.state().agent(id)).map(|g| g.name.clone());
101 ok(obj([("person", a.person.clone().into()), ("agent", a.agent.clone().into()), ("agent_name", agent_name.into())]))
102 }
103
104 // ── Objects ──
105 ("POST", ["api", "objects", "missing"]) => {
106 self.required(req)?;
107 let wanted = ids(&body(req)?, "ids")?;
108 let missing: Vec<Value> = wanted.iter().filter(|i| !self.objects.has(i)).map(|i| Value::Str(hex::encode(i))).collect();
109 ok(obj([("missing", Value::Array(missing))]))
110 }
111 ("POST", ["api", "objects"]) => {
112 self.required(req)?;
113 let mut stored = 0u64;
114 for raw in wire::unframe_objects(&req.body).map_err(|e| err(400, e))? {
115 self.objects.put_raw(raw).map_err(|e| err(400, e.to_string()))?;
116 stored += 1;
117 }
118 ok(obj([("stored", stored.into())]))
119 }
120 ("POST", ["api", "objects", "list"]) => {
121 let root = parse_id(field(&body(req)?, "root")?)?;
122 let all = self.objects.ids_under(&root).map_err(|e| err(404, e.to_string()))?;
123 ok(obj([("ids", Value::Array(all.iter().map(|i| Value::Str(hex::encode(i))).collect()))]))
124 }
125 ("POST", ["api", "objects", "fetch"]) => {
126 let wanted = ids(&body(req)?, "ids")?;
127 let raws = wanted.iter().map(|i| self.objects.get_raw(i)).collect::<Result<Vec<_>, _>>().map_err(|e| err(404, e.to_string()))?;
128 Ok(Response::bytes(wire::frame_objects(&raws)))
129 }
130
131 // ── People, agents, tokens, groups ──
132 ("POST", ["api", "people"]) => {
133 // Joining takes an invite: a person, their own token, and optionally an agent
134 // with its token. A project invite also makes them a contributor.
135 let b = body(req)?;
136 let username = field(&b, "username")?.to_string();
137 let invite = field(&b, "invite")?.to_string();
138 let display = b.get("display_name").and_then(Value::as_str).unwrap_or(&username).to_string();
139 let agent_name = b.get("agent").and_then(Value::as_str).map(str::to_string);
140 let mut store = self.store();
141 if !store.invite_open(&invite) {
142 return Err(err(403, "that invite doesn't exist or was already used"));
143 }
144 store.add_person(&username, &display).map_err(from_store)?;
145 store.use_invite(&username, &invite).map_err(from_store)?;
146 let person_token = store.issue_token(&username, None).map_err(from_store)?;
147 let (agent, agent_token) = match agent_name {
148 Some(name) => {
149 let id = store.add_agent(&username, &name).map_err(from_store)?;
150 let t = store.issue_token(&username, Some(&id)).map_err(from_store)?;
151 (Value::from(id), Value::from(t))
152 }
153 None => (Value::Null, Value::Null),
154 };
155 ok(obj([("person", username.into()), ("person_token", person_token.into()), ("agent", agent), ("agent_token", agent_token)]))
156 }
157 ("POST", ["api", "invites"]) => {
158 let a = self.required(req)?;
159 let b = body(req)?;
160 let project = match (b.get("owner").and_then(Value::as_str), b.get("project").and_then(Value::as_str)) {
161 (Some(o), Some(n)) => Some((o.to_string(), n.to_string())),
162 _ => None,
163 };
164 let code = self.store().create_invite(&a, project.as_ref().map(|(o, n)| (o.as_str(), n.as_str()))).map_err(from_store)?;
165 ok(obj([("invite", code.into())]))
166 }
167 ("POST", ["api", "invites", "accept"]) => {
168 let a = self.required(req)?;
169 let code = field(&body(req)?, "invite")?.to_string();
170 let project = self.store().use_invite(&a.person, &code).map_err(from_store)?;
171 ok(obj([("project", project.map(|(o, n)| format!("{o}/{n}")).into())]))
172 }
173 ("POST", ["api", "agents"]) => {
174 let a = self.required(req)?;
175 if a.agent.is_some() {
176 return Err(err(403, "agents are registered in person"));
177 }
178 let name = field(&body(req)?, "name")?.to_string();
179 let id = self.store().add_agent(&a.person, &name).map_err(from_store)?;
180 ok(obj([("id", id.into())]))
181 }
182 ("POST", ["api", "tokens"]) => {
183 let a = self.required(req)?;
184 if a.agent.is_some() {
185 return Err(err(403, "tokens are issued in person"));
186 }
187 let b = body(req)?;
188 let agent = b.get("agent").and_then(Value::as_str).map(str::to_string);
189 let token = self.store().issue_token(&a.person, agent.as_deref()).map_err(from_store)?;
190 ok(obj([("token", token.into())]))
191 }
192 ("POST", ["api", "groups"]) => {
193 let a = self.required(req)?;
194 let name = field(&body(req)?, "name")?.to_string();
195 self.store().create_group(&a, &name).map_err(from_store)?;
196 ok(obj([("name", name.into())]))
197 }
198 ("POST", ["api", "groups", group, "members"]) => {
199 let a = self.required(req)?;
200 let person = field(&body(req)?, "person")?.to_string();
201 self.store().add_member(&a, group, &person).map_err(from_store)?;
202 ok(obj([("added", person.into())]))
203 }
204
205 // ── Projects ──
206 ("POST", ["api", "projects"]) => {
207 let a = self.required(req)?;
208 let b = body(req)?;
209 let owner = b.get("owner").and_then(Value::as_str).unwrap_or(&a.person).to_string();
210 let name = field(&b, "name")?.to_string();
211 let public = !matches!(b.get("public"), Some(Value::Bool(false)));
212 self.store().create_project(&a, &owner, &name, public).map_err(from_store)?;
213 ok(obj([("owner", owner.into()), ("name", name.into())]))
214 }
215 (_, ["api", "projects", owner, name, rest @ ..]) => self.project_route(req, m, owner, name, rest),
216 _ => Err(err(404, "no such endpoint")),
217 }
218 }
219
220 fn project_route(&self, req: &Request, m: &str, owner: &str, name: &str, rest: &[&str]) -> Out {
221 let actor = self.actor(req)?;
222 let mut store = self.store();
223 {
224 let p = store.project(owner, name).map_err(|_| err(404, format!("no project {owner}/{name}")))?;
225 let can_read = p.public || actor.as_ref().is_some_and(|a| store.state().owns(&p.owner, &a.person));
226 if !can_read {
227 return Err(err(404, format!("no project {owner}/{name}")));
228 }
229 }
230 if m == "GET" && rest.is_empty() {
231 let p = store.project(owner, name).unwrap();
232 return ok(wire::project_json(store.state(), p));
233 }
234 let a = actor.ok_or_else(|| err(401, "sign in: send a token"))?;
235 let b = body(req)?;
236 match (m, rest) {
237 ("POST", ["points"]) => {
238 let snapshot = parse_id(field(&b, "snapshot")?)?;
239 let message = field(&b, "message")?.to_string();
240 let n = store.record_point(&a, owner, name, &snapshot, &message, None).map_err(from_store)?;
241 ok(obj([("n", n.into())]))
242 }
243 ("POST", ["move"]) => {
244 let to = field(&b, "to")?.to_string();
245 store.move_project(&a, owner, name, &to).map_err(from_store)?;
246 ok(obj([("owner", to.into())]))
247 }
248 ("POST", ["changes"]) => {
249 let intent = field(&b, "intent")?.to_string();
250 let n = store.open_change(&a, owner, name, &intent).map_err(from_store)?;
251 ok(obj([("number", n.into())]))
252 }
253 ("POST", ["changes", c, sub]) => {
254 let c: u64 = c.parse().map_err(|_| err(400, "bad change number"))?;
255 match *sub {
256 "revisions" => {
257 let base = num(&b, "base")?;
258 let steps = b
259 .get("steps")
260 .and_then(Value::as_array)
261 .ok_or_else(|| err(400, "missing steps"))?
262 .iter()
263 .map(|st| Ok(Step { id: field(st, "id")?.to_string(), snapshot: parse_id(field(st, "snapshot")?)?, message: field(st, "message")?.to_string() }))
264 .collect::<Result<Vec<_>, Response>>()?;
265 let note = b.get("note").and_then(Value::as_str);
266 let n = store.revise(&a, owner, name, c, base, &steps, note).map_err(from_store)?;
267 ok(obj([("number", (n as u64).into())]))
268 }
269 "state" => {
270 let to = ChangeState::parse(field(&b, "state")?).ok_or_else(|| err(400, "bad state"))?;
271 store.set_state(&a, owner, name, c, to).map_err(from_store)?;
272 ok(obj([("state", to.name().into())]))
273 }
274 "land" => {
275 let (first, last) = store.land(&a, owner, name, c).map_err(from_store)?;
276 ok(obj([("first", first.into()), ("last", last.into())]))
277 }
278 "messages" => {
279 store.say(&a, owner, name, c, field(&b, "body")?).map_err(from_store)?;
280 ok(obj([("ok", true.into())]))
281 }
282 "evidence" => {
283 let revision = num(&b, "revision")? as u32;
284 let content = match (b.get("blob").and_then(Value::as_str), b.get("url").and_then(Value::as_str)) {
285 (Some(blob), None) => Content::Blob(parse_id(blob)?),
286 (None, Some(url)) => Content::Url(url),
287 _ => return Err(err(400, "evidence is a blob or a url")),
288 };
289 store.attach(&a, owner, name, c, revision, field(&b, "title")?, field(&b, "type")?, content).map_err(from_store)?;
290 ok(obj([("ok", true.into())]))
291 }
292 _ => Err(err(404, "no such endpoint")),
293 }
294 }
295 _ => Err(err(404, "no such endpoint")),
296 }
297 }
298}