@artemjs/vfskit
v1.2.1
Published
Universal abstraction over any virtual file system - memory, disk, S3 and more - with composable adapters, encryption and a remote bridge. Backend kit.
Maintainers
Readme
vfskit wraps any kind of storage behind a single, small VFS interface, then lets you
compose behavior on top of it - encryption, caching, a remote bridge - and drive it from
the browser exactly as you would on the server. Anything you can read, write, and list
becomes a structured file system with files and metadata.
It ships in two faces under one brand:
@artemjs/vfskit(npm, Node) - the full kit: core + memory + node-fs + s3 + sqlite + kv + encrypt + cache + serve + remote.@artemjs/vfskit-front(npm + jsDelivr, browser) - core + memory + opfs + kv + encrypt + cache + a remote client.
Both expose identical API names, so your code looks the same on either side.
Install
npm i @artemjs/vfskit # backend / Node// browser, no build step
import { remote, wsTransport } from 'https://cdn.jsdelivr.net/npm/@artemjs/vfskit-front/+esm'Everything is a VFS
interface VFS {
read(path): Promise<Uint8Array>
write(path, data, opts?): Promise<void>
list(path, opts?): Promise<Entry[]>
stat(path): Promise<Stat>
exists(path): Promise<boolean>
mkdir(path, opts?): Promise<void>
remove(path, opts?): Promise<void>
move(from, to): Promise<void>
copy(from, to): Promise<void>
getMeta(path): Promise<Meta>
setMeta(path, meta): Promise<void>
watch(path, cb): Unsubscribe
capabilities(): Capabilities
}- Adapters implement
VFSover a backend:memory(),nodeFs(dir),s3({ client }). - Middleware wraps a
VFSand returns aVFS:encrypt(vfs, { passphrase }),cache(vfs). - Bridge connects them across the wire:
serve(vfs)on the server,remote(transport)on the client.
Compose freely. encrypt(remote(transport)) is end-to-end encryption - the server only ever
stores ciphertext.
Quick start
import { memory, nodeFs, encrypt, serve, remote, toText } from '@artemjs/vfskit'
const store = encrypt(nodeFs('./data'), { passphrase: 'hunter2' })
await store.write('/notes/todo.md', '# buy milk', { meta: { tag: 'home' } })
console.log(toText(await store.read('/notes/todo.md')))Expose a backend, drive it from anywhere:
// server
const server = serve(nodeFs('./data'))
// wire server.fetch (HTTP) or server.socket (WebSocket) into your runtime// client (browser or Node)
import { remote, wsTransport } from '@artemjs/vfskit-front'
const fs = remote(wsTransport('ws://localhost:3000'))
await fs.write('/hello.txt', 'hi')Adapters
| Adapter | Where | Metadata | Notes |
| --- | --- | --- | --- |
| memory() | anywhere | native | reference implementation; synchronous watch |
| nodeFs(dir) | Node | sidecar .vfskit/meta.json | rooted at dir; native streaming; watch via fs.watch |
| s3({ client, prefix?, pollMs? }) | Node | native object metadata | inject any S3Like client; POSIX dirs emulated with markers; watch by polling |
| sqlite(file) | Node | native column | whole VFS in one SQLite file via built-in node:sqlite; conditional writes |
| opfs(root?) | browser | sidecar manifest | persistent Origin Private File System; native file streaming |
| kv({ store, prefix? }) | anywhere | native record | over any async key-value store; ships memKv() and localStorageKv() |
Every adapter passes the same conformance suite, so a new one "just works" once it does too.
// browser-persistent storage, no server
import { opfs, kv, localStorageKv } from '@artemjs/vfskit-front'
const disk = opfs() // Origin Private File System
const ls = kv({ store: localStorageKv() }) // or back any KV: Redis, Cloudflare KV, Deno KV
// node: a whole file system in one .db
import { sqlite } from '@artemjs/vfskit'
const db = sqlite('./app.db')Bring your own storage
vfskit is just an interface. To put any backend behind the same API, write a function that
returns a VFS - a plain object literal implementing the methods above - over your store
(a database, a KV cache, localStorage, a blob service, whatever):
import { type VFS, normalize, toBytes, notFound } from '@artemjs/vfskit'
export function myVfs(store: MyStore): VFS {
return {
capabilities: () => ({ streaming: false, watch: false, atomicMove: false, nativeMeta: true, randomAccess: false, conditionalWrite: false }),
async read(path) { /* ... */ },
async write(path, data, opts) { /* ... */ },
// ...the rest of the interface
}
}Then validate it against the exact same battery every built-in adapter must pass:
import { conformanceCases } from '@artemjs/vfskit/conformance'
import { describe, it } from 'vitest'
describe('my adapter', () => {
for (const c of conformanceCases) it(c.name, () => c.run(() => myVfs(new MyStore())))
})If it passes, your storage now works everywhere vfskit works - behind encrypt(...), behind
serve(...), driven by a browser remote(...). A complete worked example (a key-value
backend) lives in examples/custom-adapter. conformanceCases is
framework-agnostic ({ name, run(makeVfs) }[]), so you can drive it from any test runner.
Encryption
AES-256-GCM via WebCrypto. A raw key, or a passphrase derived per file with PBKDF2 (random salt, 210k iterations). Tamper fails closed with a typed error. Content is encrypted by default; metadata stays as the backend stores it.
const vault = encrypt(memory(), { passphrase: 'open sesame' })Caching
cache(vfs, { ttlMs? }) serves reads from an in-memory store (write-through,
subtree-invalidated on write/remove/move/copy). Wrap a remote(...) to avoid round-trips for
hot files:
import { cache, remote, wsTransport } from '@artemjs/vfskit-front'
const fs = cache(remote(wsTransport(url)), { ttlMs: 5000 })Pass your own store to back the cache with anything (e.g. localStorage).
Concurrent writes
Adapters that report conditionalWrite give every file an opaque version token (via
stat). Pass it back as ifMatch to make a write succeed only if nobody changed the file in
between - otherwise it fails with a typed CONFLICT. ifAbsent makes a create-only write.
const { version } = await fs.stat('/doc')
await fs.write('/doc', next, { ifMatch: version }) // CONFLICT if it moved on
await fs.write('/new', data, { ifAbsent: true }) // ALREADY_EXISTS if it existsSupported by memory, nodeFs, s3, and transparently over remote(...).
Streaming
readStream(vfs, path) and writeStream(vfs, path) give Web ReadableStream /
WritableStream over any adapter - native where supported (nodeFs streams real file
handles), buffered otherwise, so the API is uniform:
import { readStream, writeStream, collect, toBytes } from '@artemjs/vfskit'
const w = (await writeStream(fs, '/big.log')).getWriter()
await w.write(toBytes('line 1\n')); await w.close()
const all = await collect(await readStream(fs, '/big.log'))encrypt and cache buffer through their own read/write, so streaming stays correct
behind them (the stream still yields plaintext; the disk still holds ciphertext).
Transports
httpTransport(url)- request/response; works on serverless/edge. Nowatch.wsTransport(url)- multiplexed; enableswatch/events.
Errors
Typed hierarchy with stable wire codes, reconstructed on the client across the bridge:
NOT_FOUND, ALREADY_EXISTS, NOT_A_DIRECTORY, IS_A_DIRECTORY, PERMISSION_DENIED,
UNSUPPORTED, CONFLICT, IO. Detect with isVfsError(e) (brand-based - survives bundling
and the RPC round-trip).
Example
examples/cloud-ide - Monaco editing files on a real-disk VFS over a
WebSocket bridge, with per-user isolation. Swapping the backend to S3 is one line.
