@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
175 lines • 7.12 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { enoent, enotdir } from "../../errors/fs.js";
import { FileType } from "../../types.js";
import { getFileInfo, getFolderInfo, listFolderItems, } from "./api.js";
import { BoxApiError } from "./client.js";
import { ACTIVE, ALL_FILES_FOLDER_ID, SHA1, TRASH_FOLDER_ID } from "./constants.js";
export function pathParts(path) {
return path.vfsPath.split('/').filter((p) => p !== '');
}
/**
* Walk folder listings to resolve each component of a path.
*
* Box has no path-addressing endpoint, so ids are resolved by listing each
* level from the mount root. Returns the Box item for every component that
* resolved, in order; a missing component, or one below a non-folder, ends
* the chain early.
*/
export async function resolveChain(accessor, parts) {
const tm = accessor.tokenManager;
let curId = accessor.rootFolderId;
const chain = [];
for (const name of parts) {
if (chain.length > 0 && chain[chain.length - 1]?.type !== 'folder')
break;
const children = await listFolderItems(tm, curId);
const match = children.find((c) => c.name === name);
if (match === undefined)
break;
chain.push(match);
curId = match.id;
}
return chain;
}
/**
* Resolve a mount-relative path to its Box item.
*
* Returns null if any component is missing, or a non-final component is not
* a folder.
*/
export async function resolveItem(accessor, parts) {
const chain = await resolveChain(accessor, parts);
if (parts.length === 0 || chain.length < parts.length)
return null;
return chain[chain.length - 1] ?? null;
}
/**
* The Box item a listed row names, in the shape a lookup answers. Mirrors
* Python's `item_of`.
*/
export function itemOf(row) {
if (row === null || row === undefined)
return null;
const sha1 = row.extra[SHA1];
const etag = row.extra.etag;
return {
id: row.extra.box_id,
type: row.type === FileType.DIRECTORY ? 'folder' : 'file',
name: row.name,
...(typeof sha1 === 'string' ? { sha1 } : {}),
...(typeof etag === 'string' ? { etag } : {}),
};
}
/**
* The folder a create at `path` lands in: the mount root's, or what is at the
* parent path. A missing or non-directory parent names the operand, as
* mkdir(2) does. Mirrors Python's `parent_folder`.
*/
export function parentFolder(accessor, path, parent) {
if (pathParts(path).length <= 1)
return accessor.rootFolderId;
if (parent === null || parent === undefined)
throw enoent(path.virtual);
if (parent.type !== FileType.DIRECTORY)
throw enotdir(path.virtual);
return parent.extra.box_id;
}
/**
* Mount-relative path of an item, from its `path_collection`.
*
* Box lists an item's ancestors from the account root down to its immediate
* parent, excluding the item itself; everything up to and including the mount
* root folder is trimmed. Null when the mount root is not among the
* ancestors, which is every item outside the mount.
*/
export function mountRelativeKey(item, rootFolderId) {
const entries = item.path_collection?.entries ?? [];
const names = [];
let collecting = false;
for (const anc of entries) {
if (collecting)
names.push(anc.name);
if (anc.id === rootFolderId)
collecting = true;
}
if (!collecting)
return null;
names.push(item.name);
return names.filter((n) => n !== '').join('/');
}
/**
* Whether a live `GET /files/{id}` answer is the active file at `path`.
*
* Box enforces unique names per folder, so a file whose live chain of names
* from the mount root equals the path is the very item walking the path would
* reach. Four checks: the item is a file, its `item_status` is active, its
* `path_collection` starts at All Files and passes no Trash folder, and its
* names below the mount root equal the path. The name comparison alone
* already rejects a trashed chain below the mount root; the All Files check
* also refuses a chain that does not start at All Files yet passes through
* the mount root with the right names below it, which the name comparison
* alone would accept, and with the Trash check catches the mount root itself
* in Trash.
*/
export function namesThisPath(accessor, item, path) {
if (item.type !== 'file' && item.type !== 'folder')
return false;
if (item.item_status !== ACTIVE)
return false;
const chain = item.path_collection?.entries ?? [];
if (chain.length === 0 || chain[0]?.id !== ALL_FILES_FOLDER_ID)
return false;
if (chain.some((anc) => anc.id === TRASH_FOLDER_ID))
return false;
return mountRelativeKey(item, accessor.rootFolderId) === pathParts(path).join('/');
}
/**
* The item a listed row names, as `liveItem` finds it at `path` now; null for
* no row. Mirrors Python's `live_item_of`.
*/
export async function liveItemOf(accessor, path, row) {
const item = itemOf(row);
return item === null ? null : liveItem(accessor, path, item);
}
/**
* What is at `path` now, asked live. A listed row can be older than the
* path's item: another client may have moved, renamed or trashed it, and a
* change made by its id would land at another path. So a check and every
* change ask again: one `GET /files/{id}` or `GET /folders/{id}` by the id the
* row names, when the answer is still the active item at exactly this path,
* else a walk of the listings from the mount root. Only the walk may call a
* path gone, so a 404 or 403 by id falls back to it rather than answering
* null. Mirrors Python's `live_item`.
*/
export async function liveItem(accessor, path, lead) {
if (lead !== null && lead.id !== '') {
let item = null;
try {
item =
lead.type === 'folder'
? await getFolderInfo(accessor.tokenManager, lead.id)
: await getFileInfo(accessor.tokenManager, lead.id);
}
catch (err) {
if (!(err instanceof BoxApiError) || (err.status !== 403 && err.status !== 404))
throw err;
console.warn(`listed id for ${path.virtual} unusable: ${String(err)}`);
}
if (item !== null && namesThisPath(accessor, item, path))
return item;
}
return resolveItem(accessor, pathParts(path));
}
//# sourceMappingURL=resolve.js.map