Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

65 lines · 3.3 KBRaw History
1import { Hono } from "hono";
2import type { Env } from "../env.js";
3import { requireAccount, requireAccountScope, type AppContext, type AppVars } from "../auth/principal.js";
4import type { Account } from "../lib/directory.js";
5import { EarlyAccess, REQUEST_STATES, declineRequest, deleteRequest, inviteRequest, requestJson, type RequestState } from "../lib/early-access.js";
6import { HttpError } from "../lib/util.js";
7
8/**
9 * Early access requests over the REST API (lib/early-access.ts), for site administrators only.
10 * To anyone else these endpoints don't exist (404), like the requests themselves.
11 */
12export const earlyAccessApi = new Hono<{ Bindings: Env; Variables: AppVars }>().basePath("/api/v3");
13
14const siteAdmin = (c: AppContext, scope: "read" | "admin"): Account => {
15 const a = requireAccount(c);
16 if (!a.site_admin || a.kind !== "user") throw new HttpError(404, "not found");
17 requireAccountScope(c, scope);
18 return a;
19};
20
21const audit = (c: AppContext) => (e: Parameters<AppContext["var"]["dir"]["audit"]>[0]) => c.get("dir").audit(e);
22
23earlyAccessApi.get("/diffinery/admin/early-access", async (c) => {
24 siteAdmin(c, "read");
25 const s = c.req.query("state");
26 if (s && !REQUEST_STATES.includes(s as RequestState)) throw new HttpError(422, `state must be one of ${REQUEST_STATES.join(", ")}`);
27 const n = Number(c.req.query("per_page") ?? 100);
28 const page = Math.max(1, Math.min(1000, Number(c.req.query("page") ?? 1) || 1));
29 const limit = Number.isFinite(n) ? Math.min(Math.max(Math.trunc(n), 1), 500) : 100;
30 const { items } = await new EarlyAccess(c.env.DB).list({ state: (s as RequestState) || undefined, q: c.req.query("q") ?? "", limit, offset: (page - 1) * limit });
31 return c.json(items.map((r) => requestJson(c.env, r)));
32});
33
34earlyAccessApi.get("/diffinery/admin/early-access/:id", async (c) => {
35 siteAdmin(c, "read");
36 const r = await new EarlyAccess(c.env.DB).get(c.req.param("id"));
37 if (!r) throw new HttpError(404, "no early access request with that id");
38 return c.json(requestJson(c.env, r));
39});
40
41/** Send the request's address the instance's invitation email. Audited. */
42earlyAccessApi.post("/diffinery/admin/early-access/:id/invite", async (c) => {
43 const me = siteAdmin(c, "admin");
44 return c.json(requestJson(c.env, await inviteRequest(c.env, c.get("dir"), audit(c), me, c.req.param("id"), "api")));
45});
46
47/** Decline a request (`{"state": "declined"}`) or put a declined one back (`{"state": "confirmed"}`). Audited; nobody is emailed. */
48earlyAccessApi.patch("/diffinery/admin/early-access/:id", async (c) => {
49 const me = siteAdmin(c, "admin");
50 let b: { state?: unknown };
51 try {
52 b = (await c.req.json()) as typeof b;
53 } catch {
54 throw new HttpError(400, "request body must be JSON");
55 }
56 if (!b || typeof b !== "object" || (b.state !== "declined" && b.state !== "confirmed")) throw new HttpError(422, 'send {"state": "declined"} to decline, or {"state": "confirmed"} to put a declined request back');
57 return c.json(requestJson(c.env, await declineRequest(c.env.DB, audit(c), me, c.req.param("id"), b.state === "confirmed", "api")));
58});
59
60/** Delete a request and its daily-limit rows (data removal). Audited without its content. */
61earlyAccessApi.delete("/diffinery/admin/early-access/:id", async (c) => {
62 const me = siteAdmin(c, "admin");
63 await deleteRequest(c.env.DB, audit(c), me, c.req.param("id"), "api");
64 return c.body(null, 204);
65});