// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // `.env.example` is the complete, documented list of the variables this app reads (plan item AT4.5): // every variable the code reads is in it, every variable in it is read by something, and each has a // comment above it saying what it is for. import { readdirSync, readFileSync, statSync } from "node:fs" import { join } from "node:path" import { describe, expect, it } from "vitest" const root = new URL("..", import.meta.url).pathname const example = readFileSync(join(root, ".env.example"), "utf8") const declared = [...example.matchAll(/^([A-Z][A-Z0-9_]+)=/gm)].map((match) => match[1] as string) const walk = (dir: string): string[] => readdirSync(dir).flatMap((name) => { if (["node_modules", ".next", "dist", ".logs", "__tests__"].includes(name)) return [] const path = join(dir, name) if (statSync(path).isDirectory()) return walk(path) return /\.(ts|tsx|mjs)$/.test(name) ? [path] : [] }) /** The variables the code of this repository reads: `process.env.X`, `env.X` and the loaders' quoted names. */ function read(): Set { const found = new Set() const files = [ ...walk(join(root, "apps")), ...walk(join(root, "config", "src")), ...walk(join(root, "scripts")), ...walk(join(root, "e2e", "support")), join(root, "better-auth", "deploy.mjs"), // kit:convex-better-auth ] for (const file of files) { const text = readFileSync(file, "utf8") for (const match of text.matchAll(/\b(?:process\.env|env)\.([A-Z][A-Z0-9_]+)/g)) found.add(match[1] as string) for (const match of text.matchAll(/\b(?:process\.env|env)\[\s*"([A-Z][A-Z0-9_]+)"\s*\]/g)) found.add(match[1] as string) } // The names config/src/env.ts holds in constants. for (const match of readFileSync(join(root, "config/src/env.ts"), "utf8").matchAll(/_VAR = "([A-Z][A-Z0-9_]+)"/g)) { found.add(match[1] as string) } for (const file of ["ai.ts", "documents.ts"]) { for (const match of readFileSync(join(root, "config/src", file), "utf8").matchAll(/_VAR = "([A-Z][A-Z0-9_]+)"/g)) { found.add(match[1] as string) } } // The platform contract and the sign-in variables are read by the kit's loaders from the same environment. const kit = (path: string) => readFileSync(join(root, "node_modules", "@conusai-kit", path), "utf8") for (const match of kit("auth/dist/config.js").matchAll(/\b((?:AUTH|OIDC)_[A-Z_]+)\b/g)) found.add(match[1] as string) for (const name of [ "CONUSAI_API_URL", "CONUSAI_API_TOKEN", "CONUSAI_PROJECT_ID", "CONUSAI_ORG_ID", "CONUSAI_ORG_SLUG", "CONUSAI_PUBLIC_URL", "CONUSAI_RELEASE", "CONUSAI_ENVIRONMENT", "CONUSAI_DSN", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_SERVICE_NAME", "OTEL_SERVICE_VERSION", "DEV_WEB_PORT", "DEV_PLATFORM_PORT", "DEV_POSTGRES_PORT", "CONVEX_SELF_HOSTED_URL", "CONVEX_SELF_HOSTED_ADMIN_KEY", "CONVEX_BACKUP_INTERVAL_HOURS", "CONVEX_BACKUP_RETENTION", "CONVEX_CLI", "CONVEX_APP_DIR", ]) { found.add(name) } return found } describe(".env.example", () => { it("lists every variable the code reads", () => { const missing = [...read()].filter((name) => !declared.includes(name)).sort() // Set by the runtime and not by a person: NODE_ENV is in the file for what it means here, and the // framework's own variable names are not this app's to document. const framework = new Set([ "NEXT_RUNTIME", "npm_execpath", "PORT", "HOST", "HOSTNAME", "E2E_BASE_URL", "E2E_PLATFORM_URL", ]) expect( missing.filter((name) => !framework.has(name)), "add these to .env.example, with a comment", ).toEqual([]) }) it("lists nothing that nothing reads", () => { const used = read() const fixed = new Set([ "POSTGRES_URL", "POSTGRES_READER_URL", "POSTGRES_MIGRATE_URL", "AI_MODEL", "LOG_LEVEL", "WORKER_HEARTBEAT_FILE", "NODE_ENV", ]) const unused = declared.filter((name) => !used.has(name) && !fixed.has(name)) expect(unused, "remove these from .env.example, or the code that reads them is not found by this test").toEqual([]) }) it("has no variable twice", () => { expect(declared.filter((name, index) => declared.indexOf(name) !== index)).toEqual([]) }) it("explains every variable: a comment sits above it, or above the block of variables it belongs to", () => { const lines = example.split("\n") const undocumented: string[] = [] for (const [index, line] of lines.entries()) { const name = /^([A-Z][A-Z0-9_]+)=/.exec(line)?.[1] if (!name) continue let above = index - 1 while (above >= 0 && /^[A-Z][A-Z0-9_]+=/.test(lines[above] as string)) above -= 1 if (!(lines[above] ?? "").startsWith("#")) undocumented.push(name) } expect(undocumented).toEqual([]) }) it("holds no real secret: the values are the development ones or empty", () => { for (const name of ["CONUSAI_API_TOKEN", "AUTH_SESSION_SECRET", "OIDC_WEB_CLIENT_SECRET", "BETTER_AUTH_SECRET"]) { const value = new RegExp(`^${name}=(.*)$`, "m").exec(example)?.[1] ?? "" expect(value === "" || /dev|replace-me/.test(value), `${name} looks like a real secret`).toBe(true) } }) })