diff --git a/src/runConfig.ts b/src/runConfig.ts index eb2f7e6..df1a27c 100644 --- a/src/runConfig.ts +++ b/src/runConfig.ts @@ -7,11 +7,15 @@ export interface Parameter { } const PARAMS_KEY = 'fallout.parameters'; +const SECRET_NAMES_KEY = 'fallout.secretNames'; +const SECRET_PREFIX = 'fallout.secret.'; /** - * Persists the local run configuration. Parameters live in workspace state — they are - * per-workspace by nature (a configuration for *this* build), and plain enough to sit - * in plugin storage. They are rendered as CLI args (`--name value`) for a local run. + * Persists the local run configuration: parameters live in workspace state (plain, + * per-workspace); secret *values* live in VS Code's SecretStorage (OS keychain-backed) + * and are never rendered into the webview — only their names are. Parameters become + * CLI args (`--name value`); secrets become environment variables so they don't leak + * into shell history or the process list. */ export class RunConfigStore { private readonly onDidChangeEmitter = new vscode.EventEmitter(); @@ -36,19 +40,49 @@ export class RunConfigStore { this.onDidChangeEmitter.fire(); } + getSecretNames(): string[] { + return this.context.workspaceState.get(SECRET_NAMES_KEY, []); + } + + async setSecret(name: string, value: string): Promise { + await this.context.secrets.store(SECRET_PREFIX + name, value); + if (!this.getSecretNames().includes(name)) { + await this.context.workspaceState.update(SECRET_NAMES_KEY, [...this.getSecretNames(), name].sort()); + } + this.onDidChangeEmitter.fire(); + } + + async removeSecret(name: string): Promise { + await this.context.secrets.delete(SECRET_PREFIX + name); + await this.context.workspaceState.update(SECRET_NAMES_KEY, this.getSecretNames().filter(n => n !== name)); + this.onDidChangeEmitter.fire(); + } + /** Parameters rendered as a CLI argument string, e.g. `--configuration Release`. */ buildArgs(): string { return this.getParameters() .map(p => `--${p.name} ${quoteArg(p.value)}`) .join(' '); } + + /** Resolves all stored secrets into an environment map for a local run. */ + async buildEnv(): Promise> { + const env: Record = {}; + for (const name of this.getSecretNames()) { + const value = await this.context.secrets.get(SECRET_PREFIX + name); + if (value !== undefined) { + env[name] = value; + } + } + return env; + } } function quoteArg(value: string): string { return /\s/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value; } -/** The "Run Configuration" webview view — a form for build parameters. */ +/** The "Run Configuration" webview view — a form for parameters and secret names. */ export class RunConfigViewProvider implements vscode.WebviewViewProvider { private view: vscode.WebviewView | undefined; @@ -75,6 +109,12 @@ export class RunConfigViewProvider implements vscode.WebviewViewProvider { case 'removeParameter': await this.store.removeParameter(String(message.name)); break; + case 'setSecret': + if (message.name && message.value) { await this.store.setSecret(String(message.name), String(message.value)); } + break; + case 'removeSecret': + await this.store.removeSecret(String(message.name)); + break; } }); } @@ -83,6 +123,7 @@ export class RunConfigViewProvider implements vscode.WebviewViewProvider { void this.view?.webview.postMessage({ type: 'state', parameters: this.store.getParameters(), + secretNames: this.store.getSecretNames(), }); } @@ -106,6 +147,7 @@ export class RunConfigViewProvider implements vscode.WebviewViewProvider { button.icon { background: transparent; color: var(--vscode-foreground); padding: 3px 6px; } .name { flex: 0 0 40%; font-family: var(--vscode-editor-font-family); } .hint { opacity: 0.65; font-size: 11px; margin: 2px 0 8px; } + .secret-val { font-family: var(--vscode-editor-font-family); opacity: 0.6; } @@ -118,6 +160,15 @@ export class RunConfigViewProvider implements vscode.WebviewViewProvider { +

Secrets

+
Stored in the OS keychain (SecretStorage); passed as environment variables. Values are never shown.
+
+
+ + + +
+