diff --git a/quc/src/worker.js b/quc/src/worker.js index c61a1fd..3112a45 100644 --- a/quc/src/worker.js +++ b/quc/src/worker.js @@ -1,6 +1,6 @@ // quc: app shell + API in one Worker (no build step; the same module runs on Cloudflare and under dev.mjs). -// GET / the shell GET /chair.svg the chair-in-the-corner motif -// GET /api/health {ok, rev} anything else 404 +// GET / the shell (start form) GET /run/:id the shell (chair view) GET /chair.svg the chair-in-the-corner motif +// GET /api/health {ok, rev} anything else 404 // Fonts: system stacks on purpose (no webfont files, no third-party requests). shortcut: swap in self-hosted fonts when the brand picks them. export const CHAIR = ` @@ -8,13 +8,24 @@ export const CHAIR = ` `; +// Inlined into the page via toString(), so the browser and the tests run the same code. Returns {repo?, task?} errors; {} is valid. +export function validate(repo, task) { + const e = {}; + if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+?(\.git)?\/?$/.test(repo)) e.repo = "Use a public GitHub repo URL, like https://github.com/owner/name."; + if (task.length < 10) e.task = "Say a bit more: at least 10 characters."; + else if (task.length > 200) e.task = "Keep it to one line: 200 characters at most."; + return e; +} + +const NOTICE = "Replays are public. Anyone with the link to a run can watch it, so don't point this at anything you wouldn't show a stranger."; + export const PAGE = ` quc - +
quc
-

Coming together.

The app shell is up. Nothing to do here yet.

checking…

- +
+
+

Leave it with the chair.

+

Point quc at a public GitHub repo, say in one line what you want done, and go do something else. It works in the corner, quietly, and you can watch the replay.

+
+
+ +
+
+ +
+

${NOTICE}

+ +
+
+ +
+ `; const headers = (type, extra = {}) => ({ "content-type": type, ...extra }); @@ -46,7 +108,7 @@ export default { if (req.method !== "GET" && req.method !== "HEAD") return new Response("method not allowed", { status: 405 }); if (pathname === "/api/health") return Response.json({ ok: true, rev: env.REV || "" }, { headers: { "cache-control": "no-store" } }); if (pathname === "/chair.svg") return new Response(CHAIR, { headers: headers("image/svg+xml", { "cache-control": "public, max-age=3600" }) }); - if (pathname === "/" || pathname === "/index.html") return new Response(PAGE, { headers: headers("text/html; charset=utf-8") }); + if (pathname === "/" || pathname === "/index.html" || /^\/run\/[\w-]+$/.test(pathname)) return new Response(PAGE, { headers: headers("text/html; charset=utf-8") }); return new Response("not found", { status: 404 }); }, }; diff --git a/quc/test/worker.test.js b/quc/test/worker.test.js index 6a75bb6..b536cdb 100644 --- a/quc/test/worker.test.js +++ b/quc/test/worker.test.js @@ -25,3 +25,25 @@ test("motif is an svg, unknown paths 404, writes are refused", async () => { assert.equal((await get("/nope")).status, 404); assert.equal((await get("/api/health", {}, "POST")).status, 405); }); + +test("validate accepts a repo + task and rejects bad shapes", async () => { + const { validate } = await import("../src/worker.js"); + const task = "Fix the flaky date test"; + assert.deepEqual(validate("https://github.com/a/b", task), {}); + assert.deepEqual(validate("https://github.com/a/b.git/", task), {}); + for (const bad of ["", "github.com/a/b", "http://github.com/a/b", "https://gitlab.com/a/b", "https://github.com/a", "https://github.com/a/b/tree/x"]) + assert.ok(validate(bad, task).repo, bad); + assert.ok(validate("https://github.com/a/b", "short").task); + assert.ok(validate("https://github.com/a/b", "x".repeat(201)).task); +}); + +test("start form, public-replay notice and run page are served; the inlined validator parses", async () => { + const html = await (await get("/")).text(); + assert.match(html, /
([\s\S]*)<\/script>/)[1]); // syntax check of the shipped script +});