grove
at @8
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 // Anyone can join: a person, their own token, and optionally an agent with its token.
134 let b = body(req)?;
135 let username = field(&b, "username")?.to_string();
136 let display = b.get("display_name").and_then(Value::as_str).unwrap_or(&username).to_string();
137 let agent_name = b.get("agent").and_then(Value::as_str).map(str::to_string);
138 let mut store = self.store();
139 store.add_person(&username, &display).map_err(from_store)?;
140 let person_token = store.issue_token(&username, None).map_err(from_store)?;
141 let (agent, agent_token) = match agent_name {
142 Some(name) => {
143 let id = store.add_agent(&username, &name).map_err(from_store)?;
144 let t = store.issue_token(&username, Some(&id)).map_err(from_store)?;
145 (Value::from(id), Value::from(t))
146 }
147 None => (Value::Null, Value::Null),
148 };
149 ok(obj([("person", username.into()), ("person_token", person_token.into()), ("agent", agent), ("agent_token", agent_token)]))
150 }
151 ("POST", ["api", "agents"]) => {
152 let a = self.required(req)?;
153 if a.agent.is_some() {
154 return Err(err(403, "agents are registered in person"));
155 }
156 let name = field(&body(req)?, "name")?.to_string();
157 let id = self.store().add_agent(&a.person, &name).map_err(from_store)?;
158 ok(obj([("id", id.into())]))
159 }
160 ("POST", ["api", "tokens"]) => {
161 let a = self.required(req)?;
162 if a.agent.is_some() {
163 return Err(err(403, "tokens are issued in person"));
164 }
165 let b = body(req)?;
166 let agent = b.get("agent").and_then(Value::as_str).map(str::to_string);
167 let token = self.store().issue_token(&a.person, agent.as_deref()).map_err(from_store)?;
168 ok(obj([("token", token.into())]))
169 }
170 ("POST", ["api", "groups"]) => {
171 let a = self.required(req)?;
172 let name = field(&body(req)?, "name")?.to_string();
173 self.store().create_group(&a, &name).map_err(from_store)?;
174 ok(obj([("name", name.into())]))
175 }
176 ("POST", ["api", "groups", group, "members"]) => {
177 let a = self.required(req)?;
178 let person = field(&body(req)?, "person")?.to_string();
179 self.store().add_member(&a, group, &person).map_err(from_store)?;
180 ok(obj([("added", person.into())]))
181 }
182
183 // ── Projects ──
184 ("POST", ["api", "projects"]) => {
185 let a = self.required(req)?;
186 let b = body(req)?;
187 let owner = b.get("owner").and_then(Value::as_str).unwrap_or(&a.person).to_string();
188 let name = field(&b, "name")?.to_string();
189 let public = !matches!(b.get("public"), Some(Value::Bool(false)));
190 self.store().create_project(&a, &owner, &name, public).map_err(from_store)?;
191 ok(obj([("owner", owner.into()), ("name", name.into())]))
192 }
193 (_, ["api", "projects", owner, name, rest @ ..]) => self.project_route(req, m, owner, name, rest),
194 _ => Err(err(404, "no such endpoint")),
195 }
196 }
197
198 fn project_route(&self, req: &Request, m: &str, owner: &str, name: &str, rest: &[&str]) -> Out {
199 let actor = self.actor(req)?;
200 let mut store = self.store();
201 {
202 let p = store.project(owner, name).map_err(|_| err(404, format!("no project {owner}/{name}")))?;
203 let can_read = p.public || actor.as_ref().is_some_and(|a| store.state().owns(&p.owner, &a.person));
204 if !can_read {
205 return Err(err(404, format!("no project {owner}/{name}")));
206 }
207 }
208 if m == "GET" && rest.is_empty() {
209 let p = store.project(owner, name).unwrap();
210 return ok(wire::project_json(store.state(), p));
211 }
212 let a = actor.ok_or_else(|| err(401, "sign in: send a token"))?;
213 let b = body(req)?;
214 match (m, rest) {
215 ("POST", ["points"]) => {
216 let snapshot = parse_id(field(&b, "snapshot")?)?;
217 let message = field(&b, "message")?.to_string();
218 let n = store.record_point(&a, owner, name, &snapshot, &message, None).map_err(from_store)?;
219 ok(obj([("n", n.into())]))
220 }
221 ("POST", ["move"]) => {
222 let to = field(&b, "to")?.to_string();
223 store.move_project(&a, owner, name, &to).map_err(from_store)?;
224 ok(obj([("owner", to.into())]))
225 }
226 ("POST", ["changes"]) => {
227 let intent = field(&b, "intent")?.to_string();
228 let n = store.open_change(&a, owner, name, &intent).map_err(from_store)?;
229 ok(obj([("number", n.into())]))
230 }
231 ("POST", ["changes", c, sub]) => {
232 let c: u64 = c.parse().map_err(|_| err(400, "bad change number"))?;
233 match *sub {
234 "revisions" => {
235 let base = num(&b, "base")?;
236 let steps = b
237 .get("steps")
238 .and_then(Value::as_array)
239 .ok_or_else(|| err(400, "missing steps"))?
240 .iter()
241 .map(|st| Ok(Step { id: field(st, "id")?.to_string(), snapshot: parse_id(field(st, "snapshot")?)?, message: field(st, "message")?.to_string() }))
242 .collect::<Result<Vec<_>, Response>>()?;
243 let note = b.get("note").and_then(Value::as_str);
244 let n = store.revise(&a, owner, name, c, base, &steps, note).map_err(from_store)?;
245 ok(obj([("number", (n as u64).into())]))
246 }
247 "state" => {
248 let to = ChangeState::parse(field(&b, "state")?).ok_or_else(|| err(400, "bad state"))?;
249 store.set_state(&a, owner, name, c, to).map_err(from_store)?;
250 ok(obj([("state", to.name().into())]))
251 }
252 "land" => {
253 let (first, last) = store.land(&a, owner, name, c).map_err(from_store)?;
254 ok(obj([("first", first.into()), ("last", last.into())]))
255 }
256 "messages" => {
257 store.say(&a, owner, name, c, field(&b, "body")?).map_err(from_store)?;
258 ok(obj([("ok", true.into())]))
259 }
260 "evidence" => {
261 let revision = num(&b, "revision")? as u32;
262 let content = match (b.get("blob").and_then(Value::as_str), b.get("url").and_then(Value::as_str)) {
263 (Some(blob), None) => Content::Blob(parse_id(blob)?),
264 (None, Some(url)) => Content::Url(url),
265 _ => return Err(err(400, "evidence is a blob or a url")),
266 };
267 store.attach(&a, owner, name, c, revision, field(&b, "title")?, field(&b, "type")?, content).map_err(from_store)?;
268 ok(obj([("ok", true.into())]))
269 }
270 _ => Err(err(404, "no such endpoint")),
271 }
272 }
273 _ => Err(err(404, "no such endpoint")),
274 }
275 }
276}