Files
SeedPass/js/packages/cli/src/configFile.ts
T
thePR0M3TH3ANandClaude Fable 5 841d0984b3 TS port slice 8: profiles, config, and the session agent
- Python-compatible ~/.seedpass layout: fingerprints.json (same shape,
  indent=4), per-profile dirs with parent_seed.enc (kdf/ct wrapper; unlock
  honors argon2id metadata and falls back across pbkdf2 iteration counts),
  index-key-encrypted seedpass_config.json.enc with ConfigManager defaults.
- CLI: fingerprint list/add/switch/remove (remove requires --yes),
  config get/set (JSON-parsed values, pin/password hashes redacted on read),
  profile-aware vault resolution (--vault override, else active profile).
- Session agent (the chosen unlock model): unix-socket daemon holding
  unlocked seeds in memory with TTL, 0600 socket; agent start/status/stop,
  vault unlock (password -> parent seed -> agent, never argv) and vault
  lock. Seed resolution order: SEEDPASS_MNEMONIC env, then agent. This
  daemon is the designed enforcement point for the section-9.3 lease/token
  layer.
- Tests cover the full no-env-mnemonic lifecycle: locked error -> unlock
  with password only -> provision/reveal through the agent -> status ->
  lock -> locked again. 148 core + 29 CLI tests green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 14:15:51 -04:00

65 lines
1.9 KiB
TypeScript

/**
* Per-profile encrypted configuration, parity with ConfigManager
* (seedpass_config.json.enc, encrypted under the index key).
*/
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { decryptPayload, deriveIndexKeyBytes, encryptV3, utf8 } from "@seedpass/core";
import { CONFIG_FILENAME } from "./appDir.js";
export const DEFAULT_RELAYS = [
"wss://relay.snort.social",
"wss://nostr.oxtr.dev",
"wss://relay.primal.net",
];
/** Defaults mirror ConfigManager.load_config. */
export function defaultConfig(): Record<string, unknown> {
return {
relays: [...DEFAULT_RELAYS],
offline_mode: false,
online_mode_notice_seen: false,
pin_hash: "",
password_hash: "",
inactivity_timeout: 900,
kdf_iterations: 200000,
kdf_mode: "pbkdf2",
argon2_time_cost: 2,
additional_backup_path: "",
backup_interval: 0,
secret_mode_enabled: false,
clipboard_clear_delay: 45,
quick_unlock_enabled: false,
nostr_max_retries: 2,
nostr_retry_delay: 1.0,
min_uppercase: 2,
min_lowercase: 2,
min_digits: 2,
min_special: 2,
};
}
export async function loadConfig(
profileDir: string,
mnemonic: string,
): Promise<Record<string, unknown>> {
const path = join(profileDir, CONFIG_FILENAME);
if (!existsSync(path)) return defaultConfig();
const key = deriveIndexKeyBytes(mnemonic);
const blob = new Uint8Array(await readFile(path));
const plain = await decryptPayload(key, blob);
return { ...defaultConfig(), ...(JSON.parse(new TextDecoder().decode(plain)) as object) };
}
export async function saveConfig(
profileDir: string,
mnemonic: string,
config: Record<string, unknown>,
): Promise<void> {
const key = deriveIndexKeyBytes(mnemonic);
const payload = await encryptV3(key, utf8(JSON.stringify(config)));
await writeFile(join(profileDir, CONFIG_FILENAME), payload, { mode: 0o600 });
}