Files
multi-simadmin/apps/api/src/production-readiness.ts
T

118 lines
3.9 KiB
TypeScript

import type Database from 'better-sqlite3';
import { stat as fsStat } from 'node:fs/promises';
import { MIGRATIONS } from './infrastructure/database/migrations.js';
import type { ReadinessResult } from './app.js';
export interface ProductionReadinessOptions {
readonly db: Database.Database;
readonly gatewayToken: string;
/** Metadata/availability probe only. This callback must never retrieve a secret. */
readonly keychainMetadataCheck: () => boolean | Promise<boolean>;
/** Release gates are deliberately required: omitting one fails readiness closed. */
readonly assetsCheck?: () => boolean | Promise<boolean>;
readonly recoveryMarkersCheck?: () => number | Promise<number>;
readonly upstreamGatewayCheck?: () => boolean | Promise<boolean>;
}
interface ExecutableMetadata {
isFile(): boolean;
readonly mode: number;
}
export async function defaultKeychainMetadataCheck(
platform: NodeJS.Platform = process.platform,
stat: (path: string) => Promise<ExecutableMetadata> = fsStat,
): Promise<boolean> {
if (platform !== 'darwin') return false;
try {
const metadata = await stat('/usr/bin/security');
return metadata.isFile() && (metadata.mode & 0o111) !== 0;
} catch {
return false;
}
}
export function createProductionReadiness(
options: ProductionReadinessOptions,
): () => Promise<ReadinessResult> {
return async () => {
const checks: Record<string, string> = {};
let ready = true;
try {
options.db.prepare('SELECT 1').get();
checks.database = 'ready';
} catch {
checks.database = 'unavailable';
ready = false;
}
try {
const expected = MIGRATIONS.at(-1)?.id ?? 0;
const row = options.db.prepare('SELECT MAX(id) AS id FROM schema_migrations').get() as {
id: number | null;
};
checks.migrations = row.id === expected ? 'ready' : 'pending';
if (row.id !== expected) ready = false;
} catch {
checks.migrations = 'unavailable';
ready = false;
}
checks.gatewayAuth = Buffer.byteLength(options.gatewayToken) >= 32 ? 'ready' : 'unconfigured';
if (checks.gatewayAuth !== 'ready') ready = false;
try {
checks.keychain = (await options.keychainMetadataCheck()) ? 'ready' : 'unavailable';
} catch {
checks.keychain = 'unavailable';
}
if (checks.keychain !== 'ready') ready = false;
try {
const row = options.db
.prepare(
"SELECT value_json FROM app_settings WHERE key = 'legacyImport.pendingCredentials'",
)
.get() as { value_json: string } | undefined;
const value: unknown = row === undefined ? 0 : JSON.parse(row.value_json);
checks.pendingCredentials =
Number.isSafeInteger(value) && (value as number) >= 0
? value === 0
? 'ready'
: `pending:${String(value)}`
: 'unavailable';
} catch {
checks.pendingCredentials = 'unavailable';
}
if (checks.pendingCredentials !== 'ready') ready = false;
if (options.recoveryMarkersCheck === undefined) {
checks.recoveryMarkers = 'unconfigured';
} else {
try {
const count = await options.recoveryMarkersCheck();
checks.recoveryMarkers =
Number.isSafeInteger(count) && count >= 0
? count === 0
? 'ready'
: `present:${String(count)}`
: 'unavailable';
} catch {
checks.recoveryMarkers = 'unavailable';
}
}
if (checks.recoveryMarkers !== 'ready') ready = false;
for (const [name, check] of [
['assets', options.assetsCheck],
['upstreamGateway', options.upstreamGatewayCheck],
] as const) {
if (check === undefined) {
checks[name] = 'unconfigured';
} else {
try {
checks[name] = (await check()) ? 'ready' : 'unavailable';
} catch {
checks[name] = 'unavailable';
}
}
if (checks[name] !== 'ready') ready = false;
}
return { ready, checks };
};
}