@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
220 lines • 9.28 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 { DROPBOX_API_BASE, DROPBOX_CONTENT_BASE, DROPBOX_TOKEN_URL, TOKEN_BUFFER_SECONDS, RESULT_HEADER, } from "./constants.js";
import { callerSignal } from "../../context/session_context.js";
import { apiRequest, loweredHeaders, stallTimeout } from "../api/client.js";
import { TokenManager as OAuthTokenManager } from "../api/oauth.js";
import { ByteSpool } from "../../io/spool.js";
import { rstripSlash } from "../../utils/slash.js";
import {} from "../../utils/ranges.js";
import { ApiError } from "../api/errors.js";
// The Dropbox-API-Arg header carries JSON, and a header is a ByteString:
// a path with a character past U+00FF makes fetch refuse the request.
// Dropbox reads JSON escapes there, so every non-ASCII character goes as
// \uXXXX, which is what python's json.dumps sends by default.
function headerJson(arg) {
return JSON.stringify(arg).replace(/[\u007f-\uffff]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`);
}
export class DropboxApiError extends ApiError {
/** Dropbox `error_summary` (e.g. "path/not_found/..", "path/conflict/folder/.."). */
summary;
constructor(message, status, summary = '') {
super(message, status);
this.summary = summary;
this.name = 'DropboxApiError';
}
}
/** An error body's `error_summary`, or '' when it carries no string. */
function summaryOf(text) {
let body;
try {
body = JSON.parse(text);
}
catch {
return '';
}
if (typeof body !== 'object' || body === null)
return '';
const summary = body.error_summary;
return typeof summary === 'string' ? summary : '';
}
function tokenUrlOf(config) {
if (config.endpoint === undefined || config.endpoint === '')
return DROPBOX_TOKEN_URL;
return `${rstripSlash(config.endpoint)}/oauth2/token`;
}
async function refreshAccessToken(config) {
const body = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: config.refreshToken,
client_id: config.clientId,
});
if (config.clientSecret !== undefined && config.clientSecret !== '') {
body.set('client_secret', config.clientSecret);
}
const data = (await apiRequest('POST', tokenUrlOf(config), {
timeout: config.timeout,
errorOf: (r, text) => new DropboxApiError(`Dropbox token refresh → ${String(r.status)} ${text}`, r.status),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
}));
return [data.access_token, data.expires_in];
}
export class DropboxTokenManager extends OAuthTokenManager {
apiBase;
contentBase;
config;
constructor(config) {
super(TOKEN_BUFFER_SECONDS, undefined, config.timeout);
this.config = config;
if (config.endpoint !== undefined && config.endpoint !== '') {
const base = `${rstripSlash(config.endpoint)}/2`;
this.apiBase = base;
this.contentBase = base;
}
else {
this.apiBase = DROPBOX_API_BASE;
this.contentBase = DROPBOX_CONTENT_BASE;
}
}
async refreshPair() {
if (this.config.refreshFn !== undefined) {
const result = await this.config.refreshFn(this.config.refreshToken);
return [result.accessToken, result.expiresIn];
}
return refreshAccessToken(this.config);
}
}
async function dropboxAuthHeaders(tm) {
const token = await tm.getToken(callerSignal());
return { Authorization: `Bearer ${token}` };
}
export async function dropboxRpc(tm, endpoint, body) {
const headers = await dropboxAuthHeaders(tm);
return apiRequest('POST', `${tm.apiBase}${endpoint}`, {
timeout: tm.timeout,
errorOf: (r, text) => new DropboxApiError(`Dropbox POST ${endpoint} → ${String(r.status)} ${text}`, r.status, summaryOf(text)),
headers: { ...headers, 'Content-Type': 'application/json' },
json: body,
});
}
/**
* Upload one file, overwriting, and return the reply as decoded. Given a
* `rev`, the upload goes in `update` mode: Dropbox stores it only while the
* file is still that revision, and answers a 409 `path/conflict` otherwise.
*
* The reply is the stored FileMetadata; it is not checked here, since the
* upload has landed once the call returns and the writer's `uploadToken`
* reads it without throwing.
*/
export async function dropboxUpload(tm, path, data, rev = null) {
if (data instanceof ByteSpool) {
if (data.size > 8 * 1024 * 1024)
return dropboxUploadSession(tm, path, data, rev);
data = await data.read(0, data.size);
}
const headers = await dropboxAuthHeaders(tm);
const arg = rev === null
? { path, mode: 'overwrite', mute: true }
: { path, mode: { '.tag': 'update', update: rev }, mute: true, autorename: false };
const resp = (await apiRequest('POST', `${tm.contentBase}/files/upload`, {
timeout: tm.timeout,
errorOf: (r, text) => new DropboxApiError(`Dropbox upload ${path} → ${String(r.status)} ${text}`, r.status, summaryOf(text)),
headers: {
...headers,
'Dropbox-API-Arg': headerJson(arg),
'Content-Type': 'application/octet-stream',
},
body: data,
read: 'response',
}));
return resp.data;
}
/**
* Download a file, or a byte range of it, with its result header: the raw
* `Dropbox-API-Result`, or null when the response carries none
* (`fingerprint.resultOf` reads it).
*/
export async function dropboxDownload(tm, path, window) {
const headers = await dropboxAuthHeaders(tm);
const resp = (await apiRequest('POST', `${tm.contentBase}/files/download`, {
timeout: tm.timeout,
errorOf: (r, text) => new DropboxApiError(`Dropbox download ${path} → ${String(r.status)} ${text}`, r.status, summaryOf(text)),
headers: { ...headers, 'Dropbox-API-Arg': headerJson({ path }) },
read: 'bytes_response',
window,
}));
return [resp.data, resp.headers[RESULT_HEADER.toLowerCase()] ?? null];
}
/**
* Stream a file's bytes; `onResponse` is handed its headers, lower-cased as
* `bytes_response` hands them, before the first chunk.
*/
export async function* dropboxDownloadStream(tm, path, onResponse) {
const headers = await dropboxAuthHeaders(tm);
const url = `${tm.contentBase}/files/download`;
const r = await stallTimeout(tm.timeout)(url, {
method: 'POST',
headers: { ...headers, 'Dropbox-API-Arg': headerJson({ path }) },
});
if (!r.ok) {
const text = await r.text().catch(() => '');
throw new DropboxApiError(`Dropbox download ${path} → ${String(r.status)} ${text}`, r.status, summaryOf(text));
}
try {
onResponse?.(loweredHeaders(r.headers));
}
catch (err) {
await r.body?.cancel().catch((cancelErr) => {
console.debug(`Dropbox download ${path}: body cancel failed`, cancelErr);
});
throw err;
}
if (r.body === null)
return;
const reader = r.body.getReader();
for (;;) {
const { value, done } = await reader.read();
if (done)
break;
yield value;
}
}
async function sessionRequest(tm, path, action, arg, data) {
return apiRequest('POST', `${tm.contentBase}/files/upload_session/${action}`, {
timeout: tm.timeout,
errorOf: (r, text) => new DropboxApiError(`Dropbox upload ${path} → ${String(r.status)} ${text}`, r.status, summaryOf(text)),
headers: {
...(await dropboxAuthHeaders(tm)),
'Content-Type': 'application/octet-stream',
'Dropbox-API-Arg': headerJson(arg),
},
body: data,
});
}
async function dropboxUploadSession(tm, path, spool, rev) {
const opened = (await sessionRequest(tm, path, 'start', { close: false }, new Uint8Array()));
const sessionId = opened.session_id;
let offset = 0;
const commit = rev === null
? { path, mode: 'overwrite', mute: true }
: { path, mode: { '.tag': 'update', update: rev }, mute: true, autorename: false };
while (offset < spool.size) {
const body = await spool.read(offset, 8 * 1024 * 1024);
await sessionRequest(tm, path, 'append_v2', { cursor: { session_id: sessionId, offset }, close: false }, body);
offset += body.length;
}
return await sessionRequest(tm, path, 'finish', { cursor: { session_id: sessionId, offset }, commit }, new Uint8Array());
}
//# sourceMappingURL=client.js.map