---
title: Box
icon: box
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`). Credentials are obtained the same way
in both runtimes, see [Box Credentials](/home/setup/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).

## 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, FuseManager, MountMode, Workspace } from '@struktoai/mirage-node'

const ws = new Workspace({ '/box': box }, { mode: MountMode.READ })
const fm = new FuseManager()
const mp = await fm.setup(ws)
// ${mp}/box/ is now a real filesystem path:
//   ls ${mp}/box/
//   find ${mp}/box -type f
//   cat ${mp}/box/example.json | jq .
await fm.close(ws)
```

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`.

## Special file types

Five Box-specific file types get a `.json` suffix appended in the vfs and return clean,
agent-friendly JSON when you `cat` them. The Box file ID is unchanged, the suffix is
purely a vfs hint that `cat` returns JSON, mirroring Google Drive's `.gdoc.json` style.

In `ls /box/`:

```
hihi.gdoc.json           # Box's Google Doc (backing bytes are .docx)
gsheet.gsheet.json       # Box's Google Sheet (.xlsx)
hihihi.gslides.json      # Box's Google Slides (.pptx)
test.boxnote.json        # Box Note
test canvas.boxcanvas.json
```

### `.boxnote.json`, Box Notes

```json
{
  "id": "...",
  "body_text": "paragraphs joined by \n",
  "paragraphs": [{ "text": "...", "authors": ["..."] }],
  "authors": { "<id>": "Author Name" },
  "last_edit_at": "..."
}
```

```bash
cat /box/foo.boxnote.json | jq -r .body_text   # plain text
cat /box/foo.boxnote.json | jq .authors        # who wrote it
```

### `.boxcanvas.json`, Box Canvas (whiteboards)

```json
{
  "id": "...",
  "widget_count": 11,
  "widgets_by_type": { "shape": 6, "link": 5 },
  "body_text": "shape labels joined by \n",
  "widgets": [{ "id": "...", "type": "shape", "text": "..." }],
  "authors": ["..."]
}
```

```bash
cat "/box/foo.boxcanvas.json" | jq -r .body_text
cat "/box/foo.boxcanvas.json" | jq .widgets_by_type
```

### `.gdoc.json` / `.gsheet.json` / `.gslides.json`, Box's V2 Google files

Box's V2 G Suite integration stores Google-format files as Office Open XML zips (`.docx` /
`.xlsx` / `.pptx`). On `cat`, Mirage uses Box's
[`representations` API](https://developer.box.com/reference/get-files-id/#param-representations)
with `X-Rep-Hints: [extracted_text]` to fetch Box's auto-extracted plain-text version, then
returns a JSON envelope:

```json
{
  "id": "...",
  "name": "hihi.gdoc.json",
  "format": "docx",
  "size": 8921,
  "modified_at": "...",
  "body_text": "auto-extracted plain text"
}
```

```bash
cat /box/hihi.gdoc.json | jq -r .body_text
cat /box/gsheet.gsheet.json | jq .format       # "xlsx"
```

This is **different from Google Drive's `.gdoc.json`**, Box uses its own API + extracted-text
representation; Google Drive uses Google's full Documents API and returns the rich Document
structure (paragraphs, named styles, lists, etc.). For real edits to the underlying Google
Doc, mount Google Drive separately with Google credentials and use the
[GDocs / GSheets / GSlides resources](/home/setup/google), `gws-docs-*` commands won't work
on Box because the file IDs are in different namespaces.

## 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`
- **Search/text**: `grep`, `rg`, `awk`, `sed`, `sort`, `uniq`, `cut`, `diff`, `cmp`, `jq`
- **Encoding**: `base64`
- **Format-aware**: `cat_parquet`, `cat_feather`, `cat_hdf5`, plus `head_*`/`grep_*`/`ls_*`/
  `stat_*`/`tail_*`/`wc_*`/`file_*`/`cut_*` variants for `.parquet`, `.feather`, `.hdf5`/`.h5`,
  `.orc` files

## 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_parquet.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/box/box_parquet.ts), parquet preview through `cat`
- [`box_boxnote.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/box/box_boxnote.ts), `.boxnote.json` decoded into clean JSON
- [`box_office.ts`](https://github.com/strukto-ai/mirage/blob/main/examples/typescript/box/box_office.ts), `.gdoc.json` / `.gsheet.json` / `.gslides.json` via Box's extracted_text representation
- [`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
