Files
SeedPass/js/packages/core/test/backup.test.ts
T
thePR0M3TH3ANandClaude Fable 5 ab569fe900 TS port slice 4: portable backup, CI, and the agent-blind CLI MVP
- Portable backup (format_version 1): TS imports Python exports, re-exports
  byte-identically under pinned nonce/timestamp, verifies the canonical
  checksum, rejects tampering and unknown versions.
- .github/workflows/ts-parity.yml: typecheck + Node/jsdom parity suite on
  js/ changes, plus a fixture-drift job that regenerates fixtures from
  Python and fails if the committed ones don't match (manifest commit field
  excluded by construction).
- packages/cli — seedpass-js MVP implementing plan section 9.3:
  * reference-first output: entry list/get/search emit sp://entry/<id> refs
    and metadata; secret-bearing fields become has_* flags
  * 'entry reveal' is the only plaintext egress command
  * 'use --clipboard|--exec|--stdin-to' delivers secrets to sinks; --exec
    injects SEEDPASS_SECRET into the child env, never argv or stdout
  * vault export/import over portable backups; capabilities --format json
  * mnemonic comes from SEEDPASS_MNEMONIC env, never the command line
- CLI tests prove the blindness properties: derived password matches the v2
  fixture, TOTP at pinned time, secrets absent from all default output and
  from sink-delivery confirmations.
- 125 core parity tests (Node + jsdom) + 11 CLI tests green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-13 10:40:56 -04:00

75 lines
2.6 KiB
TypeScript

/** Portable backup parity against Python-generated fixtures. */
import { describe, expect, it } from "vitest";
import { base64 } from "@scure/base";
import { portableBackup, mnemonics } from "@seedpass/test-vectors";
import {
importBackup,
exportBackup,
BackupImportError,
canonicalHash,
hexToBytes,
} from "@seedpass/core";
const mnemonic = () => mnemonics[portableBackup.mnemonic_id]!;
describe("portable backup", () => {
it("imports the Python encrypted export", async () => {
const index = await importBackup(JSON.stringify(portableBackup.encrypted_wrapper), {
mnemonic: mnemonic(),
});
expect(index).toEqual(portableBackup.index);
});
it("imports the Python plaintext export", async () => {
const index = await importBackup(JSON.stringify(portableBackup.plaintext_wrapper));
expect(index).toEqual(portableBackup.index);
});
it("computes the same canonical checksum as Python", () => {
expect(canonicalHash(portableBackup.index)).toBe(portableBackup.canonical_json_sha256);
});
it("re-exports byte-identically with pinned nonce and timestamp", async () => {
const w = portableBackup.encrypted_wrapper;
const pinnedNonce = base64.decode(w.payload).slice(3, 15);
const reExported = await exportBackup(portableBackup.index, {
mnemonic: mnemonic(),
fingerprint: w.fingerprint,
createdAt: w.created_at,
nonce: pinnedNonce,
});
expect(reExported).toEqual(w);
});
it("roundtrips its own encrypted export", async () => {
const wrapper = await exportBackup(portableBackup.index, {
mnemonic: mnemonic(),
fingerprint: portableBackup.encrypted_wrapper.fingerprint,
});
const index = await importBackup(JSON.stringify(wrapper), { mnemonic: mnemonic() });
expect(index).toEqual(portableBackup.index);
});
it("rejects a tampered checksum", async () => {
const bad = {
...portableBackup.plaintext_wrapper,
checksum: hexToBytes(portableBackup.canonical_json_sha256)
.reverse()
.reduce((s, b) => s + b.toString(16).padStart(2, "0"), ""),
};
await expect(importBackup(JSON.stringify(bad))).rejects.toThrow("Checksum mismatch");
});
it("rejects unsupported format versions", async () => {
const bad = { ...portableBackup.plaintext_wrapper, format_version: 2 };
await expect(importBackup(JSON.stringify(bad))).rejects.toThrow(BackupImportError);
});
it("requires the seed for seed-only backups", async () => {
await expect(
importBackup(JSON.stringify(portableBackup.encrypted_wrapper)),
).rejects.toThrow("parent seed required");
});
});