From 192d2c03d1a0b880c921e62e968bb14729711273 Mon Sep 17 00:00:00 2001 From: cashflaretech-glitch Date: Fri, 16 Oct 2026 22:19:09 +0000 Subject: [PATCH] Fix deployment security regression boundaries --- src/commands/deploy.ts | 9 +++++++ src/output/terminal.ts | 5 +++- tests/deploy-command.test.ts | 50 ++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 32d4db2..890e369 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -4,6 +4,7 @@ import { type ProjectConfigOverrides, type ResolvedProjectConfig, } from '../config/project.js'; +import { validateIdentityAlias } from '../config/schema.js'; import { superviseDevProcesses, type DevOrchestratorOptions, @@ -70,6 +71,14 @@ export function createDeployCommand( ); } + if (configuration.network !== 'testnet') { + throw new ValidationCliError( + 'Deployment is Testnet-only in the MVP. Use `--network testnet` or set network to `testnet` in stellarforge.config.json.', + ); + } + + validateIdentityAlias(configuration.identity); + const plan = resolvePlan({ cwd: projectRoot, network: configuration.network, diff --git a/src/output/terminal.ts b/src/output/terminal.ts index 2d4b3d7..83c13c3 100644 --- a/src/output/terminal.ts +++ b/src/output/terminal.ts @@ -35,6 +35,9 @@ function consumeCsiSequence(message: string, startIndex: number): number { return index; } +const SENSITIVE_KEY_PATTERN = + /((?:private[_-]?key|secret|password|passphrase|seed|mnemonic|token))\s*[:=]\s*([^\s\r\n"'`]+)/gi; + export function sanitizeTerminalText(message: string): string { let sanitized = ''; let index = 0; @@ -64,7 +67,7 @@ export function sanitizeTerminalText(message: string): string { index += 1; } - return sanitized; + return sanitized.replace(SENSITIVE_KEY_PATTERN, '$1=[REDACTED]'); } export class TerminalOutput { diff --git a/tests/deploy-command.test.ts b/tests/deploy-command.test.ts index 73a2b64..53770e8 100644 --- a/tests/deploy-command.test.ts +++ b/tests/deploy-command.test.ts @@ -98,6 +98,56 @@ describe('deploy command', () => { expect(captured.stdoutText()).toContain('[stellar:deploy] CABC123'); }); + it('rejects non-Testnet deployment targets before running subprocesses', async () => { + const command = createDeployCommand({ + cwd: () => '/workspace', + resolveConfig: () => ({ + network: 'mainnet', + identity: 'deployer', + }), + resolvePlan: () => { + throw new Error('plan resolver should not be called'); + }, + }); + + await expect( + command.parseAsync(['node', 'deploy', '--network', 'mainnet', '--source', 'deployer']), + ).rejects.toThrow('Testnet-only'); + }); + + it('redacts secret material in child output before printing it', async () => { + const child = new DeployChild(); + const captured = createCapturedTerminalOutput(); + const command = createDeployCommand({ + cwd: () => '/workspace', + output: captured.output, + resolvePlan: () => [ + { + label: 'stellar:deploy', + command: 'stellar', + args: ['contract', 'deploy', '--source-account', 'deployer', '--network', 'testnet'], + cwd: '/workspace', + }, + ], + spawnProcess: () => child, + }); + + const running = command.parseAsync([ + 'node', + 'deploy', + '--network', + 'testnet', + '--source', + 'deployer', + ]); + child.stdout.write('private-key=SECRET_DEPLOY_KEY\n'); + child.exit(0); + await running; + + expect(captured.stdoutText()).not.toContain('SECRET_DEPLOY_KEY'); + expect(captured.stdoutText()).toContain('private-key=[REDACTED]'); + }); + it('preserves a failed Stellar CLI deployment as a subprocess failure', async () => { const child = new DeployChild(); const command = createDeployCommand({