mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-05 20:32:05 +00:00
TS port slice 5: agent-blind provisioning, entry-secret parity, nostr app fix
- core entryOps: add* functions for all creatable kinds rebuild the Python EntryManager fixture index byte-for-byte under a pinned clock — field shapes, id allocation, independent TOTP derivation-index allocation, and Python's ISO timestamp format all verified. - CLI 'entry add password/totp/key-value/document/seed/managed-account/ nostr': provision-blind — returns the reference and metadata, never the secret the entry represents; tests assert the derived password / imported secret / child mnemonic appear nowhere in provisioning output. - Parity bug found and fixed by the new entry_secrets fixtures: nostr ENTRY keys derive via the default BIP-85 app 39 path (EntryManager), not app 1237 (sync client identity); the CLI had used 1237. Reveal values for password/TOTP/nostr/seed/managed-account now match Python retrieval. - 129 core parity tests (Node + jsdom) + 16 CLI tests green. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
ab569fe900
commit
e91d5d0be2
11 files changed
+785
-9
No files matched your search
@@ -13,8 +13,20 @@ import { entryMetadata, resolveEntry } from "./refs.js";
|
||||
import { materializeSecret } from "./secrets.js";
|
||||
import { clipboardSink, execSink, stdinSink } from "./sinks.js";
|
||||
import { capabilities } from "./capabilities.js";
|
||||
import { openVault, type OpenedVault } from "./vaultFile.js";
|
||||
import { importBackup, exportBackup, generateFingerprint } from "@seedpass/core";
|
||||
import { openVault, saveVault, type OpenedVault } from "./vaultFile.js";
|
||||
import {
|
||||
importBackup,
|
||||
exportBackup,
|
||||
generateFingerprint,
|
||||
addPasswordEntry,
|
||||
addTotpDeterministic,
|
||||
addTotpImported,
|
||||
addKeyValueEntry,
|
||||
addDocumentEntry,
|
||||
addSeedEntry,
|
||||
addManagedAccountEntry,
|
||||
addNostrKeyEntry,
|
||||
} from "@seedpass/core";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
export interface ProgramIo {
|
||||
@@ -99,6 +111,151 @@ export function buildProgram(io: ProgramIo = defaultIo): Command {
|
||||
io.out(JSON.stringify(rows, null, 2));
|
||||
});
|
||||
|
||||
// Provisioning (plan section 9.3): create entries, get back a reference
|
||||
// and metadata — never the secret the entry represents. Deterministic
|
||||
// artifacts are recoverable from the seed; nothing extra to back up.
|
||||
const add = entry.command("add").description("create entries (returns references, not secrets)");
|
||||
|
||||
const commonAddOptions = <T extends Command>(cmd: T): T => {
|
||||
cmd
|
||||
.option("--notes <text>", "notes", "")
|
||||
.option("--tags <tag...>", "tags")
|
||||
.option("--archived", "create archived");
|
||||
return cmd;
|
||||
};
|
||||
|
||||
async function finishAdd(vault: OpenedVault, id: string): Promise<void> {
|
||||
await saveVault(vault);
|
||||
const e = vault.index.entries[id]!;
|
||||
io.out(JSON.stringify(entryMetadata(id, e), null, 2));
|
||||
}
|
||||
|
||||
const commonOpts = (o: { notes?: string; tags?: string[]; archived?: boolean }) => ({
|
||||
notes: o.notes ?? "",
|
||||
...(o.tags !== undefined && { tags: o.tags }),
|
||||
...(o.archived !== undefined && { archived: o.archived }),
|
||||
});
|
||||
|
||||
commonAddOptions(
|
||||
add
|
||||
.command("password <label>")
|
||||
.description("deterministic password entry (derives on demand; value never shown here)")
|
||||
.option("--length <n>", "password length", "16")
|
||||
.option("--username <name>", "username")
|
||||
.option("--url <url>", "site URL"),
|
||||
).action(
|
||||
async (
|
||||
label: string,
|
||||
o: { length: string; username?: string; url?: string; notes?: string; tags?: string[]; archived?: boolean },
|
||||
) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const id = addPasswordEntry(vault.index, label, Number(o.length), {
|
||||
...commonOpts(o),
|
||||
...(o.username !== undefined && { username: o.username }),
|
||||
...(o.url !== undefined && { url: o.url }),
|
||||
});
|
||||
await finishAdd(vault, id);
|
||||
},
|
||||
);
|
||||
|
||||
commonAddOptions(
|
||||
add
|
||||
.command("totp <label>")
|
||||
.description("TOTP entry: deterministic by default, or --secret to import")
|
||||
.option("--secret <b32>", "import an existing base32 secret")
|
||||
.option("--period <s>", "period seconds", "30")
|
||||
.option("--digits <n>", "code digits", "6"),
|
||||
).action(
|
||||
async (
|
||||
label: string,
|
||||
o: { secret?: string; period: string; digits: string; notes?: string; tags?: string[]; archived?: boolean },
|
||||
) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const opts = {
|
||||
...commonOpts(o),
|
||||
period: Number(o.period),
|
||||
digits: Number(o.digits),
|
||||
};
|
||||
const id = o.secret
|
||||
? addTotpImported(vault.index, label, o.secret, opts)
|
||||
: addTotpDeterministic(vault.index, label, vault.mnemonic, opts);
|
||||
await finishAdd(vault, id);
|
||||
},
|
||||
);
|
||||
|
||||
commonAddOptions(
|
||||
add
|
||||
.command("key-value <label> <key> <value>")
|
||||
.description("store an arbitrary secret value"),
|
||||
).action(
|
||||
async (
|
||||
label: string,
|
||||
key: string,
|
||||
value: string,
|
||||
o: { notes?: string; tags?: string[]; archived?: boolean },
|
||||
) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const id = addKeyValueEntry(vault.index, label, key, value, commonOpts(o));
|
||||
await finishAdd(vault, id);
|
||||
},
|
||||
);
|
||||
|
||||
commonAddOptions(
|
||||
add
|
||||
.command("document <label> <content>")
|
||||
.description("store a text document")
|
||||
.option("--file-type <ext>", "file type", "txt"),
|
||||
).action(
|
||||
async (
|
||||
label: string,
|
||||
content: string,
|
||||
o: { fileType: string; notes?: string; tags?: string[]; archived?: boolean },
|
||||
) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const id = addDocumentEntry(vault.index, label, content, {
|
||||
...commonOpts(o),
|
||||
fileType: o.fileType,
|
||||
});
|
||||
await finishAdd(vault, id);
|
||||
},
|
||||
);
|
||||
|
||||
commonAddOptions(
|
||||
add
|
||||
.command("seed <label>")
|
||||
.description("derived seed phrase entry (12/18/24 words)")
|
||||
.option("--words <n>", "word count", "24"),
|
||||
).action(
|
||||
async (label: string, o: { words: string; notes?: string; tags?: string[]; archived?: boolean }) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const words = Number(o.words);
|
||||
if (words !== 12 && words !== 18 && words !== 24) {
|
||||
throw new Error("--words must be 12, 18 or 24");
|
||||
}
|
||||
const id = addSeedEntry(vault.index, label, {
|
||||
...commonOpts(o),
|
||||
wordCount: words as 12 | 18 | 24,
|
||||
});
|
||||
await finishAdd(vault, id);
|
||||
},
|
||||
);
|
||||
|
||||
commonAddOptions(
|
||||
add.command("managed-account <label>").description("BIP-85 managed account (12-word child seed)"),
|
||||
).action(async (label: string, o: { notes?: string; tags?: string[]; archived?: boolean }) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const id = addManagedAccountEntry(vault.index, label, vault.mnemonic, commonOpts(o));
|
||||
await finishAdd(vault, id);
|
||||
});
|
||||
|
||||
commonAddOptions(
|
||||
add.command("nostr <label>").description("derived Nostr key entry"),
|
||||
).action(async (label: string, o: { notes?: string; tags?: string[]; archived?: boolean }) => {
|
||||
const vault = await openFromOptions(program.opts());
|
||||
const id = addNostrKeyEntry(vault.index, label, commonOpts(o));
|
||||
await finishAdd(vault, id);
|
||||
});
|
||||
|
||||
entry
|
||||
.command("reveal <refOrQuery>")
|
||||
.description("PLAINTEXT EGRESS: print the secret to stdout")
|
||||
|
||||
Reference in new issue
Block a user