* clean up helpers * shared in it's own top level folder * workspaces setup * build fix * disable workspaces? * run ci * rename job-flow to gradcracker * optional dependencies * formatting? * more optional modules * allow post install runs * node bump * remove post install * add optionals * add more * formatting * comments, but im unsure * run typescript DIRECTLY * better build * camoufox simplification * lint * build process doesn't exist * build fix * lockfile * type check everything, build only for client * rename steps correctly * import from package! * fix formatting * don't fetch twice * fix concern
233 lines
7.4 KiB
TypeScript
233 lines
7.4 KiB
TypeScript
import type { SettingKey } from "@server/repositories/settings";
|
|
import * as settingsRepo from "@server/repositories/settings";
|
|
|
|
const envDefaults: Record<string, string | undefined> = { ...process.env };
|
|
|
|
const readableStringConfig: { settingKey: SettingKey; envKey: string }[] = [
|
|
{ settingKey: "llmProvider", envKey: "LLM_PROVIDER" },
|
|
{ settingKey: "llmBaseUrl", envKey: "LLM_BASE_URL" },
|
|
{ settingKey: "rxresumeEmail", envKey: "RXRESUME_EMAIL" },
|
|
{ settingKey: "ukvisajobsEmail", envKey: "UKVISAJOBS_EMAIL" },
|
|
{ settingKey: "basicAuthUser", envKey: "BASIC_AUTH_USER" },
|
|
];
|
|
|
|
const readableBooleanConfig: {
|
|
settingKey: SettingKey;
|
|
envKey: string;
|
|
defaultValue: boolean;
|
|
}[] = [];
|
|
|
|
const privateStringConfig: {
|
|
settingKey: SettingKey;
|
|
envKey: string;
|
|
hintKey: string;
|
|
}[] = [
|
|
{
|
|
settingKey: "llmApiKey",
|
|
envKey: "LLM_API_KEY",
|
|
hintKey: "llmApiKeyHint",
|
|
},
|
|
{
|
|
settingKey: "openrouterApiKey",
|
|
envKey: "OPENROUTER_API_KEY",
|
|
hintKey: "openrouterApiKeyHint",
|
|
},
|
|
{
|
|
settingKey: "rxresumePassword",
|
|
envKey: "RXRESUME_PASSWORD",
|
|
hintKey: "rxresumePasswordHint",
|
|
},
|
|
{
|
|
settingKey: "ukvisajobsPassword",
|
|
envKey: "UKVISAJOBS_PASSWORD",
|
|
hintKey: "ukvisajobsPasswordHint",
|
|
},
|
|
{
|
|
settingKey: "basicAuthPassword",
|
|
envKey: "BASIC_AUTH_PASSWORD",
|
|
hintKey: "basicAuthPasswordHint",
|
|
},
|
|
{
|
|
settingKey: "webhookSecret",
|
|
envKey: "WEBHOOK_SECRET",
|
|
hintKey: "webhookSecretHint",
|
|
},
|
|
];
|
|
|
|
export function normalizeEnvInput(
|
|
value: string | null | undefined,
|
|
): string | null {
|
|
const trimmed = value?.trim();
|
|
return trimmed ? trimmed : null;
|
|
}
|
|
|
|
function parseEnvBoolean(
|
|
raw: string | null | undefined,
|
|
defaultValue: boolean,
|
|
): boolean {
|
|
if (raw === undefined || raw === null || raw === "") return defaultValue;
|
|
if (raw === "false" || raw === "0") return false;
|
|
return true;
|
|
}
|
|
|
|
export function applyEnvValue(envKey: string, value: string | null): void {
|
|
if (value === null) {
|
|
const fallback = envDefaults[envKey];
|
|
if (fallback === undefined) {
|
|
delete process.env[envKey];
|
|
} else {
|
|
process.env[envKey] = fallback;
|
|
}
|
|
return;
|
|
}
|
|
|
|
process.env[envKey] = value;
|
|
}
|
|
|
|
export function serializeEnvBoolean(value: boolean | null): string | null {
|
|
if (value === null) return null;
|
|
return value ? "true" : "false";
|
|
}
|
|
|
|
export async function applyStoredEnvOverrides(): Promise<void> {
|
|
const safeGetSetting = async (key: SettingKey): Promise<string | null> => {
|
|
try {
|
|
return await settingsRepo.getSetting(key);
|
|
} catch (error) {
|
|
// In some test harnesses or first-boot scenarios, the DB may exist but not yet
|
|
// have the settings table. Treat this as "no overrides".
|
|
const msg = String((error as Error)?.message ?? error);
|
|
if (msg.includes("no such table") && msg.includes("settings"))
|
|
return null;
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const safeSetSetting = async (key: SettingKey, value: string | null) => {
|
|
try {
|
|
await settingsRepo.setSetting(key, value);
|
|
} catch (error) {
|
|
const msg = String((error as Error)?.message ?? error);
|
|
if (msg.includes("no such table") && msg.includes("settings")) return;
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Migration: move legacy OpenRouter key to the unified LLM key.
|
|
//
|
|
// Users only see their API keys once. If we simply switch to LLM_API_KEY without
|
|
// copying, they may be unable to recover their existing key.
|
|
const providerOverride = await safeGetSetting("llmProvider");
|
|
const legacyOpenrouterKey = normalizeEnvInput(
|
|
await safeGetSetting("openrouterApiKey"),
|
|
);
|
|
const unifiedKey = normalizeEnvInput(await safeGetSetting("llmApiKey"));
|
|
|
|
const effectiveProvider = (providerOverride ?? process.env.LLM_PROVIDER)
|
|
?.trim()
|
|
.toLowerCase();
|
|
|
|
if (
|
|
(effectiveProvider ?? "openrouter") === "openrouter" &&
|
|
legacyOpenrouterKey &&
|
|
!unifiedKey
|
|
) {
|
|
console.warn(
|
|
"[DEPRECATED] Detected stored OpenRouter API key. Migrating to LLM_API_KEY and clearing legacy storage.",
|
|
);
|
|
await safeSetSetting("llmApiKey", legacyOpenrouterKey);
|
|
await safeSetSetting("openrouterApiKey", null);
|
|
}
|
|
|
|
// Migration helper for env-based users: copy OPENROUTER_API_KEY -> LLM_API_KEY
|
|
// at runtime so the app keeps working after removing fallback logic.
|
|
if (
|
|
(effectiveProvider ?? "openrouter") === "openrouter" &&
|
|
!normalizeEnvInput(process.env.LLM_API_KEY) &&
|
|
normalizeEnvInput(process.env.OPENROUTER_API_KEY)
|
|
) {
|
|
console.warn(
|
|
"[DEPRECATED] OPENROUTER_API_KEY is deprecated. Copying to LLM_API_KEY for compatibility.",
|
|
);
|
|
const normalizedKey = normalizeEnvInput(process.env.OPENROUTER_API_KEY);
|
|
if (normalizedKey) process.env.LLM_API_KEY = normalizedKey;
|
|
}
|
|
|
|
await Promise.all([
|
|
...readableStringConfig.map(async ({ settingKey, envKey }) => {
|
|
const override = await safeGetSetting(settingKey);
|
|
if (override === null) return;
|
|
applyEnvValue(envKey, normalizeEnvInput(override));
|
|
}),
|
|
...readableBooleanConfig.map(
|
|
async ({ settingKey, envKey, defaultValue }) => {
|
|
const override = await safeGetSetting(settingKey);
|
|
if (override === null) return;
|
|
const parsed = parseEnvBoolean(override, defaultValue);
|
|
applyEnvValue(envKey, serializeEnvBoolean(parsed));
|
|
},
|
|
),
|
|
...privateStringConfig.map(async ({ settingKey, envKey }) => {
|
|
const override = await safeGetSetting(settingKey);
|
|
if (override === null) return;
|
|
applyEnvValue(envKey, normalizeEnvInput(override));
|
|
}),
|
|
]);
|
|
}
|
|
|
|
export async function getEnvSettingsData(
|
|
overrides?: Partial<Record<SettingKey, string>>,
|
|
): Promise<Record<string, string | boolean | number | null>> {
|
|
const activeOverrides = overrides || (await settingsRepo.getAllSettings());
|
|
const readableValues: Record<string, string | boolean | null> = {};
|
|
const privateValues: Record<string, string | null> = {};
|
|
|
|
for (const { settingKey, envKey } of readableStringConfig) {
|
|
const override = activeOverrides[settingKey] ?? null;
|
|
const rawValue = override ?? process.env[envKey];
|
|
readableValues[settingKey] = normalizeEnvInput(rawValue);
|
|
}
|
|
|
|
for (const { settingKey, envKey, defaultValue } of readableBooleanConfig) {
|
|
const override = activeOverrides[settingKey] ?? null;
|
|
const rawValue = override ?? process.env[envKey];
|
|
readableValues[settingKey] = parseEnvBoolean(rawValue, defaultValue);
|
|
}
|
|
|
|
for (const { settingKey, envKey, hintKey } of privateStringConfig) {
|
|
const override = activeOverrides[settingKey] ?? null;
|
|
const rawValue = override ?? process.env[envKey];
|
|
if (!rawValue) {
|
|
privateValues[hintKey] = null;
|
|
continue;
|
|
}
|
|
|
|
const hintLength =
|
|
rawValue.length > 4 ? 4 : Math.max(rawValue.length - 1, 1);
|
|
privateValues[hintKey] = rawValue.slice(0, hintLength);
|
|
}
|
|
|
|
// Backwards-compat: old clients still expect openrouterApiKeyHint.
|
|
// Always prefer the unified LLM key hint when present.
|
|
if (privateValues.llmApiKeyHint) {
|
|
privateValues.openrouterApiKeyHint = privateValues.llmApiKeyHint;
|
|
}
|
|
|
|
const basicAuthUser =
|
|
activeOverrides.basicAuthUser ?? process.env.BASIC_AUTH_USER;
|
|
const basicAuthPassword =
|
|
activeOverrides.basicAuthPassword ?? process.env.BASIC_AUTH_PASSWORD;
|
|
|
|
return {
|
|
...readableValues,
|
|
...privateValues,
|
|
basicAuthActive: Boolean(basicAuthUser && basicAuthPassword),
|
|
};
|
|
}
|
|
|
|
export const envSettingConfig = {
|
|
readableStringConfig,
|
|
readableBooleanConfig,
|
|
privateStringConfig,
|
|
};
|