Skip to content

feat(storage): implement secure filesystem vault manager with atomic writes#22

Open
Chethan-Regala wants to merge 8 commits intoAOSSIE-Org:mainfrom
Chethan-Regala:feat/fileSystem-storage-layer-vault-manager
Open

feat(storage): implement secure filesystem vault manager with atomic writes#22
Chethan-Regala wants to merge 8 commits intoAOSSIE-Org:mainfrom
Chethan-Regala:feat/fileSystem-storage-layer-vault-manager

Conversation

@Chethan-Regala
Copy link

@Chethan-Regala Chethan-Regala commented Mar 12, 2026

Summary

This PR introduces the filesystem storage subsystem for Smart Notes.

The goal of this module is to provide a secure, deterministic, and filesystem-native vault layer for managing Markdown notes locally. Instead of relying on a database as the source of truth, Smart Notes follows a local-first architecture where Markdown files on disk remain canonical.

The storage layer provides a safe abstraction over the filesystem so that higher-level components (watchers, indexers, retrievers, and AI features) can interact with notes through a consistent service API.


Storage Architecture

Below is the architecture of the vault storage layer introduced in this PR.

image

Core components

  • VaultManager

    • Main service responsible for note and folder lifecycle operations.
    • Implements the VaultService contract.
  • PathValidator

    • Prevents path traversal and vault boundary escapes.
    • Ensures all operations remain inside the vault root.
  • SafeWrite

    • Implements atomic writes using a temp file → rename strategy.
    • Prevents partial writes and corruption during crashes.
  • types.ts

    • Defines strict contracts for storage operations.
  • Demo Runner

    • A runnable script that demonstrates the full note lifecycle.

Module Structure

apps/storage/
``` id="structure"

src/
├─ VaultManager.ts
├─ PathValidator.ts
├─ SafeWrite.ts
├─ types.ts
├─ index.ts
└─ demo/
└─ VaultDemo.ts


The module exposes a **clean public API** through `index.ts`.

---

##  Key Features

### Filesystem-Native Vault

Notes are stored as **plain Markdown files on disk**.

Supported operations:

- create note
- read note
- update note
- delete note
- rename note
- recursive note discovery
- create/delete folders

This ensures the vault remains fully **portable and user-owned**.

---

### Atomic File Writes

To prevent corruption during crashes or power loss, writes follow this strategy:

temp file
→ optional fsync
→ atomic rename


This guarantees that a file is **never partially written**.

---

### Vault Boundary Security

Filesystem operations are protected through multiple layers:

- lexical path validation
- absolute vault root enforcement
- symlink resolution checks

These safeguards prevent:

- path traversal (`../`)
- accidental writes outside the vault
- symlink escape attacks

---

### Cross-Platform Compatibility

The implementation is designed to work reliably on:

- Windows
- macOS
- Linux

Special handling is included for **Windows `EPERM` fsync behavior** to maintain compatibility across platforms.

---

##  Demo Runner

A small demo is included to validate the storage module:

```text
apps/storage/src/demo/VaultDemo.ts

Build and run:

npx tsc
node dist/demo/VaultDemo.js

The demo demonstrates:

  • creating a folder
  • creating a note
  • reading the note
  • updating the note
  • listing notes
  • renaming a note
  • deleting a note

How This Fits Into Smart Notes

This storage module forms the foundation of the Smart Notes architecture.

It enables future subsystems such as:

  • filesystem watchers
  • incremental indexing
  • hybrid semantic retrieval
  • AI-assisted note search

Architecture flow:

Markdown Files (Vault)
        ↓
Storage Layer (VaultManager)
        ↓
Watcher / Event System
        ↓
Incremental Indexer
        ↓
Hybrid Retrieval Engine
        ↓
AI Context Assembly

Design Goals

This storage system was designed with the following principles:

  • Local-first
  • Offline-friendly
  • Filesystem transparency
  • Minimal dependencies
  • Deterministic behavior

The implementation intentionally relies only on Node.js filesystem APIs to keep the storage layer lightweight and maintainable.


Feedback

Feedback on the storage architecture, API design, or security considerations would be greatly appreciated.

Thanks for taking a look!

Summary by CodeRabbit

  • New Features

    • Added filesystem-based vault system with complete note management: create, read, update, delete, rename, and list operations.
    • Implemented folder management for organizing notes.
    • Introduced safe atomic file writes to prevent data corruption.
    • Added secure path validation to ensure vault containment.
  • Chores

    • Configured storage module with TypeScript development environment.

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Walkthrough

A new storage module is introduced for managing filesystem-based Markdown vaults. It provides secure path resolution and validation, atomic write operations, full CRUD semantics for notes, and directory management with containment checks.

Changes

