From 356abab90a65bd8f7a5d6f92451829c1e8c407f4 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Thu, 21 Aug 2025 11:16:27 -0400 Subject: [PATCH] feat: do insane type validation for the config --- app/routes/machines/machine.tsx | 15 ++++++- app/server/config/loader.ts | 70 ++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/app/routes/machines/machine.tsx b/app/routes/machines/machine.tsx index 660e9f4..2a8dc7d 100644 --- a/app/routes/machines/machine.tsx +++ b/app/routes/machines/machine.tsx @@ -108,7 +108,10 @@ export default function Page() {
- {node.user.name || node.user.displayName || node.user.email || node.user.id} + {node.user.name || + node.user.displayName || + node.user.email || + node.user.id}
@@ -253,7 +256,15 @@ export default function Page() { variant="flat" >
- + { +export function interpolateEnvVars(str: string): string { + return str.replace(/\$\{([^}]+)\}/g, (_, varName) => { const value = env[varName]; if (value === undefined) { throw new ConfigError(`Environment variable "${varName}" not found`); @@ -52,9 +52,9 @@ export async function loadConfig({ loadEnv, path }: EnvOverrides) { if (!loadEnv) { log.debug('config', 'Environment variable overrides are disabled'); log.debug('config', 'This also disables the loading of a .env file'); - config = await loadSecretsFromFiles(config); + const moddedConfig = await loadSecretsFromFiles(config); log.debug('config', 'Loaded file-based secrets'); - return config; + return moddedConfig; } log.info('config', 'Loading a .env file (if available)'); @@ -67,10 +67,10 @@ export async function loadConfig({ loadEnv, path }: EnvOverrides) { ); } - config = await loadSecretsFromFiles(config); + const moddedConfig = await loadSecretsFromFiles(config); log.debug('config', 'Loaded file-based secrets'); - return config; + return moddedConfig; } /** @@ -78,13 +78,40 @@ export async function loadConfig({ loadEnv, path }: EnvOverrides) { * reads that file and assigns its contents to the corresponding key * without the suffix, then removes the "_path" property. */ -const SECRET_PATH_KEYS = new Set([ +const SECRET_PATH_KEYS = [ 'pre_authkey_path', 'client_secret_path', 'headscale_api_key_path', 'cookie_secret_path', -]); -async function loadSecretsFromFiles(obj: T): Promise { +] as const; + +// For fast set hashing lookups, but we still need the array for typings +const SECRET_PATH_KEY_SET = new Set(SECRET_PATH_KEYS); + +type SecretPathKey = (typeof SECRET_PATH_KEYS)[number]; +type StripPath = S extends `${infer T}_path` ? T : never; +type KeysToPromote = Extract; +type MappedKeys = StripPath>; + +type NonNullablized = Omit | MappedKeys> & { + [K in MappedKeys]-?: string; +}; + +type NestedNonNullablized = T extends readonly (infer U)[] + ? readonly NestedNonNullablized[] + : T extends (infer U)[] + ? NestedNonNullablized[] + : T extends object + ? { + [K in keyof NonNullablized]: NestedNonNullablized< + NonNullablized[K] + >; + } + : T; + +async function loadSecretsFromFiles( + obj: T, +): Promise> { // Work with a Record so we can mutate/delete properties const record = obj as Record; @@ -97,7 +124,7 @@ async function loadSecretsFromFiles(obj: T): Promise { continue; } - if (SECRET_PATH_KEYS.has(key) && typeof val === 'string') { + if (SECRET_PATH_KEY_SET.has(key) && typeof val === 'string') { try { const path = interpolateEnvVars(val); const content = await readFile(path, 'utf8'); @@ -114,26 +141,7 @@ async function loadSecretsFromFiles(obj: T): Promise { } // Cast back to the original T so callers keep their precise type - return record as T; -} - -export async function hp_loadConfig() { - // // OIDC Related Checks - // if (config.oidc) { - // if (!config.oidc.client_secret && !config.oidc.client_secret_path) { - // log.error('CFGX', 'OIDC configuration is missing a secret, disabling'); - // log.error( - // 'CFGX', - // 'Please specify either `oidc.client_secret` or `oidc.client_secret_path`', - // ); - // } - // if (config.oidc?.strict_validation) { - // const result = await testOidc(config.oidc); - // if (!result) { - // log.error('CFGX', 'OIDC configuration failed validation, disabling'); - // } - // } - // } + return record as NestedNonNullablized; } async function validateConfigPath(path: string) {