repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/the-system/board-server.ts
Rawimport { createHash, randomUUID } from "node:crypto";
import {
createReadStream,
type Dirent,
lstatSync,
readdirSync,
readFileSync,
realpathSync,
rmSync,
} from "node:fs";
import {
createServer,
type IncomingMessage,
type Server,
type ServerResponse,
} from "node:http";
import path from "node:path";
import {
type BoardItemState,
type BoardProposal,
type BoardState,
readBoardState,
updateBoardState,
writeBoardState,
} from "./board-state.ts";
import { parseSystemConfig } from "./documents.ts";
import { writePlanStatus } from "./plan-status.ts";
import { loadProjectIndex, type ProjectIndex } from "./project-index.ts";
const BODY_LIMIT = 64 * 1024;
const SIGNATURE_BYTE_LIMIT = 1024 * 1024;
const COOKIE = "the_system_board";
const STATES = ["todo", "done", "open"] as const;
const STATUSES = ["draft", "approved"] as const;
export type BoardServer = Readonly<{ url: string; close(): void }>;
export type BoardSignature = Readonly<{ documents: string; board: string }>;
export type BoardRenderer = (
index: ProjectIndex,
board: BoardState,
proposals: readonly BoardProposal[],
revision: string,
) => Readonly<{ directory: string }>;
export function boardSignature(root: string): BoardSignature {
const documents = createHash("sha256");
const board = createHash("sha256");
const systemRoot = path.join(root, ".system");
for (const file of governedFiles(systemRoot)) {
const relative = path.relative(systemRoot, file).split(path.sep).join("/");
const digest = relative === "board.json" ? board : documents;
digest.update(`${relative}\u0000`);
const stats = lstatSync(file, { throwIfNoEntry: false });
if (!stats?.isFile()) continue;
if (stats.size > SIGNATURE_BYTE_LIMIT) {
digest.update(`${stats.size}:${stats.mtimeMs}`);
continue;
}
digest.update(readFileSync(file));
}
return { documents: documents.digest("hex"), board: board.digest("hex") };
}
function governedFiles(directory: string): readonly string[] {
let entries: Dirent[];
try {
entries = readdirSync(directory, { withFileTypes: true });
} catch {
return [];
}
return entries
.sort((left, right) => left.name.localeCompare(right.name))
.flatMap((entry) => {
const target = path.join(directory, entry.name);
if (entry.isSymbolicLink()) return [];
if (entry.isDirectory()) return governedFiles(target);
return entry.isFile() ? [target] : [];
});
}
type BoardSite = Readonly<{ signature: BoardSignature; directory: string }>;
export async function startBoardServer(
root: string,
proposals: Map<string, BoardProposal>,
render: BoardRenderer,
): Promise<BoardServer> {
const token = `${randomUUID()}-${randomUUID()}`;
const cookieName = `${COOKIE}_${token.slice(0, 8)}`;
const stale = new Set<string>();
let site = renderSite(root, proposals, render);
let renderError: string | undefined;
const ensureSite = (): BoardSite => {
const signature = boardSignature(root);
if (
signature.documents === site.signature.documents &&
signature.board === site.signature.board
)
return site;
const previous = site;
try {
site = renderSite(root, proposals, render, signature);
renderError = undefined;
} catch (error) {
renderError = error instanceof Error ? error.message : String(error);
return previous;
}
discard(previous.directory, stale);
return site;
};
let mutation = Promise.resolve();
const enqueue = <T>(work: () => T): Promise<T> => {
const result = mutation.then(work, work);
mutation = result.then(
() => undefined,
() => undefined,
);
return result;
};
const server: Server = createServer((request, response) => {
void dispatch(request, response, {
root,
proposals,
token,
cookieName,
enqueue,
ensureSite,
currentSite: () => site,
renderProblem: () => renderError,
});
});
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
server.off("error", reject);
resolve();
});
});
const address = server.address();
if (!address || typeof address === "string")
throw new Error("Failed to bind System board server");
return {
url: `http://127.0.0.1:${address.port}/?k=${token}`,
close() {
server.closeAllConnections();
server.close();
discard(site.directory, stale);
for (const directory of [...stale]) discard(directory, stale);
},
};
}
function renderSite(
root: string,
proposals: Map<string, BoardProposal>,
render: BoardRenderer,
signature = boardSignature(root),
): BoardSite {
const index = loadProjectIndex(root);
const board = readBoardState(index.root, index.prefix);
const output = render(
index,
board,
[...proposals.values()].sort((left, right) =>
left.id.localeCompare(right.id),
),
signature.documents,
);
return { signature, directory: output.directory };
}
function discard(directory: string, stale: Set<string>): void {
try {
rmSync(directory, { recursive: true, force: true });
stale.delete(directory);
} catch {
stale.add(directory);
}
}
type DispatchState = Readonly<{
root: string;
proposals: Map<string, BoardProposal>;
token: string;
cookieName: string;
enqueue<T>(work: () => T): Promise<T>;
ensureSite(): BoardSite;
currentSite(): BoardSite;
renderProblem(): string | undefined;
}>;
async function dispatch(
request: IncomingMessage,
response: ServerResponse,
state: DispatchState,
): Promise<void> {
try {
const url = new URL(request.url ?? "/", "http://127.0.0.1");
const queryToken = url.searchParams.get("k");
const cookieToken = cookie(request.headers.cookie ?? "", state.cookieName);
if (queryToken === state.token) {
response.statusCode = 303;
response.setHeader("Location", url.pathname);
response.setHeader(
"Set-Cookie",
`${state.cookieName}=${state.token}; HttpOnly; SameSite=Strict; Path=/`,
);
response.end();
return;
}
if (cookieToken !== state.token) {
respondJson(response, 403, { error: "Forbidden" });
return;
}
if (
request.method === "POST" &&
request.headers.origin !== `http://${request.headers.host}`
) {
respondJson(response, 403, { error: "Invalid board origin" });
return;
}
if (request.method === "POST" && url.pathname === "/api/state") {
const input = await readJsonBody(request);
const result = await state.enqueue(() =>
applyState(state.root, input, "manual"),
);
respondJson(response, 200, result);
return;
}
if (request.method === "POST" && url.pathname === "/api/status") {
const input = await readJsonBody(request);
const result = await state.enqueue(() => applyStatus(state.root, input));
respondJson(response, 200, result);
return;
}
if (request.method === "POST" && url.pathname === "/api/proposal") {
const input = await readJsonBody(request);
if (typeof input.id !== "string" || typeof input.state !== "string")
throw new Error("Expected assessment proposal with id and state");
const proposal = state.proposals.get(input.id);
if (!proposal || proposal.state !== input.state)
throw new Error(`No matching assessment proposal: ${input.id}`);
const result = await state.enqueue(() =>
applyState(state.root, input, "assess"),
);
state.proposals.delete(input.id);
respondJson(response, 200, result);
return;
}
if (request.method !== "GET") {
respondJson(response, 405, { error: "Method not allowed" });
return;
}
if (url.pathname === "/api/state") {
respondJson(response, 200, snapshot(state));
return;
}
const site = isDocument(url.pathname)
? state.ensureSite()
: state.currentSite();
serveFile(response, site.directory, url.pathname);
} catch (error) {
respondJson(response, 400, {
error: error instanceof Error ? error.message : String(error),
});
}
}
const isDocument = (pathname: string) =>
pathname === "/" || pathname.endsWith(".html");
function snapshot(state: DispatchState) {
const prefix = parseSystemConfig(
readFileSync(path.join(state.root, ".system/config.json"), "utf8"),
).prefix;
const board = readBoardState(state.root, prefix);
const problem = state.renderProblem();
return {
revision: boardSignature(state.root).documents,
items: board.items,
...(problem ? { problem } : {}),
};
}
function applyState(
root: string,
input: Record<string, unknown>,
source: "manual" | "assess",
) {
if (
Object.keys(input).some((key) => key !== "id" && key !== "state") ||
typeof input.id !== "string" ||
typeof input.state !== "string" ||
!STATES.includes(input.state as (typeof STATES)[number])
)
throw new Error("Expected board update with id and state");
const index = loadProjectIndex(root);
const current = readBoardState(index.root, index.prefix);
const updated = updateBoardState(
current,
index.documents,
input.id,
input.state as BoardItemState,
source,
);
writeBoardState(index.root, updated);
return { items: updated.items };
}
function applyStatus(root: string, input: Record<string, unknown>) {
if (
Object.keys(input).some((key) => key !== "id" && key !== "status") ||
typeof input.id !== "string" ||
typeof input.status !== "string" ||
!STATUSES.includes(input.status as (typeof STATUSES)[number])
)
throw new Error("Expected plan update with id and status");
const index = loadProjectIndex(root);
writePlanStatus(index, input.id, input.status as (typeof STATUSES)[number]);
return { id: input.id, status: input.status };
}
function readJsonBody(
request: IncomingMessage,
): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
let source = "";
request.setEncoding("utf8");
request.on("data", (chunk: string) => {
source += chunk;
if (Buffer.byteLength(source) > BODY_LIMIT) {
request.destroy();
reject(new Error("Board request body is too large"));
}
});
request.on("end", () => {
try {
const parsed = JSON.parse(source);
if (
typeof parsed !== "object" ||
parsed === null ||
Array.isArray(parsed)
)
throw new Error("Board request body must be an object");
resolve(parsed as Record<string, unknown>);
} catch (error) {
reject(error);
}
});
request.on("error", reject);
});
}
function serveFile(
response: ServerResponse,
directory: string,
pathname: string,
): void {
const relative = decodeURIComponent(
pathname === "/" ? "index.html" : pathname.slice(1),
);
const target = path.resolve(directory, relative);
const inside = path.relative(directory, target);
if (
inside === ".." ||
inside.startsWith(`..${path.sep}`) ||
path.isAbsolute(inside)
) {
respondJson(response, 404, { error: "Not found" });
return;
}
const stats = lstatSync(target, { throwIfNoEntry: false });
if (!stats?.isFile() || stats.isSymbolicLink()) {
respondJson(response, 404, { error: "Not found" });
return;
}
const realRoot = realpathSync(directory);
const realTarget = realpathSync(target);
const realInside = path.relative(realRoot, realTarget);
if (
realInside === ".." ||
realInside.startsWith(`..${path.sep}`) ||
path.isAbsolute(realInside)
) {
respondJson(response, 404, { error: "Not found" });
return;
}
response.statusCode = 200;
response.setHeader("Content-Type", contentType(target));
response.setHeader("Cache-Control", "no-store");
createReadStream(target).pipe(response);
}
function contentType(file: string): string {
switch (path.extname(file).toLowerCase()) {
case ".html":
return "text/html; charset=utf-8";
case ".js":
return "text/javascript; charset=utf-8";
case ".css":
return "text/css; charset=utf-8";
case ".svg":
return "image/svg+xml";
case ".png":
return "image/png";
case ".jpg":
case ".jpeg":
return "image/jpeg";
case ".gif":
return "image/gif";
case ".webp":
return "image/webp";
case ".avif":
return "image/avif";
case ".mp4":
return "video/mp4";
case ".webm":
return "video/webm";
case ".md":
return "text/markdown; charset=utf-8";
default:
return "application/octet-stream";
}
}
function respondJson(
response: ServerResponse,
status: number,
value: unknown,
): void {
if (response.headersSent) return;
response.statusCode = status;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.end(JSON.stringify(value));
}
function cookie(source: string, name: string): string | undefined {
for (const part of source.split(";")) {
const [key, ...value] = part.trim().split("=");
if (key === name) return value.join("=");
}
return undefined;
}