Skip to main content
Mirage ships BoxVFS in two runtimes:
  • @struktoai/mirage-node, uses client_id + client_secret + refresh token (rotated on each use)
  • @struktoai/mirage-browser, supports the same refresh token via PKCE (no client secret in the bundle)
Both runtimes hit the same Box v2 API endpoints (/folders/{id}/items, /files/{id}/content, /search, plus multipart upload, folder create, delete, rename/move, and copy for writes). Credentials are obtained the same way in both runtimes, see Box Credentials. Both READ and WRITE modes are supported (mount with { mode: MountMode.WRITE } to enable tee/cp/mv/rm/mkdir/touch). Pass rootFolderId to mount a single Box folder as the workspace root instead of the whole account; folder ids are stable across renames and moves. The Python package ships the same backend, see Box (Python).

Quick start: developer token

For exploration, skip the OAuth flow and use a developer token (one-button-click in the Box app console, 60-minute lifetime). The BoxVFS accepts an accessToken field that short-circuits the refresh logic:
When the token expires (Box 401s with invalid_token), regenerate it in the console and re-run. See Box Credentials -> Quick Start.

Service account (client credentials, no refresh token)

For headless server auth, skip OAuth and refresh tokens entirely. Create a Box app with the Server Authentication (Client Credentials Grant) method, authorize it once in the Box admin console (Apps -> Custom Apps Manager), and pass the enterprise ID:
Tokens are minted for the app’s service account and re-fetched automatically on expiry; there is no refresh token to rotate or persist.
The service account is a separate Box user with its own (initially empty) root folder. To see your content, share folders with the service account’s email address, shown under the app’s General Settings in the developer console.

Node (server-side, long-running)

The BoxTokenManager caches the access token in memory (5-minute safety buffer before expiry) and rotates the refresh token on every refresh. Without onRefreshTokenRotated, the rotation is in-memory only and a process restart needs a fresh refresh token.

Browser (PKCE, no client secret)

Box requires the origin to be allowlisted on the app’s Allowed Origins config (see setup). The bundled examples/typescript/browser/src/box_pkce.ts runs the full PKCE dance and persists the rotated refresh token to localStorage.

VFS mode (patchNodeFs)

Only fs.promises.* is patched, sync forms aren’t supported.

FUSE mode

Requires macFUSE on macOS or libfuse on Linux.

Path → ID resolution

Box uses numeric folder/file IDs internally (root folder is id 0). Mirage caches the path → ID mapping in the RAMIndexCacheStore attached to the VFS. The first time you ls /box/foo/bar/, it walks the tree top-down (one API call per level) and caches each entry’s ID. Subsequent reads of /box/foo/bar/anyfile.json hit the cache instead of re-walking. Listings live for the mount’s ttl (600 s by default), at most 24 hours. To force re-resolution, recreate the BoxVFS.

Reads under fresh

A Box mount accepts read: fresh. The token is the file’s sha1, which a folder listing and GET /files/{id} both carry; a download names no version, so a whole read stamps the listed sha1 only when the downloaded bytes hash to it, and a ranged read stamps nothing. modified_at is never the token: two same-size edits in one second share it. Checking a cached file costs one request, GET /files/{id} by the id the mount last listed, while the mount still holds that file’s listing row. The answer is trusted only when Box places the same active file at the same path from the mount root; a file that was moved, renamed, trashed or recreated falls back to listing each folder from the mount root, one listing per level (up to two per level when the file is gone). When that fallback confirms byte-identical content, the built-in RAM and Redis indexes retain the newly resolved file row if the original hint is still current. A later check then uses the recovered id without refreshing the parent listing. Custom indexes must implement replaceIfUnchanged to retain this row; otherwise later checks repeat the fallback walk. A persistent 403 still requires the failed point request and a listing walk on each check. Any stale, unknown or gone verdict on the mount drops its cached listings, so the checks after one walk until a cold read or an ls lists the folders again. A file Box reports without a sha1 cannot be verified: each fresh read of it downloads it again and drops the mount’s cached listings. A folder’s etag does not move when its children change, so under fresh listings are re-fetched once per command. Repeated uses within that command share the result; outside commands, the existing one-second listing trust window applies. Before fetching a nested listing, mirage resolves its folder through current parent listings up to the configured mount root. A folder moved away and replaced under the same name is therefore resolved to the replacement on the next fresh command. This costs one listing per ancestor level; a warm file check still uses one metadata request.

Box-native file types

Every Box item is served as its raw bytes, keeping its real name (no .json suffix). Box has no API to edit these formats from a structured payload, so Mirage does not render them, it hands back exactly what Box stores:
  • .boxnote / .boxcanvas, Box’s native Notes and Canvas, stored as ProseMirror-style JSON. They are text, so cat foo.boxnote | jq . works if you want to poke at the structure.
  • .gdoc / .gsheet / .gslides, Box’s V2 Google-format files, stored as Office Open XML zips (.docx / .xlsx / .pptx). These are opaque binary, like any other Office document on a Mirage mount.
For a rich, editable view of Google-format documents, mount them through Google Drive with Google credentials instead, the gws docs ... commands operate on Google file IDs and do not apply to Box.

Available commands

BoxVFS ships the same shell command set as GDriveVFS and DropboxVFS:
  • Filesystem: ls, cat, head, tail, nl, wc, stat, find, tree, du, file, realpath, basename, dirname
  • Writes (in WRITE mode): tee, cp, mv, rm (-r), mkdir (-p), touch, truncate
  • Search/text: grep, rg, awk, sed, sort, uniq, cut, diff, cmp, jq
  • Encoding: base64

Examples

End-to-end runnable scripts are in examples/typescript/box/:
  • box.ts, Workspace.shell('ls/stat/cat/tree …') shell demo
  • box_vfs.ts, patchNodeFs + native fs.promises.* calls
  • box_fuse.ts, FUSE mount with shell access in another terminal