Spaces:
Paused
Paused
| import dotenv from 'dotenv'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| import { | |
| cleanEnv, | |
| str, | |
| bool, | |
| makeValidator, | |
| makeExactValidator, | |
| num, | |
| EnvError, | |
| port, | |
| EnvMissingError, | |
| } from 'envalid'; | |
| import { randomBytes } from 'crypto'; | |
| import fs from 'fs'; | |
| /** | |
| * Bootstrap environment validation. | |
| * | |
| */ | |
| // Get __dirname equivalent in ESM | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| try { | |
| dotenv.config({ path: path.resolve(__dirname, '../../../../.env') }); | |
| } catch (error) { | |
| console.error('Error loading .env file', error); | |
| } | |
| let metadata: any = undefined; | |
| try { | |
| function getResource(resourceName: string) { | |
| const filePath = path.join( | |
| __dirname, | |
| '../../../../', | |
| 'resources', | |
| resourceName | |
| ); | |
| if (!fs.existsSync(filePath)) { | |
| throw new Error(`Resource ${resourceName} not found at ${filePath}`); | |
| } | |
| return JSON.parse(fs.readFileSync(filePath, 'utf8')); | |
| } | |
| metadata = getResource('metadata.json') || {}; | |
| } catch (error) { | |
| console.error('Error loading metadata.json file', error); | |
| } | |
| const secretKey = makeValidator((x) => { | |
| if (!/^[0-9a-fA-F]{64}$/.test(x)) { | |
| throw new EnvError('Secret key must be a 64-character hex string'); | |
| } | |
| return x; | |
| }); | |
| const commaSeparated = makeExactValidator<string[]>((x) => { | |
| if (x === '') { | |
| return []; | |
| } | |
| const parsed = x.split(',').map((item) => item.trim()); | |
| if (parsed.some((item) => item === '')) { | |
| throw new EnvError('Comma separated values cannot be empty'); | |
| } | |
| return parsed; | |
| }); | |
| const removeTrailingSlash = (x: string) => | |
| x.endsWith('/') ? x.slice(0, -1) : x; | |
| const url = makeValidator((x) => { | |
| if (x === '') { | |
| throw new EnvMissingError(`URL cannot be empty`); | |
| } | |
| try { | |
| new URL(x); | |
| } catch (e) { | |
| throw new EnvError(`Invalid URL: ${x}`); | |
| } | |
| // remove trailing slash | |
| return removeTrailingSlash(x); | |
| }); | |
| const readonly = makeValidator((x) => { | |
| if (x) { | |
| throw new EnvError('Readonly environment variable, cannot be set'); | |
| } | |
| return x; | |
| }); | |
| const proxyAuth = makeValidator((x) => { | |
| if (typeof x !== 'string') { | |
| throw new EnvError('Proxy auth must be a string'); | |
| } | |
| // comma separated list of username:password | |
| const userMap: Map<string, string> = new Map(); | |
| x.split(',').forEach((x) => { | |
| const [username, password] = x.split(':'); | |
| if (!username || !password) { | |
| throw new EnvError( | |
| 'Proxy auth must be a comma separated list of username:password pairs' | |
| ); | |
| } | |
| userMap.set(username, password); | |
| }); | |
| return userMap; | |
| }); | |
| const connectionLimits = makeValidator((x) => { | |
| if (typeof x !== 'string') { | |
| throw new EnvError('Connection limits must be a string'); | |
| } | |
| // comma separated list of username:limit where limit is a number | |
| const limitMap: Map<string, number> = new Map(); | |
| x.split(',').forEach((x) => { | |
| const [username, limitStr] = x.split(':'); | |
| if (!username || !limitStr) { | |
| throw new EnvError( | |
| 'Connection limits must be a comma separated list of username:limit pairs' | |
| ); | |
| } | |
| const limit = Number(limitStr); | |
| if (limit === -1) | |
| if (Number.isNaN(limit) || limit < 0 || !Number.isInteger(limit)) { | |
| throw new EnvError( | |
| 'Connection limit must be a positive integer or 0 for unlimited' | |
| ); | |
| } | |
| limitMap.set(username, limit); | |
| }); | |
| return limitMap; | |
| }); | |
| /** | |
| * Resolves the effective time value for a given service from a serviceTimeMap. | |
| * Checks for a service-specific entry first, then falls back to the wildcard `*`. | |
| */ | |
| export function resolveServiceTime( | |
| map: Record<string, number>, | |
| serviceId: string | |
| ): number { | |
| return map[serviceId] ?? map['*'] ?? 0; | |
| } | |
| export const Env = cleanEnv(process.env, { | |
| VERSION: readonly({ | |
| default: metadata?.version || 'unknown', | |
| desc: 'Version of the addon', | |
| }), | |
| TAG: readonly({ | |
| default: metadata?.tag || 'unknown', | |
| desc: 'Tag of the addon', | |
| }), | |
| CHANNEL: readonly({ | |
| default: (metadata?.channel as 'stable' | 'nightly' | 'dev') || 'stable', | |
| choices: ['stable', 'nightly', 'dev'], | |
| desc: 'Build channel of the addon', | |
| }), | |
| DESCRIPTION: readonly({ | |
| default: metadata?.description || 'unknown', | |
| desc: 'Description of the addon', | |
| }), | |
| NODE_ENV: str({ | |
| default: 'production', | |
| desc: 'Node environment of the addon', | |
| choices: ['production', 'development', 'test'], | |
| }), | |
| GIT_COMMIT: readonly({ | |
| default: metadata?.commitHash || 'unknown', | |
| desc: 'Git commit hash of the addon', | |
| }), | |
| BUILD_TIME: readonly({ | |
| default: metadata?.buildTime || 'unknown', | |
| desc: 'Build time of the addon', | |
| }), | |
| BUILD_COMMIT_TIME: readonly({ | |
| default: metadata?.commitTime || 'unknown', | |
| desc: 'Build commit time of the addon', | |
| }), | |
| BASE_URL: url({ | |
| desc: 'Base URL of the addon, including protocol, hostname, and optionally port', | |
| example: 'https://aiostreams.example.com', | |
| devDefault: `http://localhost:${process.env.PORT || 3000}`, | |
| }), | |
| INTERNAL_URL: url({ | |
| default: `http://localhost:${process.env.PORT || 3000}`, | |
| desc: 'Internal URL of the addon, used for internal communication between built-in addons and the server', | |
| }), | |
| INTERNAL_SECRET: readonly({ | |
| default: randomBytes(32).toString('hex'), | |
| desc: 'Internal secret for the addon, used for internal communication between built-in addons and the server', | |
| }), | |
| PORT: port({ | |
| default: 3000, | |
| desc: 'Port to run the addon on', | |
| }), | |
| SECRET_KEY: secretKey({ | |
| desc: 'Session/encryption secret used to derive keys for stored configurations. Must be a 64-character hex string. Generate with `openssl rand -hex 32`. Cannot be changed after first run. (Legacy alias `SESSION_SECRET` is still accepted for one minor release.)', | |
| example: 'Generate using: openssl rand -hex 32', | |
| }), | |
| DATABASE_URI: str({ | |
| default: 'sqlite://./data/db.sqlite', | |
| desc: 'Database URI for the addon', | |
| }), | |
| REDIS_URI: str({ | |
| default: undefined, | |
| desc: 'Redis URI for the addon', | |
| }), | |
| REDIS_TIMEOUT: num({ | |
| default: 5000, | |
| desc: 'Redis timeout for the addon', | |
| }), | |
| SETTINGS_REFRESH_INTERVAL: num({ | |
| default: 30, | |
| desc: 'How often (seconds) each instance polls the DB settings version and reloads runtime config if another instance changed it. Set 0 to disable (single-instance deployments).', | |
| }), | |
| LOG_LEVEL: str({ | |
| default: 'info', | |
| desc: 'Log level for the addon', | |
| choices: ['info', 'debug', 'warn', 'error', 'verbose', 'silly', 'http'], | |
| }), | |
| LOG_FORMAT: str({ | |
| default: 'json', | |
| desc: 'Log format for the addon', | |
| choices: ['text', 'json'], | |
| }), | |
| LOG_BUFFER_MAX_BYTES: num({ | |
| default: 67108864, | |
| desc: 'Max bytes of recent log lines kept in memory for the dashboard Logs page.', | |
| }), | |
| LOG_BUFFER_MAX_ENTRIES: num({ | |
| default: 200000, | |
| desc: 'Hard cap on the number of recent log lines kept in memory for the dashboard Logs page.', | |
| }), | |
| AIOSTREAMS_AUTH: proxyAuth({ | |
| default: new Map<string, string>(), | |
| desc: 'Authorisation credentials for this AIOStreams instance', | |
| }), | |
| AIOSTREAMS_AUTH_ADMINS: commaSeparated({ | |
| default: undefined, | |
| desc: 'Comma separated list of admin usernames. If not set, all users are admins.', | |
| }), | |
| AIOSTREAMS_AUTH_PROXY: commaSeparated({ | |
| default: undefined, | |
| desc: 'Comma separated list of usernames allowed to use the built-in proxy. If not set, all authenticated users can use the proxy.', | |
| }), | |
| AIOSTREAMS_AUTH_CONNECTIONS_LIMIT: connectionLimits({ | |
| default: undefined, | |
| desc: 'Connection limits for authenticated users', | |
| }), | |
| SYSTEM_LIFECYCLE_ENABLED: bool({ | |
| default: false, | |
| desc: 'Allow the dashboard System page to restart/stop the AIOStreams process.', | |
| }), | |
| }); | |