@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
292 lines • 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 { CALENDAR_API_BASE, DOCS_API_BASE, DRIVE_API_BASE, DRIVE_UPLOAD_BASE, FORMS_API_BASE, GMAIL_API_BASE, MAX_BACKOFF, MAX_RETRIES, RATE_LIMIT_REASONS, RETRY_STATUSES, SHEETS_API_BASE, SLIDES_API_BASE, TOKEN_BUFFER_SECONDS, TOKEN_URL, WRITE_RETRY_STATUSES, } from "./constants.js";
import { callerSignal } from "../../context/session_context.js";
import { apiRequest, apiStream } from "../api/client.js";
import { TokenManager as OAuthTokenManager } from "../api/oauth.js";
import {} from "../../utils/ranges.js";
import { ApiError } from "../api/errors.js";
/** Extract Google's API or OAuth error message, with a plain-response fallback. */
export function googleErrorMessage(body, status, reason) {
const reasonText = reason ?? '';
const fallback = body.trim() || reasonText || `HTTP ${String(status)}`;
let parsed;
try {
parsed = JSON.parse(body);
}
catch {
return fallback;
}
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
const { error, error_description: description } = parsed;
if (typeof error === 'object' && error !== null && !Array.isArray(error)) {
const { message } = error;
if (typeof message === 'string')
return message;
}
if (typeof error === 'string') {
return typeof description === 'string' && description !== ''
? `${error}: ${description}`
: error;
}
}
return fallback;
}
function errorOf(response, text) {
return new GoogleApiError(googleErrorMessage(text, response.status, response.statusText), response.status);
}
export function tokenUrl(config) {
return config.apiBase !== undefined ? `${config.apiBase}/token` : TOKEN_URL;
}
export function driveBase(tokenManager) {
const base = tokenManager.config.apiBase;
return base !== undefined ? `${base}/drive/v3` : DRIVE_API_BASE;
}
export function driveUploadBase(tokenManager) {
const base = tokenManager.config.apiBase;
return base !== undefined ? `${base}/upload/drive/v3` : DRIVE_UPLOAD_BASE;
}
export function docsBase(tokenManager) {
const base = tokenManager.config.apiBase;
return base !== undefined ? `${base}/v1` : DOCS_API_BASE;
}
export function slidesBase(tokenManager) {
const base = tokenManager.config.apiBase;
return base !== undefined ? `${base}/v1` : SLIDES_API_BASE;
}
export function sheetsBase(tokenManager) {
const base = tokenManager.config.apiBase;
return base !== undefined ? `${base}/v4` : SHEETS_API_BASE;
}
export function gmailBase(tokenManager) {
const base = tokenManager.config.apiBase;
return base !== undefined ? `${base}/gmail/v1` : GMAIL_API_BASE;
}
export function calendarBase(tokenManager) {
const base = tokenManager.config.apiBase;
return base !== undefined ? `${base}/calendar/v3` : CALENDAR_API_BASE;
}
export function formsBase(tokenManager) {
const base = tokenManager.config.apiBase;
return base !== undefined ? `${base}/v1` : FORMS_API_BASE;
}
/**
* Whether a retryable status is worth waiting out. Google answers both "slow
* down" and "you may not" with a 403, and only its error reason tells them
* apart: the APIs list the reasons in `error.errors`. Every other listed
* status is transient as it is.
*/
export function transient(status, text) {
if (status !== 403)
return true;
let parsed;
try {
parsed = JSON.parse(text);
}
catch {
return false;
}
const error = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
? parsed.error
: undefined;
const errors = typeof error === 'object' && error !== null && !Array.isArray(error)
? error.errors
: undefined;
if (!Array.isArray(errors))
return false;
return errors.some((item) => {
if (typeof item !== 'object' || item === null || Array.isArray(item))
return false;
const reason = item.reason;
return typeof reason === 'string' && RATE_LIMIT_REASONS.has(reason);
});
}
// A read, the token exchange and the start of a resumable upload retry every
// transient status and a dropped connection: none of them changes anything a
// second attempt could repeat, since an upload session creates no file until
// its last chunk commits.
export const READ_RETRY = {
statuses: RETRY_STATUSES,
maxRetries: MAX_RETRIES,
maxBackoff: MAX_BACKOFF,
delaySource: 'header',
retryTransport: true,
retryable: transient,
};
// Every other write retries only a rate-limit refusal. A 5xx or a dropped
// connection may come after the change landed, and a retried create, append
// or upload chunk would apply it twice.
export const WRITE_RETRY = {
...READ_RETRY,
statuses: WRITE_RETRY_STATUSES,
retryTransport: false,
};
export class GoogleApiError extends ApiError {
constructor(message, status) {
super(message, status);
this.name = 'GoogleApiError';
}
}
export async function refreshAccessToken(config, sleep) {
if (config.clientId === undefined || config.refreshToken === undefined) {
throw new Error('refreshAccessToken needs clientId and refreshToken; this config authenticates with a ' +
'pre-minted accessToken');
}
const body = new URLSearchParams({
client_id: config.clientId,
refresh_token: config.refreshToken,
grant_type: 'refresh_token',
});
if (config.clientSecret !== undefined && config.clientSecret !== '') {
body.set('client_secret', config.clientSecret);
}
const data = (await apiRequest('POST', tokenUrl(config), {
timeout: config.timeout,
errorOf,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
retry: READ_RETRY,
sleep,
}));
return [data.access_token, data.expires_in];
}
/**
* Manages OAuth2 access token lifecycle. `clock` is the clock token expiry
* and the waits between retries of every call made with this manager are
* read on; absent is the system clock.
*/
export class TokenManager extends OAuthTokenManager {
config;
constructor(config, clock) {
super(TOKEN_BUFFER_SECONDS, clock, config.timeout);
this.config = config;
}
/**
* A supplied token short-circuits the grant entirely, and is read every
* call rather than cached: a provider callable is the caller's own cache,
* and caching its answer here would outlive the refresh it just performed.
* Mirrors python's `TokenManager.get_token`.
*/
async getToken(signal) {
const supplied = this.config.accessToken;
if (supplied !== undefined)
return typeof supplied === 'function' ? await supplied() : supplied;
return super.getToken(signal);
}
async refreshPair() {
if (this.config.refreshFn !== undefined) {
if (this.config.refreshToken === undefined) {
throw new Error('refreshFn needs a refreshToken to hand to the caller');
}
const result = await this.config.refreshFn(this.config.refreshToken);
return [result.accessToken, result.expiresIn];
}
return refreshAccessToken(this.config, (seconds) => this.clock.sleep(seconds));
}
}
export async function googleHeaders(tm) {
const token = await tm.getToken(callerSignal());
return { Authorization: `Bearer ${token}` };
}
/**
* One Google API call: the door every request but a stream takes. It signs
* the call, rides the manager's clock and picks the retry policy from the
* method, so a new call site cannot forget any of them: absent `retry`, GET
* and HEAD read and everything else writes. A write that is safe to repeat
* (the start of a resumable upload) passes READ_RETRY.
*/
export async function googleRequest(tm, method, url, options = {}) {
const verb = method.toUpperCase();
const headers = { ...(await googleHeaders(tm)) };
if (options.json !== undefined)
headers['Content-Type'] = 'application/json';
return apiRequest(method, url, {
timeout: tm.timeout,
...options,
headers: { ...headers, ...options.headers },
errorOf: options.errorOf ?? errorOf,
retry: options.retry ?? (verb === 'GET' || verb === 'HEAD' ? READ_RETRY : WRITE_RETRY),
sleep: (seconds) => tm.clock.sleep(seconds),
});
}
export async function googleGet(tm, url, params) {
return googleRequest(tm, 'GET', url, { params });
}
export async function googlePost(tm, url, json) {
return googleRequest(tm, 'POST', url, { json });
}
export async function googlePatch(tm, url, json, params) {
return googleRequest(tm, 'PATCH', url, { params, json });
}
// Raw byte payloads (upload endpoints).
export async function googleSendBytes(tm, method, url, data, contentType, params) {
return googleRequest(tm, method, url, {
headers: { 'Content-Type': contentType },
params,
body: data,
});
}
export async function googleDelete(tm, url) {
await googleRequest(tm, 'DELETE', url, { read: 'none' });
}
export async function googleGetBytes(tm, url, window) {
return (await googleRequest(tm, 'GET', url, { read: 'bytes', window }));
}
/** GET a URL's raw bytes as they arrive. Mirrors Python's `google_get_stream`. */
export async function* googleGetStream(tm, url) {
yield* apiStream(url, {
timeout: tm.timeout,
errorOf,
headers: await googleHeaders(tm),
retry: READ_RETRY,
sleep: (seconds) => tm.clock.sleep(seconds),
});
}
export async function googleResumable(tm, method, url, metadata, spool, mimeType, params) {
const location = await googleRequest(tm, method, url, {
headers: {
'X-Upload-Content-Type': mimeType,
'X-Upload-Content-Length': String(spool.size),
},
params: { ...params, uploadType: 'resumable' },
json: metadata,
read: 'location',
retry: READ_RETRY,
});
if (typeof location !== 'string' || location === '')
throw new Error('Drive upload session returned no Location');
let offset = 0;
while (offset < spool.size) {
const data = await spool.read(offset, 8 * 1024 * 1024);
const response = (await googleRequest(tm, 'PUT', location, {
headers: {
'Content-Type': mimeType,
'Content-Range': `bytes ${String(offset)}-${String(offset + data.length - 1)}/${String(spool.size)}`,
},
body: data,
read: 'response',
}));
if (response.status === 200 || response.status === 201)
return response.data;
if (response.status !== 308)
throw new Error(`Unexpected Drive upload status: ${String(response.status)}`);
const acknowledged = response.headers.range ?? '';
const next = acknowledged === '' ? 0 : Number(acknowledged.split('-').at(-1)) + 1;
if (!Number.isSafeInteger(next) || next <= offset || next > offset + data.length)
throw new Error('Drive upload session made no progress');
offset = next;
}
throw new Error('Drive upload session did not commit');
}
//# sourceMappingURL=client.js.map