UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

292 lines • 12 kB
// ========= 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