import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { z } from 'zod/v4'; import { DEFAULT_PLUGIN_ROUTE, DEFAULT_POST_LIMITER_ACTIVE, DEFAULT_PRIVATE_DIRECTORY, DEFAULT_PUBLIC_DIRECTORY, DEFAULT_QUERY_PARAMETER_APP, DEFAULT_QUERY_PARAMETER_CSS, DEFAULT_QUERY_PARAMETER_FOLLOWING, DEFAULT_QUERY_PARAMETER_JS, DEFAULT_QUERY_PARAMETER_LOGOUT, DEFAULT_QUERY_PARAMETER_METADATA, DEFAULT_QUERY_PARAMETER_TWT, DEFAULT_QUERY_PARAMETER_TWTS, DEFAULT_ROUTE, DEFAULT_TWTXT_FILENAME, DEFAULT_UPLOAD_ACTIVE, DEFAULT_UPLOAD_ALLOWED_MIME_TYPES, DEFAULT_UPLOAD_ENCODING, DEFAULT_UPLOAD_HASH_ALGORITHM, DEFAULT_UPLOAD_KEEP_EXTENSIONS, DEFAULT_UPLOAD_ROUTE, } from './constants.js'; /* The following keys are expected to exist in `process.env`, either as listed, or without the `TWTKPR_` prefix We only have listed default values for our keys, anything for other plugins (like formidable or express-rate-limit) fall back to their own defaults (and thus are optional). */ const envSchema = z.object({ NODE_ENV: z .enum(['development', 'production', 'test']) .default('development'), // required vars - MUST be passed via ENV TWTKPR_REFRESH_SECRET: z.string().default(''), TWTKPR_ACCESS_SECRET: z.string().default(''), // vars with default values TWTKPR_DEFAULT_ROUTE: z.string().default(DEFAULT_ROUTE), TWTKPR_PLUGIN_ROUTE: z.string().default(DEFAULT_PLUGIN_ROUTE), TWTKPR_PRIVATE_DIRECTORY: z.string().default(DEFAULT_PRIVATE_DIRECTORY), TWTKPR_PUBLIC_DIRECTORY: z.string().default(DEFAULT_PUBLIC_DIRECTORY), TWTKPR_QUERY_PARAMETER_APP: z.string().default(DEFAULT_QUERY_PARAMETER_APP), TWTKPR_QUERY_PARAMETER_CSS: z.string().default(DEFAULT_QUERY_PARAMETER_CSS), TWTKPR_QUERY_PARAMETER_FOLLOWING: z .string() .default(DEFAULT_QUERY_PARAMETER_FOLLOWING), TWTKPR_QUERY_PARAMETER_JS: z.string().default(DEFAULT_QUERY_PARAMETER_JS), TWTKPR_QUERY_PARAMETER_LOGOUT: z .string() .default(DEFAULT_QUERY_PARAMETER_LOGOUT), TWTKPR_QUERY_PARAMETER_METADATA: z .string() .default(DEFAULT_QUERY_PARAMETER_METADATA), TWTKPR_QUERY_PARAMETER_TWT: z.string().default(DEFAULT_QUERY_PARAMETER_TWT), TWTKPR_QUERY_PARAMETER_TWTS: z.string().default(DEFAULT_QUERY_PARAMETER_TWTS), TWTKPR_TWTXT_FILENAME: z.string().default(DEFAULT_TWTXT_FILENAME), /** * Post limiter plugin */ // var with default value TWTKPR_POST_LIMITER_ACTIVE: z.boolean().default(DEFAULT_POST_LIMITER_ACTIVE), // optional vars TWTKPR_POST_LIMITER_WINDOW_MS: z.optional(z.number()), TWTKPR_POST_LIMITER_LIMIT: z.optional(z.union([z.number(), z.function()])), TWTKPR_POST_LIMITER_MESSAGE: z.optional(z.any()), TWTKPR_POST_LIMITER_STATUS_CODE: z.optional(z.number()), TWTKPR_POST_LIMITER_HANDLER: z.optional(z.function()), TWTKPR_POST_LIMITER_LEGACY_HEADERS: z.optional(z.boolean()), TWTKPR_POST_LIMITER_STANDARD_HEADERS: z.optional( z.union([z.boolean(), z.string()]) ), TWTKPR_POST_LIMITER_IDENTIFIER: z.optional( z.union([z.string(), z.function()]) ), TWTKPR_POST_LIMITER_STORE: z.optional(z.any()), TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR: z.optional(z.boolean()), TWTKPR_POST_LIMITER_KEY_GENERATOR: z.optional(z.function()), TWTKPR_POST_LIMITER_IPV6_SUBNET: z.optional( z.union([z.number(), z.function(), z.boolean()]) ), TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME: z.optional(z.string()), TWTKPR_POST_LIMITER_SKIP: z.optional(z.function()), TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS: z.optional(z.boolean()), TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS: z.optional(z.boolean()), TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL: z.optional(z.function()), TWTKPR_POST_LIMITER_VALIDATE: z.optional(z.union([z.boolean(), z.object()])), /** * Upload plugin */ // vars with default values TWTKPR_UPLOAD_ACTIVE: z.boolean().default(DEFAULT_UPLOAD_ACTIVE), TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: z .union([z.string(), z.array(z.string())]) .default(DEFAULT_UPLOAD_ALLOWED_MIME_TYPES), TWTKPR_UPLOAD_ROUTE: z.string().default(DEFAULT_UPLOAD_ROUTE), // optional vars TWTKPR_UPLOAD_ALLOW_EMPTY_FILES: z.optional(z.boolean()), TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS: z.optional(z.boolean()), TWTKPR_UPLOAD_DIRECTORY: z.optional(z.string()), TWTKPR_UPLOAD_ENCODING: z.string().default(DEFAULT_UPLOAD_ENCODING), TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER: z.optional(z.function()), TWTKPR_UPLOAD_FILENAME: z.optional(z.function()), TWTKPR_UPLOAD_FILTER: z.optional(z.function()), TWTKPR_UPLOAD_HASH_ALGORITHM: z .union([z.boolean(), z.string()]) .default(DEFAULT_UPLOAD_HASH_ALGORITHM), TWTKPR_UPLOAD_KEEP_EXTENSIONS: z .boolean() .default(DEFAULT_UPLOAD_KEEP_EXTENSIONS), TWTKPR_UPLOAD_MAX_FIELDS: z.optional(z.number()), TWTKPR_UPLOAD_MAX_FIELDS_SIZE: z.optional(z.number()), TWTKPR_UPLOAD_MAX_FILE_SIZE: z.optional(z.number()), TWTKPR_UPLOAD_MAX_FILES: z.optional(z.number()), TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE: z.optional(z.number()), TWTKPR_UPLOAD_MIN_FILE_SIZE: z.optional(z.number()), }); const parseEnv = () => { try { /** * there's probably an easier way to do this, spreading the keys from above and accepting either * the app (bare) or library (`TWTKPR_`-prefixed) version of said key. * But this should work for now. */ const parsedEnv = envSchema.parse({ NODE_ENV: process.env.TWTKPR_NODE_ENV || process.env.NODE_ENV, TWTKPR_ACCESS_SECRET: process.env.TWTKPR_ACCESS_SECRET || process.env.ACCESS_SECRET, TWTKPR_DEFAULT_ROUTE: process.env.TWTKPR_DEFAULT_ROUTE || process.env.DEFAULT_ROUTE, TWTKPR_PLUGIN_ROUTE: process.env.TWTKPR_PLUGIN_ROUTE || process.env.TWTKPR_PLUGIN_ROUTE, TWTKPR_PRIVATE_DIRECTORY: process.env.TWTKPR_PRIVATE_DIRECTORY || process.env.PRIVATE_DIRECTORY, TWTKPR_PUBLIC_DIRECTORY: process.env.TWTKPR_PUBLIC_DIRECTORY || process.env.PUBLIC_DIRECTORY, TWTKPR_REFRESH_SECRET: process.env.TWTKPR_REFRESH_SECRET || process.env.REFRESH_SECRET, TWTKPR_TWTXT_FILENAME: process.env.TWTKPR_TWTXT_FILENAME || process.env.TWTXT_FILENAME, TWTKPR_POST_LIMITER_ACTIVE: process.env.TWTKPR_POST_LIMITER_ACTIVE || process.env.POST_LIMITER_ACTIVE, TWTKPR_POST_LIMITER_WINDOW_MS: process.env.TWTKPR_POST_LIMITER_WINDOW_MS || process.env.POST_LIMITER_WINDOW_MS, TWTKPR_POST_LIMITER_LIMIT: process.env.TWTKPR_POST_LIMITER_LIMIT || process.env.POST_LIMITER_LIMIT, TWTKPR_POST_LIMITER_MESSAGE: process.env.TWTKPR_POST_LIMITER_MESSAGE || process.env.POST_LIMITER_MESSAGE, TWTKPR_POST_LIMITER_STATUS_CODE: process.env.TWTKPR_POST_LIMITER_STATUS_CODE || process.env.POST_LIMITER_STATUS_CODE, TWTKPR_POST_LIMITER_HANDLER: process.env.TWTKPR_POST_LIMITER_HANDLER || process.env.POST_LIMITER_HANDLER, TWTKPR_POST_LIMITER_LEGACY_HEADERS: process.env.TWTKPR_POST_LIMITER_LEGACY_HEADERS || process.env.POST_LIMITER_LEGACY_HEADERS, TWTKPR_POST_LIMITER_STANDARD_HEADERS: process.env.TWTKPR_POST_LIMITER_STANDARD_HEADERS || process.env.POST_LIMITER_STANDARD_HEADERS, TWTKPR_POST_LIMITER_IDENTIFIER: process.env.TWTKPR_POST_LIMITER_IDENTIFIER || process.env.POST_LIMITER_IDENTIFIER, TWTKPR_POST_LIMITER_STORE: process.env.TWTKPR_POST_LIMITER_STORE || process.env.POST_LIMITER_STORE, TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR: process.env.TWTKPR_POST_LIMITER_PASS_ON_STORE_ERROR || process.env.POST_LIMITER_PASS_ON_STORE_ERROR, TWTKPR_POST_LIMITER_KEY_GENERATOR: process.env.TWTKPR_POST_LIMITER_KEY_GENERATOR || process.env.POST_LIMITER_KEY_GENERATOR, TWTKPR_POST_LIMITER_IPV6_SUBNET: process.env.TWTKPR_POST_LIMITER_IPV6_SUBNET || process.env.POST_LIMITER_IPV6_SUBNET, TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME: process.env.TWTKPR_POST_LIMITER_REQUEST_PROPERTY_NAME || process.env.POST_LIMITER_REQUEST_PROPERTY_NAME, TWTKPR_POST_LIMITER_SKIP: process.env.TWTKPR_POST_LIMITER_SKIP || process.env.POST_LIMITER_SKIP, TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS: process.env.TWTKPR_POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS || process.env.POST_LIMITER_SKIP_SUCCESSFUL_REQUESTS, TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS: process.env.TWTKPR_POST_LIMITER_SKIP_FAILED_REQUESTS || process.env.POST_LIMITER_SKIP_FAILED_REQUESTS, TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL: process.env.TWTKPR_POST_LIMITER_REQUEST_WAS_SUCCESSFUL || process.env.POST_LIMITER_REQUEST_WAS_SUCCESSFUL, TWTKPR_POST_LIMITER_VALIDATE: process.env.TWTKPR_POST_LIMITER_VALIDATE || process.env.POST_LIMITER_VALIDATE, TWTKPR_QUERY_PARAMETER_APP: process.env.TWTKPR_QUERY_PARAMETER_APP || process.env.QUERY_PARAMETER_APP, TWTKPR_QUERY_PARAMETER_CSS: process.env.TWTKPR_QUERY_PARAMETER_CSS || process.env.QUERY_PARAMETER_CSS, TWTKPR_QUERY_PARAMETER_FOLLOWING: process.env.TWTKPR_QUERY_PARAMETER_FOLLOWING || process.env.QUERY_PARAMETER_FOLLOWING, TWTKPR_QUERY_PARAMETER_JS: process.env.TWTKPR_QUERY_PARAMETER_JS || process.env.QUERY_PARAMETER_JS, TWTKPR_QUERY_PARAMETER_LOGOUT: process.env.TWTKPR_QUERY_PARAMETER_LOGOUT || process.env.QUERY_PARAMETER_LOGOUT, TWTKPR_QUERY_PARAMETER_METADATA: process.env.TWTKPR_QUERY_PARAMETER_METADATA || process.env.QUERY_PARAMETER_METADATA, TWTKPR_QUERY_PARAMETER_TWT: process.env.TWTKPR_QUERY_PARAMETER_TWT || process.env.QUERY_PARAMETER_TWT, TWTKPR_QUERY_PARAMETER_TWTS: process.env.TWTKPR_QUERY_PARAMETER_TWTS || process.env.QUERY_PARAMETER_TWTS, TWTKPR_UPLOAD_ACTIVE: process.env.TWTKPR_UPLOAD_ACTIVE || process.env.UPLOAD_ACTIVE, TWTKPR_UPLOAD_ALLOW_EMPTY_FILES: process.env.TWTKPR_UPLOAD_ALLOW_EMPTY_FILES || process.env.UPLOAD_ALLOW_EMPTY_FILES, TWTKPR_UPLOAD_ALLOWED_MIME_TYPES: process.env.TWTKPR_UPLOAD_ALLOWED_MIME_TYPES || process.env.UPLOAD_ALLOWED_MIME_TYPES, TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS: process.env.TWTKPR_UPLOAD_CREATE_DIRS_FROM_UPLOADS || process.env.UPLOAD_CREATE_DIRS_FROM_UPLOADS, TWTKPR_UPLOAD_DIRECTORY: process.env.TWTKPR_UPLOAD_DIRECTORY || process.env.UPLOAD_DIRECTORY || process.env.UPLOAD_DIR, TWTKPR_UPLOAD_ENCODING: process.env.TWTKPR_UPLOAD_ENCODING || process.env.UPLOAD_ENCODING, TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER: process.env.TWTKPR_UPLOAD_FILE_WRITE_STREAM_HANDLER || process.env.UPLOAD_FILE_WRITE_STREAM_HANDLER, TWTKPR_UPLOAD_FILENAME: process.env.TWTKPR_UPLOAD_FILENAME || process.env.UPLOAD_FILENAME, TWTKPR_UPLOAD_FILTER: process.env.TWTKPR_UPLOAD_FILTER || process.env.UPLOAD_FILTER, TWTKPR_UPLOAD_HASH_ALGORITHM: process.env.TWTKPR_UPLOAD_HASH_ALGORITHM || process.env.UPLOAD_HASH_ALGORITHM, TWTKPR_UPLOAD_KEEP_EXTENSIONS: process.env.TWTKPR_UPLOAD_KEEP_EXTENSIONS || process.env.UPLOAD_KEEP_EXTENSIONS, TWTKPR_UPLOAD_MAX_FIELDS: process.env.TWTKPR_UPLOAD_MAX_FIELDS || process.env.UPLOAD_MAX_FIELDS, TWTKPR_UPLOAD_MAX_FIELDS_SIZE: process.env.TWTKPR_UPLOAD_MAX_FIELDS_SIZE || process.env.UPLOAD_MAX_FIELDS_SIZE, TWTKPR_UPLOAD_MAX_FILE_SIZE: process.env.TWTKPR_UPLOAD_MAX_FILE_SIZE || process.env.UPLOAD_MAX_FILE_SIZE, TWTKPR_UPLOAD_MAX_FILES: process.env.TWTKPR_UPLOAD_MAX_FILES || process.env.UPLOAD_MAX_FILES, TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE: process.env.TWTKPR_UPLOAD_MAX_TOTAL_FILE_SIZE || process.env.UPLOAD_MAX_TOTAL_FILE_SIZE, TWTKPR_UPLOAD_MIN_FILE_SIZE: process.env.TWTKPR_UPLOAD_MIN_FILE_SIZE || process.env.UPLOAD_MIN_FILE_SIZE, TWTKPR_UPLOAD_ROUTE: process.env.TWTKPR_UPLOAD_ROUTE || process.env.UPLOAD_ROUTE, }); if (!parsedEnv.TWTKPR_ACCESS_SECRET) throw new Error( 'Either ACCESS_SECRET or TWTKPR_ACCESS_SECRET must be provided' ); if (!parsedEnv.TWTKPR_REFRESH_SECRET) throw new Error( 'Either REFRESH_SECRET or TWTKPR_REFRESH_SECRET must be provided' ); return parsedEnv; } catch (error) { if (error instanceof z.ZodError) { console.error( 'Missing environment variables:', error.issues.flatMap((issue) => `${issue.path} or TWTKPR_${issue.path}`) ); } else { console.error(error); } process.exit(1); } }; export const env = parseEnv(); export const __dirname = resolve(dirname(fileURLToPath(import.meta.url)), '..');