Adds DOCUMENSO_API_VERSION env (default v1) plus per-port override. Introduces placeFields, placeDefaultSignatureFields, and voidDocument that hide v1 (per-field POST, pixel coords) vs v2 (bulk POST, percent + fieldMeta) differences. cancelDocument now voids in Documenso first and treats transient void failures as recoverable so the CRM stays the system of record. 16 unit specs cover dispatch, layout math, idempotent 404, and v1 pixel conversion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
const envSchema = z.object({
|
|
// Database
|
|
DATABASE_URL: z.string().url().startsWith('postgresql://'),
|
|
|
|
// Redis
|
|
REDIS_URL: z.string().url().startsWith('redis://'),
|
|
|
|
// Auth
|
|
BETTER_AUTH_SECRET: z.string().min(32),
|
|
BETTER_AUTH_URL: z.string().url(),
|
|
CSRF_SECRET: z.string().min(32),
|
|
|
|
// MinIO
|
|
MINIO_ENDPOINT: z.string().min(1),
|
|
MINIO_PORT: z.coerce.number().int().positive(),
|
|
MINIO_ACCESS_KEY: z.string().min(1),
|
|
MINIO_SECRET_KEY: z.string().min(1),
|
|
MINIO_BUCKET: z.string().min(1),
|
|
MINIO_USE_SSL: z.enum(['true', 'false']).transform((v) => v === 'true'),
|
|
|
|
// Documenso
|
|
DOCUMENSO_API_URL: z.string().url(),
|
|
DOCUMENSO_API_KEY: z.string().min(1),
|
|
DOCUMENSO_API_VERSION: z.enum(['v1', 'v2']).default('v1'),
|
|
DOCUMENSO_WEBHOOK_SECRET: z.string().min(16),
|
|
DOCUMENSO_TEMPLATE_ID_EOI: z.coerce.number().int().positive().default(8),
|
|
DOCUMENSO_CLIENT_RECIPIENT_ID: z.coerce.number().int().positive().default(192),
|
|
DOCUMENSO_DEVELOPER_RECIPIENT_ID: z.coerce.number().int().positive().default(193),
|
|
DOCUMENSO_APPROVAL_RECIPIENT_ID: z.coerce.number().int().positive().default(194),
|
|
|
|
// Email
|
|
SMTP_HOST: z.string().min(1),
|
|
SMTP_PORT: z.coerce.number().int().positive(),
|
|
SMTP_USER: z.string().optional(),
|
|
SMTP_PASS: z.string().optional(),
|
|
SMTP_FROM: z.string().optional(),
|
|
// Dev/test safety net: when set, sendEmail redirects every outbound message
|
|
// to this address regardless of the requested recipient. Leave empty in prod.
|
|
EMAIL_REDIRECT_TO: z.string().email().optional(),
|
|
|
|
// Encryption
|
|
EMAIL_CREDENTIAL_KEY: z
|
|
.string()
|
|
.length(64)
|
|
.regex(/^[0-9a-f]+$/i, 'Must be a 64-character hex string'),
|
|
|
|
// Google OAuth (optional)
|
|
GOOGLE_CLIENT_ID: z.string().optional(),
|
|
GOOGLE_CLIENT_SECRET: z.string().optional(),
|
|
|
|
// OpenAI (optional)
|
|
OPENAI_API_KEY: z.string().optional(),
|
|
|
|
// App
|
|
APP_URL: z.string().url(),
|
|
PUBLIC_SITE_URL: z.string().url(),
|
|
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
|
});
|
|
|
|
export type Env = z.infer<typeof envSchema>;
|
|
|
|
function validateEnv(): Env {
|
|
if (process.env.SKIP_ENV_VALIDATION === '1') {
|
|
return process.env as unknown as Env;
|
|
}
|
|
|
|
const result = envSchema.safeParse(process.env);
|
|
if (!result.success) {
|
|
console.error('Invalid environment variables:');
|
|
for (const issue of result.error.issues) {
|
|
console.error(` ${issue.path.join('.')}: ${issue.message}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
return result.data;
|
|
}
|
|
|
|
export const env = validateEnv();
|