npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@poe-platform/safe-fs

v0.1.827

Published

Composable filesystem with a portable core and explicit Node adapters

Downloads

151,948

Readme

safe-fs

An asynchronous filesystem interface for memory, rooted host directories, S3, and WebDAV, with mounts, copy-on-write overlays, and Node-style bridges. Portable filesystem types and POSIX helpers compile without Node types. The posixPath export from @poe-code/safe-fs/core provides basename, dirname, extname, join, isAbsolute, normalize, resolve, relative, sep, and delimiter; resolve and relative use / as their working directory in every runtime.

Under workerd and browser conditions, the root export includes portable filesystems, S3FileSystem, and MockS3Client. Supply an S3 transport to use object storage without Node globals.

The storage subpath exposes PagedStorage and IntegerTable for temporary working data. Supply {fs, cwd, env, signal} and an optional page-cache count (default 64 pages of 16 KiB). Scratch data uses retained positioned handles in env.TMPDIR or cwd, with conditional unlink and cleanup through close(). Providers must support those capabilities; there is no host fallback. Use an external backend for large data because memory filesystems retain spilled bytes. To bound aggregate pages across multiple live stores, pass the same new PagedStorageCache(16) as the third argument to each PagedStorage constructor. This shares a 256 KiB resident-page budget; cache.residentBytes reports current page retention. Eviction uses each store's filesystem, and each store still owns its close() cleanup. Use new PagedStorageCache(16, 65536) to coalesce scratch I/O into 64 KiB pages within a 1 MiB budget, for example with object-backed files. Page sizes must be multiples of 16 KiB, up to 1 MiB; individual read() calls remain limited to 16 KiB. The budget excludes caller buffers and backend storage. Call await storage.prepare() before opening a directory iterator when its snapshot includes the scratch directory. It acquires and unlinks the backing handle up front, so later spills do not mutate that directory's namespace. IntegerTable.entries() replays records in unsigned key order with a fixed 16-level node stack and batches of at most 128 records. It accepts zero-filled caller storage with allocate, read and write; mutation during traversal is rejected. Branch pointers use a separate fixed 128-entry cache.

Quickstart

Install the standalone package; it does not depend on SafeJS, Safe Bash, or the poe-code CLI. These examples use TypeScript in a Node ESM application.

npm install @poe-platform/safe-fs
import { createMemoryFileSystem, createNodeFsBridge } from "@poe-platform/safe-fs";

const storage = createMemoryFileSystem();
await storage.mkdir("/work");
await storage.writeFile("/work/note.txt", new TextEncoder().encode("hello"));
console.log(new TextDecoder().decode(await storage.readFile("/work/note.txt")));

const fs = createNodeFsBridge(storage, { cwd: "/work" });
await fs.appendFile("note.txt", " world", "utf8");
console.log(await fs.readFile("note.txt", "utf8"));

Output: hello, then hello world. Nothing touches the host filesystem. Raw adapters exchange Uint8Array values; the Node bridge adds strings, encodings, Buffer results, and stat predicates. Its cwd is the relative-path base and, by default, the confinement boundary. Set an explicit root to use a different boundary, for example { cwd: "/work", root: "/" } to address a complete provider namespace. The bridge supports unlink when the provider offers atomic file removal; unsupported methods fail without host fallback. createHostFileSystem() provides trusted, unrestricted native host access when a rooted adapter is not desired.

Under the workerd export condition, @poe-platform/safe-fs/fs/real reads the native request-local filesystem, including retained /tmp file bytes. The restricted profile refuses generic descriptors, permission/timestamp changes, exact creation modes, and owned staging; it promises neither atomic rename nor inode/version identity from placeholder native metadata. Retained reads remain available, with native seek reporting ENOTSUP. Ordinary browser bundles cannot import this backend. Node keeps its POSIX host profile.

For host storage, use await createRealFileSystem({ root: "/absolute/existing/directory" }) instead. The root must already exist; virtual / maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree. Tar extraction can use confineTrustedExtraction on an externally isolated host tree. This view retains extraction roots and checks full ancestry at publication without claiming OS-atomic staging. Atomic archive extraction requires atomic ancestry verification. MemoryFileSystem supports it, including a writable root composed with read-only mounts such as /skills/.system; the real adapter refuses it. Mount extraction delegates confinement to each selected backend and keeps other mounts read-only. Extraction inside a separate mount requires synchronous outer-ancestor guards at every mutation; backends that cannot honor those guards refuse it. Staged file publication supports nested writable mounts when every ancestor provides authoritative validation. Its trustedOwnedStaging capability checks original entries before publication and cleanup, and preserves foreign staging children. It does not advertise atomic conditional mutations: keep external writers and other in-flight writes away from the tree during these operations. Read the safety boundary below before exposing it to untrusted code.

