SPB Git
4.0 KB · 50 lines typescript
1/**2 * Secure owner bootstrap: prompts for username + password interactively (never echoed, never logged),3 * stores only the Argon2id hash, creates the workspace and the default home layout.4 * Usage (on the node): pnpm bootstrap5 */6import readline from "node:readline/promises";7import { stdin, stdout } from "node:process";8import { eq } from "drizzle-orm";9import { db, schema, sql } from "../db/index";10import { hashPassword, passwordPolicy } from "../lib/auth/password";1112// Non-interactive mode (stdin is a pipe): answers are read line by line in order: username, display name, email, password, confirm.13const piped: string[] = stdin.isTTY ? [] : (await new Promise<string>((res) => { let b = ""; stdin.setEncoding("utf8"); stdin.on("data", (c) => (b += c)); stdin.on("end", () => res(b)); })).split("\n");14const ask = async (q: string, hidden = false): Promise<string> => {15  if (!stdin.isTTY) { stdout.write(q + (hidden ? "[hidden]" : (piped[0] ?? "")) + "\n"); return (piped.shift() ?? "").trim(); }16  if (!hidden) { const rl = readline.createInterface({ input: stdin, output: stdout }); const v = await rl.question(q); rl.close(); return v.trim(); }17  return new Promise((resolve) => {18    stdout.write(q); let buf = "";19    const onData = (ch: Buffer) => { const c = ch.toString("utf8"); if (c === "\n" || c === "\r" || c === "\u0004") { stdin.setRawMode(false); stdin.pause(); stdin.off("data", onData); stdout.write("\n"); resolve(buf); } else if (c === "\u0003") process.exit(1); else if (c === "\u007f") buf = buf.slice(0, -1); else buf += c; };20    stdin.setRawMode(true); stdin.resume(); stdin.on("data", onData);21  });22};23const username = (await ask("Owner username [simon]: ")) || "simon";24const displayName = (await ask("Display name [Simon-Pierre Boucher]: ")) || "Simon-Pierre Boucher";25const email = (await ask("Email (optional): ")) || null;26let password = "";27for (;;) {28  password = await ask("Password (min 12 chars, upper/lower/digit): ", true);29  const err = passwordPolicy(password); if (err) { console.log(err); if (!stdin.isTTY) process.exit(1); continue; }30  const again = await ask("Confirm password: ", true);31  if (again !== password) { console.log("Passwords differ."); if (!stdin.isTTY) process.exit(1); continue; }32  break;33}34const passwordHash = await hashPassword(password);35password = "";36const [existing] = await db.select().from(schema.users).where(eq(schema.users.username, username)).limit(1);37let userId: string;38if (existing) { await db.update(schema.users).set({ passwordHash, displayName, email, updatedAt: new Date() }).where(eq(schema.users.id, existing.id)); userId = existing.id; console.log(`Password updated for '${username}'.`); }39else { const [u] = await db.insert(schema.users).values({ username, displayName, email, passwordHash }).returning(); userId = u.id; console.log(`User '${username}' created.`); }40const [ws] = await db.select().from(schema.workspaces).where(eq(schema.workspaces.ownerId, userId)).limit(1);41if (!ws) { await db.insert(schema.workspaces).values({ ownerId: userId, name: "Note SPBoucher AI", slug: "main" }); console.log("Workspace created."); }42const [layout] = await db.select().from(schema.dashboardLayouts).where(eq(schema.dashboardLayouts.userId, userId)).limit(1);43if (!layout) await db.insert(schema.dashboardLayouts).values({ userId, name: "Home", widgets: [44  { id: "quick", type: "quick_capture", size: "wide" }, { id: "recent", type: "recent_pages", size: "half" }, { id: "pinned", type: "pinned", size: "half" },45  { id: "tasks", type: "tasks_due", size: "half" }, { id: "projects", type: "active_projects", size: "half" }, { id: "research", type: "research_activity", size: "half" },46  { id: "graph", type: "graph_highlights", size: "half" }, { id: "summary", type: "ai_summary", size: "wide" }, { id: "chat", type: "ai_chat", size: "wide" }] });47await db.insert(schema.aiProviders).values([{ id: "openai", name: "OpenAI" }, { id: "anthropic", name: "Anthropic" }]).onConflictDoNothing();48console.log("Bootstrap complete. Only the password hash was stored.");49await sql.end();50
Files · 2c37cb5a3e2af18e5ba361996d453cab97da82c2
Menu

Theme