---
title: Box
icon: /images/box-logo.svg
description: Mount Box as a MIRAGE resource from Node or the browser via OAuth2.
---

Mirage ships `BoxResource` 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](https://developer.box.com/reference/) 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](/home/setup/box).

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)](/python/resource/box).

## 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 `BoxResource` accepts an `accessToken` field that
short-circuits the refresh logic:

```ts
import { BoxResource, MountMode, Workspace } from '@struktoai/mirage-node'

const box = new BoxResource({
  accessToken: process.env.BOX_DEVELOPER_TOKEN!,
})
const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
await ws.execute('ls /box/')
```

When the token expires (Box 401s with `invalid_token`), regenerate it in the console and
re-run. See [Box Credentials -> Quick Start](/home/setup/box#quick-start-developer-token-60-minutes-no-oauth).

## 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:

```ts
const box = new BoxResource({
  clientId: process.env.BOX_CLIENT_ID!,
  clientSecret: process.env.BOX_CLIENT_SECRET!,
  enterpriseId: process.env.BOX_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.

<Warning>
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.
</Warning>

## Node (server-side, long-running)

```bash
pnpm add @struktoai/mirage-node
```

```ts
import { BoxResource, MountMode, Workspace } from '@struktoai/mirage-node'

const box = new BoxResource({
  clientId: process.env.BOX_CLIENT_ID!,
  clientSecret: process.env.BOX_CLIENT_SECRET!,
  refreshToken: process.env.BOX_REFRESH_TOKEN!,
  // Box rotates the refresh token; persist the new one if you want to survive restarts.
  onRefreshTokenRotated: async (next) => {
    await fs.writeFile('.box-refresh', next, 'utf-8')
  },
})

const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
const res = await ws.execute('ls /box/')
console.log(res.stdoutText)
```

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)

```bash
pnpm add @struktoai/mirage-browser
```

```ts
import { BoxResource, MountMode, Workspace } from '@struktoai/mirage-browser'

// Refresh token obtained via the PKCE flow, see examples/typescript/browser/src/box_pkce.ts.
const box = new BoxResource({
  clientId: import.meta.env.VITE_BOX_CLIENT_ID,
  refreshToken: localStorage.getItem('box-refresh')!,
  onRefreshTokenRotated: (next) => localStorage.setItem('box-refresh', next),
})

const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
await ws.execute('ls /box/')
```

Box requires the origin to be allowlisted on the app's **Allowed Origins** config (see
[setup](/home/setup/box#cors-notes)). The bundled
[`examples/typescript/browser/src/box_pkce.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/browser/src/box_pkce.ts)
runs the full PKCE dance and persists the rotated refresh token to `localStorage`.

## VFS mode (`patchNodeFs`)

```ts
import { createRequire } from 'node:module'
import { BoxResource, MountMode, patchNodeFs, Workspace } from '@struktoai/mirage-node'

const require = createRequire(import.meta.url)
const fs = require('fs') as typeof import('fs')

const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
const restore = patchNodeFs(ws)

const entries = await fs.promises.readdir('/box/')
const stat = await fs.promises.stat('/box/Documents')
const bytes = await fs.promises.readFile('/box/Documents/example.json')

restore()
```

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

## FUSE mode

```ts
import { BoxResource, Mount, MountMode, Workspace } from '@struktoai/mirage-node'

const ws = new Workspace({ '/box': new Mount(box, { mode: MountMode.READ, backend: MountBackend.FUSE }) })
await ws.fuseReady()
const mp = ws.fuseMountpoint
// The box subtree is exposed at the mountpoint root, so ${mp}/ is a real path:
//   ls ${mp}/
//   find ${mp} -type f
//   cat ${mp}/example.json | jq .
await ws.close()
```

Requires [macFUSE](https://osxfuse.github.io/) 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 resource. 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.

The cache TTL defaults to 24h. To force re-resolution, recreate the `BoxResource`.

## 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](/home/setup/google) with Google credentials instead, the `gws docs ...`
commands operate on Google file IDs and do not apply to Box.

## Available commands

`BoxResource` ships the same shell command set as `GDriveResource` and `DropboxResource`:

- **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/`](https://github.com/strukto-ai/mirage/tree/main/examples/typescript/box):

- [`box.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/box/box.ts), `Workspace.execute('ls/stat/cat/tree …')` shell demo
- [`box_vfs.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/box/box_vfs.ts), `patchNodeFs` + native `fs.promises.*` calls
- [`box_fuse.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/box/box_fuse.ts), FUSE mount with shell access in another terminal