Cohort / File(s) Summary
Configuration
apps/storage/package.json, apps/storage/tsconfig.json
Added npm package setup with TypeScript development configuration targeting ES2020 with strict type-checking and commonjs module system.
Type Contracts
apps/storage/src/types.ts
Defined core interfaces: VaultNote, VaultNoteMeta, VaultOptions, and VaultService contract for vault operations (create, read, update, delete, rename notes, list metadata, folder operations).
Utilities
apps/storage/src/PathValidator.ts, apps/storage/src/SafeWrite.ts, apps/storage/src/FileSystemUtils.ts
Added path security validation with containment checks, atomic write-via-temp-file with fsync and rename semantics, and empty module placeholder.
Core Implementation
apps/storage/src/VaultManager.ts
Implemented VaultManager class providing full CRUD operations for notes, folder management, recursive directory traversal, and safe path resolution with vault containment validation and atomic writes.
Public API
apps/storage/src/index.ts
Exposed public API entry point re-exporting VaultManager, VaultService, type contracts, and writeFileAtomic utility for downstream consumers.
Demo
apps/storage/src/demo/VaultDemo.ts
Added end-to-end demonstration script exercising vault operations: folder/note creation, read/update, listing metadata, renaming, and cleanup with diagnostic logging.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant VaultManager
    participant PathValidator
    participant SafeWrite
    participant FileSystem as File System

    Client->>VaultManager: updateNote(path, content)
    VaultManager->>PathValidator: resolveVaultPath(root, path)
    PathValidator-->>VaultManager: resolvedPath
    VaultManager->>PathValidator: validateVaultPath(root, resolved)
    alt Path valid and contained
        PathValidator-->>VaultManager: ✓ validated
        VaultManager->>SafeWrite: writeFileAtomic(resolved, content)
        SafeWrite->>FileSystem: write to .tmp file
        SafeWrite->>FileSystem: fsync (best-effort)
        SafeWrite->>FileSystem: atomic rename .tmp → target
        FileSystem-->>SafeWrite: ✓ success
        SafeWrite-->>VaultManager: ✓ written
        VaultManager-->>Client: ✓ note updated
    else Path outside vault
        PathValidator-->>VaultManager: ✗ error (outside root)
        VaultManager-->>Client: ✗ error thrown
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

Typescript Lang

Poem

🐰 A vault is born in TypeScript's glow,
With paths secured and writes atomic—
Notes hop safe through validation's flow,
While fsync keeps us cool and stoic.
Hop by hop, the data grows secure! 🥕

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a secure filesystem-based vault manager with atomic writes for safe file persistence.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can generate walkthrough in a markdown collapsible section to save space.

Enable the reviews.collapse_walkthrough setting to generate walkthrough in a markdown collapsible section.

