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

110 lines
4.5 KiB
TypeScript

import { dirname, isAbsolute, join } from 'node:path';
import {
buildControlPlaneApp,
type ControlPlaneApp,
type SafeControlPlaneUpstream,
} from './control-plane.js';
import {
createCommandRunner,
createLaunchdRestartLauncher,
} from './application/system/console-update-service.js';
import { openDatabase } from './infrastructure/database/database.js';
import { migrateDatabase } from './infrastructure/database/migrations.js';
import {
createDefaultSecretStore,
resolveSecretBackend,
defaultSecretFileMetadataCheck,
} from './infrastructure/secrets/secret-backend.js';
import type { SecretStore } from './infrastructure/secrets/secret-store.js';
import { createProductionUpstream } from './infrastructure/transport/production-upstream.js';
import { createProductionReadiness, defaultKeychainMetadataCheck } from './production-readiness.js';
export interface ProductionControlPlaneOptions {
readonly databasePath: string;
readonly store?: SecretStore;
readonly upstream?: SafeControlPlaneUpstream;
readonly gatewayToken?: string;
readonly keychainMetadataCheck?: () => boolean | Promise<boolean>;
readonly secretFileMetadataCheck?: () => boolean | Promise<boolean>;
readonly assetsCheck?: () => boolean | Promise<boolean>;
readonly runtimeVersion?: string;
readonly recoveryMarkersCheck?: () => number | Promise<number>;
readonly upstreamGatewayCheck?: () => boolean | Promise<boolean>;
/** Production keeps device online state fresh on its own; tests and canaries can opt out. */
readonly heartbeatEnabled?: boolean;
/** Environment used to find the launchd label that can restart this process. */
readonly env?: Readonly<Record<string, string | undefined>>;
}
export function buildProductionControlPlane(
options: ProductionControlPlaneOptions,
): ControlPlaneApp {
if (
typeof options.databasePath !== 'string' ||
options.databasePath.length === 0 ||
!isAbsolute(options.databasePath)
) {
throw new Error('An explicit absolute SQLite database path is required');
}
const db = openDatabase(options.databasePath);
try {
migrateDatabase(db);
const env = options.env ?? process.env;
const secretFilePath = join(dirname(options.databasePath), 'secrets.json');
const store = options.store ?? createDefaultSecretStore({ env, secretFilePath });
const secretBackend = resolveSecretBackend(env);
const updateRoot = env.MULTI_SIMADMIN_UPDATE_ROOT ?? process.cwd();
const updateRunner = createCommandRunner({ cwd: updateRoot, timeoutMs: 120_000 });
const app = buildControlPlaneApp({
db,
store,
upstream: options.upstream ?? createProductionUpstream(),
runtimeVersion: options.runtimeVersion ?? '0.1.0',
backupDirectory: join(dirname(options.databasePath), 'backups'),
heartbeatEnabled: options.heartbeatEnabled ?? true,
update: {
remote: env.MULTI_SIMADMIN_UPDATE_REMOTE ?? 'origin',
root: updateRoot,
runner: updateRunner,
launcher: createLaunchdRestartLauncher({ env, runner: updateRunner }),
},
// This route-local decision is safe only in this composition: when a token is
// configured, buildApp's global onRequest hook rejects the request first.
// Keep all other control-plane compositions fail-closed by default.
...(options.gatewayToken ? { authenticateEventStream: () => true } : {}),
app: {
logger: {},
...(options.gatewayToken ? { gatewayToken: options.gatewayToken } : {}),
readiness: createProductionReadiness({
db,
gatewayToken: options.gatewayToken ?? '',
...(secretBackend === 'macos-keychain'
? {
keychainMetadataCheck:
options.keychainMetadataCheck ?? defaultKeychainMetadataCheck,
}
: {
secretFileMetadataCheck:
options.secretFileMetadataCheck ??
(async () => defaultSecretFileMetadataCheck(secretFilePath)),
}),
...(options.assetsCheck ? { assetsCheck: options.assetsCheck } : {}),
...(options.recoveryMarkersCheck
? { recoveryMarkersCheck: options.recoveryMarkersCheck }
: {}),
...(options.upstreamGatewayCheck
? { upstreamGatewayCheck: options.upstreamGatewayCheck }
: {}),
}),
},
});
app.addHook('onClose', async () => {
if (db.open) db.close();
});
return app;
} catch (error) {
if (db.open) db.close();
throw error;
}
}