110 lines
4.5 KiB
TypeScript
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;
|
|
}
|
|
}
|