mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-04 11:52:03 +00:00
fix: four real bugs found by end-to-end manual testing
Ran the full CLI surface against a freshly generated throwaway seed (profile -> agent unlock -> all 8 entry kinds -> reveal/sinks -> tokens -> audit -> backup -> relay sync -> seed-only recovery). Four defects that no unit test had caught: - clipboard sink hung the CLI forever. xclip/wl-copy fork a resident process to own the X/Wayland selection; it inherited our stdio, so any reader of the CLI's stdout blocked indefinitely, and waiting for process exit never returned. Now detached with ignored stdio, success declared once stdin is flushed. - 'vault import' read the mnemonic straight from the env, so it failed whenever the vault was unlocked through the agent -- the normal path. - 'vault import' never wrote anything; it only printed a summary. It now restores into the active vault, refuses to overwrite a non-empty vault without --yes, and keeps the old behavior behind --inspect. - A corrupt/foreign vault file surfaced the raw codec error "invalid token encoding". It now names the actual likely causes. Also documented the variadic-option limitation that a trailing flag is lost (commander stops collecting at the next dash-prefixed token): a quoted command spec now splits on whitespace, no shell involved. Regression tests for all four. 151 core + 44 CLI tests green. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
274bc0e1b8
commit
dcb8fc9219
6 files changed
+195
-24
No files matched your search
@@ -16,6 +16,23 @@ export interface SinkResult {
|
||||
/** Env var the --exec sink injects into the child process. */
|
||||
export const EXEC_ENV_VAR = "SEEDPASS_SECRET";
|
||||
|
||||
/**
|
||||
* Split a sink command spec into [command, ...args].
|
||||
*
|
||||
* Commander's variadic options stop collecting at the next token starting
|
||||
* with "-", so `--exec wc -c` loses the flag. Passing the whole command as
|
||||
* one quoted string (`--exec "wc -c"`) is the documented way to include
|
||||
* flags; split it here. Splitting is on whitespace only — no shell is
|
||||
* involved, so quoting/expansion never happens.
|
||||
*/
|
||||
export function parseCommandSpec(spec: string[]): [string, string[]] {
|
||||
const parts =
|
||||
spec.length === 1 && /\s/.test(spec[0]!) ? spec[0]!.trim().split(/\s+/) : spec;
|
||||
const [cmd, ...args] = parts;
|
||||
if (!cmd) throw new Error("empty command");
|
||||
return [cmd, args];
|
||||
}
|
||||
|
||||
/** Run a command with the secret injected as an env var (never on argv). */
|
||||
export async function execSink(
|
||||
secret: string,
|
||||
@@ -66,16 +83,53 @@ const CLIPBOARD_COMMANDS: Array<{ cmd: string; args: string[] }> = [
|
||||
{ cmd: "clip.exe", args: [] },
|
||||
];
|
||||
|
||||
/**
|
||||
* Feed a secret to one clipboard tool.
|
||||
*
|
||||
* X11/Wayland clipboard tools (xclip, wl-copy) fork a resident process that
|
||||
* owns the selection until another client takes it. So we must NOT wait for
|
||||
* the process to exit, and must NOT let it inherit our stdio — a resident
|
||||
* child holding our stdout blocks every reader of this CLI's output. We
|
||||
* detach it, ignore its stdio, and treat "stdin accepted and flushed"
|
||||
* as success.
|
||||
*/
|
||||
function feedClipboardTool(secret: string, cmd: string, args: string[]): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"], detached: true });
|
||||
let settled = false;
|
||||
const fail = (e: Error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
child.on("error", fail);
|
||||
// An immediate non-zero exit means the tool rejected the input
|
||||
// (e.g. no display); a tool that stays resident never fires this.
|
||||
child.on("exit", (code) => {
|
||||
if (code !== 0) fail(new Error(`${cmd} exited ${code}`));
|
||||
});
|
||||
child.stdin.on("error", fail);
|
||||
child.stdin.end(secret, () => {
|
||||
// Give a failing tool a moment to report before declaring success.
|
||||
setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
child.unref();
|
||||
resolve();
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Copy the secret to the system clipboard via the first available tool. */
|
||||
export async function clipboardSink(secret: string): Promise<SinkResult> {
|
||||
let lastError: unknown = new Error("no clipboard tool found");
|
||||
for (const { cmd, args } of CLIPBOARD_COMMANDS) {
|
||||
try {
|
||||
const result = await stdinSink(secret, cmd, args);
|
||||
if (result.exitCode === 0) {
|
||||
return { sink: "clipboard", detail: `copied via ${cmd}` };
|
||||
}
|
||||
lastError = new Error(`${cmd} exited ${result.exitCode}`);
|
||||
await feedClipboardTool(secret, cmd, args);
|
||||
return { sink: "clipboard", detail: `copied via ${cmd}` };
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user