import * as plugins from './plugins.js'; import { resolveAGLHomePaths, type IAGLHomeOptions, } from './classes.aglhome.js'; import type { IControllerDatabaseConfig, IControllerRuntimeConfig, IControllerRuntimeOverrides, TControllerTlsMode, } from './interfaces.config.js'; export const defaultControllerPort = 4097; export const defaultOpenCodePort = 4098; export const trustedProxySecretEnvironmentVariable = 'HARNESS_CONTROLLER_TRUSTED_PROXY_SECRET'; export interface IControllerDataDirectoryOptions extends IAGLHomeOptions {} const assertPort = (valueArg: number, nameArg: string): number => { if (!Number.isSafeInteger(valueArg) || valueArg < 1 || valueArg > 65535) { throw new Error(`${nameArg} must be an integer between 1 and 65535.`); } return valueArg; }; const effectivePort = (originArg: URL): number => { if (originArg.port) return Number(originArg.port); return originArg.protocol === 'https:' ? 443 : 80; }; const normalizeOrigin = (originArg: string): URL => { let origin: URL; try { origin = new URL(originArg); } catch { throw new Error('publicOrigin must be an absolute HTTP or HTTPS origin.'); } if ( (origin.protocol !== 'http:' && origin.protocol !== 'https:') || origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash ) { throw new Error('publicOrigin must contain only an HTTP(S) scheme, hostname, and optional port.'); } return origin; }; const normalizeRpId = (rpIdArg: string, hostnameArg: string): string => { const rpId = plugins.url.domainToASCII( rpIdArg.trim().toLowerCase().replace(/\.$/, ''), ); if (!rpId || rpId.includes('/') || rpId.includes(':') || /\s/.test(rpId)) { throw new Error('rpId must be a valid hostname without a scheme, port, or path.'); } const hostname = hostnameArg.toLowerCase().replace(/\.$/, ''); if (hostname !== rpId && !hostname.endsWith(`.${rpId}`)) { throw new Error('rpId must equal the public-origin hostname or be its registrable parent domain.'); } if (rpId === 'localhost' && hostname === 'localhost') return rpId; const parsed = plugins.tldts.parse(rpId, { allowPrivateDomains: true, extractHostname: false, validateHostname: true, }); if (parsed.isIp || parsed.domain === null || parsed.publicSuffix === rpId) { throw new Error('rpId must not be an IP address, single-label name, or public suffix.'); } return rpId; }; const resolveTlsMode = ( originArg: URL, behindTlsProxyArg: boolean, ): TControllerTlsMode => { if (originArg.protocol === 'http:') { if (originArg.hostname !== 'localhost') { throw new Error('Plain HTTP is accepted only for the localhost WebAuthn development origin.'); } if (behindTlsProxyArg) { throw new Error('The localhost HTTP mode cannot also enable proxy termination.'); } return 'local'; } if (behindTlsProxyArg) return 'proxy'; throw new Error( 'An HTTPS public origin requires --behind-tls-proxy and a trusted TLS reverse proxy.', ); }; const resolveDirectory = async (directoryArg: string): Promise => { const resolved = await plugins.fs.promises.realpath(plugins.path.resolve(directoryArg)); const stats = await plugins.fs.promises.stat(resolved); if (!stats.isDirectory()) { throw new Error(`Configured path is not a directory: ${resolved}`); } return resolved; }; export const controllerDataDirectory = ( optionsArg: IControllerDataDirectoryOptions = {}, ): string => resolveAGLHomePaths(optionsArg).root; /** @deprecated Use controllerDataDirectory(). */ export const controllerStateDirectory = controllerDataDirectory; export const readDatabaseConfig = ( optionsArg: IControllerDataDirectoryOptions = {}, ): IControllerDatabaseConfig => { const environment = optionsArg.environment ?? process.env; const mongoDbUrl = environment.HARNESS_CONTROLLER_MONGO_URL?.trim(); const mongoDbName = environment.HARNESS_CONTROLLER_MONGO_DB?.trim() || 'opencode_controller'; if (!mongoDbName || !/^[A-Za-z0-9_-]{1,63}$/.test(mongoDbName)) { throw new Error('HARNESS_CONTROLLER_MONGO_DB must use 1-63 letters, digits, underscores, or dashes.'); } if (mongoDbUrl) { return { mongoDbUrl, mongoDbName }; } const embeddedDataDirectoryOverride = environment.HARNESS_CONTROLLER_DB_DIR?.trim(); const embeddedDataDirectory = embeddedDataDirectoryOverride || resolveAGLHomePaths(optionsArg).database; if (!plugins.path.isAbsolute(embeddedDataDirectory)) { throw new Error('HARNESS_CONTROLLER_DB_DIR must be an absolute path.'); } return { mongoDbName, embeddedDataDirectory, }; }; export const readTrustedProxySecret = (tlsModeArg: TControllerTlsMode): string | undefined => { if (tlsModeArg === 'local') return undefined; const secret = process.env[trustedProxySecretEnvironmentVariable]?.trim(); if ( !secret || !/^[A-Za-z0-9_-]{43}$/u.test(secret) || Buffer.from(secret, 'base64url').byteLength !== 32 ) { throw new Error( `${trustedProxySecretEnvironmentVariable} must be a 32-byte base64url secret in proxy mode.`, ); } return secret; }; export const resolveInitialRuntimeConfig = async ( overridesArg: IControllerRuntimeOverrides, ): Promise => { const controllerPort = assertPort(overridesArg.controllerPort, 'controllerPort'); const opencodePort = assertPort( overridesArg.opencodePort ?? defaultOpenCodePort, 'opencodePort', ); if (controllerPort === opencodePort) { throw new Error('The controller and OpenCode ports must be different.'); } const origin = normalizeOrigin( overridesArg.publicOrigin ?? `http://localhost:${controllerPort}`, ); const tlsMode = resolveTlsMode(origin, overridesArg.behindTlsProxy === true); if (tlsMode !== 'proxy' && effectivePort(origin) !== controllerPort) { throw new Error('Local public-origin port must match the controller listening port.'); } const rpId = normalizeRpId(overridesArg.rpId ?? origin.hostname, origin.hostname); const projectsRoot = await resolveDirectory( overridesArg.projectsRoot ?? plugins.os.homedir(), ); return { controllerPort, publicOrigin: origin.origin, rpId, projectsRoot, opencodePort, tlsMode, }; }; export const resolveStoredRuntimeConfig = async ( storedArg: IControllerRuntimeConfig, overridesArg: IControllerRuntimeOverrides, ): Promise => { assertPort(storedArg.controllerPort, 'stored controllerPort'); assertPort(storedArg.opencodePort, 'stored opencodePort'); if (storedArg.controllerPort !== overridesArg.controllerPort) { throw new Error('Stored controller port does not match the requested management port.'); } const storedOrigin = normalizeOrigin(storedArg.publicOrigin); if (storedOrigin.origin !== storedArg.publicOrigin) { throw new Error('Stored publicOrigin is not canonical.'); } const normalizedStoredRpId = normalizeRpId(storedArg.rpId, storedOrigin.hostname); if (normalizedStoredRpId !== storedArg.rpId) { throw new Error('Stored rpId is not canonical.'); } if (storedArg.controllerPort === storedArg.opencodePort) { throw new Error('Stored controller and OpenCode ports conflict.'); } if ( storedArg.tlsMode !== 'local' && storedArg.tlsMode !== 'proxy' ) { throw new Error('Stored TLS mode is invalid.'); } const validatedTlsMode = resolveTlsMode( storedOrigin, storedArg.tlsMode === 'proxy', ); if (validatedTlsMode !== storedArg.tlsMode) { throw new Error('Stored TLS mode conflicts with the public origin or TLS material.'); } if (storedArg.tlsMode !== 'proxy' && effectivePort(storedOrigin) !== storedArg.controllerPort) { throw new Error('Stored local public-origin port does not match the controller port.'); } if (overridesArg.publicOrigin !== undefined) { const requestedOrigin = normalizeOrigin(overridesArg.publicOrigin).origin; if (requestedOrigin !== storedArg.publicOrigin) { throw new Error('publicOrigin is immutable after controller initialization.'); } } if (overridesArg.rpId !== undefined) { const requestedRpId = normalizeRpId(overridesArg.rpId, storedOrigin.hostname); if (requestedRpId !== storedArg.rpId) { throw new Error('rpId is immutable after controller initialization.'); } } if (overridesArg.opencodePort !== undefined && overridesArg.opencodePort !== storedArg.opencodePort) { throw new Error('opencodePort is immutable after controller initialization.'); } if (storedArg.tlsMode === 'proxy') { if (overridesArg.behindTlsProxy === false) { throw new Error('Stored TLS proxy mode is immutable.'); } } else { if (overridesArg.behindTlsProxy) { throw new Error('Stored localhost mode cannot be changed to TLS mode.'); } } if ( !plugins.path.isAbsolute(storedArg.projectsRoot) || plugins.path.normalize(storedArg.projectsRoot) !== storedArg.projectsRoot ) { throw new Error('Stored projects root is not an absolute normalized path.'); } if (overridesArg.projectsRoot !== undefined) { const requestedProjectsRoot = await resolveDirectory(overridesArg.projectsRoot); if (requestedProjectsRoot !== storedArg.projectsRoot) { throw new Error('The projects root is immutable after it has been adopted.'); } } return { ...storedArg }; };