2026-03-05 00:04:26 -06:00
|
|
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
|
|
|
import { writeFile, mkdir } from "fs/promises";
|
|
|
|
|
import { resolve } from "path";
|
|
|
|
|
import logger from "./logger";
|
|
|
|
|
import { ensureDataDir } from "./paths";
|
|
|
|
|
|
|
|
|
|
export interface Config {
|
|
|
|
|
auth: {
|
|
|
|
|
username: string;
|
|
|
|
|
password: string;
|
|
|
|
|
jwtSecret: string;
|
|
|
|
|
agentToken: string;
|
|
|
|
|
};
|
|
|
|
|
server: {
|
|
|
|
|
port: number;
|
|
|
|
|
host: string;
|
|
|
|
|
};
|
|
|
|
|
tls: {
|
|
|
|
|
certPath: string;
|
|
|
|
|
keyPath: string;
|
|
|
|
|
caPath: string;
|
|
|
|
|
};
|
|
|
|
|
notifications: {
|
|
|
|
|
keywords: string[];
|
|
|
|
|
minIntervalMs: number;
|
|
|
|
|
spamWindowMs: number;
|
|
|
|
|
spamWarnThreshold: number;
|
|
|
|
|
historyLimit: number;
|
|
|
|
|
webhookEnabled: boolean;
|
|
|
|
|
webhookUrl: string;
|
|
|
|
|
telegramEnabled: boolean;
|
|
|
|
|
telegramBotToken: string;
|
|
|
|
|
telegramChatId: string;
|
|
|
|
|
};
|
2026-03-06 20:50:02 -06:00
|
|
|
security: {
|
|
|
|
|
sessionTtlHours: number;
|
|
|
|
|
loginMaxAttempts: number;
|
|
|
|
|
loginWindowMinutes: number;
|
|
|
|
|
loginLockoutMinutes: number;
|
|
|
|
|
passwordMinLength: number;
|
|
|
|
|
passwordRequireUppercase: boolean;
|
|
|
|
|
passwordRequireLowercase: boolean;
|
|
|
|
|
passwordRequireNumber: boolean;
|
|
|
|
|
passwordRequireSymbol: boolean;
|
|
|
|
|
};
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_CONFIG: Config = {
|
|
|
|
|
auth: {
|
|
|
|
|
username: "admin",
|
2026-03-06 14:45:15 -06:00
|
|
|
password: "admin",
|
2026-03-05 00:04:26 -06:00
|
|
|
jwtSecret: "",
|
|
|
|
|
agentToken: "",
|
|
|
|
|
},
|
|
|
|
|
server: {
|
|
|
|
|
port: 5173,
|
|
|
|
|
host: "0.0.0.0",
|
|
|
|
|
},
|
|
|
|
|
tls: {
|
|
|
|
|
certPath: "./certs/server.crt",
|
|
|
|
|
keyPath: "./certs/server.key",
|
|
|
|
|
caPath: "",
|
|
|
|
|
},
|
|
|
|
|
notifications: {
|
|
|
|
|
keywords: ["bank", "password", "admin"],
|
|
|
|
|
minIntervalMs: 8000,
|
|
|
|
|
spamWindowMs: 60000,
|
|
|
|
|
spamWarnThreshold: 5,
|
|
|
|
|
historyLimit: 200,
|
|
|
|
|
webhookEnabled: false,
|
|
|
|
|
webhookUrl: "",
|
|
|
|
|
telegramEnabled: false,
|
|
|
|
|
telegramBotToken: "",
|
|
|
|
|
telegramChatId: "",
|
|
|
|
|
},
|
2026-03-06 20:50:02 -06:00
|
|
|
security: {
|
|
|
|
|
sessionTtlHours: 168,
|
|
|
|
|
loginMaxAttempts: 5,
|
|
|
|
|
loginWindowMinutes: 15,
|
|
|
|
|
loginLockoutMinutes: 30,
|
|
|
|
|
passwordMinLength: 6,
|
|
|
|
|
passwordRequireUppercase: false,
|
|
|
|
|
passwordRequireLowercase: false,
|
|
|
|
|
passwordRequireNumber: false,
|
|
|
|
|
passwordRequireSymbol: false,
|
|
|
|
|
},
|
2026-03-05 00:04:26 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type SaveSecrets = {
|
|
|
|
|
auth?: {
|
|
|
|
|
jwtSecret?: string;
|
|
|
|
|
agentToken?: string;
|
|
|
|
|
bootstrapPassword?: string;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function generateRandomSecret(length = 48): string {
|
|
|
|
|
const chars =
|
|
|
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
|
|
|
|
|
let secret = "";
|
|
|
|
|
const array = new Uint8Array(length);
|
|
|
|
|
crypto.getRandomValues(array);
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
|
|
secret += chars[array[i] % chars.length];
|
|
|
|
|
}
|
|
|
|
|
return secret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadSaveSecrets(savePath: string): SaveSecrets {
|
|
|
|
|
if (!existsSync(savePath)) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const raw = readFileSync(savePath, "utf-8");
|
|
|
|
|
const parsed = JSON.parse(raw);
|
|
|
|
|
return typeof parsed === "object" && parsed ? parsed : {};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn("Failed to parse save.json, regenerating missing secrets:", error);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function persistSaveSecrets(savePath: string, secrets: SaveSecrets): void {
|
|
|
|
|
try {
|
|
|
|
|
writeFileSync(savePath, JSON.stringify(secrets, null, 2));
|
|
|
|
|
logger.info(`Persisted secrets to ${savePath}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn("Failed to persist save.json secrets", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let configCache: Config | null = null;
|
|
|
|
|
|
|
|
|
|
export function loadConfig(): Config {
|
|
|
|
|
if (configCache) {
|
|
|
|
|
return configCache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let fileConfig: Partial<Config> = {};
|
|
|
|
|
const dataDir = ensureDataDir();
|
|
|
|
|
const savePath = resolve(dataDir, "save.json");
|
|
|
|
|
const savedSecrets = loadSaveSecrets(savePath);
|
|
|
|
|
let saveChanged = false;
|
|
|
|
|
|
|
|
|
|
const configPath = resolve(process.cwd(), "config.json");
|
|
|
|
|
if (existsSync(configPath)) {
|
|
|
|
|
try {
|
|
|
|
|
const content = readFileSync(configPath, "utf-8");
|
|
|
|
|
fileConfig = JSON.parse(content);
|
|
|
|
|
logger.info("Loaded configuration from config.json");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn("Failed to parse config.json, using defaults:", error);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
logger.info(
|
|
|
|
|
"No config.json found, using defaults and environment variables",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const jwtSecretFromEnv = process.env.JWT_SECRET;
|
|
|
|
|
const jwtSecret =
|
|
|
|
|
process.env.JWT_SECRET ||
|
|
|
|
|
fileConfig.auth?.jwtSecret ||
|
|
|
|
|
savedSecrets.auth?.jwtSecret ||
|
|
|
|
|
DEFAULT_CONFIG.auth.jwtSecret;
|
|
|
|
|
|
|
|
|
|
let finalJwtSecret = jwtSecret;
|
|
|
|
|
if (!finalJwtSecret) {
|
|
|
|
|
finalJwtSecret = generateRandomSecret(64);
|
|
|
|
|
saveChanged = true;
|
|
|
|
|
logger.info("No JWT secret provided, generated secure random secret");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const agentTokenFromEnv = process.env.OVERLORD_AGENT_TOKEN;
|
|
|
|
|
const agentTokenFromConfig = fileConfig.auth?.agentToken;
|
|
|
|
|
let finalAgentToken =
|
|
|
|
|
agentTokenFromEnv ||
|
|
|
|
|
agentTokenFromConfig ||
|
|
|
|
|
savedSecrets.auth?.agentToken ||
|
|
|
|
|
DEFAULT_CONFIG.auth.agentToken;
|
|
|
|
|
if (!finalAgentToken) {
|
|
|
|
|
finalAgentToken = generateRandomSecret(64);
|
|
|
|
|
saveChanged = true;
|
|
|
|
|
logger.info("No agent token provided, generated secure random token");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const passwordFromEnv = process.env.OVERLORD_PASS;
|
|
|
|
|
const passwordFromConfig = fileConfig.auth?.password;
|
|
|
|
|
let finalBootstrapPassword =
|
|
|
|
|
passwordFromEnv ||
|
|
|
|
|
passwordFromConfig ||
|
|
|
|
|
savedSecrets.auth?.bootstrapPassword ||
|
|
|
|
|
DEFAULT_CONFIG.auth.password;
|
|
|
|
|
|
|
|
|
|
const keywordsEnv = process.env.OVERLORD_NOTIFICATION_KEYWORDS;
|
|
|
|
|
const keywordsFromEnv = keywordsEnv
|
|
|
|
|
? keywordsEnv
|
|
|
|
|
.split(",")
|
|
|
|
|
.map((k) => k.trim())
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
configCache = {
|
|
|
|
|
auth: {
|
|
|
|
|
username:
|
|
|
|
|
process.env.OVERLORD_USER ||
|
|
|
|
|
fileConfig.auth?.username ||
|
|
|
|
|
DEFAULT_CONFIG.auth.username,
|
|
|
|
|
password: finalBootstrapPassword,
|
|
|
|
|
jwtSecret: finalJwtSecret,
|
|
|
|
|
agentToken: finalAgentToken,
|
|
|
|
|
},
|
|
|
|
|
server: {
|
|
|
|
|
port:
|
|
|
|
|
Number(process.env.PORT) ||
|
|
|
|
|
fileConfig.server?.port ||
|
|
|
|
|
DEFAULT_CONFIG.server.port,
|
|
|
|
|
host:
|
|
|
|
|
process.env.HOST ||
|
|
|
|
|
fileConfig.server?.host ||
|
|
|
|
|
DEFAULT_CONFIG.server.host,
|
|
|
|
|
},
|
|
|
|
|
tls: {
|
|
|
|
|
certPath:
|
|
|
|
|
process.env.OVERLORD_TLS_CERT ||
|
|
|
|
|
fileConfig.tls?.certPath ||
|
|
|
|
|
DEFAULT_CONFIG.tls.certPath,
|
|
|
|
|
keyPath:
|
|
|
|
|
process.env.OVERLORD_TLS_KEY ||
|
|
|
|
|
fileConfig.tls?.keyPath ||
|
|
|
|
|
DEFAULT_CONFIG.tls.keyPath,
|
|
|
|
|
caPath:
|
|
|
|
|
process.env.OVERLORD_TLS_CA ||
|
|
|
|
|
fileConfig.tls?.caPath ||
|
|
|
|
|
DEFAULT_CONFIG.tls.caPath,
|
|
|
|
|
},
|
|
|
|
|
notifications: {
|
|
|
|
|
keywords:
|
|
|
|
|
keywordsFromEnv.length > 0
|
|
|
|
|
? keywordsFromEnv
|
|
|
|
|
: (fileConfig.notifications?.keywords ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.keywords),
|
|
|
|
|
minIntervalMs:
|
|
|
|
|
Number(process.env.OVERLORD_NOTIFICATION_MIN_INTERVAL_MS) ||
|
|
|
|
|
fileConfig.notifications?.minIntervalMs ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.minIntervalMs,
|
|
|
|
|
spamWindowMs:
|
|
|
|
|
Number(process.env.OVERLORD_NOTIFICATION_SPAM_WINDOW_MS) ||
|
|
|
|
|
fileConfig.notifications?.spamWindowMs ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.spamWindowMs,
|
|
|
|
|
spamWarnThreshold:
|
|
|
|
|
Number(process.env.OVERLORD_NOTIFICATION_SPAM_WARN_THRESHOLD) ||
|
|
|
|
|
fileConfig.notifications?.spamWarnThreshold ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.spamWarnThreshold,
|
|
|
|
|
historyLimit:
|
|
|
|
|
Number(process.env.OVERLORD_NOTIFICATION_HISTORY_LIMIT) ||
|
|
|
|
|
fileConfig.notifications?.historyLimit ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.historyLimit,
|
|
|
|
|
webhookEnabled:
|
|
|
|
|
String(process.env.OVERLORD_NOTIFICATION_WEBHOOK_ENABLED || "").toLowerCase() === "true" ||
|
|
|
|
|
fileConfig.notifications?.webhookEnabled ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.webhookEnabled,
|
|
|
|
|
webhookUrl:
|
|
|
|
|
process.env.OVERLORD_NOTIFICATION_WEBHOOK_URL ||
|
|
|
|
|
fileConfig.notifications?.webhookUrl ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.webhookUrl,
|
|
|
|
|
telegramEnabled:
|
|
|
|
|
String(process.env.OVERLORD_NOTIFICATION_TELEGRAM_ENABLED || "").toLowerCase() === "true" ||
|
|
|
|
|
fileConfig.notifications?.telegramEnabled ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.telegramEnabled,
|
|
|
|
|
telegramBotToken:
|
|
|
|
|
process.env.OVERLORD_NOTIFICATION_TELEGRAM_BOT_TOKEN ||
|
|
|
|
|
fileConfig.notifications?.telegramBotToken ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.telegramBotToken,
|
|
|
|
|
telegramChatId:
|
|
|
|
|
process.env.OVERLORD_NOTIFICATION_TELEGRAM_CHAT_ID ||
|
|
|
|
|
fileConfig.notifications?.telegramChatId ||
|
|
|
|
|
DEFAULT_CONFIG.notifications.telegramChatId,
|
|
|
|
|
},
|
2026-03-06 20:50:02 -06:00
|
|
|
security: {
|
|
|
|
|
sessionTtlHours:
|
|
|
|
|
Number(process.env.OVERLORD_SESSION_TTL_HOURS) ||
|
|
|
|
|
fileConfig.security?.sessionTtlHours ||
|
|
|
|
|
DEFAULT_CONFIG.security.sessionTtlHours,
|
|
|
|
|
loginMaxAttempts:
|
|
|
|
|
Number(process.env.OVERLORD_LOGIN_MAX_ATTEMPTS) ||
|
|
|
|
|
fileConfig.security?.loginMaxAttempts ||
|
|
|
|
|
DEFAULT_CONFIG.security.loginMaxAttempts,
|
|
|
|
|
loginWindowMinutes:
|
|
|
|
|
Number(process.env.OVERLORD_LOGIN_WINDOW_MINUTES) ||
|
|
|
|
|
fileConfig.security?.loginWindowMinutes ||
|
|
|
|
|
DEFAULT_CONFIG.security.loginWindowMinutes,
|
|
|
|
|
loginLockoutMinutes:
|
|
|
|
|
Number(process.env.OVERLORD_LOGIN_LOCKOUT_MINUTES) ||
|
|
|
|
|
fileConfig.security?.loginLockoutMinutes ||
|
|
|
|
|
DEFAULT_CONFIG.security.loginLockoutMinutes,
|
|
|
|
|
passwordMinLength:
|
|
|
|
|
Number(process.env.OVERLORD_PASSWORD_MIN_LENGTH) ||
|
|
|
|
|
fileConfig.security?.passwordMinLength ||
|
|
|
|
|
DEFAULT_CONFIG.security.passwordMinLength,
|
|
|
|
|
passwordRequireUppercase:
|
|
|
|
|
String(process.env.OVERLORD_PASSWORD_REQUIRE_UPPERCASE || "").toLowerCase() === "true" ||
|
|
|
|
|
fileConfig.security?.passwordRequireUppercase ||
|
|
|
|
|
DEFAULT_CONFIG.security.passwordRequireUppercase,
|
|
|
|
|
passwordRequireLowercase:
|
|
|
|
|
String(process.env.OVERLORD_PASSWORD_REQUIRE_LOWERCASE || "").toLowerCase() === "true" ||
|
|
|
|
|
fileConfig.security?.passwordRequireLowercase ||
|
|
|
|
|
DEFAULT_CONFIG.security.passwordRequireLowercase,
|
|
|
|
|
passwordRequireNumber:
|
|
|
|
|
String(process.env.OVERLORD_PASSWORD_REQUIRE_NUMBER || "").toLowerCase() === "true" ||
|
|
|
|
|
fileConfig.security?.passwordRequireNumber ||
|
|
|
|
|
DEFAULT_CONFIG.security.passwordRequireNumber,
|
|
|
|
|
passwordRequireSymbol:
|
|
|
|
|
String(process.env.OVERLORD_PASSWORD_REQUIRE_SYMBOL || "").toLowerCase() === "true" ||
|
|
|
|
|
fileConfig.security?.passwordRequireSymbol ||
|
|
|
|
|
DEFAULT_CONFIG.security.passwordRequireSymbol,
|
|
|
|
|
},
|
2026-03-05 00:04:26 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (saveChanged) {
|
|
|
|
|
const nextSecrets: SaveSecrets = {
|
|
|
|
|
auth: {
|
|
|
|
|
jwtSecret: finalJwtSecret,
|
|
|
|
|
agentToken: finalAgentToken,
|
|
|
|
|
bootstrapPassword: finalBootstrapPassword,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
persistSaveSecrets(savePath, nextSecrets);
|
|
|
|
|
logger.info(
|
|
|
|
|
`Generated runtime secrets are stored at ${savePath}. Keep this file private.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (jwtSecretFromEnv) {
|
|
|
|
|
logger.info("JWT secret loaded from JWT_SECRET environment variable");
|
|
|
|
|
}
|
|
|
|
|
if (agentTokenFromEnv) {
|
|
|
|
|
logger.info("Agent token loaded from OVERLORD_AGENT_TOKEN environment variable");
|
|
|
|
|
}
|
|
|
|
|
if (passwordFromEnv) {
|
|
|
|
|
logger.info("Initial admin password loaded from OVERLORD_PASS environment variable");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return configCache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getConfig(): Config {
|
|
|
|
|
if (!configCache) {
|
|
|
|
|
return loadConfig();
|
|
|
|
|
}
|
|
|
|
|
return configCache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function updateNotificationsConfig(
|
|
|
|
|
updates: Partial<Config["notifications"]>,
|
|
|
|
|
): Promise<Config["notifications"]> {
|
|
|
|
|
const current = getConfig();
|
|
|
|
|
const keywords = (updates.keywords || current.notifications.keywords || [])
|
|
|
|
|
.map((k) => String(k).trim())
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
const deduped = Array.from(new Set(keywords));
|
|
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
|
...current.notifications,
|
|
|
|
|
...updates,
|
|
|
|
|
keywords: deduped,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
configCache = {
|
|
|
|
|
...current,
|
|
|
|
|
notifications: next,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const configPath = resolve(process.cwd(), "config.json");
|
|
|
|
|
let fileConfig: any = {};
|
|
|
|
|
if (existsSync(configPath)) {
|
|
|
|
|
try {
|
|
|
|
|
const content = readFileSync(configPath, "utf-8");
|
|
|
|
|
fileConfig = JSON.parse(content) || {};
|
|
|
|
|
} catch {
|
|
|
|
|
fileConfig = {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileConfig.notifications = next;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await mkdir(resolve(process.cwd()), { recursive: true });
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
await writeFile(configPath, JSON.stringify(fileConfig, null, 2));
|
|
|
|
|
return next;
|
|
|
|
|
}
|
2026-03-06 20:50:02 -06:00
|
|
|
|
|
|
|
|
export async function updateSecurityConfig(
|
|
|
|
|
updates: Partial<Config["security"]>,
|
|
|
|
|
): Promise<Config["security"]> {
|
|
|
|
|
const current = getConfig();
|
|
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
|
...current.security,
|
|
|
|
|
...updates,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toNumberOr = (value: unknown, fallback: number) => {
|
|
|
|
|
const parsed = Number(value);
|
|
|
|
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
next.sessionTtlHours = Math.min(24 * 30, Math.max(1, toNumberOr(next.sessionTtlHours, 168)));
|
|
|
|
|
next.loginMaxAttempts = Math.min(50, Math.max(1, toNumberOr(next.loginMaxAttempts, 5)));
|
|
|
|
|
next.loginWindowMinutes = Math.min(24 * 24, Math.max(1, toNumberOr(next.loginWindowMinutes, 15)));
|
|
|
|
|
next.loginLockoutMinutes = Math.min(24 * 24, Math.max(1, toNumberOr(next.loginLockoutMinutes, 30)));
|
|
|
|
|
next.passwordMinLength = Math.min(128, Math.max(6, toNumberOr(next.passwordMinLength, 6)));
|
|
|
|
|
next.passwordRequireUppercase = Boolean(next.passwordRequireUppercase);
|
|
|
|
|
next.passwordRequireLowercase = Boolean(next.passwordRequireLowercase);
|
|
|
|
|
next.passwordRequireNumber = Boolean(next.passwordRequireNumber);
|
|
|
|
|
next.passwordRequireSymbol = Boolean(next.passwordRequireSymbol);
|
|
|
|
|
|
|
|
|
|
configCache = {
|
|
|
|
|
...current,
|
|
|
|
|
security: next,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const configPath = resolve(process.cwd(), "config.json");
|
|
|
|
|
let fileConfig: any = {};
|
|
|
|
|
if (existsSync(configPath)) {
|
|
|
|
|
try {
|
|
|
|
|
const content = readFileSync(configPath, "utf-8");
|
|
|
|
|
fileConfig = JSON.parse(content) || {};
|
|
|
|
|
} catch {
|
|
|
|
|
fileConfig = {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileConfig.security = next;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await mkdir(resolve(process.cwd()), { recursive: true });
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
await writeFile(configPath, JSON.stringify(fileConfig, null, 2));
|
|
|
|
|
return next;
|
|
|
|
|
}
|