diff --git a/js/packages/cli/src/api/routes.ts b/js/packages/cli/src/api/routes.ts index cec88e84..95ae2507 100644 --- a/js/packages/cli/src/api/routes.ts +++ b/js/packages/cli/src/api/routes.ts @@ -745,7 +745,7 @@ export function registerRoutes(server: ApiServer, ctx: ApiContext): void { const vault = await readVault(ctx); const body = bodyObject(req); const encrypt = body["plaintext"] !== true; - const wrapper = await exportBackup(vault.index as unknown as Record, { + const wrapper = await exportBackup(vault.index, { mnemonic, fingerprint: generateFingerprint(mnemonic), encrypt, diff --git a/js/packages/cli/src/program.ts b/js/packages/cli/src/program.ts index 436bbe06..cf7f8f25 100644 --- a/js/packages/cli/src/program.ts +++ b/js/packages/cli/src/program.ts @@ -2425,7 +2425,7 @@ export function buildProgram(io: ProgramIo = defaultIo): Command { existsSync(resolved) && statSync(resolved).isDirectory() ? join(resolved, defaultBackupFilename(fingerprint)) : resolved; - const wrapper = await exportBackup(vault.index as Record, { + const wrapper = await exportBackup(vault.index, { mnemonic: vault.mnemonic, fingerprint, encrypt: !o.plaintext, diff --git a/js/packages/cli/src/tui/menus.ts b/js/packages/cli/src/tui/menus.ts index a7951df5..d2c50602 100644 --- a/js/packages/cli/src/tui/menus.ts +++ b/js/packages/cli/src/tui/menus.ts @@ -1505,7 +1505,7 @@ async function exportDatabase(s: Session): Promise { } else if (!basename(dest).includes(".")) { dest = `${dest}${BACKUP_EXTENSION}`; } - const payload = await exportBackup(s.vault.index as unknown as Record, { + const payload = await exportBackup(s.vault.index, { mnemonic: s.vault.mnemonic, fingerprint: s.fingerprint, }); diff --git a/js/packages/core/src/vault/index0.ts b/js/packages/core/src/vault/index0.ts index 37ca04fc..184a2653 100644 --- a/js/packages/core/src/vault/index0.ts +++ b/js/packages/core/src/vault/index0.ts @@ -343,7 +343,13 @@ export function normalizeViewManifest(raw: unknown): Dict { }; } -export function recomputeIndex0Stats(index0: Dict): Dict { +// Typed to exactly what it reads. It used to take a bare Dict, so every one +// of its five callers — all holding a real Index0 — had to launder it through +// `as unknown as Dict` to get in. A Pick says what the function needs, admits +// any Index0 without ceremony, and still accepts a partial one. +export function recomputeIndex0Stats( + index0: Pick, +): Dict { const events = normalizeMapping(index0["events"]); const checkpoints = normalizeMapping(index0["checkpoints"]); const heads = normalizeMapping(index0["heads"]); @@ -394,7 +400,7 @@ export function normalizeIndex0(raw: unknown): Index0 { const normalizedHead = normalizeHead(head); if (normalizedHead) normalized.heads[String(writerId)] = normalizedHead; } - normalized.stats = recomputeIndex0Stats(normalized as unknown as Dict); + normalized.stats = recomputeIndex0Stats(normalized); return normalized; } @@ -544,7 +550,7 @@ export function appendIndex0Event(payload: unknown, options: AppendEventOptions) head_hash: computeHeadHash(event), modified_ts: event["modified_ts"] as number, }; - system.stats = recomputeIndex0Stats(system as unknown as Dict); + system.stats = recomputeIndex0Stats(system); return out; } @@ -658,7 +664,7 @@ export function compactIndex0( normalized as unknown as Dict, options, ); - normalized.stats = recomputeIndex0Stats(normalized as unknown as Dict); + normalized.stats = recomputeIndex0Stats(normalized); return normalized; } @@ -850,7 +856,7 @@ export function rebuildCanonicalViewsPayload( const sortedViews: Record = {}; for (const key of Object.keys(views).sort(compareCodePoints)) sortedViews[key] = views[key]!; index0.canonical_views = sortedViews; - index0.stats = recomputeIndex0Stats(index0 as unknown as Dict); + index0.stats = recomputeIndex0Stats(index0); (out["_system"] as Dict)["index0"] = index0; return out; } @@ -1038,6 +1044,6 @@ export function mergeSystemIndex0(current: unknown, incoming: unknown): Index0 { heads: sortRecords(heads), stats: {}, }; - merged.stats = recomputeIndex0Stats(merged as unknown as Dict); + merged.stats = recomputeIndex0Stats(merged); return merged; } diff --git a/js/packages/core/src/vault/portableBackup.ts b/js/packages/core/src/vault/portableBackup.ts index 532bc8a4..be0c2c11 100644 --- a/js/packages/core/src/vault/portableBackup.ts +++ b/js/packages/core/src/vault/portableBackup.ts @@ -12,6 +12,7 @@ import { base64 } from "@scure/base"; import { z } from "zod"; import { canonicalJson, canonicalHash } from "../sync/canonical.js"; import { deriveIndexKeyBytes } from "./indexKey.js"; +import type { VaultIndex } from "../schema/entries.js"; import { decryptPayload } from "./payload.js"; import { encryptV3 } from "./aead.js"; import { utf8 } from "../util/bytes.js"; @@ -102,7 +103,11 @@ export async function importBackup( /** Export an index as a portable backup wrapper (format_version 1). */ export async function exportBackup( - index: Record, + // A live VaultIndex or a round-tripped backup payload: both are passed, and + // the function treats either as an opaque JSON document to canonicalize and + // encrypt. Naming both is what lets a caller holding a real index hand it + // over without laundering it through `as unknown as Record`. + index: VaultIndex | Record, options: { mnemonic: string; fingerprint: string;