import { describe, expect, test, beforeAll, afterAll } from "bun:test"; import { Elysia } from "elysia"; import { toErrorResponse } from "@continuum/ts-core"; import { roleSatisfies, ORG_ROLES } from "../src/auth/roles"; import { startDevAuthIssuer } from "../scripts/dev-auth"; import { Value } from "@sinclair/typebox/value"; import { printers } from "../src/db/schema"; import { farmProjection, printerProjection } from "../src/db/projections"; import { PrinterCreateSchema } from "../src/db/validators"; const USER = "11111111-1111-1111-1111-111111111111"; describe("the role ladder", () => { test("is ordered viewer < operator < owner", () => { expect(ORG_ROLES).toEqual(["viewer", "operator", "owner"]); }); // Written out in full rather than derived from the ladder: a table that // computes the expected answer the same way the implementation does agrees // with a wrong implementation. const cases: Array<[string, string, boolean]> = [ ["viewer", "viewer", true], ["viewer", "operator", false], ["viewer", "owner", false], ["operator", "viewer", true], ["operator", "operator", true], ["operator", "owner", false], ["owner", "viewer", true], ["owner", "operator", true], ["owner", "owner", true], ]; for (const [actual, minimum, expected] of cases) { test(`'${actual}' ${expected ? "satisfies" : "does not satisfy"} '${minimum}'`, () => { expect(roleSatisfies(actual as never, minimum as never)).toBe(expected); }); } test("a viewer cannot send printer commands", () => { // The boundary that moves physical machines, called out on its own // because it is the one that is expensive to get wrong. expect(roleSatisfies("viewer", "operator")).toBe(false); }); }); /** * Exercises the real verification path - `createRemoteJWKSet` against a real * JWKS endpoint over HTTP - rather than a stubbed verifier. The things that * actually go wrong here (an unsigned token accepted, a token signed by * another key accepted, an expired token accepted) are all invisible to a * test that mocks the verifier out. */ describe("requireUser", () => { let issuer: Awaited>; let app: Elysia; beforeAll(async () => { issuer = await startDevAuthIssuer(); process.env.SUPABASE_URL = issuer.url; // Imported after SUPABASE_URL is set: the verifier is built at module // load so a misconfigured deployment fails at boot, not on first request. const { requireUser } = await import("../src/auth/plugin"); app = new Elysia().onError(toErrorResponse).use(requireUser).get("/whoami", ({ user }) => user); }); afterAll(() => issuer.stop()); const get = (headers: Record = {}) => app.handle(new Request("http://localhost/whoami", { headers })); test("accepts a properly signed token and exposes the subject", async () => { const token = await issuer.mint({ sub: USER, email: "farm@example.test" }); const response = await get({ authorization: `Bearer ${token}` }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ id: USER, email: "farm@example.test" }); }); test("rejects a request with no Authorization header", async () => { const response = await get(); expect(response.status).toBe(401); expect((await response.json()).error.code).toBe("UNAUTHORIZED"); }); test("rejects a non-Bearer Authorization header", async () => { const response = await get({ authorization: "Basic dXNlcjpwYXNz" }); expect(response.status).toBe(401); }); test("rejects a token signed by a key that is not in the JWKS", async () => { const token = await issuer.mintWithForeignKey({ sub: USER }); const response = await get({ authorization: `Bearer ${token}` }); expect(response.status).toBe(401); }); test("rejects an expired token", async () => { const token = await issuer.mint({ sub: USER, expiresIn: -60 }); const response = await get({ authorization: `Bearer ${token}` }); expect(response.status).toBe(401); }); test("rejects a token issued for another audience", async () => { // Supabase issues `aud: "authenticated"` for user sessions. Its anon and // service-role keys are also valid project JWTs with other audiences, so // without this check the public anon key would authenticate as a user. const token = await issuer.mint({ sub: USER, audience: "anon" }); const response = await get({ authorization: `Bearer ${token}` }); expect(response.status).toBe(401); }); test("does not say why a token was rejected", async () => { // Distinguishing "expired" from "bad signature" tells whoever is probing // which part of a forged token to fix next. const expired = await get({ authorization: `Bearer ${await issuer.mint({ sub: USER, expiresIn: -60 })}`, }); const foreign = await get({ authorization: `Bearer ${await issuer.mintWithForeignKey({ sub: USER })}`, }); expect((await expired.json()).error.message).toBe((await foreign.json()).error.message); }); }); describe("configuration", () => { test("the process refuses to start without SUPABASE_URL", async () => { // Fail at boot, not on the first authenticated request: a backend that // starts and then 500s on every login looks like an outage somewhere // else. Run in a subprocess because the module caches its verifier. const proc = Bun.spawn(["bun", "-e", "await import('./src/auth/verifier.ts')"], { cwd: import.meta.dir + "/..", env: { ...process.env, SUPABASE_URL: "" }, stderr: "pipe", stdout: "pipe", }); expect(await proc.exited).not.toBe(0); expect(await new Response(proc.stderr).text()).toContain("SUPABASE_URL"); }); // An empty value, not an unset one, because that is what the committed // `.env.example` ships: the old `process.env.EDGE_JWT_SECRET ?? "dev-..."` // only caught null and undefined, so copying the template and filling in // everything else left the edge JWTs signed with "". Anything that knew // that - or the committed fallback literal - could mint a token for any // farm_id and open /ws/edge/v1 as that farm's gateway. test("the process refuses to start with an empty EDGE_JWT_SECRET", async () => { const proc = Bun.spawn(["bun", "-e", "await import('./src/auth/edge_secret.ts')"], { cwd: import.meta.dir + "/..", env: { ...process.env, EDGE_JWT_SECRET: "" }, stderr: "pipe", stdout: "pipe", }); expect(await proc.exited).not.toBe(0); expect(await new Response(proc.stderr).text()).toContain("EDGE_JWT_SECRET"); }); test("no route holds a hardcoded signing secret", async () => { // The old fallback was duplicated across two routes, and a third copy // would be just as exploitable as the first two, so this greps the tree // rather than the files that happened to have it. Two shapes, because // either one reintroduces the hole: a literal passed to `jwt()`, and a // defaulted read of the variable (`?? "..."` / `|| "..."`), which is // what let an empty `EDGE_JWT_SECRET` through. const grep = async (pattern: string) => { const proc = Bun.spawn(["grep", "-rnE", pattern, "src"], { cwd: import.meta.dir + "/..", stdout: "pipe", }); return new Response(proc.stdout).text(); }; expect(await grep('secret:\\s*["\'`]')).toBe(""); expect(await grep('EDGE_JWT_SECRET\\s*(\\?\\?|\\|\\|)')).toBe(""); // And the variable is read in exactly one place, so there is only ever // one line to get right. expect(await grep("process\\.env\\.EDGE_JWT_SECRET")).toContain("src/auth/edge_secret.ts"); expect((await grep("process\\.env\\.EDGE_JWT_SECRET")).trim().split("\n")).toHaveLength(1); }); }); /** * Object storage is a credential like any other, and used to be the one that * failed quietly. * * `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY` defaulted to `""`, so a * deployment that copied `.env.example` and filled in everything else * started happily. `POST /jobs` then handed a client a presigned URL signed * with an empty key, and the first symptom was a 403 from R2 - in the * client's uploader, or inside a gateway that had downloaded R2's XML error * document instead of a print. Nothing in that chain said "R2 is not * configured". */ describe("object storage configuration", () => { const importS3 = (env: Record) => Bun.spawn(["bun", "-e", "await import('./src/s3/index.ts')"], { cwd: import.meta.dir + "/..", env: { ...process.env, ...env }, stderr: "pipe", stdout: "pipe", }); // Every other variable is filled in, so the one under test is the only // thing missing and the error has to name it. Without this the first // missing variable in read order wins and the assertion passes for the // wrong reason. const complete = { R2_ACCESS_KEY_ID: "key", R2_SECRET_ACCESS_KEY: "secret", R2_ACCOUNT_ID: "acct", }; test("fails at boot when a credential is empty, naming the variable", async () => { for (const name of ["R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"]) { const proc = importS3({ ...complete, [name]: "" }); expect(await proc.exited).not.toBe(0); expect(await new Response(proc.stderr).text()).toContain(name); } }); /** * An empty string, not just an absent variable - which is the case that * actually happens, because the committed template ships these keys with * no value. `??` would have missed every one of them. */ test("treats an empty credential as missing, not as a value", async () => { const proc = importS3({ ...complete, R2_ACCESS_KEY_ID: "" }); expect(await proc.exited).not.toBe(0); expect(await new Response(proc.stderr).text()).toContain("R2_ACCESS_KEY_ID"); }); test("refuses to start with no endpoint and no account id to derive one from", async () => { const proc = importS3({ ...complete, R2_ENDPOINT: "", R2_ACCOUNT_ID: "" }); expect(await proc.exited).not.toBe(0); expect(await new Response(proc.stderr).text()).toContain("R2_ENDPOINT"); }); test("starts with a complete configuration", async () => { // R2_ENDPOINT empty on purpose: it must be derived from the account id, // which is the `||`-not-`??` case that caught this module out. const proc = importS3({ ...complete, R2_ENDPOINT: "" }); expect(await proc.exited).toBe(0); }); test("reads each credential in exactly one place", async () => { // So there is only ever one line to get right, the same rule // `EDGE_JWT_SECRET` is held to above. for (const name of ["R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"]) { const proc = Bun.spawn(["grep", "-rn", `process.env.${name}`, "src"], { cwd: import.meta.dir + "/..", stdout: "pipe", }); const hits = (await new Response(proc.stdout).text()).trim(); // Read through the `required()` helper, so the literal name appears // only where it is passed in - not as a `process.env.X` at all. expect(hits).toBe(""); } }); }); /** * The credentials this backend is not allowed to hold or hand out. * * `printers.accessCode` used to exist: writable through `POST /printers`, * excluded from the projection, and never read by any backend code path - a * printer's LAN password at rest in the cloud for no purpose. It was removed * (2026-10-19) because the credential belongs on the proxy, the only thing * that uses it, and the alternative of surfacing it in the UI was considered * and rejected: useful, but not worth the risk. * * These tests exist so that decision is enforced rather than remembered. A * column is easy to re-add, and the symptom would be silent. */ describe("credentials the backend must not hold", () => { test("a printer has no access-code column to store one in", () => { // Against the Drizzle table rather than live SQL, so it fails in CI with // no database - the point is the declaration, not this machine's schema. expect(Object.keys(printers)).not.toContain("accessCode"); }); test("a client that sends one anyway has it dropped, not stored", () => { // The create schema is open, so a stray field validates. Elysia cleans // the body against the schema, which is what actually discards it - so // the honest description is "ignored", not "rejected". Worth pinning: // if the field ever became retained, nothing else here would notice. const body = { farmId: "3f2b1c5e-0000-4000-8000-000000000000", name: "P1P", vendor: "bambu" as const, accessCode: "SECRET123", }; const cleaned = Value.Clean(PrinterCreateSchema, structuredClone(body)) as Record; expect(cleaned).not.toHaveProperty("accessCode"); expect(cleaned.name).toBe("P1P"); }); test("the farm's edge proxy token is still kept out of responses", () => { // The credential that does remain. `POST /auth/edge-token` trades it for // an edge JWT, so with it and a farm id anyone can connect as that // farm's gateway and drive its printers. expect(Object.keys(farmProjection.columns)).not.toContain("edgeProxyToken"); }); test("metadata is the remaining way a secret could reach the cloud", () => { // Not a bug to fix here, but a hole to know about: `metadata` is a // free-form jsonb column that is both writable and in the public // projection, so a well-meaning client could stash an access code in it // and the API would store it and serve it back. Removing the dedicated // column narrowed the door; it did not close it. If this assertion ever // fails because `metadata` left the projection, that is an improvement - // delete the test. expect(Object.keys(printerProjection.columns)).toContain("metadata"); }); });