feat: harden enterprise control plane

This commit is contained in:
AI Bot
2026-05-17 02:20:08 +08:00
parent 67511c31f4
commit e1aed590f8
112 changed files with 10977 additions and 2004 deletions

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { Client } from "pg";
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
@@ -7,6 +7,8 @@ import process from "node:process";
const snapshotKey = process.env.BOSS_STATE_POSTGRES_KEY?.trim() || "default";
const defaultStateFile = process.env.BOSS_STATE_FILE || path.join(process.cwd(), "data", "boss-state.json");
const defaultBackupDir = process.env.BOSS_STATE_BACKUP_DIR || path.join(path.dirname(defaultStateFile), "backups");
const defaultSchemaFile = path.join(process.cwd(), "scripts", "postgres-state-schema.sql");
const postgresTable = "boss_state_snapshots";
function usage() {
return [
@@ -14,13 +16,16 @@ function usage() {
"",
"Commands:",
" describe",
" validate-schema [--schema <file>]",
" backup-file --input <file> [--output <file>] [--dry-run]",
" export-file --input <file> --output <file> [--dry-run]",
" migrate-file-to-postgres --input <file> [--dry-run]",
" export-postgres-backup --output <file> [--dry-run]",
" restore-postgres-backup --input <file> [--dry-run]",
" rollback-postgres-to-file --output <file> [--dry-run]",
"",
"Environment:",
" BOSS_STATE_FILE, BOSS_DATABASE_URL, BOSS_STATE_POSTGRES_KEY, BOSS_STATE_BACKUP_DIR",
" BOSS_STATE_FILE, BOSS_STATE_STORE, BOSS_DATABASE_URL, BOSS_STATE_POSTGRES_KEY, BOSS_STATE_BACKUP_DIR",
].join("\n");
}
@@ -43,6 +48,11 @@ function parseArgs(argv) {
index += 1;
continue;
}
if (item === "--schema") {
options.schema = items[index + 1];
index += 1;
continue;
}
throw new Error(`UNKNOWN_OPTION:${item}`);
}
return options;
@@ -56,6 +66,30 @@ function timestampSegment() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function sha256(text) {
return createHash("sha256").update(text).digest("hex");
}
function postgresModeEnabled() {
return process.env.BOSS_STATE_STORE?.trim().toLowerCase() === "postgres";
}
function postgresConfigured() {
return Boolean(process.env.BOSS_DATABASE_URL?.trim());
}
function requirePostgresMode() {
if (!postgresModeEnabled()) {
throw new Error("BOSS_STATE_STORE_POSTGRES_REQUIRED");
}
}
function requirePostgresDatabaseUrl() {
if (!postgresConfigured()) {
throw new Error("BOSS_DATABASE_URL_REQUIRED");
}
}
function validateStateText(text, source) {
const parsed = JSON.parse(text);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -70,6 +104,37 @@ async function readStateText(filePath) {
return text;
}
function validatePostgresSchemaText(text, source) {
const compact = text.replace(/\s+/g, " ").toLowerCase();
const required = [
[/create table if not exists boss_state_snapshots/, "table"],
[/snapshot_key\s+text\s+primary key/, "snapshot_key_primary_key"],
[/state\s+jsonb\s+not null/, "state_jsonb"],
[/created_at\s+timestamptz\s+not null\s+default now\(\)/, "created_at"],
[/updated_at\s+timestamptz\s+not null\s+default now\(\)/, "updated_at"],
[/create index if not exists boss_state_snapshots_updated_at_idx/, "updated_at_index"],
];
const missing = required.filter(([pattern]) => !pattern.test(compact)).map(([, name]) => name);
if (missing.length > 0) {
throw new Error(`POSTGRES_SCHEMA_INVALID:${source}:${missing.join(",")}`);
}
return {
ok: true,
source,
table: postgresTable,
sha256: sha256(text),
};
}
async function validatePostgresSchema(options) {
const schema = path.resolve(options.schema || defaultSchemaFile);
const text = await fs.readFile(schema, "utf8");
return {
action: "validate-schema",
...validatePostgresSchemaText(text, schema),
};
}
async function ensurePostgresSchema(client) {
await client.query(`
CREATE TABLE IF NOT EXISTS boss_state_snapshots (
@@ -79,13 +144,17 @@ async function ensurePostgresSchema(client) {
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`);
await client.query(`
CREATE INDEX IF NOT EXISTS boss_state_snapshots_updated_at_idx
ON boss_state_snapshots (updated_at DESC)
`);
}
async function withPostgres(handler) {
const connectionString = process.env.BOSS_DATABASE_URL?.trim();
if (!connectionString) {
throw new Error("BOSS_DATABASE_URL_REQUIRED");
}
requirePostgresMode();
requirePostgresDatabaseUrl();
const connectionString = process.env.BOSS_DATABASE_URL.trim();
const { Client } = await import("pg");
const client = new Client({ connectionString });
await client.connect();
try {
@@ -135,8 +204,11 @@ async function exportFile(options) {
}
async function migrateFileToPostgres(options) {
requirePostgresMode();
requirePostgresDatabaseUrl();
const source = path.resolve(options.input || defaultStateFile);
const text = await readStateText(source);
const schema = await validatePostgresSchema({ schema: options.schema });
if (!options.dryRun) {
await withPostgres(async (client) => {
await ensurePostgresSchema(client);
@@ -157,11 +229,18 @@ async function migrateFileToPostgres(options) {
dryRun: options.dryRun,
source,
snapshotKey,
postgresConfigured: postgresConfigured(),
wouldConnect: !options.dryRun,
schemaValid: schema.ok,
schemaSha256: schema.sha256,
bytes: Buffer.byteLength(text),
stateSha256: sha256(text),
};
}
async function rollbackPostgresToFile(options) {
requirePostgresMode();
requirePostgresDatabaseUrl();
const output = path.resolve(options.output || defaultStateFile);
if (options.dryRun) {
return {
@@ -170,6 +249,8 @@ async function rollbackPostgresToFile(options) {
dryRun: true,
output,
snapshotKey,
postgresConfigured: postgresConfigured(),
wouldConnect: false,
};
}
const text = await withPostgres(async (client) => {
@@ -190,6 +271,115 @@ async function rollbackPostgresToFile(options) {
output,
snapshotKey,
bytes: Buffer.byteLength(text),
stateSha256: sha256(text),
};
}
function normalizeBackupPayload(text, source) {
const parsed = validateStateText(text, source);
if (parsed.metadata?.format === "boss-state-postgres-backup/v1" && parsed.state) {
return {
metadata: parsed.metadata,
state: parsed.state,
stateText: JSON.stringify(parsed.state, null, 2),
bundled: true,
};
}
return {
metadata: null,
state: parsed,
stateText: JSON.stringify(parsed, null, 2),
bundled: false,
};
}
async function exportPostgresBackup(options) {
requirePostgresMode();
const output = path.resolve(options.output || path.join(defaultBackupDir, `boss-postgres-state-${timestampSegment()}.json`));
if (options.dryRun) {
requirePostgresDatabaseUrl();
return {
ok: true,
action: "export-postgres-backup",
dryRun: true,
output,
snapshotKey,
postgresConfigured: true,
wouldConnect: false,
};
}
const stateText = await withPostgres(async (client) => {
await ensurePostgresSchema(client);
const result = await client.query("SELECT state FROM boss_state_snapshots WHERE snapshot_key = $1", [snapshotKey]);
const state = result.rows[0]?.state;
if (!state) {
throw new Error("BOSS_POSTGRES_STATE_NOT_FOUND");
}
return JSON.stringify(state, null, 2);
});
const state = validateStateText(stateText, `${postgresTable}:${snapshotKey}`);
const backup = {
metadata: {
format: "boss-state-postgres-backup/v1",
exportedAt: new Date().toISOString(),
snapshotKey,
table: postgresTable,
stateSha256: sha256(stateText),
stateBytes: Buffer.byteLength(stateText),
},
state,
};
const backupText = `${JSON.stringify(backup, null, 2)}\n`;
await fs.mkdir(path.dirname(output), { recursive: true });
await fs.writeFile(output, backupText, "utf8");
return {
ok: true,
action: "export-postgres-backup",
dryRun: false,
output,
snapshotKey,
bytes: Buffer.byteLength(backupText),
stateSha256: backup.metadata.stateSha256,
};
}
async function restorePostgresBackup(options) {
requirePostgresMode();
requirePostgresDatabaseUrl();
if (!options.input) {
throw new Error("INPUT_REQUIRED");
}
const source = path.resolve(options.input);
const text = await fs.readFile(source, "utf8");
const backup = normalizeBackupPayload(text, source);
validateStateText(backup.stateText, source);
if (!options.dryRun) {
await withPostgres(async (client) => {
await ensurePostgresSchema(client);
await client.query(
`
INSERT INTO boss_state_snapshots (snapshot_key, state, updated_at)
VALUES ($1, $2::jsonb, now())
ON CONFLICT (snapshot_key)
DO UPDATE SET state = EXCLUDED.state, updated_at = now()
`,
[snapshotKey, backup.stateText],
);
});
}
return {
ok: true,
action: "restore-postgres-backup",
dryRun: options.dryRun,
source,
snapshotKey,
postgresConfigured: postgresConfigured(),
wouldConnect: !options.dryRun,
bundled: backup.bundled,
bytes: Buffer.byteLength(backup.stateText),
stateSha256: sha256(backup.stateText),
};
}
@@ -200,14 +390,17 @@ async function main() {
jsonOut({
ok: true,
action: "describe",
mode: process.env.BOSS_STATE_STORE === "postgres" ? "postgres" : "file",
mode: postgresModeEnabled() ? "postgres" : "file",
stateFile: path.resolve(defaultStateFile),
backupDir: path.resolve(defaultBackupDir),
postgresConfigured: Boolean(process.env.BOSS_DATABASE_URL?.trim()),
postgresTable: "boss_state_snapshots",
postgresConfigured: postgresConfigured(),
postgresTable,
snapshotKey,
});
return;
case "validate-schema":
jsonOut(await validatePostgresSchema(options));
return;
case "backup-file":
jsonOut(await backupFile(options));
return;
@@ -217,6 +410,12 @@ async function main() {
case "migrate-file-to-postgres":
jsonOut(await migrateFileToPostgres(options));
return;
case "export-postgres-backup":
jsonOut(await exportPostgresBackup(options));
return;
case "restore-postgres-backup":
jsonOut(await restorePostgresBackup(options));
return;
case "rollback-postgres-to-file":
jsonOut(await rollbackPostgresToFile(options));
return;