node:http server that serves the console build and a /api/* the console accepts: its own login and session (clusterInitialized, the servers as nodes, hub flag, Dashboard and Logs only), node=<id> forwarded with that server's token, no node or cluster merged (stats, top lists, query logs, apps), every write refused with 403, a short response cache, and an unreachable server reported as hubWarnings. The dev mock takes --port and --scale so two of them differ. Vitest skips .claude/ worktrees. Co-Authored-By: Claude Opus 5.5 <[email protected]>
82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
/*
|
|
The hub's configuration, read from the environment. `parseConfig` is pure so the
|
|
tests can feed it any environment; `loadConfig` is the one that reads `process.env`.
|
|
*/
|
|
|
|
export const HUB_VERSION = '1.0.0'
|
|
|
|
export interface HubServer {
|
|
id: string
|
|
url: string
|
|
token: string
|
|
}
|
|
|
|
export interface HubConfig {
|
|
servers: HubServer[]
|
|
user: string
|
|
password: string
|
|
port: number
|
|
cacheSeconds: number
|
|
timeoutMs: number
|
|
/** Shown as `dnsServerDomain` (and `clusterDomain`) in the session. */
|
|
domain: string
|
|
/** The console's static build. */
|
|
staticDir: string
|
|
}
|
|
|
|
type Env = Record<string, string | undefined>
|
|
|
|
/* `ndns` -> `NDNS_TOKEN`, `my-dns.1` -> `MY_DNS_1_TOKEN`. */
|
|
export function tokenVariable(id: string): string {
|
|
return `${id.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_TOKEN`
|
|
}
|
|
|
|
function number(env: Env, name: string, fallback: number, min: number): number {
|
|
const raw = env[name]
|
|
if (raw === undefined || raw.trim() === '') return fallback
|
|
const value = Number(raw)
|
|
if (!Number.isFinite(value) || value < min) throw new Error(`${name} must be a number of at least ${min}.`)
|
|
return value
|
|
}
|
|
|
|
export function parseConfig(env: Env): HubConfig {
|
|
const user = env.HUB_USER ?? ''
|
|
const password = env.HUB_PASSWORD ?? ''
|
|
if (user === '' || password === '') throw new Error('HUB_USER and HUB_PASSWORD are required.')
|
|
|
|
const servers: HubServer[] = []
|
|
for (const part of (env.HUB_SERVERS ?? '').split(',')) {
|
|
const entry = part.trim()
|
|
if (entry === '') continue
|
|
const at = entry.indexOf('=')
|
|
const id = at < 0 ? '' : entry.slice(0, at).trim()
|
|
const url = at < 0 ? '' : entry.slice(at + 1).trim().replace(/\/+$/, '')
|
|
if (!/^[A-Za-z0-9._-]+$/.test(id) || id === 'cluster' || !/^https?:\/\/[^/]/.test(url)) {
|
|
throw new Error(`HUB_SERVERS entry "${entry}" is not of the form id=https://host (id: letters, digits, . _ -).`)
|
|
}
|
|
if (servers.some((s) => s.id === id)) throw new Error(`HUB_SERVERS lists "${id}" twice.`)
|
|
const variable = tokenVariable(id)
|
|
const token = env[variable] ?? ''
|
|
if (token === '') throw new Error(`${variable} is required for the server "${id}".`)
|
|
servers.push({ id, url, token })
|
|
}
|
|
if (servers.length === 0) {
|
|
throw new Error('HUB_SERVERS is required, e.g. dns=https://dns.example.net,ndns=https://ndns.example.net.')
|
|
}
|
|
|
|
return {
|
|
servers,
|
|
user,
|
|
password,
|
|
port: number(env, 'HUB_PORT', 8080, 0),
|
|
cacheSeconds: number(env, 'HUB_CACHE_SECONDS', 15, 0),
|
|
timeoutMs: number(env, 'HUB_TIMEOUT_MS', 8000, 1),
|
|
domain: env.HUB_DOMAIN?.trim() || 'hub',
|
|
staticDir: env.HUB_STATIC?.trim() || './dist',
|
|
}
|
|
}
|
|
|
|
export function loadConfig(): HubConfig {
|
|
return parseConfig(process.env)
|
|
}
|