Skip to content
Closed
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
74 changes: 74 additions & 0 deletions docs/adr/ADR-0005-project-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# ADR-0005: Project Configuration Contract

- **Status:** Accepted
- **Date:** 2026-09-11
- **Decision owners:** StellarForge Core Team

## Context

StellarForge commands increasingly need project-scoped settings such as the selected Stellar network and the named Stellar CLI identity used for deployment. Those settings must be deterministic, testable, safe to load in developer machines and CI, and compatible with the security baseline.

Executable JavaScript or TypeScript configuration would introduce code-execution risk. Environment-variable precedence would also make effective behavior less visible for the MVP. Raw signing material must not become ordinary project configuration.

## Decision

The MVP project configuration file is a single root-level file named `stellarforge.config.json`.

The schema is versioned and intentionally small:

```json
{
"version": 1,
"network": "testnet",
"identity": "deployer"
}
```

Supported fields are:

- `version`: required and currently must equal `1`;
- `network`: optional and must be one of `testnet`, `futurenet`, or `mainnet`;
- `identity`: optional and must be a named Stellar CLI identity alias.

No raw secret keys, seed phrases, mnemonic material, access tokens, passwords, private keys, or arbitrary extension fields are accepted.

## Precedence

For commands that expose an explicit CLI override, effective configuration is resolved as:

1. explicit CLI option;
2. `stellarforge.config.json`;
3. missing.

There is no implicit network default. In particular, StellarForge must never silently select Mainnet.

The presence of `mainnet` in the configuration schema does not authorize every command to use Mainnet. Commands retain their own safety boundary. The MVP `deploy` command remains Testnet-only and rejects other networks after configuration resolution.

## Loading Rules

- configuration is loaded only from the current project root;
- the file must be a regular file and must not be a symbolic link;
- the MVP file-size limit is 64 KiB;
- configuration is parsed as JSON and never executed;
- malformed or invalid configuration produces actionable errors;
- errors must not include raw configuration contents or secret values.

## Consequences

### Positive

- project behavior is deterministic and visible in source control;
- configuration parsing cannot execute project code;
- command-line overrides remain explicit and testable;
- network-sensitive behavior cannot silently fall through to Mainnet;
- secret-bearing configuration is rejected early.

### Trade-offs

- the MVP does not support environment-specific config files or environment-variable precedence;
- advanced network/custom-RPC configuration is deferred;
- schema additions require deliberate compatibility and security review.

## Follow-up

Future schema versions may add non-secret network metadata or command-specific configuration. Any extension involving credentials, RPC authentication, remote configuration, or executable plugins requires separate security review.
73 changes: 54 additions & 19 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,65 @@
# Configuration Reference

StellarForge CLI configuration should be explicit, validated, minimal, and command-scoped.
StellarForge uses an optional project-root configuration file named `stellarforge.config.json`.

## Principles
## MVP schema

- Validate configuration before side effects.
- Prefer typed schemas over ad-hoc object access.
- Never print secrets or full sensitive config values in normal output.
- Separate user/project configuration from environment-provided credentials.
- Do not silently fall back to Mainnet.
- Network-sensitive commands should require an explicit, validated network target.
```json
{
"version": 1,
"network": "testnet",
"identity": "deployer"
}
```

## MVP Direction
`version` is required and must currently be `1`.

The final configuration file format and precedence rules are not yet accepted. They should be recorded in an ADR before becoming a stable public contract.
`network` is optional and accepts `testnet`, `futurenet`, or `mainnet`. A configured network does not bypass command-specific restrictions: the MVP deploy command still permits Testnet only.

Expected configuration areas may include:
`identity` is optional and must be the name of an identity already managed by Stellar CLI. StellarForge does not accept raw secret keys, seed phrases, public StrKeys, filesystem paths, tokens, or password-like fields in project configuration.

- selected network;
- Stellar RPC endpoint/reference;
- generated-project/template metadata;
- command-specific local development options;
- deployment options that do not contain raw secrets.
## Precedence

Credentials should be referenced securely rather than committed to project configuration.
Where a command exposes a CLI override, the order is:

## Precedence
1. explicit CLI option;
2. project configuration;
3. missing.

StellarForge does not silently default a missing network to Mainnet or any other network.

For example:

```json
{
"version": 1,
"network": "testnet",
"identity": "team-deployer"
}
```

allows:

```bash
stellarforge deploy
```

to use those project settings, while:

```bash
stellarforge deploy --source release-deployer
```

overrides only the identity.

## Loading and validation

The file must:

- live at the project root;
- be a regular file rather than a symbolic link;
- be no larger than 64 KiB;
- contain valid JSON;
- contain only supported schema fields.

