grove
at @11
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 if req.path != "/api" && !req.path.starts_with("/api/") {
77 // The web pages: copy out what the page needs, then render without the lock.
78 let only = crate::web::wants(&req.path);
79 let snap = crate::web::Snapshot::of(self.store().state(), only.as_ref().map(|(o, n)| (o.as_str(), n.as_str())));
80 return crate::web::handle(&req, &snap, &self.objects);
81 }
82 match self.route(&req) {
83 Ok(r) | Err(r) => r,
84 }
85 }
86
87 fn actor(&self, req: &Request) -> Result<Option<Actor>, Response> {
88 let Some(h) = req.header("authorization") else { return Ok(None) };
89 let token = h.strip_prefix("Bearer ").ok_or_else(|| err(401, "use Authorization: Bearer <token>"))?;
90 let store = self.store();
91 store.authenticate(token.trim()).map(Some).ok_or_else(|| err(401, "unknown or revoked token"))
92 }
93
94 fn required(&self, req: &Request) -> Result<Actor, Response> {
95 self.actor(req)?.ok_or_else(|| err(401, "sign in: send a token"))
96 }
97
98 fn route(&self, req: &Request) -> Out {
99 let segs: Vec<&str> = req.path.trim_matches('/').split('/').collect();
100 let m = req.method.as_str();
101 match (m, segs.as_slice()) {
102 ("GET", ["api", "health"]) => ok(obj([("ok", true.into())])),
103 ("GET", ["api", "whoami"]) => {
104 let a = self.required(req)?;
105 let store = self.store();
106 let agent_name = a.agent.as_ref().and_then(|id| store.state().agent(id)).map(|g| g.name.clone());
107 ok(obj([("person", a.person.clone().into()), ("agent", a.agent.clone().into()), ("agent_name", agent_name.into())]))
108 }
109
110 // ── Objects ──
111 ("POST", ["api", "objects", "missing"]) => {
112 self.required(req)?;
113 let wanted = ids(&body(req)?, "ids")?;
114 let missing: Vec<Value> = wanted.iter().filter(|i| !self.objects.has(i)).map(|i| Value::Str(hex::encode(i))).collect();
115 ok(obj([("missing", Value::Array(missing))]))
116 }
117 ("POST", ["api", "objects"]) => {
118 self.required(req)?;
119 let mut stored = 0u64;
120 for raw in wire::unframe_objects(&req.body).map_err(|e| err(400, e))? {
121 self.objects.put_raw(raw).map_err(|e| err(400, e.to_string()))?;
122 stored += 1;
123 }
124 ok(obj([("stored", stored.into())]))
125 }
126 ("POST", ["api", "objects", "list"]) => {
127 let root = parse_id(field(&body(req)?, "root")?)?;
128 let all = self.objects.ids_under(&root).map_err(|e| err(404, e.to_string()))?;
129 ok(obj([("ids", Value::Array(all.iter().map(|i| Value::Str(hex::encode(i))).collect()))]))
130 }
131 ("POST", ["api", "objects", "fetch"]) => {
132 let wanted = ids(&body(req)?, "ids")?;
133 let raws = wanted.iter().map(|i| self.objects.get_raw(i)).collect::<Result<Vec<_>, _>>().map_err(|e| err(404, e.to_string()))?;
134 Ok(Response::bytes(wire::frame_objects(&raws)))
135 }
136
137 // ── People, agents, tokens, groups ──
138 ("POST", ["api", "people"]) => {
139 // Joining takes an invite: a person, their own token, and optionally an agent
140 // with its token. A project invite also makes them a contributor.
141 let b = body(req)?;
142 let username = field(&b, "username")?.to_string();
143 let invite = field(&b, "invite")?.to_string();
144 let display = b.get("display_name").and_then(Value::as_str).unwrap_or(&username).to_string();
145 let agent_name = b.get("agent").and_then(Value::as_str).map(str::to_string);
146 let mut store = self.store();
147 if !store.invite_open(&invite) {
148 return Err(err(403, "that invite doesn't exist or was already used"));
149 }
150 store.add_person(&username, &display).map_err(from_store)?;
151 store.use_invite(&username, &invite).map_err(from_store)?;
152 let person_token = store.issue_token(&username, None).map_err(from_store)?;
153 let (agent, agent_token) = match agent_name {
154 Some(name) => {
155 let id = store.add_agent(&username, &name).map_err(from_store)?;
156 let t = store.issue_token(&username, Some(&id)).map_err(from_store)?;
157 (Value::from(id), Value::from(t))
158 }
159 None => (Value::Null, Value::Null),
160 };
161 ok(obj([("person", username.into()), ("person_token", person_token.into()), ("agent", agent), ("agent_token", agent_token)]))
162 }
163 ("POST", ["api", "invites"]) => {
164 let a = self.required(req)?;
165 let b = body(req)?;
166 let project = match (b.get("owner").and_then(Value::as_str), b.get("project").and_then(Value::as_str)) {
167 (Some(o), Some(n)) => Some((o.to_string(), n.to_string())),
168 _ => None,
169 };
170 let code = self.store().create_invite(&a, project.as_ref().map(|(o, n)| (o.as_str(), n.as_str()))).map_err(from_store)?;
171 ok(obj([("invite", code.into())]))
172 }
173 ("POST", ["api", "invites", "accept"]) => {
174 let a = self.required(req)?;
175 let code = field(&body(req)?, "invite")?.to_string();
176 let project = self.store().use_invite(&a.person, &code).map_err(from_store)?;
177 ok(obj([("project", project.map(|(o, n)| format!("{o}/{n}")).into())]))
178 }
179 ("POST", ["api", "agents"]) => {
180 let a = self.required(req)?;
181 if a.agent.is_some() {
182 return Err(err(403, "agents are registered in person"));
183 }
184 let name = field(&body(req)?, "name")?.to_string();
185 let id = self.store().add_agent(&a.person, &name).map_err(from_store)?;
186 ok(obj([("id", id.into())]))
187 }
188 ("POST", ["api", "tokens"]) => {
189 let a = self.required(req)?;
190 if a.agent.is_some() {
191 return Err(err(403, "tokens are issued in person"));
192 }
193 let b = body(req)?;
194 let agent = b.get("agent").and_then(Value::as_str).map(str::to_string);
195 let token = self.store().issue_token(&a.person, agent.as_deref()).map_err(from_store)?;
196 ok(obj([("token", token.into())]))
197 }
198 ("POST", ["api", "groups"]) => {
199 let a = self.required(req)?;
200 let name = field(&body(req)?, "name")?.to_string();
201 self.store().create_group(&a, &name).map_err(from_store)?;
202 ok(obj([("name", name.into())]))
203 }
204 ("POST", ["api", "groups", group, "members"]) => {
205 let a = self.required(req)?;
206 let person = field(&body(req)?, "person")?.to_string();
207 self.store().add_member(&a, group, &person).map_err(from_store)?;
208 ok(obj([("added", person.into())]))
209 }
210
211 // ── Projects ──
212 ("POST", ["api", "projects"]) => {
213 let a = self.required(req)?;
214 let b = body(req)?;
215 let owner = b.get("owner").and_then(Value::as_str).unwrap_or(&a.person).to_string();
216 let name = field(&b, "name")?.to_string();
217 let public = !matches!(b.get("public"), Some(Value::Bool(false)));
218 self.store().create_project(&a, &owner, &name, public).map_err(from_store)?;
219 ok(obj([("owner", owner.into()), ("name", name.into())]))
220 }
221 (_, ["api", "projects", owner, name, rest @ ..]) => self.project_route(req, m, owner, name, rest),
222 _ => Err(err(404, "no such endpoint")),
223 }
224 }
225
226 fn project_route(&self, req: &Request, m: &str, owner: &str, name: &str, rest: &[&str]) -> Out {
227 let actor = self.actor(req)?;
228 let mut store = self.store();
229 {
230 let p = store.project(owner, name).map_err(|_| err(404, format!("no project {owner}/{name}")))?;
231 let can_read = p.public || actor.as_ref().is_some_and(|a| store.state().owns(&p.owner, &a.person));
232 if !can_read {
233 return Err(err(404, format!("no project {owner}/{name}")));
234 }
235 }
236 if m == "GET" && rest.is_empty() {
237 let p = store.project(owner, name).unwrap();
238 return ok(wire::project_json(store.state(), p));
239 }
240 let a = actor.ok_or_else(|| err(401, "sign in: send a token"))?;
241 let b = body(req)?;
242 match (m, rest) {
243 ("POST", ["points"]) => {
244 let snapshot = parse_id(field(&b, "snapshot")?)?;
245 let message = field(&b, "message")?.to_string();
246 let n = store.record_point(&a, owner, name, &snapshot, &message, None).map_err(from_store)?;
247 ok(obj([("n", n.into())]))
248 }
249 ("POST", ["move"]) => {
250 let to = field(&b, "to")?.to_string();
251 store.move_project(&a, owner, name, &to).map_err(from_store)?;
252 ok(obj([("owner", to.into())]))
253 }
254 ("POST", ["changes"]) => {
255 let intent = field(&b, "intent")?.to_string();
256 let n = store.open_change(&a, owner, name, &intent).map_err(from_store)?;
257 ok(obj([("number", n.into())]))
258 }
259 ("POST", ["changes", c, sub]) => {
260 let c: u64 = c.parse().map_err(|_| err(400, "bad change number"))?;
261 match *sub {
262 "revisions" => {
263 let base = num(&b, "base")?;
264 let steps = b
265 .get("steps")
266 .and_then(Value::as_array)
267 .ok_or_else(|| err(400, "missing steps"))?
268 .iter()
269 .map(|st| Ok(Step { id: field(st, "id")?.to_string(), snapshot: parse_id(field(st, "snapshot")?)?, message: field(st, "message")?.to_string() }))
270 .collect::<Result<Vec<_>, Response>>()?;
271 let note = b.get("note").and_then(Value::as_str);
272 let n = store.revise(&a, owner, name, c, base, &steps, note).map_err(from_store)?;
273 ok(obj([("number", (n as u64).into())]))
274 }
275 "state" => {
276 let to = ChangeState::parse(field(&b, "state")?).ok_or_else(|| err(400, "bad state"))?;
277 store.set_state(&a, owner, name, c, to).map_err(from_store)?;
278 ok(obj([("state", to.name().into())]))
279 }
280 "land" => {
281 let (first, last) = store.land(&a, owner, name, c).map_err(from_store)?;
282 ok(obj([("first", first.into()), ("last", last.into())]))
283 }
284 "messages" => {
285 store.say(&a, owner, name, c, field(&b, "body")?).map_err(from_store)?;
286 ok(obj([("ok", true.into())]))
287 }
288 "evidence" => {
289 let revision = num(&b, "revision")? as u32;
290 let content = match (b.get("blob").and_then(Value::as_str), b.get("url").and_then(Value::as_str)) {
291 (Some(blob), None) => Content::Blob(parse_id(blob)?),
292 (None, Some(url)) => Content::Url(url),
293 _ => return Err(err(400, "evidence is a blob or a url")),
294 };
295 store.attach(&a, owner, name, c, revision, field(&b, "title")?, field(&b, "type")?, content).map_err(from_store)?;
296 ok(obj([("ok", true.into())]))
297 }
298 _ => Err(err(404, "no such endpoint")),
299 }
300 }
301 _ => Err(err(404, "no such endpoint")),
302 }
303 }
304}