Repository Public
1/**2 * First-run setup token: 32 random bytes (base64url) in `$CR_DATA_DIR/setup-token`, mode 0600.3 * It is never printed or logged; the operator reads the file on the host. Deleted once consumed.4 */5import fs from 'node:fs/promises';6import path from 'node:path';7import type { Queryable } from '../db.js';8import { randomToken, safeEqual } from './crypto.js';910export const setupTokenPath = (dataDir: string): string => path.join(dataDir, 'setup-token');1112export async function userExists(db: Queryable): Promise<boolean> {13 return (await db.one('SELECT 1 AS x FROM users LIMIT 1')) !== null;14}1516/** Called at startup: (re)generates the token while no administrator exists, removes it otherwise. */17export async function ensureSetupToken(db: Queryable, dataDir: string): Promise<void> {18 const file = setupTokenPath(dataDir);19 if (await userExists(db)) {20 await fs.rm(file, { force: true });21 return;22 }23 await fs.mkdir(dataDir, { recursive: true, mode: 0o700 });24 await fs.rm(file, { force: true });25 await fs.writeFile(file, randomToken(32), { mode: 0o600, flag: 'wx' });26}2728export async function checkSetupToken(dataDir: string, candidate: string): Promise<boolean> {29 let expected: string;30 try {31 expected = (await fs.readFile(setupTokenPath(dataDir), 'utf8')).trim();32 } catch {33 return false;34 }35 return expected.length >= 32 && safeEqual(expected, candidate);36}37export const deleteSetupToken = (dataDir: string): Promise<void> => fs.rm(setupTokenPath(dataDir), { force: true });38