From d3146525bdb458c608d77476edc0fdd05f8d8213 Mon Sep 17 00:00:00 2001 From: Emilio Amaya Date: Wed, 28 Jan 2026 10:25:00 -0500 Subject: [PATCH] feat(paas): add --env and --env-file flags for worker deploy Add support for setting environment variables when deploying workers: - --env KEY=VALUE flag (repeatable) for individual env vars - --env-file flag to load from a .env file - --env takes precedence over --env-file for duplicate keys - Validation for reserved names (DB, BUCKET, ANALYTICS, USER_NAMESPACE) - Warning for sensitive-looking variables (SECRET, PASSWORD, KEY, etc.) Co-Authored-By: Claude Opus 4.5 --- packages/atxp/src/commands/paas/index.ts | 10 ++ packages/atxp/src/commands/paas/worker.ts | 134 ++++++++++++++++++++++ packages/atxp/src/index.ts | 4 + 3 files changed, 148 insertions(+) diff --git a/packages/atxp/src/commands/paas/index.ts b/packages/atxp/src/commands/paas/index.ts index 6ce6e94..1ccd946 100644 --- a/packages/atxp/src/commands/paas/index.ts +++ b/packages/atxp/src/commands/paas/index.ts @@ -58,6 +58,8 @@ interface PaasOptions { event?: string; groupBy?: string; enableAnalytics?: boolean; + env?: string[]; + envFile?: string; } function showPaasHelp(): void { @@ -67,6 +69,12 @@ function showPaasHelp(): void { console.log(chalk.bold('Worker Commands:')); console.log(' ' + chalk.cyan('paas worker deploy') + ' ' + chalk.yellow('') + ' Deploy a worker'); + console.log(' ' + chalk.gray('--code ') + ' Path to worker code file'); + console.log(' ' + chalk.gray('--db ') + ' Bind a database (repeatable)'); + console.log(' ' + chalk.gray('--bucket ') + ' Bind a storage bucket (repeatable)'); + console.log(' ' + chalk.gray('--env KEY=VALUE') + ' Set environment variable (repeatable)'); + console.log(' ' + chalk.gray('--env-file ') + ' Load env vars from file'); + console.log(' ' + chalk.gray('--enable-analytics') + ' Enable Analytics Engine binding'); console.log(' ' + chalk.cyan('paas worker list') + ' List all workers'); console.log(' ' + chalk.cyan('paas worker logs') + ' ' + chalk.yellow('') + ' Get worker logs'); console.log(' ' + chalk.cyan('paas worker delete') + ' ' + chalk.yellow('') + ' Delete a worker'); @@ -171,6 +179,8 @@ async function handleWorkerCommand( db: options.db, bucket: options.bucket, enableAnalytics: options.enableAnalytics, + env: options.env, + envFile: options.envFile, }); break; diff --git a/packages/atxp/src/commands/paas/worker.ts b/packages/atxp/src/commands/paas/worker.ts index 59fc9d6..bb2b822 100644 --- a/packages/atxp/src/commands/paas/worker.ts +++ b/packages/atxp/src/commands/paas/worker.ts @@ -10,6 +10,92 @@ interface WorkerDeployOptions { db?: string[]; bucket?: string[]; enableAnalytics?: boolean; + env?: string[]; + envFile?: string; +} + +// Reserved env var names that conflict with existing bindings +const RESERVED_ENV_NAMES = ['DB', 'BUCKET', 'ANALYTICS', 'USER_NAMESPACE']; + +// Patterns that suggest sensitive data (warn user about plain text storage) +const SENSITIVE_PATTERNS = [/SECRET/i, /PASSWORD/i, /KEY/i, /TOKEN/i, /CREDENTIAL/i]; + +/** + * Validate an environment variable name + * Must be a valid identifier and not reserved + */ +function validateEnvVarName(name: string): { valid: boolean; error?: string } { + // Check if it's a valid identifier (starts with letter or underscore, contains only alphanumeric and underscores) + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) { + return { valid: false, error: `Invalid env var name "${name}": must be a valid identifier (letters, numbers, underscores, cannot start with number)` }; + } + + // Check if it's reserved + if (RESERVED_ENV_NAMES.includes(name.toUpperCase())) { + return { valid: false, error: `Reserved env var name "${name}": conflicts with existing bindings (${RESERVED_ENV_NAMES.join(', ')})` }; + } + + return { valid: true }; +} + +/** + * Parse a KEY=VALUE string into key and value + */ +function parseEnvArg(arg: string): { key: string; value: string } | null { + const eqIndex = arg.indexOf('='); + if (eqIndex === -1) { + return null; + } + const key = arg.slice(0, eqIndex); + const value = arg.slice(eqIndex + 1); + return { key, value }; +} + +/** + * Parse a .env file into a key-value record + * Supports: + * - KEY=VALUE format + * - Comments starting with # + * - Empty lines + * - Quoted values (single or double quotes) + */ +function parseEnvFile(filePath: string): Record { + const absolutePath = path.resolve(filePath); + if (!fs.existsSync(absolutePath)) { + throw new Error(`Env file not found: ${absolutePath}`); + } + + const content = fs.readFileSync(absolutePath, 'utf-8'); + const result: Record = {}; + + for (const line of content.split('\n')) { + const trimmed = line.trim(); + + // Skip empty lines and comments + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + + const eqIndex = trimmed.indexOf('='); + if (eqIndex === -1) { + continue; + } + + const key = trimmed.slice(0, eqIndex).trim(); + let value = trimmed.slice(eqIndex + 1).trim(); + + // Remove surrounding quotes if present + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + + if (key) { + result[key] = value; + } + } + + return result; } interface WorkerLogsOptions { @@ -55,6 +141,51 @@ export async function workerDeployCommand( }; }); + // Process environment variables + // Start with env file (lower precedence) + const envVars: Record = {}; + + if (options.envFile) { + try { + const fileEnvVars = parseEnvFile(options.envFile); + Object.assign(envVars, fileEnvVars); + } catch (error) { + console.error(chalk.red(`Error: ${(error as Error).message}`)); + process.exit(1); + } + } + + // Process --env flags (higher precedence, overrides file) + if (options.env && options.env.length > 0) { + for (const envArg of options.env) { + const parsed = parseEnvArg(envArg); + if (!parsed) { + console.error(chalk.red(`Error: Invalid env var format "${envArg}". Expected KEY=VALUE`)); + process.exit(1); + } + envVars[parsed.key] = parsed.value; + } + } + + // Validate all env var names + for (const key of Object.keys(envVars)) { + const validation = validateEnvVarName(key); + if (!validation.valid) { + console.error(chalk.red(`Error: ${validation.error}`)); + process.exit(1); + } + } + + // Warn about sensitive-looking variables + const sensitiveVars = Object.keys(envVars).filter((key) => + SENSITIVE_PATTERNS.some((pattern) => pattern.test(key)) + ); + if (sensitiveVars.length > 0) { + console.log(chalk.yellow(`Warning: The following env vars may contain sensitive data and will be stored as plain text:`)); + console.log(chalk.yellow(` ${sensitiveVars.join(', ')}`)); + console.log(chalk.yellow(` Consider using Cloudflare Secrets for sensitive values.`)); + } + const args: Record = { name, code }; if (databaseBindings && databaseBindings.length > 0) { args.database_bindings = databaseBindings; @@ -65,6 +196,9 @@ export async function workerDeployCommand( if (options.enableAnalytics) { args.enable_analytics = true; } + if (Object.keys(envVars).length > 0) { + args.env_vars = envVars; + } const result = await callTool(SERVER, 'deploy_worker', args); console.log(result); diff --git a/packages/atxp/src/index.ts b/packages/atxp/src/index.ts index 327d807..bd2cdaa 100644 --- a/packages/atxp/src/index.ts +++ b/packages/atxp/src/index.ts @@ -57,6 +57,8 @@ interface PaasOptions { event?: string; groupBy?: string; enableAnalytics?: boolean; + env?: string[]; + envFile?: string; } // Parse command line arguments @@ -176,6 +178,8 @@ function parseArgs(): { event: getArgValue('--event', ''), groupBy: getArgValue('--group-by', ''), enableAnalytics: process.argv.includes('--enable-analytics'), + env: getAllArgValues('--env'), + envFile: getArgValue('--env-file', ''), }; // Get PAAS args (everything after 'paas' that doesn't start with -)