Files
ansiblings/packages/keyman/dist/keyman.config.js
T
Benjamin Diedrichsen 736c01216a initial transfer
2026-07-27 13:09:00 +02:00

213 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { z } from 'zod';
/**
* Configuration schema for keyman
*/
const KeymanConfigSchema = z.object({
vaultRoot: z.string().default('vault'),
keysDir: z.string().default('keys'),
tmpDir: z.string().default('tmp'),
ageKeyFile: z.string().default('age.key'),
});
/**
* Default configuration values
*/
const DEFAULT_CONFIG = {
vaultRoot: 'vault',
keysDir: 'keys',
tmpDir: 'tmp',
ageKeyFile: 'age.key',
};
const CONFIG_FILENAME = '.keymanrc.json';
/**
* Path-based properties that should be resolved relative to config file location
*/
const PATH_PROPERTIES = ['vaultRoot'];
/**
* Resolves path properties in a config object relative to the config file's directory
* @param configFile The raw config file contents
* @param configDir Directory containing the config file
* @returns Config with path properties resolved to absolute paths
*/
function resolvePathsRelativeToConfig(configFile, configDir) {
const resolved = { ...configFile };
for (const prop of PATH_PROPERTIES) {
const value = configFile[prop];
if (typeof value === 'string' && !path.isAbsolute(value)) {
resolved[prop] = path.resolve(configDir, value);
}
}
return resolved;
}
/**
* Finds all config files by traversing upwards from cwd to root
* Returns configs in order from root to cwd (parent first, child last)
* @param startDir Directory to start searching from
* @returns Array of paths to .keymanrc.json files
*/
function findConfigFiles(startDir) {
const configPaths = [];
let currentDir = startDir;
// Traverse upwards
while (true) {
const configPath = path.join(currentDir, CONFIG_FILENAME);
if (fs.existsSync(configPath)) {
configPaths.unshift(configPath); // Add to front (root first)
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
break; // Reached root
}
currentDir = parentDir;
}
// Also check home directory (lowest priority)
const homeConfig = path.join(os.homedir(), CONFIG_FILENAME);
if (fs.existsSync(homeConfig) && !configPaths.includes(homeConfig)) {
configPaths.unshift(homeConfig);
}
return configPaths;
}
/**
* Deep merges two values based on resolution strategy
*/
function mergeValue(parentValue, childValue, strategy) {
// Override strategy: child replaces parent completely
if (strategy === 'override') {
return childValue;
}
// Merge strategy (default)
if (Array.isArray(parentValue) && Array.isArray(childValue)) {
// Concatenate arrays, remove duplicates for primitives
const combined = [...parentValue, ...childValue];
if (combined.every((v) => typeof v !== 'object')) {
return [...new Set(combined)];
}
return combined;
}
if (typeof parentValue === 'object' &&
parentValue !== null &&
typeof childValue === 'object' &&
childValue !== null &&
!Array.isArray(parentValue) &&
!Array.isArray(childValue)) {
// Deep merge objects
const result = { ...parentValue };
for (const [key, value] of Object.entries(childValue)) {
if (key in result) {
result[key] = mergeValue(result[key], value, 'merge');
}
else {
result[key] = value;
}
}
return result;
}
// Primitives: child overrides parent
return childValue;
}
/**
* Merges a child config into a parent config
*/
function mergeConfigs(parent, childFile) {
const resolution = childFile.resolution || {};
const result = { ...parent };
for (const [key, value] of Object.entries(childFile)) {
if (key === 'resolution')
continue; // Skip resolution property itself
const strategy = resolution[key] || 'merge';
if (key in result) {
result[key] = mergeValue(result[key], value, strategy);
}
else {
result[key] = value;
}
}
return result;
}
/**
* Loads configuration from .keymanrc.json files
*
* Searches for `.keymanrc.json` by traversing upwards from cwd to root.
* Multiple config files are merged, with child configs overriding parent configs.
*
* Use the `resolution` property to customize merge behavior:
* ```json
* {
* "vaultRoot": "../vault",
* "resolution": {
* "vaultRoot": "override"
* }
* }
* ```
*
* @returns Validated keyman configuration
*/
export function loadConfig() {
const startDir = process.cwd();
const configPaths = findConfigFiles(startDir);
if (configPaths.length === 0) {
console.error('️ No .keymanrc.json found, using default configuration');
return DEFAULT_CONFIG;
}
// Start with defaults and merge each config file
let config = { ...DEFAULT_CONFIG };
for (const configPath of configPaths) {
try {
const content = fs.readFileSync(configPath, 'utf-8');
const rawConfig = JSON.parse(content);
// Resolve path properties relative to the config file's directory
const configDir = path.dirname(configPath);
const resolvedConfig = resolvePathsRelativeToConfig(rawConfig, configDir);
config = mergeConfigs(config, resolvedConfig);
console.error(`✅ Loaded configuration from ${configPath}`);
}
catch (error) {
if (error instanceof SyntaxError) {
console.warn(`⚠️ Skipping invalid JSON in ${configPath}: ${error.message}`);
}
else {
console.warn(`⚠️ Skipping config ${configPath}: ${error}`);
}
// Continue with other configs instead of failing entirely
}
}
// Validate the final merged result
try {
return KeymanConfigSchema.parse(config);
}
catch (error) {
if (error instanceof z.ZodError) {
console.error('❌ ERROR: Invalid merged configuration:');
error.errors.forEach((err) => {
console.error(` - ${err.path.join('.')}: ${err.message}`);
});
}
console.error('️ Falling back to default configuration');
return DEFAULT_CONFIG;
}
}
/**
* Resolves configuration paths relative to VAULT_ROOT or current directory
* @param config The keyman configuration
* @returns Resolved absolute paths
*/
export function resolveConfigPaths(config) {
// VAULT_ROOT environment variable takes precedence
const vaultRoot = path.resolve(process.env.VAULT_ROOT ?? config.vaultRoot);
return {
vaultRoot,
keysDir: path.resolve(vaultRoot, config.keysDir),
tmpDir: path.resolve(vaultRoot, config.tmpDir),
keyPath: path.resolve(vaultRoot, config.ageKeyFile),
};
}
/**
* Gets the paths of all discovered config files (for debugging)
* @returns Array of paths to .keymanrc.json files, ordered from root to cwd
*/
export function getConfigPaths() {
return findConfigFiles(process.cwd());
}