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:
thePR0M3TH3ANandClaude Fable 5 committed 2026-08-13 21:31:32 -04:00
1 parent ab569fe900
commit e91d5d0be2
11 files changed
+785 -9

No files matched your search

+159 -2
View File
@@ -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")