import { type } from "arktype"; import log from "~/utils/log"; import DockerIntegration from "./integration/docker"; import KubernetesIntegration from "./integration/kubernetes"; import ProcIntegration from "./integration/proc"; import { deprecatedField } from "./utils"; export const pathSupportedKeys = [ "server.cookie_secret", "oidc.client_secret", "oidc.headscale_api_key", "integration.agent.pre_authkey", ] as const; const serverConfig = type({ host: 'string.ip = "0.0.0.0"', port: "number.integer = 3000", base_url: "string.url?", data_path: 'string.lower = "/var/lib/headplane/"', info_secret: "string?", cookie_secret: "(32 <= string <= 32)", cookie_secure: "boolean = true", cookie_domain: "string.lower?", cookie_max_age: "number.integer = 86400", }); const partialServerConfig = type({ host: "string.ip?", port: "number.integer?", base_url: "string.url?", data_path: "string.lower?", info_secret: "string?", cookie_secret: "(32 <= string <= 32)?", cookie_secure: "boolean?", cookie_domain: "string.lower?", cookie_max_age: "number.integer?", }); const headscaleConfig = type({ url: type("string.url").pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v)), public_url: type("string.url") .pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v)) .optional(), config_path: "string.lower?", config_strict: "boolean = true", dns_records_path: "string.lower?", tls_cert_path: "string.lower?", }); const partialHeadscaleConfig = type({ url: type("string.url") .pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v)) .optional(), public_url: type("string.url") .pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v)) .optional(), config_path: "string.lower?", config_strict: "boolean?", dns_records_path: "string.lower?", tls_cert_path: "string.lower?", }); const oidcConfig = type({ enabled: "boolean = true", issuer: "string.url", client_id: "string", client_secret: "string", headscale_api_key: "string", use_pkce: "boolean = false", redirect_uri: type("string.url") .pipe((value, ctx) => { log.warn("config", "%s is deprecated and will be removed in 0.7.0", ctx.propString); const cleanedValue = new URL(value.trim()); if (cleanedValue.pathname.endsWith(`${__PREFIX__}/oidc/callback`)) { cleanedValue.pathname = cleanedValue.pathname.replace(`${__PREFIX__}/oidc/callback`, "/"); log.warn( "config", 'Please migrate to using `server.base_url` with a value of "%s"', cleanedValue.toString(), ); } return cleanedValue.toString(); }) .optional(), disable_api_key_login: "boolean = false", scope: 'string = "openid email profile"', profile_picture_source: '"oidc" | "gravatar" = "oidc"', extra_params: "Record?", authorization_endpoint: "string.url?", token_endpoint: "string.url?", userinfo_endpoint: "string.url?", token_endpoint_auth_method: '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?', // Old/deprecated options strict_validation: type("unknown").narrow(deprecatedField()).optional(), }); const partialOidcConfig = type({ enabled: "boolean?", issuer: "string.url?", client_id: "string?", client_secret: "string?", use_pkce: "boolean?", headscale_api_key: "string?", redirect_uri: "string.url?", disable_api_key_login: "boolean?", scope: "string?", extra_params: "Record?", profile_picture_source: '"oidc" | "gravatar"?', authorization_endpoint: "string.url?", token_endpoint: "string.url?", userinfo_endpoint: "string.url?", token_endpoint_auth_method: '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?', // Old/deprecated options strict_validation: type("unknown").narrow(deprecatedField()).optional(), }); const agentConfig = type({ enabled: "boolean", host_name: 'string = "headplane-agent"', pre_authkey: "string", cache_ttl: "number.integer = 180000", cache_path: 'string = "/var/lib/headplane/agent_cache.json"', executable_path: 'string = "/usr/libexec/headplane/agent"', work_dir: 'string = "/var/lib/headplane/agent"', }); const partialAgentConfig = type({ enabled: "boolean?", host_name: "string?", pre_authkey: "string?", cache_ttl: "number.integer?", cache_path: "string?", executable_path: "string?", work_dir: "string?", }); const integrationConfig = type({ docker: DockerIntegration.configSchema.full, kubernetes: KubernetesIntegration.configSchema.full, proc: ProcIntegration.configSchema.full, agent: agentConfig.optional(), }).partial(); export const partialIntegrationConfig = type({ docker: DockerIntegration.configSchema.partial, kubernetes: KubernetesIntegration.configSchema.partial, proc: ProcIntegration.configSchema.partial, agent: partialAgentConfig.optional(), }).partial(); export const headplaneConfig = type({ debug: "boolean = false", server: serverConfig, headscale: headscaleConfig, oidc: oidcConfig.optional(), integration: integrationConfig.optional(), }).onDeepUndeclaredKey("delete"); export const partialHeadplaneConfig = type({ debug: "boolean?", server: partialServerConfig.optional(), headscale: partialHeadscaleConfig.optional(), oidc: partialOidcConfig.optional(), integration: partialIntegrationConfig.optional(), }); export type HeadplaneConfig = typeof headplaneConfig.infer; export type PartialHeadplaneConfig = typeof partialHeadplaneConfig.infer; type DotNotationToObjects = T extends `${infer K}.${infer Rest}` ? { [P in K]?: DotNotationToObjects } : { [P in `${T}_path`]?: V }; type ObjectDeepMerge = T extends object ? { [K in keyof T]: T[K] extends object ? ObjectDeepMerge : T[K]; } : T; type ConfigWithPathKeys = ObjectDeepMerge< DotNotationToObjects<(typeof pathSupportedKeys)[number], string | undefined> >; export type PartialHeadplaneConfigWithPaths = PartialHeadplaneConfig & ConfigWithPathKeys;