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
4 changes: 2 additions & 2 deletions packages/atxp/src/commands/paas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ interface PaasOptions {
range?: string;
event?: string;
groupBy?: string;
enableAnalytics?: boolean;
enableAnalytics?: boolean | string;
env?: string[];
envFile?: string;
}
Expand All @@ -74,7 +74,7 @@ function showPaasHelp(): void {
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.gray('--enable-analytics [NAME]') + ' Enable Analytics Engine binding (default: ANALYTICS)');
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
68 changes: 68 additions & 0 deletions packages/atxp/src/commands/paas/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,74 @@ describe('Worker Commands', () => {
});
});

it('should enable analytics with custom binding name', async () => {
const mockCode = 'export default { fetch() { return new Response("Hello"); } }';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(mockCode);
vi.mocked(callTool).mockResolvedValue('{"success": true}');

await workerDeployCommand('my-worker', {
code: './worker.js',
enableAnalytics: 'MY_STATS',
});

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'deploy_worker', {
name: 'my-worker',
code: mockCode,
enable_analytics: 'MY_STATS',
});
});

it('should reject ANALYTICS env var when using default analytics binding', async () => {
const mockCode = 'export default { fetch() { return new Response("Hello"); } }';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(mockCode);

await expect(
workerDeployCommand('my-worker', {
code: './worker.js',
enableAnalytics: true,
env: ['ANALYTICS=test'],
})
).rejects.toThrow('process.exit called');
expect(console.error).toHaveBeenCalled();
});

it('should reject custom binding name as env var when using custom analytics binding', async () => {
const mockCode = 'export default { fetch() { return new Response("Hello"); } }';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(mockCode);

await expect(
workerDeployCommand('my-worker', {
code: './worker.js',
enableAnalytics: 'MY_STATS',
env: ['MY_STATS=test'],
})
).rejects.toThrow('process.exit called');
expect(console.error).toHaveBeenCalled();
});

it('should allow ANALYTICS env var when using different custom binding name', async () => {
const mockCode = 'export default { fetch() { return new Response("Hello"); } }';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(mockCode);
vi.mocked(callTool).mockResolvedValue('{"success": true}');

await workerDeployCommand('my-worker', {
code: './worker.js',
enableAnalytics: 'MY_STATS',
env: ['ANALYTICS=test'],
});

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'deploy_worker', {
name: 'my-worker',
code: mockCode,
enable_analytics: 'MY_STATS',
env_vars: { ANALYTICS: 'test' },
});
});

it('should exit with error when code flag is missing', async () => {
await expect(workerDeployCommand('my-worker', {})).rejects.toThrow('process.exit called');
expect(console.error).toHaveBeenCalled();
Expand Down
37 changes: 29 additions & 8 deletions packages/atxp/src/commands/paas/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@ interface WorkerDeployOptions {
code?: string;
db?: string[];
bucket?: string[];
enableAnalytics?: boolean;
enableAnalytics?: boolean | string;
env?: string[];
envFile?: string;
}

// Reserved env var names that conflict with existing bindings
const RESERVED_ENV_NAMES = ['DB', 'BUCKET', 'ANALYTICS', 'USER_NAMESPACE'];
// Base reserved env var names that conflict with existing bindings
const BASE_RESERVED_ENV_NAMES = ['DB', 'BUCKET', 'USER_NAMESPACE'];

/**
* Get reserved env var names based on analytics binding configuration
*/
function getReservedEnvNames(analyticsBindingName?: string): string[] {
// If analytics is enabled, add the binding name to reserved list
// Default binding name is 'ANALYTICS'
const bindingName = analyticsBindingName || 'ANALYTICS';
return [...BASE_RESERVED_ENV_NAMES, bindingName.toUpperCase()];
}

// Patterns that suggest sensitive data (warn user about plain text storage)
const SENSITIVE_PATTERNS = [/SECRET/i, /PASSWORD/i, /KEY/i, /TOKEN/i, /CREDENTIAL/i];
Expand All @@ -24,15 +34,15 @@ const SENSITIVE_PATTERNS = [/SECRET/i, /PASSWORD/i, /KEY/i, /TOKEN/i, /CREDENTIA
* Validate an environment variable name
* Must be a valid identifier and not reserved
*/
function validateEnvVarName(name: string): { valid: boolean; error?: string } {
function validateEnvVarName(name: string, reservedNames: 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(', ')})` };
if (reservedNames.includes(name.toUpperCase())) {
return { valid: false, error: `Reserved env var name "${name}": conflicts with existing bindings (${reservedNames.join(', ')})` };
}

return { valid: true };
Expand Down Expand Up @@ -167,9 +177,17 @@ export async function workerDeployCommand(
}
}

// Determine the analytics binding name for reserved name validation
const analyticsBindingName = typeof options.enableAnalytics === 'string'
? options.enableAnalytics
: options.enableAnalytics ? 'ANALYTICS' : undefined;
const reservedNames = options.enableAnalytics
? getReservedEnvNames(analyticsBindingName)
: BASE_RESERVED_ENV_NAMES;

// Validate all env var names
for (const key of Object.keys(envVars)) {
const validation = validateEnvVarName(key);
const validation = validateEnvVarName(key, reservedNames);
if (!validation.valid) {
console.error(chalk.red(`Error: ${validation.error}`));
process.exit(1);
Expand All @@ -194,7 +212,10 @@ export async function workerDeployCommand(
args.storage_bindings = storageBindings;
}
if (options.enableAnalytics) {
args.enable_analytics = true;
// Pass the binding name if specified, otherwise true for default 'ANALYTICS'
args.enable_analytics = typeof options.enableAnalytics === 'string'
? options.enableAnalytics
: true;
}
if (Object.keys(envVars).length > 0) {
args.env_vars = envVars;
Expand Down
12 changes: 10 additions & 2 deletions packages/atxp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ interface PaasOptions {
range?: string;
event?: string;
groupBy?: string;
enableAnalytics?: boolean;
enableAnalytics?: boolean | string;
env?: string[];
envFile?: string;
}
Expand Down Expand Up @@ -177,7 +177,15 @@ function parseArgs(): {
range: getArgValue('--range', ''),
event: getArgValue('--event', ''),
groupBy: getArgValue('--group-by', ''),
enableAnalytics: process.argv.includes('--enable-analytics'),
enableAnalytics: (() => {
const index = process.argv.indexOf('--enable-analytics');
if (index === -1) return undefined;
const nextArg = process.argv[index + 1];
// If no next arg, or next arg is another flag, return true (use default binding name)
if (!nextArg || nextArg.startsWith('-')) return true;
// Otherwise, use the provided binding name
return nextArg;
})(),
env: getAllArgValues('--env'),
envFile: getArgValue('--env-file', ''),
};
Expand Down
Loading