To create new outputs with Safe Bash's dos2unix, unix2dos, or compression commands, host adapters need atomic no-replace publication. Supply createRealFileSystem({ root, renameNoReplace }) only when that callback binds a qualified native primitive such as Linux renameat2 with RENAME_NOREPLACE. It receives resolved absolute host paths and an optional signal in its third argument. Existing destinations must reject EEXIST atomically; existence checks followed by rename and copy/delete are insufficient. Without this binding the adapter explicitly refuses those operations. See the no-replace contract.

Entry points and shared identity

| Import | Use | | --- | --- | | @poe-platform/safe-fs | Full Node API; selects the portable core under the browser and workerd conditions. | | @poe-platform/safe-fs/core | Memory, mounts, overlays, read-only, WebDAV, portable bridges, types, and errors. Bundle with the browser condition for browsers and Workers. | | @poe-platform/safe-fs/node | Host filesystem, Node bridge, S3, and configuration helpers; unavailable in browsers. | | @poe-platform/safe-fs/fs/* | Individual adapters: memory, readonly, mount, overlay, webdav, real, s3, and s3/http. S3, its mock, and the signed s3/http transport work in browsers and Workers. |

import { createMemoryFileSystem, FsError, type FileSystem } from "@poe-platform/safe-fs/core";

const fs: FileSystem = createMemoryFileSystem();

Matching scoped SafeJS and Safe Bash releases use this package's types and runtime. Existing @poe-platform/safe-js/fs, /fs/core, and /fs/node imports remain re-exports, so FsError identity is shared; do not copy its implementation or the FileSystem declaration. FileSystem is an interface, not a runtime class.

Custom filesystem views can use registerEntryView(view, async path => ({ filesystem: backing, path })) from /core so compareEntries recognizes their backing entries in either direction. The resolver can also return readOnly: true; each view can be registered once.

Legacy poe-code/safe-fs imports still use the CLI's bundled runtime. Keep an application's factories, shell, and errors in one import family. At a host-owned legacy adapter boundary, normalize foreign filesystem failures with toFsError from the receiving runtime; it retains the code, operation paths, and original cause. Preserve cancellation/control exceptions separately. Cross-family instanceof FsError is not supported without normalization.

Supported operations

| Operation | Interface | | --- | --- | | Read and write bytes | readFile, writeFile, appendFile | | Inspect entries | stat, lstat, readdir, realpath, access | | Change the namespace | mkdir, rm, rename, copyFile | | Remove an empty directory | Optional rmdir; never a recursive-delete fallback | | Links and metadata | Optional readlink, symlink, link, chmod, utimes, truncate | | Enumerate directories lazily | Optional iterateDirectory; memory, native and paged S3 backends, with mount/overlay/readonly/quota/device/scoped delegation; backend order, no sorted-array fallback | | Stream bytes | Optional readStream, writeStream, using async iterables of byte chunks | | Retain an open file | Optional open with positioned I/O and synchronization; memory and real backends support noFollow: true to atomically refuse a final symlink | | Publish immutable objects atomically | Optional publishFileConditional with opaque identity/version stats and authoritative compare-and-publish | | Publish a checked source set | Optional publishStagedFileSet publishes a staged file and retires sibling entries in one backend commit; mount views require all entries on the same supporting backend | | Compare backing entries | Optional compareEntry, returning same, distinct, or unknown |

All required methods must exist, but a backend may reject an operation with FsError, such as EROFS for a write or ENOTSUP for unsupported semantics. Check optional methods and capabilities rather than assuming every backend behaves like a local disk. Errors expose code and may include syscall, path, dest, and cause.

readBytes supports frozen host abort signals. The exported listenForAbort(signal, listener) returns a cleanup function for custom operations; if frozen host bookkeeping prevents registration, callers must check cancellation at operation boundaries, as readBytes does.

createDeviceFileSystem(fs) adds portable /dev/null whole-file, stream, and descriptor I/O. Descriptor reads return EOF and writes discard bytes; stat remains a zero-size character device and descriptor position remains zero. Truncating opens are accepted, exclusive creation fails with EEXIST, and descriptor resizing and synchronization are unsupported. Access modes, cancellation, and closed handles use the normal descriptor checks. With an authoritative object store, use createDeviceFileSystem(withObjectFileDescriptors(fs, store)) so null-device I/O never acquires or publishes an object version; ordinary files retain conditional publication. The device wrapper must be outermost for this composition.

PythonStatTranslator and createPythonRuntimeStatMapper allocate stable guest device identities without a default device-count budget. Their optional maxDevices accepts Infinity or a positive safe integer. Explicit budgets remain enforced; device numbers must still fit the disjoint application/runtime ranges of the uint32 Python stat ABI.

Python filesystem directory cursors use iterateDirectory when available, share open-handle admission with files, and drain pending reads before cleanup. Backends without lazy enumeration retain the buffered readdir fallback.

withObjectFileDescriptors(fs, store) supports large shell and Python descriptor writes when the host supplies store.createStaging: private externally backed pages keep working memory bounded without publishing the growing file after every write. Conditional publication still occurs at sync/close, and retained readers keep their old versions. Without that optional backend primitive, the optional dirty-page budget limits unflushed output only when configured. Descriptor resource limits accept Infinity and are unlimited when omitted; chunkBytes remains a positive finite integer. See the object descriptor and spill contract for backend methods, failure semantics and qualification; no provider storage is configured automatically.

Immutable flat stores can supply identityScope, opaqueIdentity and an ABA-safe opaqueVersion on file stats, then expose atomicFilePublication: true with publishFileConditional(path, source, { expected, parent, maxBytes, signal }). The host consumes bytes privately and atomically creates an absent binding (expected: null) or replaces the observed generation. Failed producers, uploads and stale writers leave the old file intact. This supports Safe Bash ZIP creation and updates without inode numbers, staging directories or permission APIs. Read the conditional publication contract before implementing the host operation; ordinary writes do not provide it.

For temporary staging that must be cleaned up after its parent moves, adapters advertising retainedStagingCleanup accept createStagedFile(..., { parent, retainCleanup: true }). Use the returned staging.cleanup.remove() and always call staging.cleanup.close() in finally. Memory and its supported wrappers retain only the owned staging entries; replacement entries remain protected. When atomicStagedFileMutation is available, publishStagedFile(..., { preserveIdentity: true, parent, destination }) updates an existing file without breaking hardlink aliases. Memory and its supported wrappers adopt staged bytes and timestamps while keeping the destination identity and mode; unsupported adapters reject this option. Overlays backed by stock Memory filesystems support conditional file mutations, directory preparation, and confined staging. Publication may omit ancestors when the destination shares the staging parent; the overlay checks the ancestry retained before staging began. See the staging contract.

Capture retainFileSystemCleanup(scoped, callback, { maxOperations }) before cancellation to clean up through a bounded view after ordinary scoped operations close. Cleanup preserves its owning scope and limits across separately bundled command runtimes, including restrictions added by filesystem wrappers. Omit maxOperations or pass Infinity for unlimited cleanup operations.

| Backend or wrapper | Use it for | | --- | --- | | createMemoryFileSystem() | Isolated, nonpersistent storage with links, permissions, timestamps, and streams; each path resolution admits at most 65,536 cumulative UTF-16 code units across the input and followed symlink targets, rejecting excess with ENAMETOOLONG before component allocation | | createRealFileSystem({ root }) | An existing host directory, with virtual paths rooted inside it; Node or qualified Workerd operations | | new S3FileSystem({ transport, bucket, … }) | Bucket/prefix storage through an explicitly supplied transport in Node, browsers, and Workers | | new WebDavFileSystem({ baseUrl, fetch, … }) | A WebDAV namespace through an explicitly supplied Fetch implementation | | createReadOnlyFileSystem(filesystem) | Rejecting writes through one view of an existing filesystem | | createMountFileSystem({ root, mounts }) | Routing absolute virtual mount paths to different filesystems | | createOverlayFileSystem({ upper, lower }) | Reading through to a lower layer and writing changes to an upper layer | | withFileSystemQuota(filesystem, { maxBytes }) | Enforcing a cumulative logical-byte ceiling across every write path |

createNodeFsBridge offers a promises-shaped subset, including recursive cp and mkdtemp. A trusted reserveReadFile callback can reserve host resources and return a release function; cancellation retains the reservation until the backend read settles. The portable createFsBridge from @poe-platform/safe-fs/bridge instead requires a caller-supplied text codec and returns Uint8Array values.

Write an adapter

A backend implements the exported FileSystem interface. It needs every non-optional method, a truthful capabilities object, byte-safe reads/writes, and appropriate errors. A configuration adapter adds validateOptions and a create factory returning that filesystem, synchronously or asynchronously.

This hypothetical catalog adapter presents a snapshot of named text documents as read-only files at /. It implements the full required interface without a host filesystem or network connection. Only the root directory and single-component filenames are supported; timestamps are synthetic and optional operations are absent.

import {
  ACCESS_MODES, FsError, createFileSystem,
  createNodeFileSystemAdapterRegistry, readConfigRecord, validatePath,
  type FileStat, type FileSystem, type FileSystemAdapterDescriptor,
  type FsOptions,
} from "@poe-platform/safe-fs";

const catalogAdapter = {
  validateOptions(options) {
    const config = readConfigRecord(options, "catalog option", ["files"]);
    const files = readConfigRecord(config.files, "catalog files");
    for (const [name, text] of Object.entries(files)) {
      if (!name || name === "." || name === ".." || name.includes("/")
        || name.includes("\0") || typeof text !== "string") {
        throw new TypeError("files must map single-component names to strings");
      }
    }
  },
  create(options): FileSystem {
    const files = new Map(Object.entries(readConfigRecord(options.files, "catalog files"))
      .map(([name, text]) => [name, new TextEncoder().encode(text as string)]));

    function lookup(path: string, options?: FsOptions) {
      if (options?.signal?.aborted) throw new FsError("ECANCELED", { path });
      validatePath(path);
      let name = "";
      let bytes: Uint8Array | undefined;
      for (const part of path.split("/")) {
        if (bytes !== undefined) throw new FsError("ENOTDIR", { path });
        if (part === "" || part === "." || part === "..") continue;
        bytes = files.get(part);
        if (bytes === undefined) throw new FsError("ENOENT", { path });
        name = part;
      }
      return { path: `/${name}`, bytes };
    }

    async function inspect(path: string, options?: FsOptions): Promise<FileStat> {
      const { bytes } = lookup(path, options);
      return {
        type: bytes === undefined ? "directory" : "file",
        size: bytes?.byteLength ?? 0,
        mode: bytes === undefined ? 0o40555 : 0o100444,
        atimeMs: 0, mtimeMs: 0, ctimeMs: 0,
      };
    }

    async function denyWrite(): Promise<never> {
      throw new FsError("EROFS");
    }

    return {
      capabilities: { readOnly: true },
      async readFile(path, options) {
        const { bytes } = lookup(path, options);
        if (bytes === undefined) throw new FsError("EISDIR", { path });
        const limit = options?.maxBytes ?? Number.MAX_SAFE_INTEGER;
        if (!Number.isSafeInteger(limit) || limit < 0) throw new FsError("EINVAL", { path });
        if (bytes.byteLength > limit) throw new FsError("EFBIG", { path });
        return bytes.slice();
      },
      stat: inspect,
      lstat: inspect,
      async readdir(path, options) {
        if (lookup(path, options).bytes !== undefined) throw new FsError("ENOTDIR", { path });
        return [...files.keys()].sort().map(name => ({ name, type: "file" as const }));
      },
      async realpath(path, options) {
        const entry = lookup(path, options);
        return entry.path;
      },
      async access(path, mode = ACCESS_MODES.F_OK, options) {
        const entry = lookup(path, options);
        if (!Number.isInteger(mode) || mode < 0 || mode > 7) throw new FsError("EINVAL", { path });
        if (mode & ACCESS_MODES.W_OK) throw new FsError("EROFS", { path });
        if ((mode & ACCESS_MODES.X_OK) && entry.bytes !== undefined) {
          throw new FsError("EACCES", { path });
        }
      },
      writeFile: denyWrite, appendFile: denyWrite, mkdir: denyWrite,
      rm: denyWrite, rename: denyWrite, copyFile: denyWrite,
    };
  },
} satisfies FileSystemAdapterDescriptor;

const registry = createNodeFileSystemAdapterRegistry(new Map([["catalog", catalogAdapter]]));
const catalog = await createFileSystem({
  type: "catalog",
  options: { files: { "welcome.txt": "Hello from the catalog" } },
}, { registry });

console.log(new TextDecoder().decode(await catalog.readFile("/welcome.txt")));

Output: Hello from the catalog. readFile returns a copy so callers cannot mutate the stored snapshot. Reads check cancellation and maxBytes; required mutations always reject with EROFS. stat and lstat share an implementation because this adapter has no symlinks.

createFileSystem validates options before calling the factory. For Workers, import createPortableFileSystemAdapterRegistry and createFileSystem from @poe-code/safe-fs/core; the portable registry includes memory and caller-supplied adapters, without ambient filesystem access. The Node registry includes memory and real, accepts new names, and rejects attempts to replace those defaults. For an explicit allowlist without either default, pass new Map([["catalog", catalogAdapter]]) directly as registry. Names select registered factories; they do not load modules from config.

For a mutable backend, preserve exclusive-create behavior, return independent byte buffers, handle cancellation and size limits, and declare only capabilities you actually provide. Use FsError for filesystem failures. Do not invent entry identity from matching paths or metadata; omit compareEntry when identity is unknown.

Options

There are no package environment variables, implicit credentials, or automatic .env loading. Applications supply configuration, transports, and credentials explicitly. Factories accept the same options as their corresponding classes.

Local storage, composition, and bridges

| API | Options and defaults | | --- | --- | | createFileSystem(config, { registry }) | Required config.type; config.options defaults to an empty record. registry is required. Built-in memory accepts optional file, retained-byte, metadata, total-byte, maxPathBytes and maxPathComponents quotas (omitted, undefined, or Infinity disables each quota); built-in real requires root. | | createNodeFileSystemAdapterRegistry(extensions?) | Optional map of additional adapter descriptors; defaults to only memory and real. | | Memory / read-only | Memory accepts independent optional maxFileBytes, maxRetainedBytes, maxMetadataUnits and maxBytes quotas, all unlimited by default. Read-only takes the backing filesystem, without an options object. | | Real | Required root: existing absolute host directory; the constructor/factory also accepts the root string directly. | | Mount | Required root: fallback filesystem. mounts defaults to {} and maps absolute virtual paths to filesystems. | | Overlay | Required upper and lower; maxBufferBytes is unlimited unless configured; it and per-read maxBytes accept explicit Infinity. | | Quota | withFileSystemQuota requires a nonnegative safe-integer maxBytes. It serializes mutations and counts files, symlinks, copies, hard links, truncation, and streaming writes. | | Node bridge | cwd defaults to /, must be an absolute virtual path; optional lifetime signal and trusted readFileMaxBytes cap forwarded to backend reads before copy/decode. | | Portable bridge | Same cwd, signal and readFileMaxBytes, plus required codec with isEncoding, encode, and decode functions. | | Catalog example | Required files: a plain record of single-component filenames to text strings; no other options. |

Per-operation options

Every raw filesystem operation accepts an optional signal. Additional fields are:

| Operation | Options | | --- | --- | | readFile | maxBytes: nonnegative safe-integer byte cap; Infinity adds no per-read cap | | readdir | maxEntries: nonnegative safe-integer entry cap; omission or Infinity preserves backend limits and ordering | | writeFile, writeStream | flag: w (default, replace), wx (exclusive create), a (append), or ax (exclusive append/create); optional mode | | appendFile | Optional mode | | mkdir | recursive (default false); optional mode | | rm | recursive and force (default false) | | copyFile | exclusive (default false) | | readStream | start (default 0), endExclusive (default end of file), chunkSize (built-in default 64 KiB) |

validatePath(path, { maxPathBytes, maxPathComponents }) accepts optional UTF-8 byte and component quotas; both default to Infinity. Memory accepts the same constructor options and counts cumulative symlink expansion. Device operations and WebDAV stat, access, and writes accept these quotas under pathLimits. WebDAV accepts maxTimestampPropertyBytes for timestamp metadata, also unlimited by default.

access takes a separate mode bitmask from ACCESS_MODES. chmod takes a mode, utimes takes millisecond timestamps, and truncate takes a byte length (default 0). For conditional chmod, check capabilitiesFor(path, { conditionalChmod: true }) (or capabilities) and require conditionalChmod: true; supply parent, expected, and complete root-to-parent ancestors together. An optional mutation-free commitGuard must return literal true synchronously. Memory validates at its metadata commit; mount and supported Memory overlays preserve wrapper ancestry. Real uses its existing externally isolated host-tree boundary and does not prevent races with other processes. Backend limits still apply. Node-shaped bridge methods translate their own options rather than accepting these raw option objects; see the bridge signatures.

Cross-mount streamed copies start reading the source before opening the destination for writing. An initial read failure leaves the destination unchanged; later read or write failures can leave partial destination bytes.

Overlay copy-up, copy, stream replacement/append and resize transfer retained ranges in 64 KiB chunks through the injected upper's stream writer. Resize streams the retained prefix and bounded zero chunks. Stream writes stage input before publication; cancellation or transfer failure leaves the destination unchanged and cleans staging. Stock Memory overlays transfer staged storage into existing upper inodes, preserving hardlinks without an additional whole-file buffer. readFile and writeFile remain buffering convenience APIs; use readStream/writeStream for large payloads. maxBufferBytes retains its existing optional total-byte admission semantics, not a working-memory guarantee.

A streaming overlay needs an upper with atomicRename, streamingWrite, retained reads (retainedRead plus openReadFile), exclusive create, directory mutation, permissions and timestamps. Both layers must provide retained, bounded range reads with stable file and directory identities. For example, inject these host-authorized adapters with createOverlayFileSystem({ upper: externalSpool, lower: retainedSource }), then use overlay.writeStream(path, source). The upper must await each consumed chunk, keep transport queues bounded, and preserve ownership until publication. Supply bounded chunks at the input boundary; the overlay cannot undo a payload-wide allocation already made by its caller. Whole-file-only uppers explicitly reject transfer operations with ENOTSUP; they do not silently collect streams.

For Workers, externalSpool must store payloads outside isolate RAM and satisfy that contract, including atomic rename and retained identity. Memory and the S3 mock store payloads in RAM and are not external spools. Stock S3/WebDAV do not provide the atomic rename/retained-read contract needed for an overlay upper; setting a capability flag does not add it. A host must inject a qualified external adapter. On Node, rooted Real adapters provide the streaming overlay configuration under their documented external-tree isolation requirement. Mount and read-only wrappers forward the selected backend's capabilities; quota streaming additionally requires a real streaming-append route. S3's buffered append convenience path reports streamingAppend: false.

The reusable metadata-only fixture in tests/fixtures/bounded-filesystem.ts generates file ranges and consumes slow writes without storing payload bytes. tests/overlay-bounded.test.ts checks fixed transfer sizes, source backpressure, cancellation/error cleanup, byte results, and Memory publication allocation/identity. These deterministic conformance checks do not qualify a deployed Worker backend; follow the Worker QA plan for runtime measurements.

readFileStream(fs, path, options) reads through the caller's native stream or retained range handle without calling readFile. It accepts the stream range/window options above, defaults to a 64 KiB read window, and closes owned readers on completion, failure or cancellation. skipStream: true selects retained ranges after an already-attempted stream reports ENOTSUP. Chunks are borrowed until the iterator advances. Native adapters must honor the requested window and backpressure. Whole-file-only backends receive ENOTSUP; use a streaming or retained-range backend for sequential Worker operations. This helper bounds its I/O window, not the consuming command's parser or output capture.

collectBytes(source, { maxBytes, maxMemoryBytes, signal }) snapshots streamed chunks into one growing buffer. maxBytes and maxMemoryBytes accept explicit Infinity. maxMemoryBytes limits owned capacity, the current input's full backing buffer, and overlapping allocations during growth; exhaustion throws EFBIG. Collection limits are disabled by default on Node, browser, and Worker targets. Each call owns its budget; concurrent collections do not consume one another’s allowance. The returned view may retain geometric spare capacity. Configured budgets cover collection, not caller-retained results, transport buffering, archive decoding, strings, or the rest of the runtime; use streaming APIs and limit concurrent workloads for larger inputs.

S3FileSystem requires transport and bucket. A transport supplies headObject, getObject, putObject, deleteObject, copyObject, and listObjectsV2; see the transport interface.

createS3NamespaceFileSystem resource limits (maxBytes, maxEntries, maxManifestBytes, maxAttempts, and inherited descriptor limits) are unlimited when omitted or set to Infinity; positive safe integers enable finite limits. Namespace paths have no implicit length cap; configured manifest budgets still apply.

createS3Transport(client, capabilities?) wraps a compatible client with explicit streamingRead, streamingWrite, conditionalPut, conditionalCopy, and conditionalDelete flags (all absent by default). Direct transport requests accept an optional abortSignal.

For in-memory S3 simulations, new MockS3Client({ buckets, pageSize?, now?, authorize? }) requires bucket names. pageSize defaults to 1,000 (range 1–1,000), now defaults to the current date/time, and optional authorize(request) can reject requests. It does not verify a real provider.

| Optional field | Default / meaning | | --- | --- | | prefix | Empty; canonical relative object-key prefix | | readOnly | false | | allowNonAtomicRename | true; rename copies then deletes, without rollback guarantees | | pageSize | 1,000; range 1–1,000 | | maxReadBytes | Unlimited unless configured | | maxStreamBytes | Unlimited unless configured | | maxListEntries | Unlimited unless configured | | removalLimits.maxRequests | Unlimited unless configured; transport calls per rm, including lookup, listing, and deletes | | removalLimits.maxListEntries | Unlimited unless configured; returned listing entries in aggregate per rm, including lookup | | removalLimits.maxDeleteObjects | Unlimited unless configured; objects per rm | | compareEntry | Optional trusted backing-identity callback |

Removal limits apply even when shell filesystem-call limits admit a recursive rm as one operation. Removal limits default to unlimited and accept a positive safe integer or Infinity. Read, stream, listing, and request limits also accept explicit Infinity. Traversal stops at the listing/request cap and rejects with EFBIG; all delete requests must fit the remaining request budget before the first mutation. Nonrecursive removal checks for children using pages of at most two entries. Configure finite removalLimits to bound the work in deployments that need them. These limits count adapter transport calls; retries inside a supplied transport need their own limit. Remote failures or concurrent writers can still cause partial deletion after preflight.

For larger trees, a trusted integration can process one bounded batch per request/job using its explicitly supplied transport. This example uses at most 17 transport calls and retains at most 16 summaries; repeat in a later job until done. The prefix must come from trusted deployment configuration, include the filesystem's configured prefix, and end in /. This deliberately bypasses filesystem collision checks and deletes directory markers as well as files; serialize it with writers when complete removal is required.

async function removeBatch(transport, bucket, trustedDirectoryPrefix, abortSignal) {
  const options = { abortSignal };
  const page = await transport.listObjectsV2({
    Bucket: bucket, Prefix: trustedDirectoryPrefix, MaxKeys: 16,
  }, options);
  const objects = page.Contents ?? [];
  if (objects.length > 16 || page.CommonPrefixes?.length
    || objects.some(object => !object.Key?.startsWith(trustedDirectoryPrefix))
    || typeof page.IsTruncated !== "boolean") {
    throw new Error("Invalid batch listing");
  }
  for (const object of objects) {
    await transport.deleteObject({ Bucket: bucket, Key: object.Key }, options);
  }
  return { deleted: objects.length, done: !page.IsTruncated };
}

Each batch lists from the beginning because previous keys have been deleted; it does not reuse continuation tokens across mutations. A job runner should cap the number of batches and schedule remaining work separately.

createS3HttpTransport requires endpoint (an origin without path or credentials), region, and credentials. Credentials contain accessKeyId, secretAccessKey, and optional sessionToken, or come from an async provider receiving { signal }.

| Optional field | Default / meaning | | --- | --- | | addressingStyle | path; alternative virtual-hosted requires a DNS endpoint | | listUrlEncoding | percent; alternative form | | allowInsecureHttp | false; HTTPS required unless explicitly enabled | | maxPutBytes, maxGetBytes | Unlimited when omitted or Infinity; positive safe integers set byte caps | | maxXmlBytes | Unlimited when omitted or Infinity; positive safe integers set a byte cap | | requestTimeoutMs | Unlimited unless configured | | enableCopy | true; disabling uses a buffered GET/PUT fallback | | verifiedConditionalOperations | Optional put, copy, delete booleans, each defaulting to false; enable only after verifying the server's semantics | | clock | Current date/time function, used for signing | | request | Optional Node HTTP request factory; Node defaults to HTTP(S), browsers and Workers use global fetch |

WebDavFileSystem requires baseUrl and fetch.

| Optional field | Default / meaning | | --- | --- | | headers | Empty; explicit authentication/custom headers. Protocol-reserved headers are rejected; authorization and cookies require HTTPS. | | requestStreamSupport | native for global Fetch, otherwise false; accepts native or a boolean declaration for the injected transport | | maxResponseBytes | Unlimited unless configured; applies to decoded response bytes | | maxXmlBytes | Unlimited unless configured; applies before metadata decoding/parsing | | xmlLimits | Optional maxNodes, maxContentNodes, maxDepth, and maxAttributes; each accepts a positive safe integer or Infinity, and is unlimited unless configured | | maxEntries | Unlimited unless configured | | timeoutMs | Unlimited unless configured | | overwritePolicy | lock; alternative etag uses conditional overwrites | | atomicEmptyDirectory | Optional trusted binding with the canonical namespaceUrl and removeEmptyDirectory callback; required for strict empty-only rmdir | | compareEntry | Optional trusted backing-identity callback on Node; unavailable under browser policy |

Known identity Content-Length responses use one result buffer; other responses grow storage up to the configured ceiling and return a view without a final copy. Growth can temporarily retain the old and new buffers (up to three times the response size), plus transport chunks. Hosts can configure per-response limits to budget for metadata parsing, text decoding, transport buffers, and concurrent reads. Use streaming reads for large files.

WebDAV maxEntries, maxResponseBytes, maxXmlBytes, and per-read maxBytes accept explicit Infinity. WebDAV metadata structure limits are disabled by default; configure xmlLimits independently of maxEntries. Text is bounded by maxXmlBytes. Exceeding a structural budget reports EFBIG; parsing yields cooperatively so caller cancellation and timeoutMs remain active.

See the binding types before implementing atomic directory removal. A recursive WebDAV DELETE does not satisfy that contract.

Safety boundary and limitations

  • Not an OS sandbox. Rooted storage and bridge confinement check paths and symlinks, but checks and host operations can race concurrent changes. Use a separate OS isolation boundary for hostile workloads. A custom adapter or transport is trusted code with its own host authority.
  • A read-only view is not an immutable store. Mount views serialize guest symlink, rename, and removal operations against active path operations. Finish or close read streams before awaiting these namespace changes. Other references can still change the backing filesystem; this coordination applies only to operations through the same mount view. Overlay reads require retained handles and stable object and ancestor identities; unsupported backends or changed read admission fail with ENOTSUP. Overlays with two stock MemoryFileSystem layers support race-safe patch publication, conditional file writes/removal, no-replace rename, and confined directory creation/removal and file writes. Compression file output, mv -n, sed -i, and ssconvert work with these layers. Upper-file writes retain file identity; lower-file writes copy up without changing lower bytes. Publication checks both layers, retains logical directory identity across copy-up, and leaves lower bytes intact; wrapped or customized layers remain unsupported. Overlays are not transactions; cancellation and cleanup do not guarantee rollback. Cross-mount rename can fail with EXDEV.
  • Remote storage is not a POSIX disk. S3 and WebDAV do not provide hardlinks or symlinks. S3 rename is non-atomic and may leave partial copies/deletions; S3RenameError reports the phase and affected keys. Strong empty-only removal and conditional writes depend on backend support, not a prior listing.
  • The bridges are partial Node compatibility layers. No synchronous/callback API, file handles, watchers, bigint stats, flush/retry support, or cp dereference/timestamp-preservation options. Missing optional operations fail rather than being approximated with destructive alternatives.
  • Browser support is filesystem-only. The browser and workerd export conditions select the portable surface: memory, mounts, overlays, read-only, WebDAV, and the codec-based bridge. S3 is available through fs/s3 with an explicit transport, including its portable mock and fs/s3/http. The HTTP transport uses fetch in browsers and Workers and requires server CORS support in browsers. Fetch rejects keys containing dot segments that the URL parser would normalize, and never follows redirects. Node request factories are only supported in Node. Real storage, the Node bridge, and the configuration registry are not browser exports. WebDAV still needs server CORS support; no OPFS or directory-handle adapter is included. This does not make the SafeJS runtime browser-compatible.
  • Portable temporary names require secure randomness. Browser overlay staging and portable bridge mkdtemp require crypto.randomUUID or crypto.getRandomValues; without either, they fail with ENOTSUP rather than use Math.random.
  • Allocation and identity may be unknown. Optional FileStat.allocatedBytes is provider-reported allocation, not logical length or reclaimable space. Do not infer identity from size, timestamps, or inode numbers across unrelated backends.

MemoryFileSystem.confineExtraction(roots) returns a view for archive extraction. It retains each root directory and its ancestors, refuses symlink ancestry at mutation commit, and keeps streamed writes attached to the opened file node. Other adapters omit this operation unless they can enforce the same boundary. Trusted host decorators must explicitly preserve confinement when forwarding it; custom mutation hooks must preserve the atomic commit contract.