feat: add deployable alpha silo

This commit is contained in:
2026-07-11 00:25:45 +08:00
parent 44557da499
commit 9e954790dc
57 changed files with 2792 additions and 400 deletions
+158
View File
@@ -0,0 +1,158 @@
#!/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<void> {
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<void> {
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<void> {
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<string, unknown>, 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<string, unknown>, 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<string, unknown> {
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;
});
+365
View File
@@ -0,0 +1,365 @@
import { randomUUID } from "node:crypto";
import type { Prisma, PrismaClient } from "@prisma/client";
import {
fingerprintFeishuAppId,
validateFeishuApplicationCredential,
type FeishuApplicationCredentialInput,
} from "../connections/feishuApplicationConnections.js";
import {
decryptStoredProviderCredential,
ProviderConnectionService,
type ProviderCredentialInput,
} from "../connections/providerConnections.js";
import { probeFeishuApplication, type FeishuReadinessProbe } from "../connections/feishuReadiness.js";
import { probeOpenRouterCredential, type ProviderReadinessProbe } from "../connections/providerReadiness.js";
import { deterministicFeishuUserId, scopedFeishuPrincipalId } from "../feishu/identityNamespace.js";
import { LocalSecretEnvelope } from "../security/secretEnvelope.js";
export interface AlphaSiloBootstrapInput {
readonly organization: { readonly id: string; readonly slug: string; readonly name: string };
readonly owner: { readonly openId: string; readonly displayName: string; readonly unionId?: string };
readonly feishu: FeishuApplicationCredentialInput;
readonly provider: ProviderCredentialInput & { readonly providerId: string };
readonly teams?: readonly {
readonly slug: string;
readonly name: string;
readonly description?: string;
}[];
}
export interface AlphaSiloBootstrapResult {
readonly organizationId: string;
readonly ownerUserId: string;
readonly feishuConnectionId: string;
readonly initialized: boolean;
readonly providerConfigured: boolean;
}
/**
* Idempotently bootstrap the one Organization allowed in an Alpha Silo.
* Secrets are accepted only in-memory and persisted as authenticated envelopes.
*/
export async function bootstrapAlphaSilo(
prisma: PrismaClient,
secrets: LocalSecretEnvelope,
input: AlphaSiloBootstrapInput,
probes: {
readonly feishu?: FeishuReadinessProbe;
readonly provider?: ProviderReadinessProbe;
} = {},
): Promise<AlphaSiloBootstrapResult> {
const normalized = validateInput(input);
const existing = await loadExistingSilo(prisma, normalized.organization.id, normalized.owner.openId);
let initialized = false;
let ownerUserId: string;
let feishuConnectionId: string;
if (existing === null) {
await (probes.feishu ?? probeFeishuApplication)(normalized.feishu);
const initializedSilo = await initializeSilo(prisma, secrets, normalized);
initialized = true;
ownerUserId = initializedSilo.ownerUserId;
feishuConnectionId = initializedSilo.feishuConnectionId;
} else {
ownerUserId = existing.ownerUserId;
feishuConnectionId = existing.feishuConnectionId;
const expectedFingerprint = fingerprintFeishuAppId(normalized.feishu.appId);
if (existing.appIdentityFingerprint !== expectedFingerprint) {
throw new Error("bootstrap Feishu app identity does not match the existing Silo");
}
}
const providerService = new ProviderConnectionService(
prisma,
secrets,
probes.provider ?? probeOpenRouterCredential,
);
const existingProviders = await providerService.list(normalized.organization.id);
const existingProvider = existingProviders.find((connection) => connection.providerId === normalized.provider.providerId);
if (existingProvider === undefined) {
await providerService.rotateByok({
organizationId: normalized.organization.id,
actorUserId: ownerUserId,
providerId: normalized.provider.providerId,
baseUrl: normalized.provider.baseUrl,
authToken: normalized.provider.authToken,
...(normalized.provider.anthropicApiKey !== undefined
? { anthropicApiKey: normalized.provider.anthropicApiKey }
: {}),
});
} else if (existingProvider.status !== "ACTIVE" || existingProvider.mode !== "BYOK") {
throw new Error(`bootstrap provider ${normalized.provider.providerId} exists but is not ACTIVE BYOK`);
} else {
const stored = await loadExistingProviderCredential(
prisma,
secrets,
normalized.organization.id,
normalized.provider.providerId,
);
if (stored.baseUrl !== normalized.provider.baseUrl || stored.authToken !== normalized.provider.authToken ||
stored.anthropicApiKey !== (normalized.provider.anthropicApiKey ?? "")) {
throw new Error(`bootstrap provider ${normalized.provider.providerId} credential does not match existing Silo`);
}
}
await ensureBootstrapTeams(prisma, normalized.organization.id, ownerUserId, normalized.teams ?? []);
return {
organizationId: normalized.organization.id,
ownerUserId,
feishuConnectionId,
initialized,
providerConfigured: true,
};
}
async function ensureBootstrapTeams(
prisma: PrismaClient,
organizationId: string,
ownerUserId: string,
teams: NonNullable<AlphaSiloBootstrapInput["teams"]>,
): Promise<void> {
await prisma.$transaction(async (tx) => {
for (const team of teams) {
const existing = await tx.team.findFirst({
where: { organizationId, slug: team.slug, archivedAt: null },
select: { id: true, name: true, description: true },
});
if (existing !== null && (existing.name !== team.name || existing.description !== (team.description ?? null))) {
throw new Error(`bootstrap Team ${team.slug} does not match the existing Silo`);
}
const teamId = existing?.id ?? (await tx.team.create({
data: {
organizationId,
slug: team.slug,
name: team.name,
...(team.description !== undefined ? { description: team.description } : {}),
},
select: { id: true },
})).id;
const membership = await tx.teamMembership.findFirst({
where: { teamId, userId: ownerUserId, revokedAt: null },
select: { id: true },
});
if (membership === null) await tx.teamMembership.create({ data: { teamId, userId: ownerUserId } });
}
});
}
async function initializeSilo(
prisma: PrismaClient,
secrets: LocalSecretEnvelope,
input: AlphaSiloBootstrapInput,
): Promise<{ readonly ownerUserId: string; readonly feishuConnectionId: string }> {
const connectionId = randomUUID();
const secretVersionId = randomUUID();
const identityId = randomUUID();
const ownerUserId = deterministicFeishuUserId(connectionId, input.owner.openId);
const appIdentityFingerprint = fingerprintFeishuAppId(input.feishu.appId);
const payload = validateFeishuApplicationCredential(input.feishu);
const envelope = secrets.encryptJson({
purpose: "feishu-application",
organizationId: input.organization.id,
connectionId,
secretVersionId,
}, payload);
await prisma.$transaction(async (tx) => {
const count = await tx.organization.count();
if (count !== 0) throw new Error(`Silo bootstrap requires an empty Organization set; found ${count}`);
await tx.organization.create({ data: { ...input.organization } });
await tx.organizationProjectSettings.create({
data: { organizationId: input.organization.id, membersCanCreateProjects: true },
});
await tx.folder.create({
data: {
organizationId: input.organization.id,
name: "Inbox",
sortKey: "000000",
},
});
await tx.user.create({
data: {
id: ownerUserId,
feishuOpenId: scopedFeishuPrincipalId("USER", connectionId, input.owner.openId),
displayName: input.owner.displayName,
},
});
await tx.organizationFeishuApplicationConnection.create({
data: {
id: connectionId,
organizationId: input.organization.id,
appIdentityFingerprint,
status: "DRAFT",
},
});
await tx.feishuUserIdentity.create({
data: {
id: identityId,
connectionId,
userId: ownerUserId,
openId: input.owner.openId,
...(input.owner.unionId !== undefined ? { unionId: input.owner.unionId } : {}),
},
});
await tx.organizationMembership.create({
data: { organizationId: input.organization.id, userId: ownerUserId, role: "OWNER" },
});
for (const team of input.teams ?? []) {
await tx.team.create({
data: {
organizationId: input.organization.id,
slug: team.slug,
name: team.name,
...(team.description !== undefined ? { description: team.description } : {}),
memberships: { create: { userId: ownerUserId } },
},
});
}
await tx.feishuApplicationCredentialVersion.create({
data: {
id: secretVersionId,
connectionId,
version: 1,
envelopeVersion: envelope.version,
keyId: envelope.keyId,
envelope: envelope as unknown as Prisma.InputJsonValue,
createdByUserId: ownerUserId,
},
});
await tx.organizationFeishuApplicationConnection.update({
where: { id: connectionId },
data: {
status: "ACTIVE",
activeSecretVersionId: secretVersionId,
activatedAt: new Date(),
},
});
await tx.auditEntry.create({
data: {
organizationId: input.organization.id,
actorUserId: ownerUserId,
action: "alpha_silo.bootstrapped",
metadata: {
connectionId,
ownerIdentityId: identityId,
envelopeVersion: envelope.version,
keyId: envelope.keyId,
},
},
});
});
return { ownerUserId, feishuConnectionId: connectionId };
}
async function loadExistingSilo(
prisma: PrismaClient,
organizationId: string,
ownerOpenId: string,
): Promise<{
readonly ownerUserId: string;
readonly feishuConnectionId: string;
readonly appIdentityFingerprint: string;
} | null> {
const organizations = await prisma.organization.findMany({
take: 2,
include: {
memberships: {
where: { role: "OWNER", revokedAt: null },
take: 2,
include: { user: { include: { feishuIdentities: true } } },
},
feishuApplicationConnection: true,
},
});
if (organizations.length === 0) return null;
if (organizations.length !== 1 || organizations[0]!.id !== organizationId) {
throw new Error("bootstrap database is not the configured single-Organization Silo");
}
const organization = organizations[0]!;
if (organization.status !== "ACTIVE" || organization.memberships.length !== 1 ||
organization.feishuApplicationConnection?.status !== "ACTIVE") {
throw new Error("existing Silo is incomplete or inactive; manual repair is required");
}
const ownerIdentity = organization.memberships[0]!.user.feishuIdentities.find(
(identity) => identity.connectionId === organization.feishuApplicationConnection!.id,
);
if (ownerIdentity?.openId !== ownerOpenId) {
throw new Error("bootstrap owner identity does not match the existing Silo");
}
return {
ownerUserId: organization.memberships[0]!.userId,
feishuConnectionId: organization.feishuApplicationConnection.id,
appIdentityFingerprint: organization.feishuApplicationConnection.appIdentityFingerprint,
};
}
async function loadExistingProviderCredential(
prisma: PrismaClient,
secrets: LocalSecretEnvelope,
organizationId: string,
providerId: string,
): Promise<{ readonly baseUrl: string; readonly authToken: string; readonly anthropicApiKey: string }> {
const connection = await prisma.organizationProviderConnection.findUnique({
where: { organizationId_providerId: { organizationId, providerId } },
include: { activeSecretVersion: true },
});
const version = connection?.activeSecretVersion;
if (connection === null || connection === undefined || version === null || version === undefined ||
connection.status !== "ACTIVE" || version.retiredAt !== null) {
throw new Error(`active provider connection not found: ${providerId}`);
}
return decryptStoredProviderCredential(secrets, {
organizationId,
connectionId: connection.id,
providerId,
secretVersionId: version.id,
envelopeVersion: version.envelopeVersion,
keyId: version.keyId,
envelope: version.envelope,
});
}
function validateInput(input: AlphaSiloBootstrapInput): AlphaSiloBootstrapInput {
const required = (value: string, label: string): string => {
const normalized = value.trim();
if (normalized === "") throw new Error(`${label} is required`);
return normalized;
};
const slug = required(input.organization.slug, "Organization slug");
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(slug)) {
throw new Error("Organization slug must be lowercase alphanumeric with optional hyphens");
}
if (input.provider.providerId.trim() !== "openrouter") {
throw new Error("Alpha Silo currently requires providerId=openrouter");
}
return {
organization: {
id: required(input.organization.id, "Organization id"),
slug,
name: required(input.organization.name, "Organization name"),
},
owner: {
openId: required(input.owner.openId, "Owner Feishu openId"),
displayName: required(input.owner.displayName, "Owner displayName"),
...(input.owner.unionId !== undefined ? { unionId: required(input.owner.unionId, "Owner unionId") } : {}),
},
feishu: input.feishu,
provider: input.provider,
...(input.teams !== undefined
? {
teams: input.teams.map((team) => {
const teamSlug = required(team.slug, "Team slug");
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(teamSlug)) {
throw new Error(`invalid Team slug: ${teamSlug}`);
}
return {
slug: teamSlug,
name: required(team.name, `Team ${teamSlug} name`),
...(team.description !== undefined ? { description: team.description.trim() } : {}),
};
}),
}
: {}),
};
}
+7 -2
View File
@@ -32,6 +32,7 @@ interface Options {
readonly stateDir: string;
readonly cacheDir: string;
readonly workspaceRoot: string;
readonly serviceUnit: string;
readonly bwrapBin: string;
readonly socatBin: string;
readonly pgIsReadyBin: string;
@@ -79,6 +80,7 @@ function deploymentInput(options: Options, env: DeploymentEnv): DeploymentPrefli
stateDir: options.stateDir,
cacheDir: options.cacheDir,
workspaceRoot: options.workspaceRoot,
expectedServiceUnit: options.serviceUnit,
env,
};
}
@@ -97,6 +99,7 @@ async function canonicalizeInput(input: DeploymentPreflightInput): Promise<Deplo
stateDir,
cacheDir,
workspaceRoot,
expectedServiceUnit: input.expectedServiceUnit,
env: { ...input.env, HUB_PROJECT_WORKSPACE_ROOT: workspaceRoot },
};
}
@@ -155,8 +158,8 @@ async function validateNodeVersion(nodeBin: string): Promise<void> {
}
const version = stdout.trim().replace(/^v/, "");
const major = Number(version.split(".")[0]);
if (!Number.isInteger(major) || major < 20) {
throw new Error(`Node.js 20 or newer is required; found ${version || "unknown"}`);
if (!Number.isInteger(major) || major < 24) {
throw new Error(`Node.js 24 or newer is required; found ${version || "unknown"}`);
}
console.log(`[preflight] Node.js: ${version}`);
}
@@ -479,6 +482,7 @@ function parseArgs(argv: readonly string[]): Options {
"--state-dir",
"--cache-dir",
"--workspace-root",
"--service-unit",
"--bwrap-bin",
"--socat-bin",
"--pg-isready-bin",
@@ -508,6 +512,7 @@ function parseArgs(argv: readonly string[]): Options {
stateDir: required(values, "--state-dir"),
cacheDir: required(values, "--cache-dir"),
workspaceRoot: required(values, "--workspace-root"),
serviceUnit: required(values, "--service-unit"),
bwrapBin: required(values, "--bwrap-bin"),
socatBin: required(values, "--socat-bin"),
pgIsReadyBin: required(values, "--pg-isready-bin"),
+24 -3
View File
@@ -10,6 +10,7 @@ export interface DeploymentPreflightInput {
readonly stateDir: string;
readonly cacheDir: string;
readonly workspaceRoot: string;
readonly expectedServiceUnit: string;
readonly env: DeploymentEnv;
}
@@ -89,9 +90,13 @@ export function validateDeploymentPreflight(input: DeploymentPreflightInput): De
}
}
readRequired(input.env, "FEISHU_APP_ID", problems);
readRequired(input.env, "FEISHU_APP_SECRET", problems);
readRequired(input.env, "FEISHU_BOT_OPEN_ID", problems);
readRequired(input.env, "HUB_SILO_ORGANIZATION_ID", problems);
const serviceUnit = readRequired(input.env, "HUB_SYSTEMD_UNIT", problems);
if (serviceUnit !== undefined && !/^cph-hub-[a-z0-9][a-z0-9-]*\.service$/.test(serviceUnit)) {
problems.push("HUB_SYSTEMD_UNIT must match cph-hub-<instance>.service");
} else if (serviceUnit !== undefined && serviceUnit !== input.expectedServiceUnit) {
problems.push(`HUB_SYSTEMD_UNIT must equal installed unit ${input.expectedServiceUnit}`);
}
const cphBin = readRequired(input.env, "CPH_BIN", problems);
if (cphBin !== undefined && !isAbsoluteNormalized(cphBin)) {
@@ -99,6 +104,7 @@ export function validateDeploymentPreflight(input: DeploymentPreflightInput): De
}
let bind: ServerBinding | undefined;
readRequired(input.env, "PORT", problems);
try {
bind = readServerBinding(input.env);
} catch (error) {
@@ -116,6 +122,13 @@ export function validateDeploymentPreflight(input: DeploymentPreflightInput): De
}
validateOptionalPositiveInteger(input.env, "HUB_AGENT_MAX_TURNS", problems);
readRequiredPositiveInteger(input.env, "HUB_AGENT_MAX_CONCURRENT_RUNS", problems);
readRequiredPositiveInteger(input.env, "HUB_AGENT_MAX_RUN_SECONDS", problems);
readRequiredPositiveInteger(input.env, "HUB_HTTP_BODY_LIMIT_BYTES", problems);
readRequiredPositiveInteger(input.env, "HUB_MAX_FILES_PER_MESSAGE", problems);
readRequiredPositiveInteger(input.env, "HUB_MAX_FILE_BYTES", problems);
readRequiredPositiveInteger(input.env, "HUB_HTTP_REQUESTS_PER_MINUTE", problems);
readRequiredPositiveInteger(input.env, "HUB_FEISHU_EVENTS_PER_MINUTE", problems);
validateOptionalBoolean(input.env, "HUB_FEISHU_LISTENER_ENABLED", problems);
if (problems.length > 0) {
@@ -147,6 +160,14 @@ function validateOptionalPositiveInteger(env: DeploymentEnv, name: string, probl
}
}
function readRequiredPositiveInteger(env: DeploymentEnv, name: string, problems: string[]): void {
const value = readRequired(env, name, problems);
if (value === undefined) return;
if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
problems.push(`${name} must be a positive integer`);
}
}
function validateOptionalBoolean(env: DeploymentEnv, name: string, problems: string[]): void {
const raw = env[name]?.trim().toLowerCase();
if (raw === undefined || raw === "") return;
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env node
import { realpath, stat } from "node:fs/promises";
import { PrismaClient } from "@prisma/client";
import { verifyStoredFeishuApplicationEnvelopes } from "../connections/feishuApplicationConnections.js";
import { verifyStoredProviderEnvelopes } from "../connections/providerConnections.js";
import { loadLocalSecretKeyringFile, LocalSecretEnvelope } from "../security/secretEnvelope.js";
import { readSiloOrganizationId, requireSiloOrganization } from "./silo.js";
async function main(): Promise<void> {
const keyringFile = parseArgs(process.argv.slice(2));
const databaseUrl = process.env["DATABASE_URL"]?.trim();
const workspaceRoot = process.env["HUB_PROJECT_WORKSPACE_ROOT"]?.trim();
if (databaseUrl === undefined || databaseUrl === "") throw new Error("DATABASE_URL is required");
if (workspaceRoot === undefined || workspaceRoot === "") throw new Error("HUB_PROJECT_WORKSPACE_ROOT is required");
const workspace = await realpath(workspaceRoot);
if (!(await stat(workspace)).isDirectory()) throw new Error("restored workspace root is not a directory");
const secrets = new LocalSecretEnvelope(await loadLocalSecretKeyringFile(keyringFile));
const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } }, log: [] });
try {
const organization = await requireSiloOrganization(prisma, readSiloOrganizationId());
const provider = await verifyStoredProviderEnvelopes(prisma, secrets);
const feishu = await verifyStoredFeishuApplicationEnvelopes(prisma, secrets);
const [activeProviders, activeFeishu] = await Promise.all([
prisma.organizationProviderConnection.count({
where: { organizationId: organization.id, status: "ACTIVE", activeSecretVersionId: { not: null } },
}),
prisma.organizationFeishuApplicationConnection.count({
where: { organizationId: organization.id, status: "ACTIVE", activeSecretVersionId: { not: null } },
}),
]);
if (activeProviders < 1) throw new Error("restored Silo has no ACTIVE Provider Connection");
if (activeFeishu !== 1) throw new Error(`restored Silo must have one ACTIVE Feishu Connection; found ${activeFeishu}`);
console.log(JSON.stringify({ organizationId: organization.id, providerEnvelopes: provider, feishuEnvelopes: feishu }));
} finally {
await prisma.$disconnect();
}
}
function parseArgs(argv: readonly string[]): string {
if (argv.length !== 2 || argv[0] !== "--keyring-file" || argv[1] === undefined || argv[1] === "") {
throw new Error("usage: restore-preflight --keyring-file /root/recovery/secret-keyring.json");
}
return argv[1];
}
main().catch((error: unknown) => {
console.error(`[restore-preflight] failed: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
});
+12 -4
View File
@@ -5,7 +5,6 @@ import { PrismaClient } from "@prisma/client";
import { rotateLocalKek } from "../security/localKekRotation.js";
const execFileAsync = promisify(execFile);
const SERVICE_UNIT = "cph-hub.service";
async function main(): Promise<void> {
if (process.getuid?.() !== 0) {
@@ -30,22 +29,31 @@ async function main(): Promise<void> {
}
async function requireStoppedService(): Promise<void> {
const serviceUnit = readServiceUnit();
let activeState: string;
try {
const { stdout } = await execFileAsync(
"systemctl",
["show", "--property=ActiveState", "--value", SERVICE_UNIT],
["show", "--property=ActiveState", "--value", serviceUnit],
{ timeout: 10_000 },
);
activeState = stdout.trim();
} catch {
throw new Error(`cannot verify that ${SERVICE_UNIT} is stopped`);
throw new Error(`cannot verify that ${serviceUnit} is stopped`);
}
if (activeState !== "inactive" && activeState !== "failed") {
throw new Error(`${SERVICE_UNIT} must be stopped before local KEK rotation; state=${activeState || "unknown"}`);
throw new Error(`${serviceUnit} must be stopped before local KEK rotation; state=${activeState || "unknown"}`);
}
}
function readServiceUnit(): string {
const serviceUnit = process.env["HUB_SYSTEMD_UNIT"]?.trim();
if (serviceUnit === undefined || !/^cph-hub-[a-z0-9][a-z0-9-]*\.service$/.test(serviceUnit)) {
throw new Error("HUB_SYSTEMD_UNIT must name this Silo unit (cph-hub-<instance>.service)");
}
return serviceUnit;
}
function parseKeyringFile(argv: readonly string[]): string {
if (argv.length !== 2 || argv[0] !== "--keyring-file" || argv[1] === undefined || argv[1] === "") {
throw new Error("usage: rotate-secret-kek --keyring-file /absolute/path/to/secret-keyring.json");
+51
View File
@@ -0,0 +1,51 @@
import type { PrismaClient } from "@prisma/client";
export interface SiloOrganization {
readonly id: string;
readonly slug: string;
readonly name: string;
}
/**
* Read the deployment-pinned Organization identity. A Silo process must never
* infer its tenant from request data or from whichever row happens to exist.
*/
export function readSiloOrganizationId(
env: Readonly<Record<string, string | undefined>> = process.env,
): string {
const organizationId = env["HUB_SILO_ORGANIZATION_ID"]?.trim();
if (organizationId === undefined || organizationId === "") {
throw new Error("HUB_SILO_ORGANIZATION_ID is required");
}
return organizationId;
}
/**
* Prove the database is a single-tenant Silo before accepting traffic.
* Archived rows count too: pointing a Silo process at a pooled or reused
* database is a deployment error, not a condition to paper over.
*/
export async function requireSiloOrganization(
prisma: PrismaClient,
organizationId: string,
): Promise<SiloOrganization> {
const [count, organization] = await Promise.all([
prisma.organization.count(),
prisma.organization.findUnique({
where: { id: organizationId },
select: { id: true, slug: true, name: true, status: true },
}),
]);
if (count !== 1) {
throw new Error(`Silo database must contain exactly one Organization; found ${count}`);
}
if (organization === null) {
throw new Error(
`Silo Organization mismatch: configured ${organizationId} is not the sole database Organization`,
);
}
if (organization.status !== "ACTIVE") {
throw new Error(`Silo Organization ${organizationId} is ${organization.status}`);
}
return { id: organization.id, slug: organization.slug, name: organization.name };
}
+25
View File
@@ -0,0 +1,25 @@
export class SiloFixedWindowRateLimiter {
private windowStartedAt: number;
private used = 0;
constructor(readonly limit: number, readonly windowMs: number, now = Date.now()) {
if (!Number.isSafeInteger(limit) || limit <= 0) throw new Error("rate limit must be a positive integer");
if (!Number.isSafeInteger(windowMs) || windowMs <= 0) throw new Error("rate window must be a positive integer");
this.windowStartedAt = now;
}
consume(now = Date.now()): { readonly allowed: true } | { readonly allowed: false; readonly retryAfterSeconds: number } {
if (now >= this.windowStartedAt + this.windowMs) {
this.windowStartedAt = now;
this.used = 0;
}
if (this.used >= this.limit) {
return {
allowed: false,
retryAfterSeconds: Math.max(1, Math.ceil((this.windowStartedAt + this.windowMs - now) / 1000)),
};
}
this.used += 1;
return { allowed: true };
}
}