#!/usr/bin/env node import { lstat, readFile } from "node:fs/promises"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { PrismaClient } from "@prisma/client"; import { bootstrapAlphaSilo, type AlphaSiloBootstrapInput } from "./bootstrap-silo.js"; import { loadLocalSecretKeyringFile, LocalSecretEnvelope } from "../security/secretEnvelope.js"; import { readSiloOrganizationId } from "./silo.js"; const execFileAsync = promisify(execFile); async function main(): Promise { if (process.getuid?.() !== 0) throw new Error("Alpha Silo bootstrap must run as root"); await requireStoppedSilo(); const { configFile, keyringFile } = parseArgs(process.argv.slice(2)); await requireRootPrivateFile(configFile, "bootstrap config"); await requireRootPrivateFile(keyringFile, "secret keyring"); const databaseUrl = process.env["DATABASE_URL"]?.trim(); if (databaseUrl === undefined || databaseUrl === "") throw new Error("DATABASE_URL is required"); const input = parseConfig(await readFile(configFile, "utf8")); const configuredOrganizationId = readSiloOrganizationId(); if (input.organization.id !== configuredOrganizationId) { throw new Error( `bootstrap Organization ${input.organization.id} does not match HUB_SILO_ORGANIZATION_ID ${configuredOrganizationId}`, ); } const secrets = new LocalSecretEnvelope(await loadLocalSecretKeyringFile(keyringFile)); const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } }, log: [] }); try { const result = await bootstrapAlphaSilo(prisma, secrets, input); console.log(JSON.stringify(result)); } finally { await prisma.$disconnect(); } } async function requireStoppedSilo(): Promise { const unit = process.env["HUB_SYSTEMD_UNIT"]?.trim(); if (unit === undefined || !/^cph-hub-[a-z0-9][a-z0-9-]*\.service$/.test(unit)) { throw new Error("HUB_SYSTEMD_UNIT must name the Silo being bootstrapped"); } let state: string; try { const result = await execFileAsync("systemctl", ["show", "--property=ActiveState", "--value", unit], { timeout: 10_000, }); state = result.stdout.trim(); } catch (error) { throw new Error(`cannot verify that ${unit} is stopped`, { cause: error }); } if (state !== "inactive" && state !== "failed") { throw new Error(`${unit} must be stopped before bootstrap; state=${state || "unknown"}`); } } function parseArgs(argv: readonly string[]): { readonly configFile: string; readonly keyringFile: string } { if (argv.length !== 4 || argv[0] !== "--config-file" || argv[2] !== "--keyring-file" || argv[1] === undefined || argv[1] === "" || argv[3] === undefined || argv[3] === "") { throw new Error("usage: bootstrap-silo --config-file /root/bootstrap.json --keyring-file /root/keyring.json"); } return { configFile: argv[1], keyringFile: argv[3] }; } async function requireRootPrivateFile(path: string, label: string): Promise { const metadata = await lstat(path); if (metadata.isSymbolicLink() || !metadata.isFile()) throw new Error(`${label} is not a regular file: ${path}`); if (metadata.uid !== 0 || (metadata.mode & 0o077) !== 0) { throw new Error(`${label} must be root-owned and inaccessible by group/others: ${path}`); } } function parseConfig(source: string): AlphaSiloBootstrapInput { let value: unknown; try { value = JSON.parse(source); } catch (error) { throw new Error("bootstrap config is not valid JSON", { cause: error }); } if (!isRecord(value) || !isRecord(value["organization"]) || !isRecord(value["owner"]) || !isRecord(value["feishu"]) || !isRecord(value["provider"])) { throw new Error("bootstrap config must contain organization, owner, feishu, and provider objects"); } const organization = value["organization"]; const owner = value["owner"]; const feishu = value["feishu"]; const provider = value["provider"]; const teams = value["teams"]; if (teams !== undefined && !Array.isArray(teams)) throw new Error("bootstrap teams must be an array"); return { organization: { id: stringField(organization, "id"), slug: stringField(organization, "slug"), name: stringField(organization, "name"), }, owner: { openId: stringField(owner, "openId"), displayName: stringField(owner, "displayName"), ...(optionalStringField(owner, "unionId") !== undefined ? { unionId: optionalStringField(owner, "unionId")! } : {}), }, feishu: { appId: stringField(feishu, "appId"), appSecret: stringField(feishu, "appSecret"), botOpenId: stringField(feishu, "botOpenId"), ...(optionalStringField(feishu, "verificationToken") !== undefined ? { verificationToken: optionalStringField(feishu, "verificationToken")! } : {}), ...(optionalStringField(feishu, "encryptKey") !== undefined ? { encryptKey: optionalStringField(feishu, "encryptKey")! } : {}), }, provider: { providerId: stringField(provider, "providerId"), baseUrl: stringField(provider, "baseUrl"), authToken: stringField(provider, "authToken"), ...(optionalStringField(provider, "anthropicApiKey") !== undefined ? { anthropicApiKey: optionalStringField(provider, "anthropicApiKey")! } : {}), }, ...(Array.isArray(teams) ? { teams: teams.map((team, index) => { if (!isRecord(team)) throw new Error(`bootstrap teams[${index}] must be an object`); return { slug: stringField(team, "slug"), name: stringField(team, "name"), ...(optionalStringField(team, "description") !== undefined ? { description: optionalStringField(team, "description")! } : {}), }; }), } : {}), }; } function stringField(value: Record, name: string): string { const field = value[name]; if (typeof field !== "string" || field.trim() === "") throw new Error(`bootstrap ${name} is required`); return field; } function optionalStringField(value: Record, name: string): string | undefined { const field = value[name]; if (field === undefined) return undefined; if (typeof field !== "string") throw new Error(`bootstrap ${name} must be a string`); return field; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } main().catch((error: unknown) => { console.error(`[bootstrap-silo] failed: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; });