Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ProjectConfigOverrides,
type ResolvedProjectConfig,
} from '../config/project.js';
import { validateIdentityAlias } from '../config/schema.js';
import {
superviseDevProcesses,
type DevOrchestratorOptions,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion src/output/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
50 changes: 50 additions & 0 deletions tests/deploy-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down