Until formally decided, do not invent implicit precedence between CLI flags, project configuration, and environment variables. Command implementation issues must document required input sources explicitly.
Validation failures are designed to be actionable without echoing raw file contents or sensitive values.
48 changes: 39 additions & 9 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Command } from 'commander';
import {
resolveProjectConfig,
type ProjectConfigOverrides,
type ResolvedProjectConfig,
} from '../config/project.js';
import {
superviseDevProcesses,
type DevOrchestratorOptions,
Expand All @@ -8,42 +13,67 @@ import {
resolveTestnetDeploymentPlan,
type TestnetDeploymentInput,
} from '../deployment/project.js';
import { ValidationCliError } from '../errors/errors.js';
import { TerminalOutput } from '../output/terminal.js';

export interface DeployCommandOptions extends DevOrchestratorOptions {
readonly cwd?: () => string;
readonly resolveConfig?: (
cwd: string,
overrides: ProjectConfigOverrides,
) => ResolvedProjectConfig;
readonly resolvePlan?: (
input: TestnetDeploymentInput,
) => readonly DevProcessSpec[];
}

interface DeployCommandFlags {
readonly network: string;
readonly source: string;
readonly network?: string;
readonly source?: string;
}

export function createDeployCommand(
options: DeployCommandOptions = {},
): Command {
const cwd = options.cwd ?? process.cwd;
const output = options.output ?? new TerminalOutput();
const resolveConfig = options.resolveConfig ?? resolveProjectConfig;
const resolvePlan = options.resolvePlan ?? resolveTestnetDeploymentPlan;

return new Command('deploy')
.description('Deploy a supported StellarForge project to Stellar Testnet.')
.requiredOption(
.option(
'--network <network>',
'Deployment network. MVP requires the explicit value `testnet`.',
'Deployment network. CLI value overrides stellarforge.config.json.',
)
.requiredOption(
.option(
'--source <identity>',
'Named Stellar CLI identity alias used to sign the deployment.',
'Named Stellar CLI identity alias. CLI value overrides project configuration.',
)
.action(async (flags: DeployCommandFlags) => {
const projectRoot = cwd();
const overrides: ProjectConfigOverrides = {
...(flags.network === undefined ? {} : { network: flags.network }),
...(flags.source === undefined ? {} : { identity: flags.source }),
};
const configuration = resolveConfig(projectRoot, overrides);

if (configuration.network === undefined) {
throw new ValidationCliError(
'Deployment network is required. Pass `--network testnet` or set an explicit network in stellarforge.config.json.',
);
}

if (configuration.identity === undefined) {
throw new ValidationCliError(
'Deployment identity is required. Pass `--source <identity>` or set a named identity in stellarforge.config.json.',
);
}

const plan = resolvePlan({
cwd: cwd(),
network: flags.network,
source: flags.source,
cwd: projectRoot,
network: configuration.network,
source: configuration.identity,
});

output.info('Deploying smart contract to Stellar Testnet.');
Expand Down
88 changes: 88 additions & 0 deletions src/config/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { existsSync, lstatSync, readFileSync, type Stats } from 'node:fs';
import { resolve } from 'node:path';
import { ValidationCliError } from '../errors/errors.js';
import {
validateIdentityAlias,
validateStellarForgeConfig,
type StellarForgeConfig,
} from './schema.js';

export const PROJECT_CONFIG_FILENAME = 'stellarforge.config.json';
const MAX_CONFIG_BYTES = 64 * 1024;

export interface ProjectConfigOverrides {
readonly network?: string;
readonly identity?: string;
}

export interface ResolvedProjectConfig {
readonly network?: string;
readonly identity?: string;
}

export function loadProjectConfig(cwd: string): StellarForgeConfig | undefined {
const projectRoot = resolve(cwd);
const configPath = resolve(projectRoot, PROJECT_CONFIG_FILENAME);

if (!existsSync(configPath)) {
return undefined;
}

let metadata: Stats;
try {
metadata = lstatSync(configPath);
} catch (error) {
throw new ValidationCliError(
'Unable to inspect stellarforge.config.json.',
{ cause: error },
);
}

if (metadata.isSymbolicLink()) {
throw new ValidationCliError(
'stellarforge.config.json must be a regular project file, not a symbolic link.',
);
}

if (!metadata.isFile()) {
throw new ValidationCliError(
'stellarforge.config.json must be a regular project file.',
);
}

if (metadata.size > MAX_CONFIG_BYTES) {
throw new ValidationCliError(
'stellarforge.config.json is larger than the 64 KiB MVP limit.',
);
}

let parsed: unknown;
try {
parsed = JSON.parse(readFileSync(configPath, 'utf8')) as unknown;
} catch (error) {
throw new ValidationCliError(
'stellarforge.config.json is not valid JSON.',
{ cause: error },
);
}

return validateStellarForgeConfig(parsed);
}

export function resolveProjectConfig(
cwd: string,
overrides: ProjectConfigOverrides = {},
): ResolvedProjectConfig {
const projectConfig = loadProjectConfig(cwd);

const identity =
overrides.identity === undefined
? projectConfig?.identity
: validateIdentityAlias(overrides.identity);
const network = overrides.network ?? projectConfig?.network;

return {
...(network === undefined ? {} : { network }),
...(identity === undefined ? {} : { identity }),
};
}
Loading
Loading