Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/atxp/src/commands/paas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ interface PaasOptions {
event?: string;
groupBy?: string;
enableAnalytics?: boolean;
env?: string[];
envFile?: string;
}

function showPaasHelp(): void {
Expand All @@ -67,6 +69,12 @@ function showPaasHelp(): void {

console.log(chalk.bold('Worker Commands:'));
console.log(' ' + chalk.cyan('paas worker deploy') + ' ' + chalk.yellow('<name>') + ' Deploy a worker');
console.log(' ' + chalk.gray('--code <file>') + ' Path to worker code file');
console.log(' ' + chalk.gray('--db <binding:name>') + ' Bind a database (repeatable)');
console.log(' ' + chalk.gray('--bucket <binding:name>') + ' Bind a storage bucket (repeatable)');
console.log(' ' + chalk.gray('--env KEY=VALUE') + ' Set environment variable (repeatable)');
console.log(' ' + chalk.gray('--env-file <path>') + ' 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('<name>') + ' Get worker logs');
console.log(' ' + chalk.cyan('paas worker delete') + ' ' + chalk.yellow('<name>') + ' Delete a worker');
Expand Down Expand Up @@ -171,6 +179,8 @@ async function handleWorkerCommand(
db: options.db,
bucket: options.bucket,
enableAnalytics: options.enableAnalytics,
env: options.env,
envFile: options.envFile,
});
break;

Expand Down
134 changes: 134 additions & 0 deletions packages/atxp/src/commands/paas/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> {
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<string, string> = {};

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 {
Expand Down Expand Up @@ -55,6 +141,51 @@ export async function workerDeployCommand(
};
});

// Process environment variables
// Start with env file (lower precedence)
const envVars: Record<string, string> = {};

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<string, unknown> = { name, code };
if (databaseBindings && databaseBindings.length > 0) {
args.database_bindings = databaseBindings;
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions packages/atxp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ interface PaasOptions {
event?: string;
groupBy?: string;
enableAnalytics?: boolean;
env?: string[];
envFile?: string;
}

// Parse command line arguments
Expand Down Expand Up @@ -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 -)
Expand Down
Loading