chore: format auth code and update runtime

This commit is contained in:
2026-07-29 22:11:08 -04:00
parent 892479dceb
commit 22e6e721a7
9 changed files with 132 additions and 73 deletions
+2 -1
View File
@@ -16,7 +16,8 @@ services:
depends_on:
db:
condition: service_healthy
expose: ["8090"]
# Nginx Proxy Manager routes https://roast.srmr.xyz to this host port.
ports: ["8090:8090"]
restart: unless-stopped
db:
image: postgres:16-alpine
+20 -20
View File
@@ -8,7 +8,7 @@
"name": "roast-planner-webapp",
"version": "0.1.0",
"dependencies": {
"@earendil-works/pi-coding-agent": "^0.82.1",
"@earendil-works/pi-coding-agent": "^0.83.0",
"bcryptjs": "^3.0.3",
"express": "^5.0.1",
"pg": "^8.22.0"
@@ -19,15 +19,15 @@
}
},
"node_modules/@earendil-works/pi-coding-agent": {
"version": "0.82.1",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.82.1.tgz",
"integrity": "sha512-zbkAhoIuDPMF3pKuja0ajZabrMWU29FUMV9A/XMXT/XC1yXs5xt6t6t13GogQFsDrDqbFP4DkZQO1w8rWRAzYA==",
"version": "0.83.0",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.83.0.tgz",
"integrity": "sha512-uYhF+FsZxogoSX/AxBcUdiY+ZklubwaXyAoEGA2eQwsHcyEAhUYIKh/WLXe/a8+k8eTCmxb+ZN2Zo9mzQtzbWw==",
"hasShrinkwrap": true,
"license": "MIT",
"dependencies": {
"@earendil-works/pi-agent-core": "^0.82.1",
"@earendil-works/pi-ai": "^0.82.1",
"@earendil-works/pi-tui": "^0.82.1",
"@earendil-works/pi-agent-core": "^0.83.0",
"@earendil-works/pi-ai": "^0.83.0",
"@earendil-works/pi-tui": "^0.83.0",
"@silvia-odwyer/photon-node": "0.3.4",
"chalk": "5.6.2",
"cross-spawn": "7.0.6",
@@ -40,7 +40,7 @@
"minimatch": "10.2.5",
"proper-lockfile": "4.1.2",
"semver": "7.8.0",
"typebox": "1.1.38",
"typebox": "1.3.7",
"undici": "8.5.0",
"yaml": "2.9.0"
},
@@ -490,14 +490,14 @@
}
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": {
"version": "0.82.1",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.82.1.tgz",
"version": "0.83.0",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.83.0.tgz",
"license": "MIT",
"dependencies": {
"@earendil-works/pi-ai": "^0.82.1",
"@earendil-works/pi-ai": "^0.83.0",
"diff": "8.0.4",
"ignore": "7.0.5",
"typebox": "1.1.38",
"typebox": "1.3.7",
"yaml": "2.9.0"
},
"engines": {
@@ -505,8 +505,8 @@
}
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": {
"version": "0.82.1",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.82.1.tgz",
"version": "0.83.0",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.83.0.tgz",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "0.91.1",
@@ -519,7 +519,7 @@
"https-proxy-agent": "7.0.6",
"openai": "6.26.0",
"partial-json": "0.1.7",
"typebox": "1.1.38"
"typebox": "1.3.7"
},
"bin": {
"pi-ai": "dist/cli.js"
@@ -529,8 +529,8 @@
}
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": {
"version": "0.82.1",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.82.1.tgz",
"version": "0.83.0",
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.83.0.tgz",
"license": "MIT",
"dependencies": {
"get-east-asian-width": "1.6.0",
@@ -1724,9 +1724,9 @@
"license": "0BSD"
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": {
"version": "1.1.38",
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz",
"integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==",
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz",
"integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==",
"license": "MIT"
},
"node_modules/@earendil-works/pi-coding-agent/node_modules/undici": {
+1 -1
View File
@@ -10,7 +10,7 @@
"test": "node --test"
},
"dependencies": {
"@earendil-works/pi-coding-agent": "^0.82.1",
"@earendil-works/pi-coding-agent": "^0.83.0",
"bcryptjs": "^3.0.3",
"express": "^5.0.1",
"pg": "^8.22.0"
+3 -1
View File
@@ -37,7 +37,9 @@ document.querySelector("#save").addEventListener("click", async () => {
"content-type": "application/json",
"x-csrf-token": csrf(),
},
body: JSON.stringify({ enabled: document.querySelector("#signup").checked }),
body: JSON.stringify({
enabled: document.querySelector("#signup").checked,
}),
});
await load();
});
+3 -3
View File
@@ -19,6 +19,6 @@ form.addEventListener("submit", (event) => {
event.preventDefault();
submit("/api/auth/login");
});
document.querySelector("#signup").addEventListener("click", () =>
submit("/api/auth/signup"),
);
document
.querySelector("#signup")
.addEventListener("click", () => submit("/api/auth/signup"));
+6 -2
View File
@@ -409,7 +409,8 @@ function autosave() {
clearTimeout(autosave._t);
autosave._t = setTimeout(() => {
try {
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(state.plan));
if (storageKey)
localStorage.setItem(storageKey, JSON.stringify(state.plan));
// Local storage is namespaced per authenticated account; the account copy is authoritative when online.
if (navigator.onLine && csrfToken()) {
const method = remotePlanId ? "PUT" : "POST";
@@ -455,7 +456,10 @@ function loadFromStorage(userId) {
// A shared browser must never retain a previous account's local-only draft.
for (let i = localStorage.length - 1; i >= 0; i--) {
const key = localStorage.key(i);
if ((key?.startsWith(`${STORAGE_PREFIX}:`) && key !== storageKey) || key === "roastPlannerPlan.v1")
if (
(key?.startsWith(`${STORAGE_PREFIX}:`) && key !== storageKey) ||
key === "roastPlannerPlan.v1"
)
localStorage.removeItem(key);
}
const raw = localStorage.getItem(storageKey);
+32 -25
View File
@@ -28,7 +28,14 @@ export function createApp({ db, root, env = process.env } = {}) {
const buckets = new Map();
const MAX_RATE_BUCKETS = 10_000;
const rateLimit = (name, max, windowMs) => {
if (!Number.isInteger(max) || max < 1 || max > 1_000 || !Number.isInteger(windowMs) || windowMs < 1_000 || windowMs > 3_600_000)
if (
!Number.isInteger(max) ||
max < 1 ||
max > 1_000 ||
!Number.isInteger(windowMs) ||
windowMs < 1_000 ||
windowMs > 3_600_000
)
throw new Error("Invalid rate-limit configuration");
return (req, res, next) => {
const key = `${name}:${req.ip}`;
@@ -53,7 +60,11 @@ export function createApp({ db, root, env = process.env } = {}) {
// identifies its proxy. A numeric hop count is unsafe when the topology changes.
app.set("trust proxy", env.TRUST_PROXY || false);
app.use((req, res, next) => {
if (req.path.startsWith("/api/") || req.path === "/app" || req.path === "/admin")
if (
req.path.startsWith("/api/") ||
req.path === "/app" ||
req.path === "/admin"
)
res.set("Cache-Control", "no-store, private");
res.set({
"X-Content-Type-Options": "nosniff",
@@ -146,14 +157,12 @@ export function createApp({ db, root, env = process.env } = {}) {
const email = emailOf(req.body.email),
password = req.body.password;
if (!/^\S+@\S+\.\S+$/.test(email) || !PASSWORD_OK(password))
return res
.status(400)
.json({
ok: false,
code: "invalid_credentials",
error:
"Use a valid email and a password of at least 12 characters.",
});
return res.status(400).json({
ok: false,
code: "invalid_credentials",
error:
"Use a valid email and a password of at least 12 characters.",
});
const setting = await db.query(
"SELECT value FROM app_settings WHERE key='signup_enabled'",
);
@@ -168,13 +177,11 @@ export function createApp({ db, root, env = process.env } = {}) {
).rows[0];
const s = await createSession(user);
setSessionCookie(res, s.raw, 14 * 864e5, s.csrfToken);
res
.status(201)
.json({
ok: true,
user: { email: user.email, role: user.role },
csrfToken: s.csrfToken,
});
res.status(201).json({
ok: true,
user: { email: user.email, role: user.role },
csrfToken: s.csrfToken,
});
} catch (e) {
if (e.code === "23505")
return res.status(409).json({ ok: false, code: "email_exists" });
@@ -196,7 +203,9 @@ export function createApp({ db, root, env = process.env } = {}) {
return res.status(409).json({ ok: false, code: "bootstrap_used" });
const bootstrap = String(req.body.setupToken || "");
if (!env.BOOTSTRAP_SETUP_TOKEN)
return res.status(503).json({ ok: false, code: "bootstrap_unavailable" });
return res
.status(503)
.json({ ok: false, code: "bootstrap_unavailable" });
if (
bootstrap.length !== env.BOOTSTRAP_SETUP_TOKEN.length ||
!crypto.timingSafeEqual(
@@ -222,13 +231,11 @@ export function createApp({ db, root, env = process.env } = {}) {
).rows[0];
const s = await createSession(user);
setSessionCookie(res, s.raw, 14 * 864e5, s.csrfToken);
res
.status(201)
.json({
ok: true,
user: { email: user.email, role: user.role },
csrfToken: s.csrfToken,
});
res.status(201).json({
ok: true,
user: { email: user.email, role: user.role },
csrfToken: s.csrfToken,
});
} catch (e) {
next(e);
}
+57 -18
View File
@@ -20,11 +20,16 @@ async function setup() {
});
const pg = mem.adapters.createPg();
const db = new pg.Pool();
await db.query(`CREATE TABLE users(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),email text UNIQUE NOT NULL,password_hash text NOT NULL,role text NOT NULL DEFAULT 'user',created_at timestamptz DEFAULT now()); CREATE TABLE sessions(token_hash text PRIMARY KEY,user_id uuid NOT NULL REFERENCES users(id),csrf_hash text NOT NULL,expires_at timestamptz NOT NULL,created_at timestamptz DEFAULT now()); CREATE TABLE roast_plans(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),user_id uuid NOT NULL REFERENCES users(id),plan jsonb NOT NULL,created_at timestamptz DEFAULT now(),updated_at timestamptz DEFAULT now()); CREATE TABLE app_settings(key text PRIMARY KEY,value text NOT NULL); INSERT INTO app_settings VALUES('signup_enabled','true')`);
await db.query(
`CREATE TABLE users(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),email text UNIQUE NOT NULL,password_hash text NOT NULL,role text NOT NULL DEFAULT 'user',created_at timestamptz DEFAULT now()); CREATE TABLE sessions(token_hash text PRIMARY KEY,user_id uuid NOT NULL REFERENCES users(id),csrf_hash text NOT NULL,expires_at timestamptz NOT NULL,created_at timestamptz DEFAULT now()); CREATE TABLE roast_plans(id uuid PRIMARY KEY DEFAULT gen_random_uuid(),user_id uuid NOT NULL REFERENCES users(id),plan jsonb NOT NULL,created_at timestamptz DEFAULT now(),updated_at timestamptz DEFAULT now()); CREATE TABLE app_settings(key text PRIMARY KEY,value text NOT NULL); INSERT INTO app_settings VALUES('signup_enabled','true')`,
);
const app = createApp({
db,
root,
env: { NODE_ENV: "test", BOOTSTRAP_SETUP_TOKEN: "a-secure-bootstrap-token" },
env: {
NODE_ENV: "test",
BOOTSTRAP_SETUP_TOKEN: "a-secure-bootstrap-token",
},
});
return { db, app, agent: request.agent(app) };
}
@@ -43,23 +48,38 @@ test("strict CSP/static modules, no-store data, auth lifecycle, and ownership sh
const landing = await anonymous.get("/");
assert.equal(landing.status, 200);
assert.match(landing.headers["content-security-policy"], /default-src 'self'/);
assert.match(
landing.headers["content-security-policy"],
/default-src 'self'/,
);
assert.doesNotMatch(
landing.headers["content-security-policy"],
/(?:default-src|script-src)[^;]*unsafe-inline/,
);
assert.match(landing.text, /<script type="module" src="\/js\/landing\.js"><\/script>/);
assert.match(
landing.text,
/<script type="module" src="\/js\/landing\.js"><\/script>/,
);
assert.doesNotMatch(landing.text, /<script type="module">/);
const adminHtml = await anonymous.get("/admin");
assert.equal(adminHtml.status, 401);
assert.equal(adminHtml.headers["cache-control"], "no-store, private");
assert.equal((await anonymous.get("/api/plans")).status, 401);
assert.equal((await anonymous.get("/api/plans")).headers["cache-control"], "no-store, private");
assert.match((await anonymous.get("/js/admin.js")).text, /async function load/);
assert.equal(
(await anonymous.get("/api/plans")).headers["cache-control"],
"no-store, private",
);
assert.match(
(await anonymous.get("/js/admin.js")).text,
/async function load/,
);
const mainScript = await anonymous.get("/js/main.js");
assert.match(mainScript.text, /roastPlannerPlan\.v2/);
assert.match(mainScript.text, /localStorage\.removeItem\(key\)/);
assert.match((await anonymous.get("/sw.js")).text, /Never serve authenticated/);
assert.match(
(await anonymous.get("/sw.js")).text,
/Never serve authenticated/,
);
const one = await signup(first, "[email protected]");
const two = await signup(second, "[email protected]");
@@ -68,9 +88,12 @@ test("strict CSP/static modules, no-store data, auth lifecycle, and ownership sh
const plan = await first
.post("/api/plans")
.set("x-csrf-token", one.csrf)
.send({ plan: { fields: { "0.1": "Private" } } });
.send({ plan: { fields: { 0.1: "Private" } } });
assert.equal(plan.status, 201);
assert.equal((await first.get("/api/plans")).headers["cache-control"], "no-store, private");
assert.equal(
(await first.get("/api/plans")).headers["cache-control"],
"no-store, private",
);
assert.equal(
(
await second
@@ -84,13 +107,22 @@ test("strict CSP/static modules, no-store data, auth lifecycle, and ownership sh
const login = request.agent(app);
assert.equal(
(await login.post("/api/auth/login").send({ email: "[email protected]", password })).status,
(
await login
.post("/api/auth/login")
.send({ email: "[email protected]", password })
).status,
200,
);
assert.equal((await login.get("/api/auth/me")).status, 200);
const loginCsrf = (await login.post("/api/auth/login").send({ email: "[email protected]", password })).body.csrfToken;
const loginCsrf = (
await login
.post("/api/auth/login")
.send({ email: "[email protected]", password })
).body.csrfToken;
assert.equal(
(await login.post("/api/auth/logout").set("x-csrf-token", loginCsrf)).status,
(await login.post("/api/auth/logout").set("x-csrf-token", loginCsrf))
.status,
200,
);
assert.equal((await login.get("/api/auth/me")).status, 401);
@@ -113,10 +145,17 @@ test("strict CSP/static modules, no-store data, auth lifecycle, and ownership sh
200,
);
assert.equal(
(await anonymous.post("/api/auth/signup").send({ email: "[email protected]", password })).status,
(
await anonymous
.post("/api/auth/signup")
.send({ email: "[email protected]", password })
).status,
403,
);
assert.equal((await db.query("SELECT count(*)::int AS count FROM users")).rows[0].count, 3);
assert.equal(
(await db.query("SELECT count(*)::int AS count FROM users")).rows[0].count,
3,
);
});
test("bootstrap token is optional after first setup and unavailable before setup without one", async () => {
@@ -142,10 +181,10 @@ test("bootstrap token is optional after first setup and unavailable before setup
).status,
503,
);
await db.query("INSERT INTO users(email,password_hash,role) VALUES($1,$2,'admin')", [
"[email protected]",
"not-used-in-this-test",
]);
await db.query(
"INSERT INTO users(email,password_hash,role) VALUES($1,$2,'admin')",
["[email protected]", "not-used-in-this-test"],
);
assert.equal(
(
await request(noTokenApp).post("/api/auth/bootstrap").send({
+8 -2
View File
@@ -23,7 +23,9 @@ test("Docker image starts against PostgreSQL and applies migrations", async (t)
t.skip("Docker daemon is unavailable");
return;
}
const temp = await mkdtemp(path.join(os.tmpdir(), "roast-planner-container-"));
const temp = await mkdtemp(
path.join(os.tmpdir(), "roast-planner-container-"),
);
const agentDir = path.join(temp, "pi-agent");
await mkdir(agentDir);
const envFile = path.join(temp, "compose.env");
@@ -66,7 +68,11 @@ test("Docker image starts against PostgreSQL and applies migrations", async (t)
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
}
assert.equal(lastError, null, "application never became ready in its container");
assert.equal(
lastError,
null,
"application never became ready in its container",
);
const migration = await compose([
"exec",
"-T",