Store typescript on PDS, run it on kasta
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149import { Hono } from "@hono/hono";import type { DatabaseSync } from "node:sqlite";import { resolveHandle } from "./atproto.ts";import { syncDid } from "./sync.ts";import { getKastByHandle } from "./db.ts";import { runKast } from "./runner.ts";import { isAllowed } from "./allowlist.ts";import { log } from "./log.ts";
function extractHandle(host: string, baseHost: string): string | null { const suffix = `.${baseHost}`; if (host.endsWith(suffix)) { const handle = host.slice(0, -suffix.length); return handle || null; } return null;}
export function createApp( db: DatabaseSync, baseUrl: string, allowlist: Set<string>,): Hono { const baseHost = new URL(baseUrl).host; const app = new Hono();
app.get("/health", (c) => c.json({ ok: true }));
app.post("/sync", async (c) => { let body: { did?: string; handle?: string }; try { body = await c.req.json(); } catch { log("WARN", "Sync rejected: invalid JSON body"); return c.json( { error: "Request body must be JSON with 'did' or 'handle'" }, 400, ); }
if (!body.did && !body.handle) { log("WARN", "Sync rejected: missing did/handle"); return c.json({ error: "'did' or 'handle' is required" }, 400); }
let did: string; let handle: string; let pdsUrl: string;
try { if (body.handle) { log("INFO", "Resolving identity for sync", { handle: body.handle }); const identity = await resolveHandle(body.handle); did = identity.did; handle = body.handle; pdsUrl = identity.pdsUrl; log("INFO", "Identity resolved", { handle, did, pdsUrl }); } else { return c.json( { error: "Provide 'handle' (DID-only sync not yet supported)" }, 400, ); } } catch (err) { log("ERROR", "Identity resolution failed", { handle: body.handle, error: (err as Error).message, }); return c.json( { error: `Identity resolution failed: ${(err as Error).message}` }, 422, ); }
if (!isAllowed(allowlist, handle, did)) { log("WARN", "Sync denied: not in allowlist", { handle, did }); return c.json({ error: "Forbidden" }, 403); }
log("INFO", "Starting sync", { handle, did }); try { const result = await syncDid(db, did, handle, pdsUrl, baseUrl); log("INFO", "Sync complete", { handle, did, synced: result.synced.length, }); return c.json(result); } catch (err) { log("ERROR", "Sync failed", { handle, did, error: (err as Error).message }); return c.json({ error: `Sync failed: ${(err as Error).message}` }, 500); } });
// Kast execution — all HTTP verbs, handle extracted from subdomain app.all("/:name", async (c) => { const host = c.req.header("host") ?? ""; const subdomain = extractHandle(host, baseHost); if (!subdomain) { return c.json({ error: "Not Found" }, 404); } // Handles use dots (e.g. wilb.me) which are encoded as hyphens in the // subdomain to stay within a single DNS label (e.g. wilb-me.kasta.example.com). const handle = subdomain.replaceAll("-", "."); const name = c.req.param("name"); const method = c.req.method;
log("INFO", "Kast request received", { method, handle, name });
const kast = getKastByHandle(db, handle, name); if (!kast) { log("WARN", "Kast not found", { handle, name }); return c.json({ error: `Kast not found: ${handle}/${name}` }, 404); }
if (!isAllowed(allowlist, kast.handle, kast.did)) { log("WARN", "Kast execution denied: not in allowlist", { handle: kast.handle, did: kast.did, name, }); return c.json({ error: "Forbidden" }, 403); }
log("INFO", "Executing kast", { handle: kast.handle, did: kast.did, name }); try { const response = await runKast(kast.files, kast.entrypoint, c.req.raw); log("INFO", "Kast execution complete", { handle: kast.handle, name, status: response.status, }); return response; } catch (err) { log("ERROR", "Kast execution failed", { handle: kast.handle, did: kast.did, name, error: (err as Error).message, }); return new Response(`Internal error: ${(err as Error).message}`, { status: 500, }); } });
return app;}