feat: harden authenticated deployment

This commit is contained in:
2026-07-29 22:04:40 -04:00
parent 432dd2176f
commit 892479dceb
20 changed files with 3677 additions and 1298 deletions
+150 -7
View File
@@ -1,15 +1,158 @@
import test from "node:test";
import assert from "node:assert/strict";
import crypto from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import request from "supertest";
import { newDb } from "pg-mem";
import { createApp } from "../server/app.js";
async function setup() { const mem=newDb(); mem.public.registerFunction({name:"gen_random_uuid",returns:"uuid",implementation:()=>crypto.randomUUID(), impure:true}); 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')`); return request.agent(createApp({db,root:path.resolve(path.dirname(fileURLToPath(import.meta.url)),".."),env:{NODE_ENV:"test",BOOTSTRAP_SETUP_TOKEN:"a-secure-bootstrap-token"}})); }
async function signup(agent,email) { const r=await agent.post('/api/auth/signup').send({email,password:'this is a long password'}); return {r,csrf:r.body.csrfToken}; }
test('headers, auth, csrf, ownership, admin and signup toggle',async()=>{const a=await setup(), b=await setup(); const root=await a.get('/'); assert.equal(root.status,200);assert.match(root.headers['content-security-policy'],/default-src 'self'/); assert.equal((await a.get('/api/plans')).status,401); assert.match((await a.get('/sw.js')).text, /Never serve authenticated/); assert.equal((await a.get('/app')).status,401);
const one=await signup(a,'[email protected]'); assert.equal(one.r.status,201); const plan=await a.post('/api/plans').set('x-csrf-token',one.csrf).send({plan:{fields:{'0.1':'Private'}}});assert.equal(plan.status,201); assert.equal((await a.put(`/api/plans/${plan.body.plan.id}`).send({plan:{}})).status,403);
const other=await signup(b,'[email protected]'); assert.equal((await b.put(`/api/plans/${plan.body.plan.id}`).set('x-csrf-token',other.csrf).send({plan:{}})).status,404);
const admin=await a.post('/api/auth/bootstrap').send({email:'[email protected]',password:'this is an admin password',setupToken:'a-secure-bootstrap-token'}); assert.equal(admin.status,201); const c=admin.body.csrfToken; assert.equal((await a.put('/api/admin/signup-enabled').set('x-csrf-token',c).send({enabled:false})).status,200); assert.equal((await a.post('/api/auth/signup').send({email:'[email protected]',password:'this is a long password'})).status,403); assert.equal((await a.get('/api/admin/users')).status,200);});
test('bootstrap rejects invalid token and cannot be reused',async()=>{const a=await setup();assert.equal((await a.post('/api/auth/bootstrap').send({email:'[email protected]',password:'this is an admin password',setupToken:'wrong'})).status,403);const r=await a.post('/api/auth/bootstrap').send({email:'[email protected]',password:'this is an admin password',setupToken:'a-secure-bootstrap-token'});assert.equal(r.status,201);assert.equal((await a.post('/api/auth/bootstrap').send({email:'[email protected]',password:'this is an admin password',setupToken:'a-secure-bootstrap-token'})).status,409);});
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const password = "this is a long password";
async function setup() {
const mem = newDb();
mem.public.registerFunction({
name: "gen_random_uuid",
returns: "uuid",
implementation: () => crypto.randomUUID(),
impure: true,
});
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')`);
const app = createApp({
db,
root,
env: { NODE_ENV: "test", BOOTSTRAP_SETUP_TOKEN: "a-secure-bootstrap-token" },
});
return { db, app, agent: request.agent(app) };
}
async function signup(agent, email) {
const response = await agent
.post("/api/auth/signup")
.send({ email, password });
return { response, csrf: response.body.csrfToken };
}
test("strict CSP/static modules, no-store data, auth lifecycle, and ownership share one database", async () => {
const { db, app, agent: first } = await setup();
const second = request.agent(app);
const anonymous = request.agent(app);
const landing = await anonymous.get("/");
assert.equal(landing.status, 200);
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.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/);
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/);
const one = await signup(first, "[email protected]");
const two = await signup(second, "[email protected]");
assert.equal(one.response.status, 201);
assert.equal(two.response.status, 201);
const plan = await first
.post("/api/plans")
.set("x-csrf-token", one.csrf)
.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 second
.put(`/api/plans/${plan.body.plan.id}`)
.set("x-csrf-token", two.csrf)
.send({ plan: {} })
).status,
404,
);
assert.equal((await second.get("/api/plans")).body.plans.length, 0);
const login = request.agent(app);
assert.equal(
(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;
assert.equal(
(await login.post("/api/auth/logout").set("x-csrf-token", loginCsrf)).status,
200,
);
assert.equal((await login.get("/api/auth/me")).status, 401);
const admin = await first.post("/api/auth/bootstrap").send({
email: "[email protected]",
password,
setupToken: "a-secure-bootstrap-token",
});
assert.equal(admin.status, 201);
assert.equal((await first.get("/api/admin/users")).status, 200);
assert.equal((await second.get("/api/admin/users")).status, 403);
assert.equal(
(
await first
.put("/api/admin/signup-enabled")
.set("x-csrf-token", admin.body.csrfToken)
.send({ enabled: false })
).status,
200,
);
assert.equal(
(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);
});
test("bootstrap token is optional after first setup and unavailable before setup without one", async () => {
const { app, db } = await setup();
const agent = request.agent(app);
assert.equal(
(
await agent.post("/api/auth/bootstrap").send({
email: "[email protected]",
password,
setupToken: "wrong",
})
).status,
403,
);
const noTokenApp = createApp({ db, root, env: { NODE_ENV: "test" } });
assert.equal(
(
await request(noTokenApp).post("/api/auth/bootstrap").send({
email: "[email protected]",
password,
})
).status,
503,
);
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({
email: "[email protected]",
password,
})
).status,
409,
);
});
+83
View File
@@ -0,0 +1,83 @@
import test from "node:test";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const root = path.resolve(import.meta.dirname, "..");
async function dockerAvailable() {
try {
await execFileAsync("docker", ["info"], { timeout: 15_000 });
return true;
} catch {
return false;
}
}
test("Docker image starts against PostgreSQL and applies migrations", async (t) => {
if (!(await dockerAvailable())) {
t.skip("Docker daemon is unavailable");
return;
}
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");
await writeFile(
envFile,
`POSTGRES_PASSWORD=container-test-password\nBOOTSTRAP_SETUP_TOKEN=\nPI_AGENT_CONFIG_DIR=${agentDir}\n`,
);
const project = `roastplanner${Date.now()}`;
const compose = (args, options = {}) =>
execFileAsync(
"docker",
["compose", "--project-name", project, "--env-file", envFile, ...args],
{ cwd: root, timeout: 120_000, ...options },
);
t.after(async () => {
try {
await compose(["down", "--volumes", "--remove-orphans"]);
} catch {
// Preserve the startup failure rather than masking it with cleanup.
}
});
await compose(["up", "--build", "--detach"]);
let lastError;
for (let attempt = 0; attempt < 30; attempt++) {
try {
await compose([
"exec",
"-T",
"app",
"node",
"--input-type=module",
"-e",
"const response = await fetch('http://127.0.0.1:8090/'); process.exit(response.ok ? 0 : 1)",
]);
lastError = null;
break;
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
}
assert.equal(lastError, null, "application never became ready in its container");
const migration = await compose([
"exec",
"-T",
"db",
"psql",
"-U",
"roast",
"-d",
"roast",
"-tAc",
"SELECT count(*) FROM schema_migrations WHERE filename = '001_auth.sql'",
]);
assert.equal(migration.stdout.trim(), "1");
});