CLI: auto-create local secrets key file during onboard
Add ensureLocalSecretsKeyFile helper that generates a random 32-byte master key during onboard if using local_encrypted provider. Move resolveRuntimeLikePath to cli/src/utils/ for reuse by secrets-key and existing check modules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import pc from "picocolors";
|
||||
import { configExists, readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
|
||||
import type { PaperclipConfig } from "../config/schema.js";
|
||||
import { ensureAgentJwtSecret, resolveAgentJwtEnvFile } from "../config/env.js";
|
||||
import { ensureLocalSecretsKeyFile } from "../config/secrets-key.js";
|
||||
import { promptDatabase } from "../prompts/database.js";
|
||||
import { promptLlm } from "../prompts/llm.js";
|
||||
import { promptLogging } from "../prompts/logging.js";
|
||||
@@ -132,6 +133,13 @@ export async function onboard(opts: { config?: string }): Promise<void> {
|
||||
secrets,
|
||||
};
|
||||
|
||||
const keyResult = ensureLocalSecretsKeyFile(config, resolveConfigPath(opts.config));
|
||||
if (keyResult.status === "created") {
|
||||
p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`);
|
||||
} else if (keyResult.status === "existing") {
|
||||
p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`));
|
||||
}
|
||||
|
||||
writeConfig(config, opts.config);
|
||||
|
||||
p.note(
|
||||
|
||||
48
cli/src/config/secrets-key.ts
Normal file
48
cli/src/config/secrets-key.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { PaperclipConfig } from "./schema.js";
|
||||
import { resolveRuntimeLikePath } from "../utils/path-resolver.js";
|
||||
|
||||
export type EnsureSecretsKeyResult =
|
||||
| { status: "created"; path: string }
|
||||
| { status: "existing"; path: string }
|
||||
| { status: "skipped_env"; path: null }
|
||||
| { status: "skipped_provider"; path: null };
|
||||
|
||||
export function ensureLocalSecretsKeyFile(
|
||||
config: Pick<PaperclipConfig, "secrets">,
|
||||
configPath?: string,
|
||||
): EnsureSecretsKeyResult {
|
||||
if (config.secrets.provider !== "local_encrypted") {
|
||||
return { status: "skipped_provider", path: null };
|
||||
}
|
||||
|
||||
const envMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||
if (envMasterKey && envMasterKey.trim().length > 0) {
|
||||
return { status: "skipped_env", path: null };
|
||||
}
|
||||
|
||||
const keyFileOverride = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
||||
const configuredPath =
|
||||
keyFileOverride && keyFileOverride.trim().length > 0
|
||||
? keyFileOverride.trim()
|
||||
: config.secrets.localEncrypted.keyFilePath;
|
||||
const keyFilePath = resolveRuntimeLikePath(configuredPath, configPath);
|
||||
|
||||
if (fs.existsSync(keyFilePath)) {
|
||||
return { status: "existing", path: keyFilePath };
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(keyFilePath), { recursive: true });
|
||||
fs.writeFileSync(keyFilePath, randomBytes(32).toString("base64"), {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
try {
|
||||
fs.chmodSync(keyFilePath, 0o600);
|
||||
} catch {
|
||||
// best effort
|
||||
}
|
||||
return { status: "created", path: keyFilePath };
|
||||
}
|
||||
23
cli/src/utils/path-resolver.ts
Normal file
23
cli/src/utils/path-resolver.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function unique(items: string[]): string[] {
|
||||
return Array.from(new Set(items));
|
||||
}
|
||||
|
||||
export function resolveRuntimeLikePath(value: string, configPath?: string): string {
|
||||
if (path.isAbsolute(value)) return value;
|
||||
|
||||
const cwd = process.cwd();
|
||||
const configDir = configPath ? path.dirname(configPath) : null;
|
||||
const workspaceRoot = configDir ? path.resolve(configDir, "..") : cwd;
|
||||
|
||||
const candidates = unique([
|
||||
path.resolve(workspaceRoot, "server", value),
|
||||
path.resolve(workspaceRoot, value),
|
||||
path.resolve(cwd, value),
|
||||
...(configDir ? [path.resolve(configDir, value)] : []),
|
||||
]);
|
||||
|
||||
return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
|
||||
}
|
||||
Reference in New Issue
Block a user