19 changed files with 218 additions and 57 deletions
Binary file not shown.
@ -1,11 +1,11 @@ |
|||||
import './env'; |
|
||||
import { defineConfig } from 'drizzle-kit'; |
import { defineConfig } from 'drizzle-kit'; |
||||
|
import { env } from '../env'; |
||||
|
|
||||
export default defineConfig({ |
export default defineConfig({ |
||||
out: './migrations', |
out: './migrations', |
||||
schema: './lib/schema/*', |
schema: './lib/schema/*', |
||||
dialect: 'sqlite', |
dialect: 'sqlite', |
||||
dbCredentials: { |
dbCredentials: { |
||||
url: process.env.DATABASE_URL! |
url: env.DATABASE_URL, |
||||
}, |
}, |
||||
}); |
}); |
||||
|
|||||
@ -1,4 +1,4 @@ |
|||||
|
|
||||
import { config } from 'dotenv'; |
// Re-export env from the centralized env package.
|
||||
|
// This file is kept for backward-compatible side-effect imports.
|
||||
config({ path: '../../.env' }); |
export { env } from '../env'; |
||||
|
|||||
@ -0,0 +1,36 @@ |
|||||
|
import { config } from "dotenv"; |
||||
|
import path from "node:path"; |
||||
|
import { fileURLToPath } from "node:url"; |
||||
|
import { envSchema, type Env } from "./schema"; |
||||
|
|
||||
|
// ── Load .env from project root ──
|
||||
|
// Resolve relative to this file: packages/env/ → ../../.env = project root
|
||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
||||
|
const projectRoot = path.resolve(__dirname, "..", ".."); |
||||
|
config({ path: path.resolve(projectRoot, ".env") }); |
||||
|
|
||||
|
// ── Parse & validate ──
|
||||
|
function parseEnv(): Env { |
||||
|
const raw: Record<string, string | undefined> = {}; |
||||
|
|
||||
|
// Collect only the keys we care about from process.env
|
||||
|
for (const key of Object.keys(envSchema.shape)) { |
||||
|
raw[key] = process.env[key]; |
||||
|
} |
||||
|
|
||||
|
const result = envSchema.safeParse(raw); |
||||
|
|
||||
|
if (!result.success) { |
||||
|
const errors = result.error.issues |
||||
|
.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`) |
||||
|
.join("\n"); |
||||
|
throw new Error(`Environment variable validation failed:\n${errors}`); |
||||
|
} |
||||
|
|
||||
|
return result.data; |
||||
|
} |
||||
|
|
||||
|
export const env: Readonly<Env> = parseEnv(); |
||||
|
|
||||
|
// Re-export the type for consumers
|
||||
|
export type { Env } from "./schema"; |
||||
@ -0,0 +1,12 @@ |
|||||
|
{ |
||||
|
"name": "env", |
||||
|
"version": "0.1.0", |
||||
|
"type": "module", |
||||
|
"exports": { |
||||
|
".": "./index.ts" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"dotenv": "17.4.1", |
||||
|
"zod": "4.3.6" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
import { z } from "zod"; |
||||
|
|
||||
|
/** |
||||
|
* Zod schema for all environment variables used across the project. |
||||
|
* |
||||
|
* Every `process.env.X` access should be replaced by importing `env` from |
||||
|
* this package and using the typed, validated value. |
||||
|
* |
||||
|
* To add a new env var: |
||||
|
* 1. Add it to this schema with the correct type, default, and validation. |
||||
|
* 2. The typed `env` object is auto-generated from the schema's output. |
||||
|
*/ |
||||
|
|
||||
|
export const envSchema = z.object({ |
||||
|
// ── Database ──
|
||||
|
DATABASE_URL: z.string().min(1, "DATABASE_URL is required"), |
||||
|
|
||||
|
// ── Node / Runtime ──
|
||||
|
NODE_ENV: z |
||||
|
.enum(["development", "production", "test"]) |
||||
|
.default("development"), |
||||
|
|
||||
|
// ── Logging (logger package) ──
|
||||
|
LOG_MAX_SIZE: z.coerce.number().int().positive().default(10 * 1024 * 1024), // 10 MB
|
||||
|
LOG_MAX_BACKUPS: z.coerce.number().int().nonnegative().default(5), |
||||
|
|
||||
|
// ── Static file serving ──
|
||||
|
STATIC_DIR: z.string().default("static"), |
||||
|
UPLOAD_SUBDIR: z.string().default("upload"), |
||||
|
|
||||
|
// ── Scheduler ──
|
||||
|
SCHEDULER_MAX_CONCURRENCY: z.coerce.number().int().positive().default(5), |
||||
|
SCHEDULER_LOG_RETENTION_DAYS: z.coerce.number().int().positive().default(30), |
||||
|
|
||||
|
// ── App ──
|
||||
|
APP_URL: z.string().min(1, "APP_URL is required"), |
||||
|
NITRO_PORT: z.coerce.number().int().positive(), |
||||
|
|
||||
|
// ── OAuth: GitHub ──
|
||||
|
GITHUB_CLIENT_ID: z.string().default(""), |
||||
|
GITHUB_CLIENT_SECRET: z.string().default(""), |
||||
|
|
||||
|
// ── OAuth: Gitea ──
|
||||
|
GITEA_CLIENT_ID: z.string().default(""), |
||||
|
GITEA_CLIENT_SECRET: z.string().default(""), |
||||
|
GITEA_URL: z.string().default(""), |
||||
|
|
||||
|
// ── Bootstrap (seed) ──
|
||||
|
BOOTSTRAP_ADMIN_USERNAME: z.string().optional(), |
||||
|
BOOTSTRAP_ADMIN_PASSWORD: z.string().optional(), |
||||
|
}); |
||||
|
|
||||
|
/** Inferred type of the parsed env object. */ |
||||
|
export type Env = z.infer<typeof envSchema>; |
||||
Loading…
Reference in new issue