grove
at @6
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 pub fn handle(&self, req: Request) -> Response {
70 match self.route(&req) {
71 Ok(r) | Err(r) => r,
72 }
73 }
74
75 fn actor(&self, req: &Request) -> Result<Option<Actor>, Response> {
76 let Some(h) = req.header("authorization") else { return Ok(None) };
77 let token = h.strip_prefix("Bearer ").ok_or_else(|| err(401, "use Authorization: Bearer <token>"))?;
78 let store = self.store.lock().unwrap();
79 store.authenticate(token.trim()).map(Some).ok_or_else(|| err(401, "unknown or revoked token"))
80 }
81
82 fn required(&self, req: &Request) -> Result<Actor, Response> {
83 self.actor(req)?.ok_or_else(|| err(401, "sign in: send a token"))
84 }
85
86 fn route(&self, req: &Request) -> Out {
87 let segs: Vec<&str> = req.path.trim_matches('/').split('/').collect();
88 let m = req.method.as_str();
89 match (m, segs.as_slice()) {
90 ("GET", ["api", "health"]) => ok(obj([("ok", true.into())])),
91 ("GET", ["api", "whoami"]) => {
92 let a = self.required(req)?;
93 let store = self.store.lock().unwrap();
94 let agent_name = a.agent.as_ref().and_then(|id| store.state().agent(id)).map(|g| g.name.clone());
95 ok(obj([("person", a.person.clone().into()), ("agent", a.agent.clone().into()), ("agent_name", agent_name.into())]))
96 }
97
98 // ── Objects ──
99 ("POST", ["api", "objects", "missing"]) => {
100 self.required(req)?;
101 let wanted = ids(&body(req)?, "ids")?;
102 let missing: Vec<Value> = wanted.iter().filter(|i| !self.objects.has(i)).map(|i| Value::Str(hex::encode(i))).collect();
103 ok(obj([("missing", Value::Array(missing))]))
104 }
105 ("POST", ["api", "objects"]) => {
106 self.required(req)?;
107 let mut stored = 0u64;
108 for raw in wire::unframe_objects(&req.body).map_err(|e| err(400, e))? {
109 self.objects.put_raw(raw).map_err(|e| err(400, e.to_string()))?;
110 stored += 1;
111 }
112 ok(obj([("stored", stored.into())]))
113 }
114 ("POST", ["api", "objects", "list"]) => {
115 let root = parse_id(field(&body(req)?, "root")?)?;
116 let all = self.objects.ids_under(&root).map_err(|e| err(404, e.to_string()))?;
117 ok(obj([("ids", Value::Array(all.iter().map(|i| Value::Str(hex::encode(i))).collect()))]))
118 }
119 ("POST", ["api", "objects", "fetch"]) => {
120 let wanted = ids(&body(req)?, "ids")?;
121 let raws = wanted.iter().map(|i| self.objects.get_raw(i)).collect::<Result<Vec<_>, _>>().map_err(|e| err(404, e.to_string()))?;
122 Ok(Response::bytes(wire::frame_objects(&raws)))
123 }
124
125 // ── People, agents, tokens, groups ──
126 ("POST", ["api", "agents"]) => {
127 let a = self.required(req)?;
128 if a.agent.is_some() {
129 return Err(err(403, "agents are registered in person"));
130 }
131 let name = field(&body(req)?, "name")?.to_string();
132 let id = self.store.lock().unwrap().add_agent(&a.person, &name).map_err(from_store)?;
133 ok(obj([("id", id.into())]))
134 }
135 ("POST", ["api", "tokens"]) => {
136 let a = self.required(req)?;
137 if a.agent.is_some() {
138 return Err(err(403, "tokens are issued in person"));
139 }
140 let b = body(req)?;
141 let agent = b.get("agent").and_then(Value::as_str).map(str::to_string);
142 let token = self.store.lock().unwrap().issue_token(&a.person, agent.as_deref()).map_err(from_store)?;
143 ok(obj([("token", token.into())]))
144 }
145 ("POST", ["api", "groups"]) => {
146 let a = self.required(req)?;
147 let name = field(&body(req)?, "name")?.to_string();
148 self.store.lock().unwrap().create_group(&a, &name).map_err(from_store)?;
149 ok(obj([("name", name.into())]))
150 }
151 ("POST", ["api", "groups", group, "members"]) => {
152 let a = self.required(req)?;
153 let person = field(&body(req)?, "person")?.to_string();
154 self.store.lock().unwrap().add_member(&a, group, &person).map_err(from_store)?;
155 ok(obj([("added", person.into())]))
156 }
157
158 // ── Projects ──
159 ("POST", ["api", "projects"]) => {
160 let a = self.required(req)?;
161 let b = body(req)?;
162 let owner = b.get("owner").and_then(Value::as_str).unwrap_or(&a.person).to_string();
163 let name = field(&b, "name")?.to_string();
164 let public = !matches!(b.get("public"), Some(Value::Bool(false)));
165 self.store.lock().unwrap().create_project(&a, &owner, &name, public).map_err(from_store)?;
166 ok(obj([("owner", owner.into()), ("name", name.into())]))
167 }
168 (_, ["api", "projects", owner, name, rest @ ..]) => self.project_route(req, m, owner, name, rest),
169 _ => Err(err(404, "no such endpoint")),
170 }
171 }
172
173 fn project_route(&self, req: &Request, m: &str, owner: &str, name: &str, rest: &[&str]) -> Out {
174 let actor = self.actor(req)?;
175 let mut store = self.store.lock().unwrap();
176 {
177 let p = store.project(owner, name).map_err(|_| err(404, format!("no project {owner}/{name}")))?;
178 let can_read = p.public || actor.as_ref().is_some_and(|a| store.state().owns(&p.owner, &a.person));
179 if !can_read {
180 return Err(err(404, format!("no project {owner}/{name}")));
181 }
182 }
183 if m == "GET" && rest.is_empty() {
184 let p = store.project(owner, name).unwrap();
185 return ok(wire::project_json(store.state(), p));
186 }
187 let a = actor.ok_or_else(|| err(401, "sign in: send a token"))?;
188 let b = body(req)?;
189 match (m, rest) {
190 ("POST", ["points"]) => {
191 let snapshot = parse_id(field(&b, "snapshot")?)?;
192 let message = field(&b, "message")?.to_string();
193 let n = store.record_point(&a, owner, name, &snapshot, &message, None).map_err(from_store)?;
194 ok(obj([("n", n.into())]))
195 }
196 ("POST", ["move"]) => {
197 let to = field(&b, "to")?.to_string();
198 store.move_project(&a, owner, name, &to).map_err(from_store)?;
199 ok(obj([("owner", to.into())]))
200 }
201 ("POST", ["changes"]) => {
202 let intent = field(&b, "intent")?.to_string();
203 let n = store.open_change(&a, owner, name, &intent).map_err(from_store)?;
204 ok(obj([("number", n.into())]))
205 }
206 ("POST", ["changes", c, sub]) => {
207 let c: u64 = c.parse().map_err(|_| err(400, "bad change number"))?;
208 match *sub {
209 "revisions" => {
210 let base = num(&b, "base")?;
211 let steps = b
212 .get("steps")
213 .and_then(Value::as_array)
214 .ok_or_else(|| err(400, "missing steps"))?
215 .iter()
216 .map(|st| Ok(Step { id: field(st, "id")?.to_string(), snapshot: parse_id(field(st, "snapshot")?)?, message: field(st, "message")?.to_string() }))
217 .collect::<Result<Vec<_>, Response>>()?;
218 let note = b.get("note").and_then(Value::as_str);
219 let n = store.revise(&a, owner, name, c, base, &steps, note).map_err(from_store)?;
220 ok(obj([("number", (n as u64).into())]))
221 }
222 "state" => {
223 let to = ChangeState::parse(field(&b, "state")?).ok_or_else(|| err(400, "bad state"))?;
224 store.set_state(&a, owner, name, c, to).map_err(from_store)?;
225 ok(obj([("state", to.name().into())]))
226 }
227 "land" => {
228 let (first, last) = store.land(&a, owner, name, c).map_err(from_store)?;
229 ok(obj([("first", first.into()), ("last", last.into())]))
230 }
231 "messages" => {
232 store.say(&a, owner, name, c, field(&b, "body")?).map_err(from_store)?;
233 ok(obj([("ok", true.into())]))
234 }
235 "evidence" => {
236 let revision = num(&b, "revision")? as u32;
237 let content = match (b.get("blob").and_then(Value::as_str), b.get("url").and_then(Value::as_str)) {
238 (Some(blob), None) => Content::Blob(parse_id(blob)?),
239 (None, Some(url)) => Content::Url(url),
240 _ => return Err(err(400, "evidence is a blob or a url")),
241 };
242 store.attach(&a, owner, name, c, revision, field(&b, "title")?, field(&b, "type")?, content).map_err(from_store)?;
243 ok(obj([("ok", true.into())]))
244 }
245 _ => Err(err(404, "no such endpoint")),
246 }
247 }
248 _ => Err(err(404, "no such endpoint")),
249 }
250 }
251}