stack/docker/local-emulator/generate-env-development.mjs
BilalG1 88d3317b22
local emulator security and features fixes (#1247)
<!--

Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md

-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added Stripe, OAuth, and Freestyle mock services to the local emulator
* Introduced `emulator run` CLI command to execute applications with
emulator credentials automatically injected
  * Enhanced credential management for local development

* **Improvements**
  * Improved ARM64 QEMU emulation with cross-architecture support
  * Better error detection and logging during emulator provisioning
  * Added example middleware configuration with authentication support
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-14 15:36:24 -07:00

206 lines
9.8 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(scriptDir, "..", "..");
const outputPath = path.join(scriptDir, ".env.development");
const backendEnvPath = path.join(rootDir, "apps", "backend", ".env.development");
const dashboardEnvPath = path.join(rootDir, "apps", "dashboard", ".env.development");
const args = process.argv.slice(2);
if (args.length > 1 || (args[0] != null && args[0] !== "--check")) {
throw new Error("Usage: node docker/local-emulator/generate-env-development.mjs [--check]");
}
const parseEnvFile = (filePath) => {
const env = new Map();
for (const rawLine of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
const trimmedLine = rawLine.trim();
if (trimmedLine === "" || trimmedLine.startsWith("#")) {
continue;
}
const separatorIndex = rawLine.indexOf("=");
if (separatorIndex < 0) {
throw new Error(`Invalid env line in ${filePath}: ${rawLine}`);
}
const key = rawLine.slice(0, separatorIndex).trim();
const value = rawLine.slice(separatorIndex + 1);
env.set(key, value);
}
return env;
};
const backendEnv = parseEnvFile(backendEnvPath);
const dashboardEnv = parseEnvFile(dashboardEnvPath);
const getRequiredEnvValue = (sourceName, envMap, key) => {
const value = envMap.get(key);
if (value == null) {
throw new Error(`Missing ${key} in ${sourceName}; update the generator or source env file.`);
}
return value;
};
const fromSource = (sourceName, envMap, key) => ({
type: "entry",
key,
value: getRequiredEnvValue(sourceName, envMap, key),
});
const literal = (key, value) => ({
type: "entry",
key,
value,
});
const comment = (value) => ({
type: "comment",
value,
});
const blank = () => ({
type: "blank",
});
const entries = [
comment("# Generated by docker/local-emulator/generate-env-development.mjs"),
comment("# Do not edit manually; update apps/backend/.env.development, apps/dashboard/.env.development, or this generator."),
blank(),
comment("# Public emulator/app credentials"),
literal("NEXT_PUBLIC_STACK_DOCS_BASE_URL", "https://docs.stack-auth.com"),
literal("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "true"),
fromSource("apps/dashboard/.env.development", dashboardEnv, "NEXT_PUBLIC_STACK_PROJECT_ID"),
fromSource("apps/dashboard/.env.development", dashboardEnv, "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"),
fromSource("apps/dashboard/.env.development", dashboardEnv, "STACK_SECRET_SERVER_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SERVER_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_CHANGELOG_URL"),
blank(),
comment("# Seed/project defaults"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_ENABLE_DUMMY_PROJECT"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS"),
// STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is generated per-VM at boot
// by docker/local-emulator/qemu/cloud-init/emulator/user-data and injected via
// /run/stack-auth/local-emulator.env. SECRET_SERVER_KEY and SUPER_SECRET_ADMIN_KEY
// are intentionally omitted so the seed script leaves them null on the internal
// project; per-project credentials come from /api/v1/internal/local-emulator/project.
blank(),
comment("# Third-party/test integrations"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SVIX_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_OPENAI_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_OPENROUTER_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_STRIPE_SECRET_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_STRIPE_WEBHOOK_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_RESEND_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_RESEND_WEBHOOK_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_API_TOKEN"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_ACCOUNT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_API_BASE_URL"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_FREESTYLE_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_VERCEL_SANDBOX_TOKEN"),
fromSource("apps/backend/.env.development", backendEnv, "CRON_SECRET"),
blank(),
comment("# Storage, queueing, and analytics"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_REGION"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_ACCESS_KEY_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_SECRET_ACCESS_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_BUCKET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_PRIVATE_BUCKET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_REGION"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_ACCESS_KEY_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_SECRET_ACCESS_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_TOKEN"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_CURRENT_SIGNING_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_NEXT_SIGNING_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_ADMIN_USER"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_ADMIN_PASSWORD"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_EXTERNAL_PASSWORD"),
blank(),
comment("# Email and dashboard integration"),
literal("STACK_EMAIL_PORT", "2500"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_SECURE"),
literal("STACK_EMAIL_USERNAME", "does-not-matter"),
literal("STACK_EMAIL_PASSWORD", "does-not-matter"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_SENDER"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_PROJECT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_SECRET_TOKEN"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_USE_INBUCKET"),
fromSource("apps/dashboard/.env.development", dashboardEnv, "STACK_FEATUREBASE_JWT_SECRET"),
blank(),
comment("# Mock OAuth defaults"),
literal("STACK_FORWARD_MOCK_OAUTH_SERVER", "false"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_GITHUB_CLIENT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_GITHUB_CLIENT_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_GOOGLE_CLIENT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_GOOGLE_CLIENT_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_MICROSOFT_CLIENT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_MICROSOFT_CLIENT_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SPOTIFY_CLIENT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SPOTIFY_CLIENT_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS"),
blank(),
comment("# Internal service endpoints (defaults for docker-compose; overridden in QEMU)"),
literal("STACK_DATABASE_CONNECTION_STRING", "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@127.0.0.1:5432/stackframe"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_HOST"),
literal("STACK_SVIX_SERVER_URL", "http://127.0.0.1:8071"),
literal("STACK_S3_ENDPOINT", "http://127.0.0.1:9090"),
literal("STACK_QSTASH_URL", "http://127.0.0.1:8080"),
literal("STACK_CLICKHOUSE_URL", "http://127.0.0.1:8123"),
literal("STACK_CLICKHOUSE_DATABASE", "default"),
literal("STACK_EMAIL_MONITOR_INBUCKET_API_URL", "http://127.0.0.1:9001"),
literal("BACKEND_PORT", "8102"),
literal("DASHBOARD_PORT", "8101"),
];
const seenKeys = new Set();
for (const entry of entries) {
if (entry.type !== "entry") {
continue;
}
if (seenKeys.has(entry.key)) {
throw new Error(`Duplicate env key in generator: ${entry.key}`);
}
seenKeys.add(entry.key);
}
const content = `${entries.map((entry) => {
if (entry.type === "blank") {
return "";
}
if (entry.type === "comment") {
return entry.value;
}
return `${entry.key}=${entry.value}`;
}).join("\n")}\n`;
if (args[0] === "--check") {
const currentContent = fs.readFileSync(outputPath, "utf8");
if (currentContent !== content) {
throw new Error(`${path.relative(rootDir, outputPath)} is out of date. Run pnpm run emulator:generate-env.`);
}
console.log(`${path.relative(rootDir, outputPath)} is up to date.`);
} else {
fs.writeFileSync(outputPath, content);
console.log(`Wrote ${path.relative(rootDir, outputPath)}.`);
}