Files
moeka-project/server/apps/auth/src/env.ts
T

98 lines
3.9 KiB
TypeScript

import type { InferOutput } from 'valibot'
import { exit } from 'node:process'
import { useLogger } from '@guiiai/logg'
import { array, integer, minValue, nonEmpty, object, optional, parse, picklist, pipe, string, transform, url } from 'valibot'
import { GoogleNativeClientIdsSchema } from './google-client-ids'
function optionalIntegerFromString(defaultValue: number, envKey: string, minimum: number) {
return optional(
pipe(
string(),
nonEmpty(`${envKey} must not be empty`),
transform(input => Number(input)),
integer(`${envKey} must be an integer`),
minValue(minimum, `${envKey} must be at least ${minimum}`),
),
String(defaultValue),
)
}
const AdditionalTrustedOriginsSchema = pipe(
string(),
transform(raw => raw.split(',').map(origin => origin.trim()).filter(Boolean)),
array(pipe(
string(),
url('ADDITIONAL_TRUSTED_ORIGINS entries must be valid URLs'),
transform(origin => new URL(origin).origin),
)),
transform(origins => [...new Set(origins)]),
)
const AuthEnvSchema = object({
HOST: optional(string(), '0.0.0.0'),
PORT: optionalIntegerFromString(3000, 'PORT', 1),
PUBLIC_URL: optional(string(), 'http://localhost:3000'),
RESOURCE_SERVER_URL: optional(string(), 'http://localhost:3001'),
RATE_LIMIT_TRUSTED_PROXY: optional(picklist(['railway'])),
AUTH_UI_URL: optional(string(), 'https://accounts.airi.build/ui'),
ADDITIONAL_TRUSTED_ORIGINS: optional(AdditionalTrustedOriginsSchema, ''),
DATABASE_URL: pipe(string(), nonEmpty('DATABASE_URL is required')),
REDIS_URL: pipe(string(), nonEmpty('REDIS_URL is required')),
BETTER_AUTH_SECRET: pipe(string(), nonEmpty('BETTER_AUTH_SECRET is required')),
AUTH_GOOGLE_CLIENT_ID: pipe(string(), nonEmpty('AUTH_GOOGLE_CLIENT_ID is required')),
AUTH_GOOGLE_NATIVE_CLIENT_IDS: optional(GoogleNativeClientIdsSchema),
AUTH_GOOGLE_CLIENT_SECRET: pipe(string(), nonEmpty('AUTH_GOOGLE_CLIENT_SECRET is required')),
AUTH_GITHUB_CLIENT_ID: pipe(string(), nonEmpty('AUTH_GITHUB_CLIENT_ID is required')),
AUTH_GITHUB_CLIENT_SECRET: pipe(string(), nonEmpty('AUTH_GITHUB_CLIENT_SECRET is required')),
AUTH_APPLE_CLIENT_ID: optional(string(), ''),
AUTH_APPLE_APP_BUNDLE_IDENTIFIERS: optional(
pipe(
string(),
transform(raw => [...new Set(
raw
.split(',')
.map(bundleIdentifier => bundleIdentifier.trim())
.filter(Boolean),
)]),
),
'',
),
AUTH_APPLE_TEAM_ID: optional(string(), ''),
AUTH_APPLE_KEY_ID: optional(string(), ''),
AUTH_APPLE_PRIVATE_KEY_PEM: optional(
pipe(
string(),
// Deployment dashboards commonly store multiline secrets with escaped
// newlines. jose's PKCS8 importer requires the original PEM layout.
transform(raw => raw.replaceAll(String.raw`\n`, '\n')),
),
'',
),
RESEND_API_KEY: optional(string(), ''),
RESEND_FROM_EMAIL: optional(string(), 'noreply@airi.moeru.ai'),
RESEND_FROM_NAME: optional(string(), 'Project AIRI'),
DB_POOL_MAX: optionalIntegerFromString(20, 'DB_POOL_MAX', 1),
DB_POOL_IDLE_TIMEOUT_MS: optionalIntegerFromString(30000, 'DB_POOL_IDLE_TIMEOUT_MS', 1),
DB_POOL_CONNECTION_TIMEOUT_MS: optionalIntegerFromString(5000, 'DB_POOL_CONNECTION_TIMEOUT_MS', 1),
DB_POOL_KEEPALIVE_INITIAL_DELAY_MS: optionalIntegerFromString(10000, 'DB_POOL_KEEPALIVE_INITIAL_DELAY_MS', 1),
OTEL_SERVICE_NAME: optional(string(), 'auth-server'),
OTEL_EXPORTER_OTLP_ENDPOINT: optional(string()),
})
/** Environment owned exclusively by the standalone Auth process. */
export type AuthEnv = InferOutput<typeof AuthEnvSchema>
/** Parses only Auth-owned configuration; business-only secrets are ignored. */
export function parseAuthEnv(inputEnv: Record<string, string> | NodeJS.ProcessEnv): AuthEnv {
try {
return parse(AuthEnvSchema, inputEnv)
}
catch (err) {
useLogger().withError(err).error('Invalid auth environment variables')
exit(1)
}
}