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