// The vault page shows secret names to any active team member and carries
// what public/vault.js needs to decrypt values in the browser: the vault id,
// the member's registered public key (to reject a wrong pasted key before
// trying it) and whether a grant exists. It must never carry ciphertext or a
// wrapped key — those are fetched by the script from the session-authed API.
process.env.DATABASE_URL = process.env.PWA_TEST_DATABASE_URL || ":memory:";
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import express from "express";
import { splitVaultName, vaultPageBody, KEY_HELP } from "../src/lib/vault-page.mjs";
const here = dirname(fileURLToPath(import.meta.url));
const { db, run, isPostgres } = await import("../src/db.mjs");
const { pagesRouter } = await import("../src/routes/pages.mjs");
async function migrate() {
if (isPostgres) {
await db.execute("DROP SCHEMA public CASCADE");
await db.execute("CREATE SCHEMA public");
}
for (const file of ["001_auth.sql", "002_credshare.sql"]) {
const sql = readFileSync(join(here, "..", "src", isPostgres ? "migrations-pg" : "migrations", file), "utf8");
for (const statement of sql.split(/;\s*$/m).map((s) => s.trim()).filter(Boolean)) {
await db.execute(statement);
}
}
}
async function serve(user) {
const app = express();
app.use((req, _res, next) => { req.user = user; req.csrfToken = "t"; next(); });
app.use(pagesRouter);
const server = app.listen(0);
await new Promise((resolve) => server.once("listening", resolve));
const base = `http://127.0.0.1:${server.address().port}`;
return {
async get(path) {
const res = await fetch(`${base}${path}`, { redirect: "manual" });
return { status: res.status, headers: res.headers, text: await res.text() };
},
close: () => new Promise((resolve) => server.close(resolve))
};
}
const now = Date.now();
await migrate();
for (const [uid, email] of [["u-ann", "ann@example.com"], ["u-bob", "bob@example.com"], ["u-eve", "eve@example.com"]]) {
await run(`INSERT INTO users (id, email, created_at) VALUES (?,?,?)`, [uid, email, now]);
}
await run(`INSERT INTO credshare_teams (id, slug, name, created_by, created_at) VALUES ('t1','acme','acme','u-ann',?)`, [now]);
for (const [mid, uid, email] of [["m1", "u-ann", "ann@example.com"], ["m2", "u-bob", "bob@example.com"]]) {
await run(`INSERT INTO credshare_members (id, team_id, user_id, email, role, status, joined_at, created_at) VALUES (?,?,?,?,?,?,?,?)`,
[mid, "t1", uid, email, "member", "active", now, now]);
}
await run(`INSERT INTO credshare_keys (user_id, public_key, updated_at) VALUES ('u-ann','ANN_PUBLIC_KEY',?)`, [now]);
await run(`INSERT INTO credshare_vaults (id, team_id, name, created_by, created_at) VALUES ('v1','t1','api--prod','u-ann',?)`, [now]);
await run(`INSERT INTO credshare_vault_grants (vault_id, user_id, wrapped_dek, granted_by, created_at) VALUES ('v1','u-ann','WRAPPED_DEK_SECRET','u-ann',?)`, [now]);
for (const name of ["DATABASE_URL", "STRIPE_KEY"]) {
await run(`INSERT INTO credshare_secrets (vault_id, name, nonce, ciphertext, fingerprint, version, updated_by, updated_at) VALUES (?,?,?,?,?,?,?,?)`,
["v1", name, "NONCE_" + name, "CIPHERTEXT_" + name, "fp", 2, "u-ann", now]);
}
test("a member with a grant sees names, their public key, and the unlock form", async () => {
const app = await serve({ id: "u-ann", email: "ann@example.com" });
try {
const res = await app.get("/teams/acme/vaults/v1");
assert.equal(res.status, 200);
assert.match(res.headers.get("cache-control") || "", /no-store/);
assert.match(res.text, /data-key-name="DATABASE_URL"/);
assert.match(res.text, /data-key-name="STRIPE_KEY"/);
assert.match(res.text, /data-public-key="ANN_PUBLIC_KEY"/);
assert.match(res.text, /data-has-grant="1"/);
assert.match(res.text, /data-role="unlock"/);
assert.match(res.text, /logicsrc teams key/);
assert.match(res.text, /src="\/vendor\/libsodium\.js"/);
assert.match(res.text, /src="\/vault\.js"/);
} finally {
await app.close();
}
});
test("the page never embeds ciphertext, nonces or the wrapped key", async () => {
const app = await serve({ id: "u-ann", email: "ann@example.com" });
try {
const { text } = await app.get("/teams/acme/vaults/v1");
assert.doesNotMatch(text, /CIPHERTEXT_|NONCE_|WRAPPED_DEK_SECRET/);
} finally {
await app.close();
}
});
test("a member with no key is offered a browser-made key, not a paste box", async () => {
const app = await serve({ id: "u-bob", email: "bob@example.com" });
try {
const { status, text } = await app.get("/teams/acme/vaults/v1");
assert.equal(status, 200);
assert.match(text, /data-public-key=""/);
assert.match(text, /data-has-grant="0"/);
assert.match(text, /data-action="generate"/);
assert.doesNotMatch(text, /data-role="unlock"/);
assert.match(text, /logicsrc teams grant acme api prod bob@example\.com/);
} finally {
await app.close();
}
});
test("a non-member and an unknown vault both fall through to 404", async () => {
const eve = await serve({ id: "u-eve", email: "eve@example.com" });
try {
assert.equal((await eve.get("/teams/acme/vaults/v1")).status, 404);
} finally {
await eve.close();
}
const ann = await serve({ id: "u-ann", email: "ann@example.com" });
try {
assert.equal((await ann.get("/teams/acme/vaults/nope")).status, 404);
} finally {
await ann.close();
}
});
test("the dashboard links each vault to its page", async () => {
const app = await serve({ id: "u-ann", email: "ann@example.com" });
try {
const { text } = await app.get("/dashboard");
assert.match(text, /href="\/teams\/acme\/vaults\/v1"/);
} finally {
await app.close();
}
});
test("vault names split the way the CLI splits them", () => {
assert.deepEqual(splitVaultName("api--prod"), { project: "api", env: "prod" });
assert.deepEqual(splitVaultName("my-app--staging"), { project: "my-app", env: "staging" });
assert.equal(splitVaultName("legacy"), null);
assert.equal(splitVaultName("a--b--c"), null);
});
test("names are escaped", () => {
const html = vaultPageBody({
team: { slug: "acme" },
vault: { id: "v", name: "x--y" },
secrets: [{ name: `
`, version: 1, updated_at: now }],
hasGrant: true,
publicKey: "pk",
email: "a@b.c"
});
assert.doesNotMatch(html, /
{
assert.match(KEY_HELP, /logicsrc teams key/);
assert.match(KEY_HELP, /jq -r \.keys\.secretKey ~\/\.config\/logicsrc\/identity\.json/);
});