Lines 193-274javascript
193 if (config.$schema && !config.schema) config.schema = config.$schema;
194 const profile = env.HOLOREPO_CONFIG_PROFILE || config.activeProfile || config.profile;
195 if (profile && config.profiles?.[profile]) {
196 config = deepMerge(config, config.profiles[profile]);
197 config.activeProfile = profile;
199 if (env.HOLOREPO_STORAGE_ROOT) config.storage = { ...(config.storage || {}), root: env.HOLOREPO_STORAGE_ROOT };
200 if (env.HOLOREPO_DATABASE_URL) config.database = { ...(config.database || {}), transport: 'pg', connectionEnv: 'HOLOREPO_DATABASE_URL' };
201 if (env.DATABASE_URL && !env.HOLOREPO_DATABASE_URL) config.database = { ...(config.database || {}), transport: 'pg', connectionEnv: 'DATABASE_URL' };
202 config.__source = fs.existsSync(resolvedPath) ? resolvedPath : 'default';
203 config.__path = resolvedPath;
207export function redactConnectionString(value) {
208 const raw = String(value || '');
211 const url = new URL(raw);
212 if (url.username) url.username = 'redacted';
213 if (url.password) url.password = 'redacted';
214 for (const key of [...url.searchParams.keys()]) {
215 if (/password|passwd|token|secret|api[_-]?key|private[_-]?key|sslkey|credential/iu.test(key)) {
216 url.searchParams.set(key, 'redacted');
219 if (url.hash) url.hash = '#redacted';
220 return url.toString();
223 .replace(/\/\/([^:@/\s]+):([^@/\s]+)@/u, '//redacted:redacted@')
224 .replace(/([?&](?:password|passwd|token|secret|api[_-]?key|private[_-]?key|sslkey|credential)=)[^&#\s]*/giu, '$1redacted')
225 .replace(/#.*$/u, '#redacted');
229function redactNestedSecrets(value, parents = []) {
230 if (Array.isArray(value)) return value.map((child) => redactNestedSecrets(child, parents));
231 if (!value || typeof value !== 'object') {
232 if (typeof value === 'string' && /-----BEGIN [A-Z ]*PRIVATE KEY-----/u.test(value)) return '<redacted>';
235 return Object.fromEntries(Object.entries(value).map(([key, child]) => {
236 const referenceKey = /(?:Env|Ref)$/u.test(key);
237 const secretKey = !referenceKey && /secret|token|password|private|connectionString|api[_-]?key|access[_-]?key|credential/iu.test(key);
238 const transportScope = parents.some((parent) => ['ssl', 'tls', 'auth', 'credential', 'credentials', 'signing'].includes(String(parent).toLowerCase()));
239 const normalizedKey = key.replace(/([a-z])([A-Z])/gu, '$1_$2');
240 const transportKey = transportScope && /(?:^|_)(?:key|pfx|passphrase)$/iu.test(normalizedKey);
241 if ((secretKey || transportKey) && child) return [key, '<redacted>'];
242 return [key, redactNestedSecrets(child, [...parents, key])];
246export function redactHoloRepoConfig(config, env = process.env) {
247 const redacted = clone(config);
248 delete redacted.__source;
249 delete redacted.__path;
250 if (redacted.database?.url) {
251 redacted.database.url = redactConnectionString(redacted.database.url);
253 if (redacted.database?.conn?.password) {
254 redacted.database.conn.password = '<redacted>';
256 const envName = config?.database?.connectionEnv;
257 if (envName && env[envName]) {
258 redacted.database = { ...(redacted.database || {}), connection: redactConnectionString(env[envName]) };
260 return redactNestedSecrets(redacted);
263export function validateHoloRepoConfig(config) {
266 if (!config || typeof config !== 'object') return { ok: false, errors: ['config must be an object'], warnings };
267 if (config.schema !== HOLOREPO_CONFIG_SCHEMA) errors.push(`schema must be ${HOLOREPO_CONFIG_SCHEMA}`);
268 if (config.productName !== 'HoloRepo') errors.push('productName must be HoloRepo');
269 if (!config.ecosystem?.id) errors.push('ecosystem.id is required');
270 if (!config.storage?.root) errors.push('storage.root is required');
271 if (!config.git?.backend) errors.push('git.backend is required');
272 if (!config.git?.root && !config.git?.remoteEnv) warnings.push('git has no root or remoteEnv');
273 if (config.database?.connectionString) errors.push('database.connectionString is not allowed; use connectionEnv');
274 if (config.database?.transport && !['pg', 'direct-pg', 'direct-psql', 'docker-psql', 'ssh-docker-psql', 'disabled'].includes(config.database.transport)) {