Files
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

263 lines
9.6 KiB
Plaintext

---
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).
## 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, 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
{
"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