Files
SeedPass/js/packages/core/src/index.ts
T
thePR0M3TH3ANandClaude Fable 5 55879eaa1b security: durable vault writes, path validation, and safe restore semantics
Continues the security-review fixes.

Durability and concurrency:
- Vault, config, fingerprint-registry and parent-seed writes are now atomic:
  temp file in the same directory, fsync, rename, then an fsync of the
  directory. Previously the target was truncated and rewritten in place, so
  a crash could destroy the vault.
- The lock now spans the whole read-modify-write cycle, not just the write.
  Serializing writes alone still lost changes -- two commands each read the
  same index, mutated their copy, and wrote in turn. Every mutating command
  goes through mutateVault(), which holds the lock from open to save. A test
  runs five concurrent writers and asserts all five entries survive.
- 0600 is now enforced on existing files too: writeFile's mode argument only
  applies on creation, so a loosened vault stayed loosened.
- The index is schema-validated before it can reach disk.

Path handling:
- Profile fingerprints are validated (16 uppercase hex) before being used in
  any path, and removeProfile additionally checks the resolved directory
  still sits directly under the app directory. A tampered registry could
  otherwise have steered a recursive delete elsewhere.
- Malformed entries in fingerprints.json are dropped on read.

Restore semantics:
- `nostr restore` merges remote state into the local vault instead of
  replacing it. Replacing silently discarded entries created since the last
  sync; that now requires --replace --yes, and any overwritten vault is
  copied aside first.
- Deltas are bound to the manifest they extend (filtered on the e tag, and
  re-checked locally), so a relay cannot replay a validly-signed delta from
  another snapshot lineage into a restore.
- Manifest selection breaks created_at ties by relay order, so a same-second
  republish cannot leave a restore on a stale snapshot.
- mergeIndexPayloads gained an index0 option. The default still refuses to
  merge populated Python atlas state rather than drop it; restore uses
  preserve-current, keeping the local block verbatim, because Python
  recomputes canonical views on load and the default would block every
  restore into a Python-created profile.

193 core + 69 CLI tests green; 36/36 cross-implementation checks green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 23:10:20 -04:00

173 lines
3.8 KiB
TypeScript

/**
* @seedpass/core — SeedPass protocol core (TypeScript port).
*
* Python remains the normative reference until the compatibility matrix is
* green; see docs/typescript_web_extension_port_plan.md.
*/
export {
Bip85,
isValidMnemonic,
assertValidMnemonic,
type DeriveEntropyOptions,
} from "./derive/bip85.js";
export {
generatePassword,
passwordPolicyFromRecord,
derivePasswordDk,
type PasswordPolicy,
LEGACY_PASSWORD_GEN_VERSION,
CURRENT_PASSWORD_GEN_VERSION,
MIN_PASSWORD_LENGTH,
MAX_PASSWORD_LENGTH,
DEFAULT_PASSWORD_LENGTH,
SAFE_SPECIAL_CHARS,
} from "./derive/password.js";
export { deriveTotpSecret, totpCodeAt, hotp } from "./derive/totp.js";
export {
deriveNostrKeys,
hexToBech32,
bech32ToHex,
NOSTR_KEY_APP_ID,
type NostrKeys,
} from "./derive/nostr.js";
export { generateFingerprint } from "./derive/fingerprint.js";
export {
derivePgpKey,
pgpFingerprint,
PGP_CREATED_AT,
type PgpKeyPair,
} from "./derive/pgp.js";
export {
deriveSshKeyPair,
deriveSshEntropy,
sshPublicKeyOpenSsh,
type SshKeyPair,
} from "./derive/ssh.js";
export { deriveIndexKey, deriveIndexKeyBytes } from "./vault/indexKey.js";
export { decryptV3, encryptV3, isV3Payload } from "./vault/aead.js";
export {
deriveKeyFromPassword,
deriveKeyFromPasswordArgon2,
kdfConfigSchema,
type KdfConfig,
} from "./vault/passwordKdf.js";
export { fernetDecrypt, FernetError } from "./vault/fernet.js";
export { decryptPayload, parseEncryptedFile, type EncryptedFile } from "./vault/payload.js";
export {
entrySchema,
vaultIndexSchema,
parseVaultIndex,
customFieldSchema,
passwordEntrySchema,
totpEntrySchema,
sshEntrySchema,
seedEntrySchema,
pgpEntrySchema,
nostrEntrySchema,
keyValueEntrySchema,
managedAccountEntrySchema,
documentEntrySchema,
UnsupportedSchemaVersionError,
CURRENT_SCHEMA_VERSION,
type Entry,
type VaultIndex,
} from "./schema/entries.js";
export {
applyMigrations,
needsMigration,
SchemaMigrationError,
} from "./schema/migrations.js";
export {
importBackup,
exportBackup,
portableBackupSchema,
BackupImportError,
PORTABLE_FORMAT_VERSION,
type PortableBackup,
} from "./vault/portableBackup.js";
export {
addPasswordEntry,
addTotpDeterministic,
addTotpImported,
addSshKeyEntry,
addNostrKeyEntry,
addKeyValueEntry,
addDocumentEntry,
addSeedEntry,
addManagedAccountEntry,
addPgpKeyEntry,
nextIndex,
nextTotpIndex,
isoFromUnix,
systemClock,
type Clock,
} from "./vault/entryOps.js";
export {
modifyEntry,
archiveEntry,
restoreEntry,
addLink,
removeLink,
getLinks,
normalizeLinks,
type ModifyChanges,
type EntryLink,
type ResolvedLink,
} from "./vault/entryMod.js";
export { canonicalJson, canonicalHash } from "./sync/canonical.js";
export {
mergeIndexPayloads,
emptyIndex0,
safeInt,
TOMBSTONE_RETENTION_CAP,
MERGE_STRATEGY,
type MergeOptions,
} from "./sync/merge.js";
export {
KIND_MANIFEST,
KIND_SNAPSHOT_CHUNK,
KIND_DELTA,
chunkMetaSchema,
manifestSchema,
parseManifest,
prepareSnapshot,
reassembleSnapshot,
gzipCompress,
gzipDecompress,
deriveKeyIndex,
manifestIdFromNonce,
newManifestId,
ChunkVerificationError,
type ChunkMeta,
type Manifest,
} from "./sync/snapshot.js";
export {
computeEventId,
serializeEventForId,
signEvent,
verifyEvent,
signerPublicKeyHex,
buildChunkEvent,
buildManifestEvent,
buildDeltaEvent,
reqMessage,
eventMessage,
closeMessage,
parseRelayMessage,
type NostrEvent,
type UnsignedEvent,
type Filter,
type RelayMessage,
} from "./sync/events.js";
export { RelayPool, type PublishResult, type RelayPoolOptions } from "./sync/relay.js";
export {
publishSnapshot,
fetchLatestSnapshot,
publishDelta,
fetchDeltasSince,
type PublishedSnapshot,
type FetchedSnapshot,
} from "./sync/syncFlows.js";
export * from "./util/bytes.js";