// Réglages de la découverte auto des repos, persistés dans la table `settings` (clé/valeur). // Frontière de sécurité : ces clés sont NON sensibles et n'entrent dans l'allow-list du PATCH // /api/v1/settings que via les validateurs ci-dessous. Aucun secret ne transite par ici. import { getSetting } from '../db/index.js'; import type { Db } from '../db/index.js'; import { isSafeAbsolutePath } from './git.js'; export const SCAN_ROOTS_KEY = 'scan_roots'; export const SCAN_INTERVAL_KEY = 'scan_interval_min'; /** Intervalle par défaut du re-scan périodique (minutes). 0 = désactivé. */ export const DEFAULT_SCAN_INTERVAL_MIN = 5; /** Borne haute de l'intervalle (24 h) et nombre maximal de racines. */ export const MAX_SCAN_INTERVAL_MIN = 1440; export const MAX_SCAN_ROOTS = 16; /** * Racines à scanner. Défaut : AUCUNE racine → aucun scan (clean install). * L'utilisateur ajoute ses racines via Réglages → Découverte. Lecture tolérante (JSON malformé → []). */ export function readScanRoots(db: Db): string[] { const raw = getSetting(db, SCAN_ROOTS_KEY); if (!raw) return []; return normalizeScanRoots(safeParse(raw)) ?? []; } /** Intervalle périodique en minutes (0 = désactivé). Défaut DEFAULT_SCAN_INTERVAL_MIN. */ export function readScanIntervalMin(db: Db): number { const raw = getSetting(db, SCAN_INTERVAL_KEY); if (raw === null) return DEFAULT_SCAN_INTERVAL_MIN; const n = Number(raw); return Number.isInteger(n) && n >= 0 && n <= MAX_SCAN_INTERVAL_MIN ? n : DEFAULT_SCAN_INTERVAL_MIN; } /** * Valide/normalise une liste de racines : tableau de chemins absolus normalisés (isSafeAbsolutePath), * jamais `/` (scan catastrophique), dédupliqués, ≤ MAX_SCAN_ROOTS. Retourne null si invalide (⇒ 400). * Une liste vide est valide (revient au défaut côté lecture). */ export function normalizeScanRoots(raw: unknown): string[] | null { if (!Array.isArray(raw)) return null; if (raw.length > MAX_SCAN_ROOTS) return null; const out: string[] = []; for (const item of raw) { if (typeof item !== 'string') return null; const p = item.trim(); if (!isSafeAbsolutePath(p) || p === '/') return null; if (!out.includes(p)) out.push(p); } return out; } /** Valide un intervalle (entier 0–MAX_SCAN_INTERVAL_MIN). Retourne null si invalide. */ export function normalizeScanIntervalMin(raw: unknown): number | null { if (typeof raw !== 'number' || !Number.isInteger(raw)) return null; if (raw < 0 || raw > MAX_SCAN_INTERVAL_MIN) return null; return raw; } function safeParse(raw: string): unknown { try { return JSON.parse(raw); } catch { return null; } }