IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

env.ts

Buka raw
import "server-only";

import { z } from "zod";

const envSchema = z.object({
  NODE_ENV: z
    .enum(["development", "test", "production"])
    .default("development"),
  DATABASE_URL: z
    .string()
    .url()
    .default("postgresql://postgres:postgres@127.0.0.1:5432/home_server"),
  PG_MAX_CONNECTIONS: z.coerce.number().int().min(1).max(20).default(10),
  METRICS_CACHE_TTL_MS: z.coerce.number().int().min(250).default(2_000),
  METRICS_PUBLISH_INTERVAL_MS: z.coerce.number().int().min(500).default(2_000),
  SSE_HEARTBEAT_MS: z.coerce.number().int().min(1_000).default(15_000),
  WEBSOCKET_ENABLED: z
    .string()
    .optional()
    .transform((value) => value !== "false"),
  AUTH_SESSION_SECRET: z
    .string()
    .min(16)
    .default("dev-session-secret-change-me"),
  AUTH_SESSION_HOURS: z.coerce
    .number()
    .int()
    .min(1)
    .max(24 * 30)
    .default(168),
  AUTH_PRIMARY_USERNAME: z.string().min(3).default("admin"),
  AUTH_PRIMARY_PASSWORD: z.string().optional(),
  AUTH_TOTP_ENCRYPTION_KEY: z.string().optional(),
  LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
  LOG_FILE_PATH: z.string().default("logs/home-server.log"),
  LOG_TO_FILE: z
    .string()
    .optional()
    .transform((value) => value !== "false"),
  STORE_CATALOG_TTL_MS: z.coerce
    .number()
    .int()
    .min(5_000)
    .default(6 * 60 * 60_000), // 6 hours
  STORE_MAX_CONCURRENT_OPERATIONS: z.coerce.number().int().min(1).max(20).default(3),
  STORE_STACKS_ROOT: z.string().optional(),
  STORE_APP_DATA_ROOT: z.string().optional(),
  FILES_ROOT: z.string().optional(),
  FILES_ALLOW_HIDDEN: z
    .string()
    .optional()
    .transform((value) => value === "true"),
  TERMINAL_WS_REQUIRE_AUTH: z
    .string()
    .optional()
    .transform((value) => value !== "false"),
  TERMINAL_MAX_SESSIONS_PER_USER: z.coerce
    .number()
    .int()
    .min(1)
    .max(10)
    .default(2),
  TERMINAL_IDLE_TIMEOUT_MS: z.coerce
    .number()
    .int()
    .min(10_000)
    .default(900_000),
  TERMINAL_MAX_SESSION_MS: z.coerce
    .number()
    .int()
    .min(60_000)
    .default(3_600_000),
  APP_GRID_REAL_ACTIONS: z
    .string()
    .optional()
    .transform((value) => value !== "false"),
  STORE_PRESERVE_INSTALLED_COMPOSE: z
    .string()
    .optional()
    .transform((value) => value !== "false"),
  FILES_ENHANCED_OPS: z
    .string()
    .optional()
    .transform((value) => value !== "false"),
  FILES_NETWORK_MOUNT_UID: z.coerce.number().int().min(0).optional(),
  FILES_NETWORK_MOUNT_GID: z.coerce.number().int().min(0).optional(),
  DOCKER_SOCKET_PATH: z.string().default("/var/run/docker.sock"),
  DOCKER_COMPOSE_TIMEOUT_MS: z.coerce
    .number()
    .int()
    .min(30_000)
    .default(300_000),
  FILES_SEARCH_TIMEOUT_MS: z.coerce
    .number()
    .int()
    .min(1_000)
    .default(10_000),
  DBUS_HELPER_SOCKET_PATH: z
    .string()
    .default("/run/home-server/dbus-helper.sock"),
  DEMO_MODE: z
    .string()
    .optional()
    .transform((value) => value === "true"),
  HOMEIO_TELEMETRY: z
    .string()
    .optional()
    .transform((value) => value !== "false"),
  HOMEIO_TELEMETRY_URL: z
    .string()
    .url()
    .default("https://homeio.app/api/stats"),
});

const parsedEnv = envSchema.safeParse(process.env);

if (!parsedEnv.success) {
  const issues = parsedEnv.error.issues
    .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
    .join("; ");
  throw new Error(`Invalid server environment: ${issues}`);
}

const unsafeProductionSessionSecrets = new Set([
  "dev-session-secret-change-me",
  "change-me-to-a-random-32-char-secret",
]);

const isNextProductionBuild =
  process.env.NEXT_PHASE === "phase-production-build" ||
  process.env.npm_lifecycle_event === "build";

if (parsedEnv.data.NODE_ENV === "production" && !isNextProductionBuild) {
  if (
    parsedEnv.data.AUTH_SESSION_SECRET.length < 32 ||
    unsafeProductionSessionSecrets.has(parsedEnv.data.AUTH_SESSION_SECRET)
  ) {
    throw new Error(
      "Invalid server environment: AUTH_SESSION_SECRET must be a non-default value with at least 32 characters in production",
    );
  }
}

const defaultStacksRoot =
  parsedEnv.data.NODE_ENV === "production" ? "/DATA/AppData" : "DATA/AppData";
const defaultAppDataRoot =
  parsedEnv.data.NODE_ENV === "production" ? "/DATA/AppData" : "DATA/AppData";
const defaultFilesRoot =
  parsedEnv.data.NODE_ENV === "production" ? "/DATA" : "DATA";

export const serverEnv = {
  ...parsedEnv.data,
  STORE_STACKS_ROOT: parsedEnv.data.STORE_STACKS_ROOT ?? defaultStacksRoot,
  STORE_APP_DATA_ROOT: parsedEnv.data.STORE_APP_DATA_ROOT ?? defaultAppDataRoot,
  FILES_ROOT: parsedEnv.data.FILES_ROOT ?? defaultFilesRoot,
  FILES_ALLOW_HIDDEN: parsedEnv.data.FILES_ALLOW_HIDDEN ?? false,
};
byte
4.776
baris
158
Jenis
ts
Commit terakhir
16 Oktober 2026