From b61be400d2deb7dc9286ba0da9f8670ee657d57b Mon Sep 17 00:00:00 2001 From: Emilio Amaya Date: Wed, 28 Jan 2026 10:37:37 -0500 Subject: [PATCH 1/2] feat(paas): allow configurable Analytics Engine binding name - Update --enable-analytics to accept optional binding name - Default to 'ANALYTICS' when no name is specified - Dynamically update reserved env var names based on binding name - Update help text to document new [NAME] syntax Closes ATXP-1423 Co-Authored-By: Claude Opus 4.5 --- packages/atxp/src/commands/paas/index.ts | 4 +-- packages/atxp/src/commands/paas/worker.ts | 37 ++++++++++++++++++----- packages/atxp/src/index.ts | 12 ++++++-- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/packages/atxp/src/commands/paas/index.ts b/packages/atxp/src/commands/paas/index.ts index 1ccd946..50a0b31 100644 --- a/packages/atxp/src/commands/paas/index.ts +++ b/packages/atxp/src/commands/paas/index.ts @@ -57,7 +57,7 @@ interface PaasOptions { range?: string; event?: string; groupBy?: string; - enableAnalytics?: boolean; + enableAnalytics?: boolean | string; env?: string[]; envFile?: string; } @@ -74,7 +74,7 @@ function showPaasHelp(): void { 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.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('') + ' Get worker logs'); console.log(' ' + chalk.cyan('paas worker delete') + ' ' + chalk.yellow('') + ' Delete a worker'); diff --git a/packages/atxp/src/commands/paas/worker.ts b/packages/atxp/src/commands/paas/worker.ts index bb2b822..2fe5a3e 100644 --- a/packages/atxp/src/commands/paas/worker.ts +++ b/packages/atxp/src/commands/paas/worker.ts @@ -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]; @@ -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 }; @@ -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); @@ -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; diff --git a/packages/atxp/src/index.ts b/packages/atxp/src/index.ts index bd2cdaa..e02dea1 100644 --- a/packages/atxp/src/index.ts +++ b/packages/atxp/src/index.ts @@ -56,7 +56,7 @@ interface PaasOptions { range?: string; event?: string; groupBy?: string; - enableAnalytics?: boolean; + enableAnalytics?: boolean | string; env?: string[]; envFile?: string; } @@ -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', ''), }; From 774f7b6fe8ec50cad2cb3716ccd226f5f71f9fc5 Mon Sep 17 00:00:00 2001 From: Emilio Amaya Date: Wed, 28 Jan 2026 10:50:39 -0500 Subject: [PATCH 2/2] test: add tests for custom analytics binding name - Test custom binding name passed to enable_analytics - Test reserved env var validation with default ANALYTICS binding - Test reserved env var validation with custom binding name - Test that ANALYTICS env var is allowed when using different custom binding Co-Authored-By: Claude Opus 4.5 --- .../atxp/src/commands/paas/worker.test.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/atxp/src/commands/paas/worker.test.ts b/packages/atxp/src/commands/paas/worker.test.ts index 5a47b83..e6ea044 100644 --- a/packages/atxp/src/commands/paas/worker.test.ts +++ b/packages/atxp/src/commands/paas/worker.test.ts @@ -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();