> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Box

> 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 theme={null}
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 theme={null}
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 theme={null}
pnpm add @struktoai/mirage-node
```

```ts theme={null}
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 theme={null}
pnpm add @struktoai/mirage-browser
```

```ts theme={null}
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 theme={null}
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 theme={null}
import { BoxResource, Mount, MountMode, Workspace } from '@struktoai/mirage-node'

const ws = new Workspace({ '/box': new Mount(box, { mode: MountMode.READ, fuse: true }) })
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`.

## 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 theme={null}
{
  "id": "...",
  "body_text": "paragraphs joined by \n",
  "paragraphs": [{ "text": "...", "authors": ["..."] }],
  "authors": { "<id>": "Author Name" },
  "last_edit_at": "..."
}
```

```bash theme={null}
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 theme={null}
{
  "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 theme={null}
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 theme={null}
{
  "id": "...",
  "name": "hihi.gdoc.json",
  "format": "docx",
  "size": 8921,
  "modified_at": "...",
  "body_text": "auto-extracted plain text"
}
```

```bash theme={null}
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