@github-actions github-actions bot added size/XL and removed size/XL labels Mar 12, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/storage/package.json`:
- Around line 5-7: The package.json "main" currently points to index.js but your
build emits to dist/, so update package.json to point "main" to the emitted
entrypoint (e.g., "dist/index.js") and add a "types" field (e.g.,
"dist/index.d.ts") for TS consumers; also ensure your tsconfig.json has
"outDir": "dist" and "declaration": true so declaration files are emitted for
the "types" entry (verify the produced filenames match the values you put in
"main" and "types").

In `@apps/storage/src/demo/VaultDemo.ts`:
- Around line 65-69: The demo currently constructs VaultManager with
options.rootPath pointing to "demo-vault" without ensuring that directory exists
and only removes notes/ on cleanup; modify the flow to explicitly create the
demo root before new VaultManager(options) (use fs.mkdir or equivalent to ensure
options.rootPath exists), and move the teardown into a try/finally where the
finally calls fs.rm(options.rootPath, { recursive: true, force: true }) to
remove the entire demo root; update both the setup/teardown around VaultManager
instantiation (references: VaultOptions, options.rootPath, VaultManager) and the
other demo cleanup block (the notes/ removal at the later section) to rely on
the single root removal so the demo is rerunnable and leaves no residue.

In `@apps/storage/src/SafeWrite.ts`:
- Around line 80-95: After the rename(tempPath, filePath) call in SafeWrite.ts,
perform a best-effort sync of the parent directory (use path.dirname(filePath)),
by opening the directory with open(path.dirname(filePath), "r"), calling
dirHandle.sync(), and then closing dirHandle; wrap this in a try/catch that
ignores errors so platforms that don't support directory fsync don't fail the
write; keep the existing EPERM handling for Windows and ensure this
directory-sync step runs after the rename to provide the claimed crash-safety
for rename directory entries.

In `@apps/storage/src/types.ts`:
- Around line 232-244: The deleteFolder(path: string) contract must explicitly
forbid deleting the vault root: update the deleteFolder docs and implementation
to validate the resolved/normalized target and throw if it equals the vault root
or is a root-like input (e.g., "", ".", "/", or the resolved root path), and
also reject upward-traversal inputs (e.g., paths that resolve outside the
vault). In other words, inside deleteFolder ensure you normalize/resolve the
path against the vault root, check that it is not equal to the vault root and
that it remains inside the vault boundary, and if it fails these checks throw a
descriptive error instead of proceeding with recursive deletion.
- Around line 208-218: listNotes() currently leaves ordering undefined which
violates the storage layer's deterministic behavior requirement; change
implementations and the interface contract for listNotes() to return a
deterministically sorted array (e.g., sort VaultNoteMeta[] by a stable key such
as normalized note path, falling back to mtime for ties) and update any
callers/implementations to rely on that ordering; ensure the documentation
comment for listNotes() (and the VaultNoteMeta contract) states the exact sort
rule (e.g., "sorted ascending by normalized path, then by modification time") so
behavior is stable across platforms.
- Around line 150-206: The public API methods createNote, readNote, updateNote,
deleteNote, and renameNote currently accept arbitrary paths; update the contract
to require vault-relative Markdown paths (must end with ".md") by clarifying the
JSDoc for each method and adding a runtime validation requirement:
implementations must validate the input path endsWith(".md") and reject/throw a
clear error if not (also validate both oldPath and newPath in renameNote).
Mention this rule in the comments for createNote, readNote, updateNote,
deleteNote, and renameNote so callers and implementers enforce Markdown-only
note paths.

In `@apps/storage/src/VaultManager.ts`:
- Around line 112-123: When fs.realpath(resolved) throws ENOENT, don't skip
containment checks; instead walk up from resolved to find the nearest existing
ancestor, call fs.realpath on that ancestor and use that resolved ancestor path
when calling validateVaultPath(realRoot, realAncestorRealPath). Update the
try/catch in VaultManager where fs.realpath(this.options.rootPath) and
fs.realpath(resolved) are awaited so that ENOENT triggers the ancestor
resolution logic, then validate against the real root via validateVaultPath;
this prevents createNote/createFolder and rename destination paths from escaping
the vault via symlinks.
- Around line 311-313: deleteFolder currently resolves user input via
resolveSafe then calls fs.rm recursively, which allows callers to delete the
entire vault when folderPath is ""/"." or an absolute root; update deleteFolder
to detect and refuse any attempt to remove the vault root by comparing the
resolved path against the vault root (e.g., this.vaultRoot or whatever base path
is used by resolveSafe) and throw a clear error instead of calling fs.rm; ensure
you also normalize/realpath both sides (use path.resolve or fs.realpath) so
symlinks/relative forms like "." are caught, then only call fs.rm when the
resolved path is strictly inside the vault and not equal to the root.
- Around line 146-157: The preflight stat() check in createNote (around the
resolved variable before writeFileAtomic) and the identical pattern in
renameNote create a race where a concurrent writer can win between stat() and
write/rename; replace the stat()-then-write flow with an atomic create that
fails if the file exists (use fs.open(resolved, 'wx') or equivalent) so creation
is atomic and respects the no-overwrite contract, and adjust renameNote to
perform the destination-exclusive create/replace via an atomic primitive instead
of preflight stat() before fs.rename(); ensure you close the file descriptor and
handle the error path by propagating the "already exists" error when open('wx')
fails.

In `@apps/storage/tsconfig.json`:
- Around line 14-17: The base tsconfig.json currently sets "rootDir": "./src"
which causes "outside rootDir" errors for top-level tooling/tests; remove
"rootDir" from the base tsconfig.json and keep only non-emit settings (e.g.,
"outDir" can also be moved), then create a build-specific tsconfig (e.g.,
tsconfig.build.json) that extends the base and sets "rootDir": "./src" and
"outDir": "./dist"; also update "include" in the base tsconfig to ["**/*.ts"]
(or ["src/**/*.ts","**/*.ts"] as needed) so the base config covers all package
.ts files while the build config controls emit-specific options.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: db44915d-1494-4250-b985-06fc06dbc076

📥 Commits

Reviewing files that changed from the base of the PR and between a3ccb2b and dd1e912.

⛔ Files ignored due to path filters (1)
  • apps/storage/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • apps/storage/package.json
  • apps/storage/src/FileSystemUtils.ts
  • apps/storage/src/PathValidator.ts
  • apps/storage/src/SafeWrite.ts
  • apps/storage/src/VaultManager.ts
  • apps/storage/src/demo/VaultDemo.ts
  • apps/storage/src/index.ts
  • apps/storage/src/types.ts
  • apps/storage/tsconfig.json

@Chethan-Regala
Copy link
Author

Thanks for the suggestion ! I've updated the implementation to address this and pushed the fix in the latest commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant