feat(config): fix previously documented env var formula not working (#6726)

This commit is contained in:
Elian Doran 2025-08-25 19:23:35 +03:00 committed by GitHub
commit e793b2f661
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 893 additions and 83 deletions

View File

@ -0,0 +1,348 @@
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "fs";
import ini from "ini";
// Mock dependencies
vi.mock("fs");
vi.mock("./data_dir.js", () => ({
default: {
CONFIG_INI_PATH: "/test/config.ini"
}
}));
vi.mock("./resource_dir.js", () => ({
default: {
RESOURCE_DIR: "/test/resources"
}
}));
describe("Config Service", () => {
let originalEnv: NodeJS.ProcessEnv;
beforeEach(() => {
// Save original environment
originalEnv = { ...process.env };
// Clear all TRILIUM env vars
Object.keys(process.env).forEach(key => {
if (key.startsWith("TRILIUM_")) {
delete process.env[key];
}
});
// Mock fs to return empty config
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockImplementation((path) => {
if (String(path).includes("config-sample.ini")) {
return "" as any; // Return string for INI parsing
}
// Return empty INI config as string
return `
[General]
[Network]
[Session]
[Sync]
[MultiFactorAuthentication]
[Logging]
` as any;
});
// Clear module cache to reload config with new env vars
vi.resetModules();
});
afterEach(() => {
// Restore original environment
process.env = originalEnv;
vi.clearAllMocks();
});
describe("Environment Variable Naming", () => {
it("should use standard environment variables following TRILIUM_[SECTION]_[KEY] pattern", async () => {
// Set standard env vars
process.env.TRILIUM_GENERAL_INSTANCENAME = "test-instance";
process.env.TRILIUM_NETWORK_CORSALLOWORIGIN = "https://example.com";
process.env.TRILIUM_SYNC_SYNCSERVERHOST = "sync.example.com";
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "https://auth.example.com";
process.env.TRILIUM_LOGGING_RETENTIONDAYS = "30";
const { default: config } = await import("./config.js");
expect(config.General.instanceName).toBe("test-instance");
expect(config.Network.corsAllowOrigin).toBe("https://example.com");
expect(config.Sync.syncServerHost).toBe("sync.example.com");
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://auth.example.com");
expect(config.Logging.retentionDays).toBe(30);
});
it("should maintain backward compatibility with alias environment variables", async () => {
// Set alias/legacy env vars
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://legacy.com";
process.env.TRILIUM_SYNC_SERVER_HOST = "legacy-sync.com";
process.env.TRILIUM_OAUTH_BASE_URL = "https://legacy-auth.com";
process.env.TRILIUM_LOGGING_RETENTION_DAYS = "60";
const { default: config } = await import("./config.js");
expect(config.Network.corsAllowOrigin).toBe("https://legacy.com");
expect(config.Sync.syncServerHost).toBe("legacy-sync.com");
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://legacy-auth.com");
expect(config.Logging.retentionDays).toBe(60);
});
it("should prioritize standard env vars over aliases when both are set", async () => {
// Set both standard and alias env vars - standard should win
process.env.TRILIUM_NETWORK_CORSALLOWORIGIN = "standard-cors.com";
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "alias-cors.com";
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "standard-auth.com";
process.env.TRILIUM_OAUTH_BASE_URL = "alias-auth.com";
const { default: config } = await import("./config.js");
expect(config.Network.corsAllowOrigin).toBe("standard-cors.com");
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("standard-auth.com");
});
it("should handle all CORS environment variables correctly", async () => {
// Test with standard naming
process.env.TRILIUM_NETWORK_CORSALLOWORIGIN = "*";
process.env.TRILIUM_NETWORK_CORSALLOWMETHODS = "GET,POST,PUT";
process.env.TRILIUM_NETWORK_CORSALLOWHEADERS = "Content-Type,Authorization";
let { default: config } = await import("./config.js");
expect(config.Network.corsAllowOrigin).toBe("*");
expect(config.Network.corsAllowMethods).toBe("GET,POST,PUT");
expect(config.Network.corsAllowHeaders).toBe("Content-Type,Authorization");
// Clear and test with alias naming
delete process.env.TRILIUM_NETWORK_CORSALLOWORIGIN;
delete process.env.TRILIUM_NETWORK_CORSALLOWMETHODS;
delete process.env.TRILIUM_NETWORK_CORSALLOWHEADERS;
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://app.com";
process.env.TRILIUM_NETWORK_CORS_ALLOW_METHODS = "GET,POST";
process.env.TRILIUM_NETWORK_CORS_ALLOW_HEADERS = "X-Custom-Header";
vi.resetModules();
config = (await import("./config.js")).default;
expect(config.Network.corsAllowOrigin).toBe("https://app.com");
expect(config.Network.corsAllowMethods).toBe("GET,POST");
expect(config.Network.corsAllowHeaders).toBe("X-Custom-Header");
});
it("should handle all OAuth/MFA environment variables correctly", async () => {
// Test with standard naming
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL = "https://oauth.standard.com";
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID = "standard-client-id";
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET = "standard-secret";
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL = "https://issuer.standard.com";
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME = "Standard Auth";
process.env.TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON = "standard-icon.png";
let { default: config } = await import("./config.js");
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://oauth.standard.com");
expect(config.MultiFactorAuthentication.oauthClientId).toBe("standard-client-id");
expect(config.MultiFactorAuthentication.oauthClientSecret).toBe("standard-secret");
expect(config.MultiFactorAuthentication.oauthIssuerBaseUrl).toBe("https://issuer.standard.com");
expect(config.MultiFactorAuthentication.oauthIssuerName).toBe("Standard Auth");
expect(config.MultiFactorAuthentication.oauthIssuerIcon).toBe("standard-icon.png");
// Clear and test with alias naming
Object.keys(process.env).forEach(key => {
if (key.startsWith("TRILIUM_MULTIFACTORAUTHENTICATION_")) {
delete process.env[key];
}
});
process.env.TRILIUM_OAUTH_BASE_URL = "https://oauth.alias.com";
process.env.TRILIUM_OAUTH_CLIENT_ID = "alias-client-id";
process.env.TRILIUM_OAUTH_CLIENT_SECRET = "alias-secret";
process.env.TRILIUM_OAUTH_ISSUER_BASE_URL = "https://issuer.alias.com";
process.env.TRILIUM_OAUTH_ISSUER_NAME = "Alias Auth";
process.env.TRILIUM_OAUTH_ISSUER_ICON = "alias-icon.png";
vi.resetModules();
config = (await import("./config.js")).default;
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://oauth.alias.com");
expect(config.MultiFactorAuthentication.oauthClientId).toBe("alias-client-id");
expect(config.MultiFactorAuthentication.oauthClientSecret).toBe("alias-secret");
expect(config.MultiFactorAuthentication.oauthIssuerBaseUrl).toBe("https://issuer.alias.com");
expect(config.MultiFactorAuthentication.oauthIssuerName).toBe("Alias Auth");
expect(config.MultiFactorAuthentication.oauthIssuerIcon).toBe("alias-icon.png");
});
it("should handle all Sync environment variables correctly", async () => {
// Test with standard naming
process.env.TRILIUM_SYNC_SYNCSERVERHOST = "sync-standard.com";
process.env.TRILIUM_SYNC_SYNCSERVERTIMEOUT = "60000";
process.env.TRILIUM_SYNC_SYNCPROXY = "proxy-standard.com";
let { default: config } = await import("./config.js");
expect(config.Sync.syncServerHost).toBe("sync-standard.com");
expect(config.Sync.syncServerTimeout).toBe("60000");
expect(config.Sync.syncProxy).toBe("proxy-standard.com");
// Clear and test with alias naming
delete process.env.TRILIUM_SYNC_SYNCSERVERHOST;
delete process.env.TRILIUM_SYNC_SYNCSERVERTIMEOUT;
delete process.env.TRILIUM_SYNC_SYNCPROXY;
process.env.TRILIUM_SYNC_SERVER_HOST = "sync-alias.com";
process.env.TRILIUM_SYNC_SERVER_TIMEOUT = "30000";
process.env.TRILIUM_SYNC_SERVER_PROXY = "proxy-alias.com";
vi.resetModules();
config = (await import("./config.js")).default;
expect(config.Sync.syncServerHost).toBe("sync-alias.com");
expect(config.Sync.syncServerTimeout).toBe("30000");
expect(config.Sync.syncProxy).toBe("proxy-alias.com");
});
});
describe("INI Config Integration", () => {
it("should fall back to INI config when no env vars are set", async () => {
// Mock INI config with values
vi.mocked(fs.readFileSync).mockImplementation((path) => {
if (String(path).includes("config-sample.ini")) {
return "" as any;
}
return `
[General]
instanceName=ini-instance
[Network]
corsAllowOrigin=https://ini-cors.com
port=9000
[Sync]
syncServerHost=ini-sync.com
[MultiFactorAuthentication]
oauthBaseUrl=https://ini-oauth.com
[Logging]
retentionDays=45
` as any;
});
const { default: config } = await import("./config.js");
expect(config.General.instanceName).toBe("ini-instance");
expect(config.Network.corsAllowOrigin).toBe("https://ini-cors.com");
expect(config.Network.port).toBe("9000");
expect(config.Sync.syncServerHost).toBe("ini-sync.com");
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("https://ini-oauth.com");
expect(config.Logging.retentionDays).toBe(45);
});
it("should prioritize env vars over INI config", async () => {
// Mock INI config with values
vi.mocked(fs.readFileSync).mockImplementation((path) => {
if (String(path).includes("config-sample.ini")) {
return "" as any;
}
return `
[General]
instanceName=ini-instance
[Network]
corsAllowOrigin=https://ini-cors.com
` as any;
});
// Set env vars that should override INI
process.env.TRILIUM_GENERAL_INSTANCENAME = "env-instance";
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN = "https://env-cors.com"; // Using alias
const { default: config } = await import("./config.js");
expect(config.General.instanceName).toBe("env-instance");
expect(config.Network.corsAllowOrigin).toBe("https://env-cors.com");
});
});
describe("Type Transformations", () => {
it("should correctly transform boolean values", async () => {
process.env.TRILIUM_GENERAL_NOAUTHENTICATION = "true";
process.env.TRILIUM_GENERAL_NOBACKUP = "1";
process.env.TRILIUM_GENERAL_READONLY = "false";
process.env.TRILIUM_NETWORK_HTTPS = "0";
const { default: config } = await import("./config.js");
expect(config.General.noAuthentication).toBe(true);
expect(config.General.noBackup).toBe(true);
expect(config.General.readOnly).toBe(false);
expect(config.Network.https).toBe(false);
});
it("should correctly transform integer values", async () => {
process.env.TRILIUM_SESSION_COOKIEMAXAGE = "3600";
process.env.TRILIUM_LOGGING_RETENTIONDAYS = "7";
const { default: config } = await import("./config.js");
expect(config.Session.cookieMaxAge).toBe(3600);
expect(config.Logging.retentionDays).toBe(7);
});
it("should use default values for invalid integers", async () => {
process.env.TRILIUM_SESSION_COOKIEMAXAGE = "invalid";
process.env.TRILIUM_LOGGING_RETENTION_DAYS = "not-a-number"; // Using alias
const { default: config } = await import("./config.js");
expect(config.Session.cookieMaxAge).toBe(21 * 24 * 60 * 60); // Default
expect(config.Logging.retentionDays).toBe(90); // Default
});
});
describe("Default Values", () => {
it("should use correct default values when no config is provided", async () => {
const { default: config } = await import("./config.js");
// General defaults
expect(config.General.instanceName).toBe("");
expect(config.General.noAuthentication).toBe(false);
expect(config.General.noBackup).toBe(false);
expect(config.General.noDesktopIcon).toBe(false);
expect(config.General.readOnly).toBe(false);
// Network defaults
expect(config.Network.host).toBe("0.0.0.0");
expect(config.Network.port).toBe("3000");
expect(config.Network.https).toBe(false);
expect(config.Network.certPath).toBe("");
expect(config.Network.keyPath).toBe("");
expect(config.Network.trustedReverseProxy).toBe(false);
expect(config.Network.corsAllowOrigin).toBe("");
expect(config.Network.corsAllowMethods).toBe("");
expect(config.Network.corsAllowHeaders).toBe("");
// Session defaults
expect(config.Session.cookieMaxAge).toBe(21 * 24 * 60 * 60);
// Sync defaults
expect(config.Sync.syncServerHost).toBe("");
expect(config.Sync.syncServerTimeout).toBe("120000");
expect(config.Sync.syncProxy).toBe("");
// OAuth defaults
expect(config.MultiFactorAuthentication.oauthBaseUrl).toBe("");
expect(config.MultiFactorAuthentication.oauthClientId).toBe("");
expect(config.MultiFactorAuthentication.oauthClientSecret).toBe("");
expect(config.MultiFactorAuthentication.oauthIssuerBaseUrl).toBe("https://accounts.google.com");
expect(config.MultiFactorAuthentication.oauthIssuerName).toBe("Google");
expect(config.MultiFactorAuthentication.oauthIssuerIcon).toBe("");
// Logging defaults
expect(config.Logging.retentionDays).toBe(90);
});
});
});

View File

@ -1,3 +1,24 @@
/**
*
* TRILIUM CONFIGURATION RESOLUTION ORDER
*
*
* Priority Source Example
*
* 1 Environment Variables TRILIUM_NETWORK_PORT=8080
* (Highest Priority - Overrides all)
*
* 2 config.ini File [Network]
* (User Configuration) port=8080
*
* 3 Default Values port='3000'
* (Lowest Priority - Fallback) (hardcoded defaults)
*
*
* IMPORTANT: Environment variables ALWAYS override config.ini values!
*
*/
import ini from "ini";
import fs from "fs";
import dataDir from "./data_dir.js";
@ -5,153 +26,594 @@ import path from "path";
import resourceDir from "./resource_dir.js";
import { envToBoolean, stringToInt } from "./utils.js";
/**
* Path to the sample configuration file that serves as a template for new installations.
* This file contains all available configuration options with documentation.
*/
const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini");
/**
* Initialize config.ini file if it doesn't exist.
* On first run, copies the sample configuration to the data directory,
* allowing users to customize their settings.
*/
if (!fs.existsSync(dataDir.CONFIG_INI_PATH)) {
const configSample = fs.readFileSync(configSampleFilePath).toString("utf8");
fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample);
}
const iniConfig = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
/**
* Type definition for the parsed INI configuration structure.
* The ini parser returns an object with string keys and values that can be
* strings, booleans, numbers, or nested objects.
*/
type IniConfigValue = string | number | boolean | null | undefined;
type IniConfigSection = Record<string, IniConfigValue>;
type IniConfig = Record<string, IniConfigSection | IniConfigValue>;
/**
* Parse the config.ini file into a JavaScript object.
* This object contains all user-defined configuration from the INI file,
* which will be merged with environment variables and defaults.
*/
const iniConfig = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8")) as IniConfig;
/**
* Complete type-safe configuration interface for Trilium.
* This interface defines all configuration options available through
* environment variables, config.ini, or defaults.
*/
export interface TriliumConfig {
/** General application settings */
General: {
/** Custom instance name for identifying this Trilium instance */
instanceName: string;
/** Whether to disable authentication (useful for local-only instances) */
noAuthentication: boolean;
/** Whether to disable automatic backups */
noBackup: boolean;
/** Whether to prevent desktop icon creation (desktop app only) */
noDesktopIcon: boolean;
/** Whether to run in read-only mode (prevents all data modifications) */
readOnly: boolean;
};
/** Network and server configuration */
Network: {
/** Host/IP address to bind the server to (e.g., '0.0.0.0' for all interfaces) */
host: string;
/** Port number for the HTTP/HTTPS server */
port: string;
/** Whether to enable HTTPS (requires certPath and keyPath) */
https: boolean;
/** Path to SSL certificate file (required when https=true) */
certPath: string;
/** Path to SSL private key file (required when https=true) */
keyPath: string;
/** Trust reverse proxy headers (boolean or specific IP/subnet string) */
trustedReverseProxy: boolean | string;
/** CORS allowed origins (comma-separated list or '*' for all) */
corsAllowOrigin: string;
/** CORS allowed methods (comma-separated HTTP methods) */
corsAllowMethods: string;
/** CORS allowed headers (comma-separated header names) */
corsAllowHeaders: string;
};
/** Session management configuration */
Session: {
/** Maximum age of session cookies in seconds (default: 21 days) */
cookieMaxAge: number;
};
/** Synchronization settings for multi-instance setups */
Sync: {
/** URL of the sync server to connect to */
syncServerHost: string;
/** Timeout for sync operations in milliseconds */
syncServerTimeout: string;
/** Proxy URL for sync connections (if behind corporate proxy) */
syncProxy: string;
};
/** Multi-factor authentication and OAuth/OpenID configuration */
MultiFactorAuthentication: {
/** Base URL for OAuth authentication endpoint */
oauthBaseUrl: string;
/** OAuth client ID from your identity provider */
oauthClientId: string;
/** OAuth client secret from your identity provider */
oauthClientSecret: string;
/** Base URL of the OAuth issuer (e.g., 'https://accounts.google.com') */
oauthIssuerBaseUrl: string;
/** Display name of the OAuth provider (shown in UI) */
oauthIssuerName: string;
/** URL to the OAuth provider's icon/logo */
oauthIssuerIcon: string;
};
/** Logging configuration */
Logging: {
/**
* The number of days to keep the log files around. When rotating the logs, log files created by Trilium older than the specified amount of time will be deleted.
* The number of days to keep the log files around. When rotating the logs,
* log files created by Trilium older than the specified amount of time will be deleted.
*/
retentionDays: number;
}
}
/**
* Default retention period for log files in days.
* After this period, old log files are automatically deleted during rotation.
*/
export const LOGGING_DEFAULT_RETENTION_DAYS = 90;
//prettier-ignore
const config: TriliumConfig = {
/**
* Configuration value source with precedence handling.
* This interface defines how each configuration value is resolved from multiple sources.
*/
interface ConfigValue<T> {
/**
* Standard environment variable name following TRILIUM_[SECTION]_[KEY] pattern.
* This is the primary way to override configuration via environment.
*/
standardEnvVar?: string;
/**
* Alternative environment variable names for additional flexibility.
* These provide shorter or more intuitive names for common settings.
*/
aliasEnvVars?: string[];
/**
* Function to retrieve the value from the parsed INI configuration.
* Returns undefined if the value is not set in config.ini.
*/
iniGetter: () => IniConfigValue | IniConfigSection;
/**
* Default value used when no environment variable or INI value is found.
* This ensures every configuration has a sensible default.
*/
defaultValue: T;
/**
* Optional transformer function to convert string values to the correct type.
* Common transformers handle boolean and integer conversions.
*/
transformer?: (value: unknown) => T;
}
/**
* Core configuration resolution function.
*
* Resolves configuration values using a clear precedence order:
* 1. Standard environment variable (highest priority) - Follows TRILIUM_[SECTION]_[KEY] pattern
* 2. Alias environment variables - Alternative names for convenience and compatibility
* 3. INI config file value - User-defined settings in config.ini
* 4. Default value (lowest priority) - Fallback to ensure valid configuration
*
* This precedence allows for flexible configuration management:
* - Environment variables for container/cloud deployments
* - config.ini for traditional installations
* - Defaults ensure the application always has valid settings
*
* @param config - Configuration value definition with sources and transformers
* @returns The resolved configuration value with appropriate type
*/
function getConfigValue<T>(config: ConfigValue<T>): T {
// Check standard env var first
if (config.standardEnvVar && process.env[config.standardEnvVar] !== undefined) {
const value = process.env[config.standardEnvVar];
return config.transformer ? config.transformer(value) : value as T;
}
// Check alternative env vars for additional flexibility
if (config.aliasEnvVars) {
for (const aliasVar of config.aliasEnvVars) {
if (process.env[aliasVar] !== undefined) {
const value = process.env[aliasVar];
return config.transformer ? config.transformer(value) : value as T;
}
}
}
// Check INI config
const iniValue = config.iniGetter();
if (iniValue !== undefined && iniValue !== null && iniValue !== '') {
return config.transformer ? config.transformer(iniValue) : iniValue as T;
}
// Return default
return config.defaultValue;
}
/**
* Helper function to safely access INI config sections.
* The ini parser can return either a section object or a primitive value,
* so we need to check the type before accessing nested properties.
*
* @param sectionName - The name of the INI section to access
* @returns The section object or undefined if not found or not an object
*/
function getIniSection(sectionName: string): IniConfigSection | undefined {
const section = iniConfig[sectionName];
if (section && typeof section === 'object' && !Array.isArray(section)) {
return section as IniConfigSection;
}
return undefined;
}
/**
* Transform a value to boolean, handling various input formats.
*
* This function provides flexible boolean parsing to handle different
* configuration sources (environment variables, INI files, etc.):
* - String "true"/"false" (case-insensitive)
* - String "1"/"0"
* - Numeric 1/0
* - Actual boolean values
* - Any other value defaults to false
*
* @param value - The value to transform (string, number, boolean, etc.)
* @returns The boolean value or false as default
*/
function transformBoolean(value: unknown): boolean {
// First try the standard envToBoolean function which handles "true"/"false" strings
const result = envToBoolean(String(value));
if (result !== undefined) return result;
// Handle numeric boolean values (both string and number types)
if (value === "1" || value === 1) return true;
if (value === "0" || value === 0) return false;
// Default to false for any other value
return false;
}
/**
* Complete configuration mapping that defines how each setting is resolved.
*
* This mapping structure:
* 1. Mirrors the INI file sections for consistency
* 2. Defines multiple sources for each configuration value
* 3. Provides type transformers where needed
* 4. Maintains compatibility with various environment variable formats
*
* Environment Variable Patterns:
* - Standard: TRILIUM_[SECTION]_[KEY] (e.g., TRILIUM_GENERAL_INSTANCENAME)
* - Aliases: Shorter alternatives (e.g., TRILIUM_OAUTH_BASE_URL)
*
* Both patterns are equally valid and can be used based on preference.
* The standard pattern provides consistency, while aliases offer convenience.
*/
const configMapping = {
General: {
instanceName:
process.env.TRILIUM_GENERAL_INSTANCENAME || iniConfig.General.instanceName || "",
noAuthentication:
envToBoolean(process.env.TRILIUM_GENERAL_NOAUTHENTICATION) || iniConfig.General.noAuthentication || false,
noBackup:
envToBoolean(process.env.TRILIUM_GENERAL_NOBACKUP) || iniConfig.General.noBackup || false,
noDesktopIcon:
envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false,
readOnly:
envToBoolean(process.env.TRILIUM_GENERAL_READONLY) || iniConfig.General.readOnly || false
instanceName: {
standardEnvVar: 'TRILIUM_GENERAL_INSTANCENAME',
iniGetter: () => getIniSection("General")?.instanceName,
defaultValue: ''
},
noAuthentication: {
standardEnvVar: 'TRILIUM_GENERAL_NOAUTHENTICATION',
iniGetter: () => getIniSection("General")?.noAuthentication,
defaultValue: false,
transformer: transformBoolean
},
noBackup: {
standardEnvVar: 'TRILIUM_GENERAL_NOBACKUP',
iniGetter: () => getIniSection("General")?.noBackup,
defaultValue: false,
transformer: transformBoolean
},
noDesktopIcon: {
standardEnvVar: 'TRILIUM_GENERAL_NODESKTOPICON',
iniGetter: () => getIniSection("General")?.noDesktopIcon,
defaultValue: false,
transformer: transformBoolean
},
readOnly: {
standardEnvVar: 'TRILIUM_GENERAL_READONLY',
iniGetter: () => getIniSection("General")?.readOnly,
defaultValue: false,
transformer: transformBoolean
}
},
Network: {
host:
process.env.TRILIUM_NETWORK_HOST || iniConfig.Network.host || "0.0.0.0",
port:
process.env.TRILIUM_NETWORK_PORT || iniConfig.Network.port || "3000",
https:
envToBoolean(process.env.TRILIUM_NETWORK_HTTPS) || iniConfig.Network.https || false,
certPath:
process.env.TRILIUM_NETWORK_CERTPATH || iniConfig.Network.certPath || "",
keyPath:
process.env.TRILIUM_NETWORK_KEYPATH || iniConfig.Network.keyPath || "",
trustedReverseProxy:
process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false,
corsAllowOrigin:
process.env.TRILIUM_NETWORK_CORS_ALLOW_ORIGIN || iniConfig.Network.corsAllowOrigin || "",
corsAllowMethods:
process.env.TRILIUM_NETWORK_CORS_ALLOW_METHODS || iniConfig.Network.corsAllowMethods || "",
corsAllowHeaders:
process.env.TRILIUM_NETWORK_CORS_ALLOW_HEADERS || iniConfig.Network.corsAllowHeaders || ""
host: {
standardEnvVar: 'TRILIUM_NETWORK_HOST',
iniGetter: () => getIniSection("Network")?.host,
defaultValue: '0.0.0.0'
},
port: {
standardEnvVar: 'TRILIUM_NETWORK_PORT',
iniGetter: () => getIniSection("Network")?.port,
defaultValue: '3000'
},
https: {
standardEnvVar: 'TRILIUM_NETWORK_HTTPS',
iniGetter: () => getIniSection("Network")?.https,
defaultValue: false,
transformer: transformBoolean
},
certPath: {
standardEnvVar: 'TRILIUM_NETWORK_CERTPATH',
iniGetter: () => getIniSection("Network")?.certPath,
defaultValue: ''
},
keyPath: {
standardEnvVar: 'TRILIUM_NETWORK_KEYPATH',
iniGetter: () => getIniSection("Network")?.keyPath,
defaultValue: ''
},
trustedReverseProxy: {
standardEnvVar: 'TRILIUM_NETWORK_TRUSTEDREVERSEPROXY',
iniGetter: () => getIniSection("Network")?.trustedReverseProxy,
defaultValue: false as boolean | string
},
corsAllowOrigin: {
standardEnvVar: 'TRILIUM_NETWORK_CORSALLOWORIGIN',
// alternative with underscore format
aliasEnvVars: ['TRILIUM_NETWORK_CORS_ALLOW_ORIGIN'],
iniGetter: () => getIniSection("Network")?.corsAllowOrigin,
defaultValue: ''
},
corsAllowMethods: {
standardEnvVar: 'TRILIUM_NETWORK_CORSALLOWMETHODS',
// alternative with underscore format
aliasEnvVars: ['TRILIUM_NETWORK_CORS_ALLOW_METHODS'],
iniGetter: () => getIniSection("Network")?.corsAllowMethods,
defaultValue: ''
},
corsAllowHeaders: {
standardEnvVar: 'TRILIUM_NETWORK_CORSALLOWHEADERS',
// alternative with underscore format
aliasEnvVars: ['TRILIUM_NETWORK_CORS_ALLOW_HEADERS'],
iniGetter: () => getIniSection("Network")?.corsAllowHeaders,
defaultValue: ''
}
},
Session: {
cookieMaxAge:
parseInt(String(process.env.TRILIUM_SESSION_COOKIEMAXAGE)) || parseInt(iniConfig?.Session?.cookieMaxAge) || 21 * 24 * 60 * 60 // 21 Days in Seconds
cookieMaxAge: {
standardEnvVar: 'TRILIUM_SESSION_COOKIEMAXAGE',
iniGetter: () => getIniSection("Session")?.cookieMaxAge,
defaultValue: 21 * 24 * 60 * 60, // 21 Days in Seconds
transformer: (value: unknown) => parseInt(String(value)) || 21 * 24 * 60 * 60
}
},
Sync: {
syncServerHost:
process.env.TRILIUM_SYNC_SERVER_HOST || iniConfig?.Sync?.syncServerHost || "",
syncServerTimeout:
process.env.TRILIUM_SYNC_SERVER_TIMEOUT || iniConfig?.Sync?.syncServerTimeout || "120000",
syncProxy:
// additionally checking in iniConfig for inconsistently named syncProxy for backwards compatibility
process.env.TRILIUM_SYNC_SERVER_PROXY || iniConfig?.Sync?.syncProxy || iniConfig?.Sync?.syncServerProxy || ""
syncServerHost: {
standardEnvVar: 'TRILIUM_SYNC_SYNCSERVERHOST',
// alternative format
aliasEnvVars: ['TRILIUM_SYNC_SERVER_HOST'],
iniGetter: () => getIniSection("Sync")?.syncServerHost,
defaultValue: ''
},
syncServerTimeout: {
standardEnvVar: 'TRILIUM_SYNC_SYNCSERVERTIMEOUT',
// alternative format
aliasEnvVars: ['TRILIUM_SYNC_SERVER_TIMEOUT'],
iniGetter: () => getIniSection("Sync")?.syncServerTimeout,
defaultValue: '120000'
},
syncProxy: {
standardEnvVar: 'TRILIUM_SYNC_SYNCPROXY',
// alternative shorter formats
aliasEnvVars: ['TRILIUM_SYNC_SERVER_PROXY'],
// The INI config uses 'syncServerProxy' key for historical reasons (see config-sample.ini)
// We check both 'syncProxy' and 'syncServerProxy' for backward compatibility with old configs
iniGetter: () => getIniSection("Sync")?.syncProxy || getIniSection("Sync")?.syncServerProxy,
defaultValue: ''
}
},
MultiFactorAuthentication: {
oauthBaseUrl:
process.env.TRILIUM_OAUTH_BASE_URL || iniConfig?.MultiFactorAuthentication?.oauthBaseUrl || "",
oauthClientId:
process.env.TRILIUM_OAUTH_CLIENT_ID || iniConfig?.MultiFactorAuthentication?.oauthClientId || "",
oauthClientSecret:
process.env.TRILIUM_OAUTH_CLIENT_SECRET || iniConfig?.MultiFactorAuthentication?.oauthClientSecret || "",
oauthIssuerBaseUrl:
process.env.TRILIUM_OAUTH_ISSUER_BASE_URL || iniConfig?.MultiFactorAuthentication?.oauthIssuerBaseUrl || "https://accounts.google.com",
oauthIssuerName:
process.env.TRILIUM_OAUTH_ISSUER_NAME || iniConfig?.MultiFactorAuthentication?.oauthIssuerName || "Google",
oauthIssuerIcon:
process.env.TRILIUM_OAUTH_ISSUER_ICON || iniConfig?.MultiFactorAuthentication?.oauthIssuerIcon || ""
oauthBaseUrl: {
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL',
// alternative shorter format (commonly used)
aliasEnvVars: ['TRILIUM_OAUTH_BASE_URL'],
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthBaseUrl,
defaultValue: ''
},
oauthClientId: {
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID',
// alternative format
aliasEnvVars: ['TRILIUM_OAUTH_CLIENT_ID'],
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthClientId,
defaultValue: ''
},
oauthClientSecret: {
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET',
// alternative format
aliasEnvVars: ['TRILIUM_OAUTH_CLIENT_SECRET'],
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthClientSecret,
defaultValue: ''
},
oauthIssuerBaseUrl: {
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL',
// alternative format
aliasEnvVars: ['TRILIUM_OAUTH_ISSUER_BASE_URL'],
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthIssuerBaseUrl,
defaultValue: 'https://accounts.google.com'
},
oauthIssuerName: {
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME',
// alternative format
aliasEnvVars: ['TRILIUM_OAUTH_ISSUER_NAME'],
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthIssuerName,
defaultValue: 'Google'
},
oauthIssuerIcon: {
standardEnvVar: 'TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON',
// alternative format
aliasEnvVars: ['TRILIUM_OAUTH_ISSUER_ICON'],
iniGetter: () => getIniSection("MultiFactorAuthentication")?.oauthIssuerIcon,
defaultValue: ''
}
},
Logging: {
retentionDays:
stringToInt(process.env.TRILIUM_LOGGING_RETENTION_DAYS) ??
stringToInt(iniConfig?.Logging?.retentionDays) ??
LOGGING_DEFAULT_RETENTION_DAYS
retentionDays: {
standardEnvVar: 'TRILIUM_LOGGING_RETENTIONDAYS',
// alternative with underscore format
aliasEnvVars: ['TRILIUM_LOGGING_RETENTION_DAYS'],
iniGetter: () => getIniSection("Logging")?.retentionDays,
defaultValue: LOGGING_DEFAULT_RETENTION_DAYS,
transformer: (value: unknown) => stringToInt(String(value)) ?? LOGGING_DEFAULT_RETENTION_DAYS
}
}
};
export default config;
/**
* Build the final configuration object by resolving all values through the mapping.
*
* This creates the runtime configuration used throughout the application by:
* 1. Iterating through each section and key in the mapping
* 2. Calling getConfigValue() to resolve each setting with proper precedence
* 3. Applying type transformers where needed (booleans, integers)
* 4. Returning a fully typed TriliumConfig object
*
* The resulting config object is immutable at runtime and represents
* the complete application configuration.
*/
const config: TriliumConfig = {
General: {
instanceName: getConfigValue(configMapping.General.instanceName),
noAuthentication: getConfigValue(configMapping.General.noAuthentication),
noBackup: getConfigValue(configMapping.General.noBackup),
noDesktopIcon: getConfigValue(configMapping.General.noDesktopIcon),
readOnly: getConfigValue(configMapping.General.readOnly)
},
Network: {
host: getConfigValue(configMapping.Network.host),
port: getConfigValue(configMapping.Network.port),
https: getConfigValue(configMapping.Network.https),
certPath: getConfigValue(configMapping.Network.certPath),
keyPath: getConfigValue(configMapping.Network.keyPath),
trustedReverseProxy: getConfigValue(configMapping.Network.trustedReverseProxy),
corsAllowOrigin: getConfigValue(configMapping.Network.corsAllowOrigin),
corsAllowMethods: getConfigValue(configMapping.Network.corsAllowMethods),
corsAllowHeaders: getConfigValue(configMapping.Network.corsAllowHeaders)
},
Session: {
cookieMaxAge: getConfigValue(configMapping.Session.cookieMaxAge)
},
Sync: {
syncServerHost: getConfigValue(configMapping.Sync.syncServerHost),
syncServerTimeout: getConfigValue(configMapping.Sync.syncServerTimeout),
syncProxy: getConfigValue(configMapping.Sync.syncProxy)
},
MultiFactorAuthentication: {
oauthBaseUrl: getConfigValue(configMapping.MultiFactorAuthentication.oauthBaseUrl),
oauthClientId: getConfigValue(configMapping.MultiFactorAuthentication.oauthClientId),
oauthClientSecret: getConfigValue(configMapping.MultiFactorAuthentication.oauthClientSecret),
oauthIssuerBaseUrl: getConfigValue(configMapping.MultiFactorAuthentication.oauthIssuerBaseUrl),
oauthIssuerName: getConfigValue(configMapping.MultiFactorAuthentication.oauthIssuerName),
oauthIssuerIcon: getConfigValue(configMapping.MultiFactorAuthentication.oauthIssuerIcon)
},
Logging: {
retentionDays: getConfigValue(configMapping.Logging.retentionDays)
}
};
/**
* =====================================================================
* ENVIRONMENT VARIABLE REFERENCE
* =====================================================================
*
* Trilium supports flexible environment variable configuration with multiple
* naming patterns. Both formats below are equally valid and can be used
* based on your preference.
*
* CONFIGURATION PRECEDENCE:
* 1. Environment variables (highest priority)
* 2. config.ini file values
* 3. Default values (lowest priority)
*
* FULL FORMAT VARIABLES (following TRILIUM_[SECTION]_[KEY] pattern):
* ====================================================================
*
* General Section:
* - TRILIUM_GENERAL_INSTANCENAME : Custom instance identifier
* - TRILIUM_GENERAL_NOAUTHENTICATION : Disable auth (true/false)
* - TRILIUM_GENERAL_NOBACKUP : Disable backups (true/false)
* - TRILIUM_GENERAL_NODESKTOPICON : No desktop icon (true/false)
* - TRILIUM_GENERAL_READONLY : Read-only mode (true/false)
*
* Network Section:
* - TRILIUM_NETWORK_HOST : Bind address (e.g., "0.0.0.0")
* - TRILIUM_NETWORK_PORT : Server port (e.g., "8080")
* - TRILIUM_NETWORK_HTTPS : Enable HTTPS (true/false)
* - TRILIUM_NETWORK_CERTPATH : SSL certificate file path
* - TRILIUM_NETWORK_KEYPATH : SSL private key file path
* - TRILIUM_NETWORK_TRUSTEDREVERSEPROXY : Trust proxy headers (true/false/IP)
* - TRILIUM_NETWORK_CORSALLOWORIGIN : CORS allowed origins
* - TRILIUM_NETWORK_CORSALLOWMETHODS : CORS allowed HTTP methods
* - TRILIUM_NETWORK_CORSALLOWHEADERS : CORS allowed headers
*
* Session Section:
* - TRILIUM_SESSION_COOKIEMAXAGE : Cookie lifetime in seconds
*
* Sync Section:
* - TRILIUM_SYNC_SYNCSERVERHOST : Sync server URL
* - TRILIUM_SYNC_SYNCSERVERTIMEOUT : Sync timeout in milliseconds
* - TRILIUM_SYNC_SYNCPROXY : Proxy URL for sync
*
* Multi-Factor Authentication Section:
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL : OAuth base URL
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID : OAuth client ID
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET : OAuth client secret
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL : OAuth issuer URL
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME : OAuth provider name
* - TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON : OAuth provider icon
*
* Logging Section:
* - TRILIUM_LOGGING_RETENTIONDAYS : Log retention period in days
*
* SHORTER ALTERNATIVE VARIABLES (equally valid, for convenience):
* ================================================================
*
* Network CORS (with underscores):
* - TRILIUM_NETWORK_CORS_ALLOW_ORIGIN : Same as TRILIUM_NETWORK_CORSALLOWORIGIN
* - TRILIUM_NETWORK_CORS_ALLOW_METHODS : Same as TRILIUM_NETWORK_CORSALLOWMETHODS
* - TRILIUM_NETWORK_CORS_ALLOW_HEADERS : Same as TRILIUM_NETWORK_CORSALLOWHEADERS
*
* Sync (with SERVER prefix):
* - TRILIUM_SYNC_SERVER_HOST : Same as TRILIUM_SYNC_SYNCSERVERHOST
* - TRILIUM_SYNC_SERVER_TIMEOUT : Same as TRILIUM_SYNC_SYNCSERVERTIMEOUT
* - TRILIUM_SYNC_SERVER_PROXY : Same as TRILIUM_SYNC_SYNCPROXY
*
* OAuth (simplified without section name):
* - TRILIUM_OAUTH_BASE_URL : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL
* - TRILIUM_OAUTH_CLIENT_ID : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID
* - TRILIUM_OAUTH_CLIENT_SECRET : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET
* - TRILIUM_OAUTH_ISSUER_BASE_URL : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL
* - TRILIUM_OAUTH_ISSUER_NAME : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME
* - TRILIUM_OAUTH_ISSUER_ICON : Same as TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON
*
* Logging (with underscore):
* - TRILIUM_LOGGING_RETENTION_DAYS : Same as TRILIUM_LOGGING_RETENTIONDAYS
*
* BOOLEAN VALUES:
* - Accept: "true", "false", "1", "0", 1, 0
* - Default to false for invalid values
*
* EXAMPLES:
* export TRILIUM_NETWORK_PORT="8080" # Using full format
* export TRILIUM_OAUTH_CLIENT_ID="my-client-id" # Using shorter alternative
* export TRILIUM_GENERAL_NOAUTHENTICATION="true" # Boolean value
* export TRILIUM_SYNC_SERVER_HOST="https://sync.example.com" # Using alternative with SERVER
*/
/**
* The exported configuration object used throughout the Trilium application.
* This object is resolved once at startup and remains immutable during runtime.
*
* To override any setting:
* 1. Set an environment variable (see documentation above)
* 2. Edit config.ini in your data directory
* 3. Defaults will be used if neither is provided
*
* @example
* // Accessing configuration in other modules:
* import config from './services/config.js';
*
* if (config.General.noAuthentication) {
* // Skip authentication checks
* }
*
* const server = https.createServer({
* cert: fs.readFileSync(config.Network.certPath),
* key: fs.readFileSync(config.Network.keyPath)
* });
*/
export default config;