Next.js app with a browser-side buttplug/buttplug-wasm control layer (server never touches real-time device commands), SQLite storage via Drizzle, single-secret auth, recordings/replay with device remapping, a usage stats dashboard, Docker deployment, and Gitea CI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W8WkFF5ppURBAB918593Eb
26 lines
907 B
TypeScript
26 lines
907 B
TypeScript
import { z } from "zod";
|
|
|
|
// ACCESS_PASSWORD gates the login form; AUTH_SECRET signs the session JWT.
|
|
// These are deliberately separate - reusing the login password as the
|
|
// signing key would let a JWT-signing weakness leak the login secret itself.
|
|
const envSchema = z.object({
|
|
ACCESS_PASSWORD: z.string().min(1, "ACCESS_PASSWORD must be set"),
|
|
AUTH_SECRET: z.string().min(16, "AUTH_SECRET must be at least 16 characters"),
|
|
DATABASE_PATH: z.string().min(1).default("./data/app.db"),
|
|
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).optional(),
|
|
});
|
|
|
|
export type Env = z.infer<typeof envSchema>;
|
|
|
|
let cached: Env | undefined;
|
|
|
|
export function getEnv(): Env {
|
|
if (cached) return cached;
|
|
const parsed = envSchema.safeParse(process.env);
|
|
if (!parsed.success) {
|
|
throw new Error(`Invalid environment configuration: ${parsed.error.message}`);
|
|
}
|
|
cached = parsed.data;
|
|
return cached;
|
|
}
|