@noy-db/to-file 0.6.0 → 0.7.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ interface ExportBlobsToDirectoryResult {
|
|
|
79
79
|
* summary suitable for logging / audit.
|
|
80
80
|
*
|
|
81
81
|
* Caller MUST already hold whatever capability the vault demands
|
|
82
|
-
* (`
|
|
82
|
+
* (`assertCanExport('plaintext', 'blob')`) — this function delegates to
|
|
83
83
|
* `vault.exportBlobs()`, which performs the capability check itself.
|
|
84
84
|
*/
|
|
85
85
|
declare function exportBlobsToDirectory(vault: Vault, targetDir: string, options?: ExportBlobsToDirectoryOptions): Promise<ExportBlobsToDirectoryResult>;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/atomic-write.ts","../src/export-blobs-to-directory.ts"],"sourcesContent":["/**\n * **@noy-db/to-file** — JSON file store for NOYDB (USB / local disk).\n *\n * Maps the NOYDB hierarchy directly to the filesystem:\n *\n * ```\n * {dir}/\n * {vault}/\n * {collection}/\n * {id}.json ← EncryptedEnvelope, optionally pretty-printed\n * _keyring/\n * {userId}.json ← wrapped DEKs for this user\n * _sync/\n * meta.json ← sync metadata\n * ```\n *\n * ## When to use\n *\n * - **USB stick workflow** — the data directory lives on a removable drive.\n * Plug in, unlock, work offline, eject. No cloud dependency.\n * - **Local development** — simple, inspectable files; no Docker or cloud\n * credentials required.\n * - **Single-user desktop apps** — Electron, Tauri, or any Node.js app that\n * writes to a local directory.\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `false` — no atomic compare-and-swap at the FS layer |\n * | `serverWriteTime` | `true` — local filesystem clock; solo-writer only |\n * | `listVaults` | ✓ — enumerates subdirectories |\n * | `listPage` | ✓ — cursor-based pagination over sorted filenames |\n * | `ping` | ✓ — `stat(dir)` |\n *\n * ## Atomicity\n *\n * The filesystem has `rename` but no atomic CAS — `casAtomic` is `false`,\n * and the `expectedVersion` check is read-then-write, so it is advisory\n * under concurrent writers. Per-record writes do go through\n * `{id}.json.{pid}.{n}.tmp` + rename, so a write interrupted partway (a\n * laptop dropping Wi-Fi mid-write to a mounted share, a USB stick pulled\n * during a flush) can never leave a truncated `{id}.json` behind — readers\n * see the complete previous file or the complete new one. Orphaned `.tmp`\n * sidecars from a crashed process are invisible to `list`, `listPage` and\n * `loadAll`, which only accept `.json`.\n *\n * This is atomicity of *visibility*, not durability: surviving a power cut\n * would additionally require fsyncing the file and its directory, which is\n * deliberately not paid per record.\n *\n * ## Pod helpers\n *\n * {@link savePod} and {@link loadPod} are thin wrappers around the hub\n * `writePod` / `readPod` primitives that pipe bytes to/from `node:fs`.\n *\n * @packageDocumentation\n */\n\nimport { readFile, mkdir, readdir, unlink, stat } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { atomicWrite } from './atomic-write.js'\nimport type {\n NoydbStore,\n EncryptedEnvelope,\n VaultSnapshot,\n StoreDescriptor,\n StoreFactory,\n StoreLocator,\n} from '@noy-db/hub/to'\nimport { ConflictError } from '@noy-db/hub/to'\nimport type {\n Vault,\n WritePodOptions,\n PodReadResult,\n} from '@noy-db/hub'\nimport { writePod, readPod } from '@noy-db/hub'\n\n/**\n * Options for `toFile()`.\n *\n * Files are laid out as `{dir}/{vault}/{collection}/{id}.json`.\n * Internal collections (`_keyring`, `_sync`) follow the same pattern\n * under their vault directory.\n */\nexport interface JsonFileOptions {\n /** Base directory for NOYDB data. */\n dir: string\n /** Pretty-print JSON files. Default: true. */\n pretty?: boolean\n /** Clock uncertainty bound (ms). Default: 0. */\n clockUncertaintyMs?: number\n}\n\n/**\n * Create a JSON file adapter.\n * Maps the NOYDB hierarchy to the filesystem:\n *\n * ```\n * {dir}/{vault}/{collection}/{id}.json\n * {dir}/{vault}/_keyring/{userId}.json\n * ```\n */\nexport function toFile(options: JsonFileOptions): NoydbStore {\n const { dir, pretty = true } = options\n\n function recordPath(vault: string, collection: string, id: string): string {\n return join(dir, vault, collection, `${id}.json`)\n }\n\n function collectionDir(vault: string, collection: string): string {\n return join(dir, vault, collection)\n }\n\n async function ensureDir(path: string): Promise<void> {\n await mkdir(path, { recursive: true })\n }\n\n async function fileExists(path: string): Promise<boolean> {\n try {\n await stat(path)\n return true\n } catch {\n return false\n }\n }\n\n function serialize(envelope: EncryptedEnvelope): string {\n return pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)\n }\n\n return {\n name: 'file',\n capabilities: {\n casAtomic: false,\n serverWriteTime: true,\n auth: { kind: 'filesystem', required: false, flow: 'static' },\n },\n\n async getStoreTime() {\n const now = Date.now()\n const ε = options.clockUncertaintyMs ?? 0\n return { earliest: now - ε, latest: now + ε }\n },\n\n async get(vault, collection, id) {\n const path = recordPath(vault, collection, id)\n try {\n const content = await readFile(path, 'utf-8')\n return JSON.parse(content) as EncryptedEnvelope\n } catch {\n return null\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const path = recordPath(vault, collection, id)\n\n if (expectedVersion !== undefined && await fileExists(path)) {\n const existing = JSON.parse(await readFile(path, 'utf-8')) as EncryptedEnvelope\n if (existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await ensureDir(collectionDir(vault, collection))\n await atomicWrite(path, serialize(envelope))\n },\n\n async delete(vault, collection, id) {\n const path = recordPath(vault, collection, id)\n try {\n await unlink(path)\n } catch {\n // File doesn't exist — that's fine\n }\n },\n\n async list(vault, collection) {\n const dirPath = collectionDir(vault, collection)\n try {\n const entries = await readdir(dirPath)\n return entries\n .filter(f => f.endsWith('.json'))\n .map(f => f.slice(0, -5)) // remove .json extension\n } catch {\n return []\n }\n },\n\n async loadAll(vault) {\n const compDir = join(dir, vault)\n const snapshot: VaultSnapshot = {}\n\n try {\n const collections = await readdir(compDir)\n for (const collName of collections) {\n if (collName.startsWith('_')) continue // skip _keyring, _sync\n const collPath = join(compDir, collName)\n const collStat = await stat(collPath)\n if (!collStat.isDirectory()) continue\n\n const records: Record<string, EncryptedEnvelope> = {}\n const files = await readdir(collPath)\n for (const file of files) {\n if (!file.endsWith('.json')) continue\n const id = file.slice(0, -5)\n const content = await readFile(join(collPath, file), 'utf-8')\n records[id] = JSON.parse(content) as EncryptedEnvelope\n }\n snapshot[collName] = records\n }\n } catch {\n // Directory doesn't exist — return empty snapshot\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collName, records] of Object.entries(data)) {\n const collDir = collectionDir(vault, collName)\n await ensureDir(collDir)\n for (const [id, envelope] of Object.entries(records)) {\n await atomicWrite(join(collDir, `${id}.json`), serialize(envelope))\n }\n }\n },\n\n async ping() {\n try {\n await stat(dir)\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Enumerate every top-level vault subdirectory under the\n * configured base directory. Used by\n * `Noydb.listAccessibleVaults()`.\n *\n * The implementation is `readdir(dir)` filtered to entries that\n * are themselves directories — files at the top level (READMEs,\n * .DS_Store, etc.) are skipped, and missing base directory\n * returns an empty array rather than throwing. Result order is\n * filesystem-defined; consumers that want stable order should\n * sort themselves.\n */\n async listVaults() {\n let entries: string[]\n try {\n entries = await readdir(dir)\n } catch {\n return []\n }\n const compartments: string[] = []\n for (const entry of entries) {\n try {\n const entryStat = await stat(join(dir, entry))\n if (entryStat.isDirectory()) compartments.push(entry)\n } catch {\n // Entry vanished between readdir and stat — skip silently.\n }\n }\n return compartments\n },\n\n /**\n * Paginate over a collection. Cursor is a numeric offset (as a string)\n * into the sorted filename list. Files are sorted alphabetically so\n * pages are stable across runs and across processes that share the\n * same data directory.\n *\n * The default `limit` is 100. Each item carries its decoded envelope\n * so callers don't need an extra `get()` round-trip per id.\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const dirPath = collectionDir(vault, collection)\n let files: string[]\n try {\n files = await readdir(dirPath)\n } catch {\n return { items: [], nextCursor: null }\n }\n\n const ids = files\n .filter(f => f.endsWith('.json'))\n .map(f => f.slice(0, -5))\n .sort()\n\n const start = cursor ? parseInt(cursor, 10) : 0\n const end = Math.min(start + limit, ids.length)\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (let i = start; i < end; i++) {\n const id = ids[i]!\n try {\n const content = await readFile(join(dirPath, `${id}.json`), 'utf-8')\n items.push({ id, envelope: JSON.parse(content) as EncryptedEnvelope })\n } catch {\n // File disappeared between readdir and readFile — skip silently.\n }\n }\n\n return {\n items,\n nextCursor: end < ids.length ? String(end) : null,\n }\n },\n }\n}\n\n// ─── Store-locator descriptor (#945 — `local`-class reference impl) ──\n\n/**\n * Builds the `StoreDescriptor` form of a `toFile()` store: `kind: 'file'`,\n * `class: 'local'`, and a serializable `address` carrying the base\n * directory (same value `JsonFileOptions.dir` would take directly).\n *\n * Credentialless — `to-file` never needs a `StoreCredentialSource`.\n */\nexport function fileStoreDescriptor(dir: string): StoreDescriptor {\n return { kind: 'file', class: 'local', address: { dir } }\n}\n\n/**\n * `StoreFactory` for `to-file`: reconstructs the same store `toFile()`\n * builds, from a `StoreDescriptor` (as produced by {@link fileStoreDescriptor}).\n *\n * `to-file` is a credentialless local store, so `opts.credentials` is\n * unused. `opts.binding` may carry a device-local directory override — a\n * bare string or `{ dir }` — applied in place of `descriptor.address.dir`\n * when present (e.g. a different mount point on this device than what the\n * descriptor was authored with).\n */\nexport const fileStoreFactory: StoreFactory = (descriptor, opts) => {\n const address = descriptor.address as { dir: string }\n const binding = opts.binding as { dir?: string } | string | undefined\n\n let dir: string\n if (typeof binding === 'string') {\n dir = binding\n } else if (binding && binding.dir !== undefined) {\n dir = binding.dir\n } else {\n dir = address.dir\n }\n\n return toFile({ dir })\n}\n\n/** Registers {@link fileStoreFactory} under the `'file'` kind on `locator`. */\nexport function registerFileStore(locator: StoreLocator): void {\n locator.register('file', fileStoreFactory)\n}\n\n// ─── .noydb pod helpers ────────────────────────────────────\n\n/**\n * Write a `.noydb` container for a vault to a local file.\n *\n * Thin wrapper around `writePod` from `@noy-db/hub` — the hub\n * primitive returns a `Uint8Array`, this helper just pipes it to\n * disk after ensuring the parent directory exists. Use the same\n * options as the hub primitive.\n *\n * **Path convention** is up to the caller — `.noydb` is the\n * recommended extension. Consumers using cloud-sync folders\n * should name files by the bundle handle (available via\n * `vault.getPodHandle()`) rather than the vault\n * name to avoid leaking metadata at the filesystem layer:\n *\n * ```ts\n * const handle = await company.getPodHandle()\n * await savePod(`./pods/${handle}.noydb`, company)\n * ```\n *\n * The container is staged in a `.tmp` sidecar and renamed into\n * place (#1040), so a reader — or a cloud-sync daemon watching the\n * folder — never observes a partially-written pod under its final\n * name. A pod is past `PIPE_BUF` essentially always, so the\n * previous bare `writeFile` genuinely did race with concurrent\n * readers despite the docstring that claimed otherwise.\n */\nexport async function savePod(\n path: string,\n vault: Vault,\n opts: WritePodOptions = {},\n): Promise<void> {\n const bytes = await writePod(vault, opts)\n // Ensure the parent directory exists — the write does NOT\n // create intermediate directories on its own. Recursive mkdir\n // is a no-op when the directory already exists.\n await mkdir(dirname(path), { recursive: true })\n await atomicWrite(path, bytes)\n}\n\n/**\n * Read and verify a `.noydb` container from a local file.\n *\n * Returns the parsed header plus the unwrapped `dump()` JSON\n * string ready to feed to `vault.load(json, secret)`.\n * Throws `PodIntegrityError` from `@noy-db/hub` if the body\n * bytes don't match the integrity hash declared in the header\n * (the pod was modified between write and read), or any\n * format error from the hub reader if the bytes aren't a valid\n * pod at all.\n *\n * Does NOT take a secret — the pod reader is purely a\n * format layer. Restoring a vault from the returned dump\n * JSON requires a separate `vault.load()` call with the\n * secret, mirroring the split between\n * `readPod()` and `vault.load()` in hub.\n */\nexport async function loadPod(path: string): Promise<PodReadResult> {\n const bytes = await readFile(path)\n // node:fs.readFile returns a Buffer, which is a Uint8Array\n // subclass — `readPod` accepts Uint8Array directly,\n // no copy needed.\n return readPod(bytes)\n}\n\n\n// Export-blobs FS materializer — wraps `vault.exportBlobs()` with\n// target-profile filename sanitization, Zip-Slip path containment, and\n// collision policy. Lives in `to-file` (not core) because hub stays\n// portable across browser/Node and shouldn't import `node:fs`.\nexport {\n exportBlobsToDirectory,\n} from './export-blobs-to-directory.js'\nexport type {\n ExportBlobsToDirectoryOptions,\n ExportBlobsToDirectoryResult,\n CollisionStrategy,\n} from './export-blobs-to-directory.js'\n","import { writeFile, unlink, rename } from 'node:fs/promises'\n\n/** Disambiguates concurrent temp files within a single process. */\nlet tmpCounter = 0\n\n/**\n * Write `content` to `path` without ever exposing a partial file under\n * that name: stage the bytes in a sidecar, then `rename` over the target.\n *\n * `rename(2)` is atomic within a directory on POSIX, and `fs.rename`\n * replaces the target atomically on Windows, so a reader sees either the\n * complete old file or the complete new one — never a truncation. This\n * matters most on the network drives `to-file` advertises: a laptop losing\n * Wi-Fi mid-write to a mounted share would otherwise leave a `{id}.json`\n * that no longer parses, which fails `loadAll()` for the whole vault\n * rather than for the one record.\n *\n * The sidecar carries pid + a process-local counter so concurrent writes\n * (same process, or several machines on one share) never collide. Its name\n * does not end in `.json`, which is what keeps an orphan from a crashed\n * process invisible to `list`, `listPage` and `loadAll` — pinned by a test,\n * since those filters are what make the sidecar safe.\n *\n * Atomicity of *visibility* only. Surviving a power cut additionally needs\n * the file and its directory fsynced, which is deliberately not paid per\n * record here.\n */\nexport async function atomicWrite(\n path: string,\n content: string | Uint8Array,\n): Promise<void> {\n const tmp = `${path}.${process.pid}.${tmpCounter++}.tmp`\n try {\n await writeFile(tmp, content)\n await rename(tmp, path)\n } catch (err) {\n // Leave no residue behind on our own failure path.\n await unlink(tmp).catch(() => {})\n throw err\n }\n}\n","/**\n * `exportBlobsToDirectory(vault, targetDir, opts)` — bulk blob\n * extraction into a real filesystem directory, with target-profile\n * filename sanitization and Zip-Slip path containment built in\n *.\n *\n * Wraps `vault.exportBlobs()` (the framework-agnostic async iterable\n * in core) with the FS-write concerns that don't belong in core:\n *\n * - sanitize filenames per a target profile (`posix`, `windows`,\n * `macos-smb`, `zip`, `url-path`, `s3-key`, `opaque`),\n * - guard against path-escape after sanitization (`PathEscapeError`),\n * - resolve filename collisions (`suffix` / `overwrite` / `fail` /\n * custom callback),\n * - emit a sidecar `manifest.json` when the profile is `'opaque'`,\n * mapping opaque ids back to the original record-supplied\n * filenames.\n *\n * @module\n */\n\nimport { mkdir } from 'node:fs/promises'\nimport { atomicWrite } from './atomic-write.js'\nimport { resolve, sep, dirname, extname } from 'node:path'\nimport type { Vault } from '@noy-db/hub'\nimport { PathEscapeError } from '@noy-db/hub'\nimport { sanitizeFilename, type FilenameProfile } from '@noy-db/hub/util'\n\n/** Strategy for resolving two records that sanitize to the same name. */\nexport type CollisionStrategy =\n | 'suffix'\n | 'overwrite'\n | 'fail'\n | ((existing: string, attempt: number) => string)\n\nexport interface ExportBlobsToDirectoryOptions {\n /**\n * Filename profile to sanitize against. Default: `'macos-smb'` —\n * the most restrictive intersection of the rules adopters\n * typically hit. Pick a more specific profile when you know the\n * exact destination.\n */\n readonly filenameProfile?: FilenameProfile\n /**\n * How to handle two blobs whose sanitized filenames collide.\n * Default: `'suffix'`.\n */\n readonly onCollision?: CollisionStrategy\n /**\n * Optional collection allowlist forwarded to `vault.exportBlobs`.\n */\n readonly collections?: readonly string[]\n /**\n * Optional record predicate forwarded to `vault.exportBlobs`.\n */\n readonly where?: (\n record: unknown,\n context: { collection: string; id: string },\n ) => boolean\n /**\n * Optional resume cursor forwarded to `vault.exportBlobs`.\n */\n readonly afterBlobId?: string\n /**\n * External abort signal forwarded to `vault.exportBlobs`.\n */\n readonly signal?: AbortSignal\n}\n\nexport interface ExportBlobsToDirectoryResult {\n /** Total blobs written. */\n readonly written: number\n /** Total bytes written across all blobs. */\n readonly bytes: number\n /** Pairs of `{ blobId, path }` for every blob that landed on disk. */\n readonly entries: ReadonlyArray<{ blobId: string; path: string }>\n /**\n * When `filenameProfile === 'opaque'`, the absolute path of the\n * `manifest.json` sidecar. `null` for every other profile.\n */\n readonly manifestPath: string | null\n}\n\ninterface OpaqueManifestEntry {\n readonly opaqueName: string\n readonly originalName: string\n readonly collection: string\n readonly recordId: string\n readonly slot: string\n readonly blobId: string\n readonly mimeType?: string\n}\n\n/**\n * Materialize every blob in the vault into `targetDir`. Returns a\n * summary suitable for logging / audit.\n *\n * Caller MUST already hold whatever capability the vault demands\n * (`canExportPlaintext['blob']`) — this function delegates to\n * `vault.exportBlobs()`, which performs the capability check itself.\n */\nexport async function exportBlobsToDirectory(\n vault: Vault,\n targetDir: string,\n options: ExportBlobsToDirectoryOptions = {},\n): Promise<ExportBlobsToDirectoryResult> {\n const profile: FilenameProfile = options.filenameProfile ?? 'macos-smb'\n const onCollision: CollisionStrategy = options.onCollision ?? 'suffix'\n\n const absTargetDir = resolve(targetDir)\n await mkdir(absTargetDir, { recursive: true })\n const containmentPrefix = absTargetDir + sep\n\n // Track filenames already used in this run so collision resolution\n // is deterministic and cheap (no extra stat() per attempt).\n const used = new Set<string>()\n const entries: { blobId: string; path: string }[] = []\n const opaqueEntries: OpaqueManifestEntry[] = []\n let totalBytes = 0\n\n const handle = vault.exportBlobs({\n ...(options.collections && { collections: options.collections }),\n ...(options.where && { where: options.where }),\n ...(options.afterBlobId && { afterBlobId: options.afterBlobId }),\n ...(options.signal && { signal: options.signal }),\n })\n\n for await (const blob of handle) {\n const original = blob.meta.filename\n const sanitizeOpts =\n profile === 'opaque'\n ? { profile, opaqueId: blob.blobId } as const\n : { profile } as const\n const candidate = sanitizeFilename(original, sanitizeOpts)\n const finalName = resolveCollision(candidate, used, onCollision)\n used.add(finalName)\n\n const absPath = resolve(absTargetDir, finalName)\n if (absPath !== absTargetDir && !absPath.startsWith(containmentPrefix)) {\n throw new PathEscapeError({ attempted: finalName, targetDir: absTargetDir })\n }\n\n await mkdir(dirname(absPath), { recursive: true })\n await atomicWrite(absPath, blob.bytes)\n entries.push({ blobId: blob.blobId, path: absPath })\n totalBytes += blob.bytes.byteLength\n\n if (profile === 'opaque') {\n const entry: OpaqueManifestEntry = {\n opaqueName: finalName,\n originalName: original,\n collection: blob.recordRef.collection,\n recordId: blob.recordRef.id,\n slot: blob.recordRef.slot,\n blobId: blob.blobId,\n ...(blob.meta.mimeType !== undefined && { mimeType: blob.meta.mimeType }),\n }\n opaqueEntries.push(entry)\n }\n }\n\n let manifestPath: string | null = null\n if (profile === 'opaque') {\n manifestPath = resolve(absTargetDir, 'manifest.json')\n const json = JSON.stringify(\n {\n format: 'noydb-opaque-export',\n version: 1,\n entries: opaqueEntries,\n },\n null,\n 2,\n )\n await atomicWrite(manifestPath, json)\n }\n\n return {\n written: entries.length,\n bytes: totalBytes,\n entries,\n manifestPath,\n }\n}\n\nfunction resolveCollision(\n candidate: string,\n used: Set<string>,\n strategy: CollisionStrategy,\n): string {\n if (!used.has(candidate)) return candidate\n if (strategy === 'overwrite') return candidate\n if (strategy === 'fail') {\n throw new Error(`exportBlobsToDirectory: filename collision on \"${candidate}\"`)\n }\n // `'suffix'` and the function-callback path both build a sequence\n // of attempts and pick the first non-colliding one.\n for (let attempt = 1; attempt < 10_000; attempt++) {\n const next =\n typeof strategy === 'function'\n ? strategy(candidate, attempt)\n : addSuffix(candidate, attempt)\n if (!used.has(next)) return next\n }\n throw new Error(`exportBlobsToDirectory: collision suffix exhausted for \"${candidate}\"`)\n}\n\nfunction addSuffix(name: string, attempt: number): string {\n const ext = extname(name)\n if (ext.length > 0 && ext.length < name.length) {\n const stem = name.slice(0, name.length - ext.length)\n return `${stem}-${attempt}${ext}`\n }\n return `${name}-${attempt}`\n}\n"],"mappings":"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","names":["mkdir","unlink","dirname","mkdir","unlink","dirname"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/atomic-write.ts","../src/export-blobs-to-directory.ts"],"sourcesContent":["/**\n * **@noy-db/to-file** — JSON file store for NOYDB (USB / local disk).\n *\n * Maps the NOYDB hierarchy directly to the filesystem:\n *\n * ```\n * {dir}/\n * {vault}/\n * {collection}/\n * {id}.json ← EncryptedEnvelope, optionally pretty-printed\n * _keyring/\n * {userId}.json ← wrapped DEKs for this user\n * _sync/\n * meta.json ← sync metadata\n * ```\n *\n * ## When to use\n *\n * - **USB stick workflow** — the data directory lives on a removable drive.\n * Plug in, unlock, work offline, eject. No cloud dependency.\n * - **Local development** — simple, inspectable files; no Docker or cloud\n * credentials required.\n * - **Single-user desktop apps** — Electron, Tauri, or any Node.js app that\n * writes to a local directory.\n *\n * ## Capabilities\n *\n * | Capability | Value |\n * |---|---|\n * | `casAtomic` | `false` — no atomic compare-and-swap at the FS layer |\n * | `serverWriteTime` | `true` — local filesystem clock; solo-writer only |\n * | `listVaults` | ✓ — enumerates subdirectories |\n * | `listPage` | ✓ — cursor-based pagination over sorted filenames |\n * | `ping` | ✓ — `stat(dir)` |\n *\n * ## Atomicity\n *\n * The filesystem has `rename` but no atomic CAS — `casAtomic` is `false`,\n * and the `expectedVersion` check is read-then-write, so it is advisory\n * under concurrent writers. Per-record writes do go through\n * `{id}.json.{pid}.{n}.tmp` + rename, so a write interrupted partway (a\n * laptop dropping Wi-Fi mid-write to a mounted share, a USB stick pulled\n * during a flush) can never leave a truncated `{id}.json` behind — readers\n * see the complete previous file or the complete new one. Orphaned `.tmp`\n * sidecars from a crashed process are invisible to `list`, `listPage` and\n * `loadAll`, which only accept `.json`.\n *\n * This is atomicity of *visibility*, not durability: surviving a power cut\n * would additionally require fsyncing the file and its directory, which is\n * deliberately not paid per record.\n *\n * ## Pod helpers\n *\n * {@link savePod} and {@link loadPod} are thin wrappers around the hub\n * `writePod` / `readPod` primitives that pipe bytes to/from `node:fs`.\n *\n * @packageDocumentation\n */\n\nimport { readFile, mkdir, readdir, unlink, stat } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { atomicWrite } from './atomic-write.js'\nimport type {\n NoydbStore,\n EncryptedEnvelope,\n VaultSnapshot,\n StoreDescriptor,\n StoreFactory,\n StoreLocator,\n} from '@noy-db/hub/to'\nimport { ConflictError } from '@noy-db/hub/to'\nimport type {\n Vault,\n WritePodOptions,\n PodReadResult,\n} from '@noy-db/hub'\nimport { writePod, readPod } from '@noy-db/hub'\n\n/**\n * Options for `toFile()`.\n *\n * Files are laid out as `{dir}/{vault}/{collection}/{id}.json`.\n * Internal collections (`_keyring`, `_sync`) follow the same pattern\n * under their vault directory.\n */\nexport interface JsonFileOptions {\n /** Base directory for NOYDB data. */\n dir: string\n /** Pretty-print JSON files. Default: true. */\n pretty?: boolean\n /** Clock uncertainty bound (ms). Default: 0. */\n clockUncertaintyMs?: number\n}\n\n/**\n * Create a JSON file adapter.\n * Maps the NOYDB hierarchy to the filesystem:\n *\n * ```\n * {dir}/{vault}/{collection}/{id}.json\n * {dir}/{vault}/_keyring/{userId}.json\n * ```\n */\nexport function toFile(options: JsonFileOptions): NoydbStore {\n const { dir, pretty = true } = options\n\n function recordPath(vault: string, collection: string, id: string): string {\n return join(dir, vault, collection, `${id}.json`)\n }\n\n function collectionDir(vault: string, collection: string): string {\n return join(dir, vault, collection)\n }\n\n async function ensureDir(path: string): Promise<void> {\n await mkdir(path, { recursive: true })\n }\n\n async function fileExists(path: string): Promise<boolean> {\n try {\n await stat(path)\n return true\n } catch {\n return false\n }\n }\n\n function serialize(envelope: EncryptedEnvelope): string {\n return pretty ? JSON.stringify(envelope, null, 2) : JSON.stringify(envelope)\n }\n\n return {\n name: 'file',\n capabilities: {\n casAtomic: false,\n serverWriteTime: true,\n auth: { kind: 'filesystem', required: false, flow: 'static' },\n },\n\n async getStoreTime() {\n const now = Date.now()\n const ε = options.clockUncertaintyMs ?? 0\n return { earliest: now - ε, latest: now + ε }\n },\n\n async get(vault, collection, id) {\n const path = recordPath(vault, collection, id)\n try {\n const content = await readFile(path, 'utf-8')\n return JSON.parse(content) as EncryptedEnvelope\n } catch {\n return null\n }\n },\n\n async put(vault, collection, id, envelope, expectedVersion) {\n const path = recordPath(vault, collection, id)\n\n if (expectedVersion !== undefined && await fileExists(path)) {\n const existing = JSON.parse(await readFile(path, 'utf-8')) as EncryptedEnvelope\n if (existing._v !== expectedVersion) {\n throw new ConflictError(existing._v, `Version conflict: expected ${expectedVersion}, found ${existing._v}`)\n }\n }\n\n await ensureDir(collectionDir(vault, collection))\n await atomicWrite(path, serialize(envelope))\n },\n\n async delete(vault, collection, id) {\n const path = recordPath(vault, collection, id)\n try {\n await unlink(path)\n } catch {\n // File doesn't exist — that's fine\n }\n },\n\n async list(vault, collection) {\n const dirPath = collectionDir(vault, collection)\n try {\n const entries = await readdir(dirPath)\n return entries\n .filter(f => f.endsWith('.json'))\n .map(f => f.slice(0, -5)) // remove .json extension\n } catch {\n return []\n }\n },\n\n async loadAll(vault) {\n const compDir = join(dir, vault)\n const snapshot: VaultSnapshot = {}\n\n try {\n const collections = await readdir(compDir)\n for (const collName of collections) {\n if (collName.startsWith('_')) continue // skip _keyring, _sync\n const collPath = join(compDir, collName)\n const collStat = await stat(collPath)\n if (!collStat.isDirectory()) continue\n\n const records: Record<string, EncryptedEnvelope> = {}\n const files = await readdir(collPath)\n for (const file of files) {\n if (!file.endsWith('.json')) continue\n const id = file.slice(0, -5)\n const content = await readFile(join(collPath, file), 'utf-8')\n records[id] = JSON.parse(content) as EncryptedEnvelope\n }\n snapshot[collName] = records\n }\n } catch {\n // Directory doesn't exist — return empty snapshot\n }\n\n return snapshot\n },\n\n async saveAll(vault, data) {\n for (const [collName, records] of Object.entries(data)) {\n const collDir = collectionDir(vault, collName)\n await ensureDir(collDir)\n for (const [id, envelope] of Object.entries(records)) {\n await atomicWrite(join(collDir, `${id}.json`), serialize(envelope))\n }\n }\n },\n\n async ping() {\n try {\n await stat(dir)\n return true\n } catch {\n return false\n }\n },\n\n /**\n * Enumerate every top-level vault subdirectory under the\n * configured base directory. Used by\n * `Noydb.listAccessibleVaults()`.\n *\n * The implementation is `readdir(dir)` filtered to entries that\n * are themselves directories — files at the top level (READMEs,\n * .DS_Store, etc.) are skipped, and missing base directory\n * returns an empty array rather than throwing. Result order is\n * filesystem-defined; consumers that want stable order should\n * sort themselves.\n */\n async listVaults() {\n let entries: string[]\n try {\n entries = await readdir(dir)\n } catch {\n return []\n }\n const compartments: string[] = []\n for (const entry of entries) {\n try {\n const entryStat = await stat(join(dir, entry))\n if (entryStat.isDirectory()) compartments.push(entry)\n } catch {\n // Entry vanished between readdir and stat — skip silently.\n }\n }\n return compartments\n },\n\n /**\n * Paginate over a collection. Cursor is a numeric offset (as a string)\n * into the sorted filename list. Files are sorted alphabetically so\n * pages are stable across runs and across processes that share the\n * same data directory.\n *\n * The default `limit` is 100. Each item carries its decoded envelope\n * so callers don't need an extra `get()` round-trip per id.\n */\n async listPage(vault, collection, cursor, limit = 100) {\n const dirPath = collectionDir(vault, collection)\n let files: string[]\n try {\n files = await readdir(dirPath)\n } catch {\n return { items: [], nextCursor: null }\n }\n\n const ids = files\n .filter(f => f.endsWith('.json'))\n .map(f => f.slice(0, -5))\n .sort()\n\n const start = cursor ? parseInt(cursor, 10) : 0\n const end = Math.min(start + limit, ids.length)\n\n const items: Array<{ id: string; envelope: EncryptedEnvelope }> = []\n for (let i = start; i < end; i++) {\n const id = ids[i]!\n try {\n const content = await readFile(join(dirPath, `${id}.json`), 'utf-8')\n items.push({ id, envelope: JSON.parse(content) as EncryptedEnvelope })\n } catch {\n // File disappeared between readdir and readFile — skip silently.\n }\n }\n\n return {\n items,\n nextCursor: end < ids.length ? String(end) : null,\n }\n },\n }\n}\n\n// ─── Store-locator descriptor (#945 — `local`-class reference impl) ──\n\n/**\n * Builds the `StoreDescriptor` form of a `toFile()` store: `kind: 'file'`,\n * `class: 'local'`, and a serializable `address` carrying the base\n * directory (same value `JsonFileOptions.dir` would take directly).\n *\n * Credentialless — `to-file` never needs a `StoreCredentialSource`.\n */\nexport function fileStoreDescriptor(dir: string): StoreDescriptor {\n return { kind: 'file', class: 'local', address: { dir } }\n}\n\n/**\n * `StoreFactory` for `to-file`: reconstructs the same store `toFile()`\n * builds, from a `StoreDescriptor` (as produced by {@link fileStoreDescriptor}).\n *\n * `to-file` is a credentialless local store, so `opts.credentials` is\n * unused. `opts.binding` may carry a device-local directory override — a\n * bare string or `{ dir }` — applied in place of `descriptor.address.dir`\n * when present (e.g. a different mount point on this device than what the\n * descriptor was authored with).\n */\nexport const fileStoreFactory: StoreFactory = (descriptor, opts) => {\n const address = descriptor.address as { dir: string }\n const binding = opts.binding as { dir?: string } | string | undefined\n\n let dir: string\n if (typeof binding === 'string') {\n dir = binding\n } else if (binding && binding.dir !== undefined) {\n dir = binding.dir\n } else {\n dir = address.dir\n }\n\n return toFile({ dir })\n}\n\n/** Registers {@link fileStoreFactory} under the `'file'` kind on `locator`. */\nexport function registerFileStore(locator: StoreLocator): void {\n locator.register('file', fileStoreFactory)\n}\n\n// ─── .noydb pod helpers ────────────────────────────────────\n\n/**\n * Write a `.noydb` container for a vault to a local file.\n *\n * Thin wrapper around `writePod` from `@noy-db/hub` — the hub\n * primitive returns a `Uint8Array`, this helper just pipes it to\n * disk after ensuring the parent directory exists. Use the same\n * options as the hub primitive.\n *\n * **Path convention** is up to the caller — `.noydb` is the\n * recommended extension. Consumers using cloud-sync folders\n * should name files by the bundle handle (available via\n * `vault.getPodHandle()`) rather than the vault\n * name to avoid leaking metadata at the filesystem layer:\n *\n * ```ts\n * const handle = await company.getPodHandle()\n * await savePod(`./pods/${handle}.noydb`, company)\n * ```\n *\n * The container is staged in a `.tmp` sidecar and renamed into\n * place (#1040), so a reader — or a cloud-sync daemon watching the\n * folder — never observes a partially-written pod under its final\n * name. A pod is past `PIPE_BUF` essentially always, so the\n * previous bare `writeFile` genuinely did race with concurrent\n * readers despite the docstring that claimed otherwise.\n */\nexport async function savePod(\n path: string,\n vault: Vault,\n opts: WritePodOptions = {},\n): Promise<void> {\n const bytes = await writePod(vault, opts)\n // Ensure the parent directory exists — the write does NOT\n // create intermediate directories on its own. Recursive mkdir\n // is a no-op when the directory already exists.\n await mkdir(dirname(path), { recursive: true })\n await atomicWrite(path, bytes)\n}\n\n/**\n * Read and verify a `.noydb` container from a local file.\n *\n * Returns the parsed header plus the unwrapped `dump()` JSON\n * string ready to feed to `vault.load(json, secret)`.\n * Throws `PodIntegrityError` from `@noy-db/hub` if the body\n * bytes don't match the integrity hash declared in the header\n * (the pod was modified between write and read), or any\n * format error from the hub reader if the bytes aren't a valid\n * pod at all.\n *\n * Does NOT take a secret — the pod reader is purely a\n * format layer. Restoring a vault from the returned dump\n * JSON requires a separate `vault.load()` call with the\n * secret, mirroring the split between\n * `readPod()` and `vault.load()` in hub.\n */\nexport async function loadPod(path: string): Promise<PodReadResult> {\n const bytes = await readFile(path)\n // node:fs.readFile returns a Buffer, which is a Uint8Array\n // subclass — `readPod` accepts Uint8Array directly,\n // no copy needed.\n return readPod(bytes)\n}\n\n\n// Export-blobs FS materializer — wraps `vault.exportBlobs()` with\n// target-profile filename sanitization, Zip-Slip path containment, and\n// collision policy. Lives in `to-file` (not core) because hub stays\n// portable across browser/Node and shouldn't import `node:fs`.\nexport {\n exportBlobsToDirectory,\n} from './export-blobs-to-directory.js'\nexport type {\n ExportBlobsToDirectoryOptions,\n ExportBlobsToDirectoryResult,\n CollisionStrategy,\n} from './export-blobs-to-directory.js'\n","import { writeFile, unlink, rename } from 'node:fs/promises'\n\n/** Disambiguates concurrent temp files within a single process. */\nlet tmpCounter = 0\n\n/**\n * Write `content` to `path` without ever exposing a partial file under\n * that name: stage the bytes in a sidecar, then `rename` over the target.\n *\n * `rename(2)` is atomic within a directory on POSIX, and `fs.rename`\n * replaces the target atomically on Windows, so a reader sees either the\n * complete old file or the complete new one — never a truncation. This\n * matters most on the network drives `to-file` advertises: a laptop losing\n * Wi-Fi mid-write to a mounted share would otherwise leave a `{id}.json`\n * that no longer parses, which fails `loadAll()` for the whole vault\n * rather than for the one record.\n *\n * The sidecar carries pid + a process-local counter so concurrent writes\n * (same process, or several machines on one share) never collide. Its name\n * does not end in `.json`, which is what keeps an orphan from a crashed\n * process invisible to `list`, `listPage` and `loadAll` — pinned by a test,\n * since those filters are what make the sidecar safe.\n *\n * Atomicity of *visibility* only. Surviving a power cut additionally needs\n * the file and its directory fsynced, which is deliberately not paid per\n * record here.\n */\nexport async function atomicWrite(\n path: string,\n content: string | Uint8Array,\n): Promise<void> {\n const tmp = `${path}.${process.pid}.${tmpCounter++}.tmp`\n try {\n await writeFile(tmp, content)\n await rename(tmp, path)\n } catch (err) {\n // Leave no residue behind on our own failure path.\n await unlink(tmp).catch(() => {})\n throw err\n }\n}\n","/**\n * `exportBlobsToDirectory(vault, targetDir, opts)` — bulk blob\n * extraction into a real filesystem directory, with target-profile\n * filename sanitization and Zip-Slip path containment built in\n *.\n *\n * Wraps `vault.exportBlobs()` (the framework-agnostic async iterable\n * in core) with the FS-write concerns that don't belong in core:\n *\n * - sanitize filenames per a target profile (`posix`, `windows`,\n * `macos-smb`, `zip`, `url-path`, `s3-key`, `opaque`),\n * - guard against path-escape after sanitization (`PathEscapeError`),\n * - resolve filename collisions (`suffix` / `overwrite` / `fail` /\n * custom callback),\n * - emit a sidecar `manifest.json` when the profile is `'opaque'`,\n * mapping opaque ids back to the original record-supplied\n * filenames.\n *\n * @module\n */\n\nimport { mkdir } from 'node:fs/promises'\nimport { atomicWrite } from './atomic-write.js'\nimport { resolve, sep, dirname, extname } from 'node:path'\nimport type { Vault } from '@noy-db/hub'\nimport { PathEscapeError } from '@noy-db/hub'\nimport { sanitizeFilename, type FilenameProfile } from '@noy-db/hub/util'\n\n/** Strategy for resolving two records that sanitize to the same name. */\nexport type CollisionStrategy =\n | 'suffix'\n | 'overwrite'\n | 'fail'\n | ((existing: string, attempt: number) => string)\n\nexport interface ExportBlobsToDirectoryOptions {\n /**\n * Filename profile to sanitize against. Default: `'macos-smb'` —\n * the most restrictive intersection of the rules adopters\n * typically hit. Pick a more specific profile when you know the\n * exact destination.\n */\n readonly filenameProfile?: FilenameProfile\n /**\n * How to handle two blobs whose sanitized filenames collide.\n * Default: `'suffix'`.\n */\n readonly onCollision?: CollisionStrategy\n /**\n * Optional collection allowlist forwarded to `vault.exportBlobs`.\n */\n readonly collections?: readonly string[]\n /**\n * Optional record predicate forwarded to `vault.exportBlobs`.\n */\n readonly where?: (\n record: unknown,\n context: { collection: string; id: string },\n ) => boolean\n /**\n * Optional resume cursor forwarded to `vault.exportBlobs`.\n */\n readonly afterBlobId?: string\n /**\n * External abort signal forwarded to `vault.exportBlobs`.\n */\n readonly signal?: AbortSignal\n}\n\nexport interface ExportBlobsToDirectoryResult {\n /** Total blobs written. */\n readonly written: number\n /** Total bytes written across all blobs. */\n readonly bytes: number\n /** Pairs of `{ blobId, path }` for every blob that landed on disk. */\n readonly entries: ReadonlyArray<{ blobId: string; path: string }>\n /**\n * When `filenameProfile === 'opaque'`, the absolute path of the\n * `manifest.json` sidecar. `null` for every other profile.\n */\n readonly manifestPath: string | null\n}\n\ninterface OpaqueManifestEntry {\n readonly opaqueName: string\n readonly originalName: string\n readonly collection: string\n readonly recordId: string\n readonly slot: string\n readonly blobId: string\n readonly mimeType?: string\n}\n\n/**\n * Materialize every blob in the vault into `targetDir`. Returns a\n * summary suitable for logging / audit.\n *\n * Caller MUST already hold whatever capability the vault demands\n * (`assertCanExport('plaintext', 'blob')`) — this function delegates to\n * `vault.exportBlobs()`, which performs the capability check itself.\n */\nexport async function exportBlobsToDirectory(\n vault: Vault,\n targetDir: string,\n options: ExportBlobsToDirectoryOptions = {},\n): Promise<ExportBlobsToDirectoryResult> {\n const profile: FilenameProfile = options.filenameProfile ?? 'macos-smb'\n const onCollision: CollisionStrategy = options.onCollision ?? 'suffix'\n\n const absTargetDir = resolve(targetDir)\n await mkdir(absTargetDir, { recursive: true })\n const containmentPrefix = absTargetDir + sep\n\n // Track filenames already used in this run so collision resolution\n // is deterministic and cheap (no extra stat() per attempt).\n const used = new Set<string>()\n const entries: { blobId: string; path: string }[] = []\n const opaqueEntries: OpaqueManifestEntry[] = []\n let totalBytes = 0\n\n const handle = vault.exportBlobs({\n ...(options.collections && { collections: options.collections }),\n ...(options.where && { where: options.where }),\n ...(options.afterBlobId && { afterBlobId: options.afterBlobId }),\n ...(options.signal && { signal: options.signal }),\n })\n\n for await (const blob of handle) {\n const original = blob.meta.filename\n const sanitizeOpts =\n profile === 'opaque'\n ? { profile, opaqueId: blob.blobId } as const\n : { profile } as const\n const candidate = sanitizeFilename(original, sanitizeOpts)\n const finalName = resolveCollision(candidate, used, onCollision)\n used.add(finalName)\n\n const absPath = resolve(absTargetDir, finalName)\n if (absPath !== absTargetDir && !absPath.startsWith(containmentPrefix)) {\n throw new PathEscapeError({ attempted: finalName, targetDir: absTargetDir })\n }\n\n await mkdir(dirname(absPath), { recursive: true })\n await atomicWrite(absPath, blob.bytes)\n entries.push({ blobId: blob.blobId, path: absPath })\n totalBytes += blob.bytes.byteLength\n\n if (profile === 'opaque') {\n const entry: OpaqueManifestEntry = {\n opaqueName: finalName,\n originalName: original,\n collection: blob.recordRef.collection,\n recordId: blob.recordRef.id,\n slot: blob.recordRef.slot,\n blobId: blob.blobId,\n ...(blob.meta.mimeType !== undefined && { mimeType: blob.meta.mimeType }),\n }\n opaqueEntries.push(entry)\n }\n }\n\n let manifestPath: string | null = null\n if (profile === 'opaque') {\n manifestPath = resolve(absTargetDir, 'manifest.json')\n const json = JSON.stringify(\n {\n format: 'noydb-opaque-export',\n version: 1,\n entries: opaqueEntries,\n },\n null,\n 2,\n )\n await atomicWrite(manifestPath, json)\n }\n\n return {\n written: entries.length,\n bytes: totalBytes,\n entries,\n manifestPath,\n }\n}\n\nfunction resolveCollision(\n candidate: string,\n used: Set<string>,\n strategy: CollisionStrategy,\n): string {\n if (!used.has(candidate)) return candidate\n if (strategy === 'overwrite') return candidate\n if (strategy === 'fail') {\n throw new Error(`exportBlobsToDirectory: filename collision on \"${candidate}\"`)\n }\n // `'suffix'` and the function-callback path both build a sequence\n // of attempts and pick the first non-colliding one.\n for (let attempt = 1; attempt < 10_000; attempt++) {\n const next =\n typeof strategy === 'function'\n ? strategy(candidate, attempt)\n : addSuffix(candidate, attempt)\n if (!used.has(next)) return next\n }\n throw new Error(`exportBlobsToDirectory: collision suffix exhausted for \"${candidate}\"`)\n}\n\nfunction addSuffix(name: string, attempt: number): string {\n const ext = extname(name)\n if (ext.length > 0 && ext.length < name.length) {\n const stem = name.slice(0, name.length - ext.length)\n return `${stem}-${attempt}${ext}`\n }\n return `${name}-${attempt}`\n}\n"],"mappings":"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","names":["mkdir","unlink","dirname","mkdir","unlink","dirname"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/to-file",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-pre.1",
|
|
4
4
|
"description": "JSON file adapter for noy-db — encrypted document store on local disk, USB sticks, or network drives",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"node": ">=22.0.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@noy-db/hub": "0.
|
|
35
|
+
"@noy-db/hub": "0.7.0-pre.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.0.0",
|
|
39
|
-
"@noy-db/
|
|
40
|
-
"@noy-db/
|
|
39
|
+
"@noy-db/test-adapter-conformance": "0.7.0-pre.1",
|
|
40
|
+
"@noy-db/hub": "0.7.0-pre.1"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"noy-db",
|