UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

135 lines • 5.26 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 { eacces, enoent } from "../../errors/fs.js"; import { mimeTypeFor } from "../../utils/filetype.js"; import { NO_RETRY, apiRequest } from "../api/client.js"; import { DISCORD_API, GONE_STATUS, MAX_RETRIES, REFUSED_STATUSES } from "./constants.js"; import { ApiError } from "../api/errors.js"; const RATE_LIMIT_RETRY = { ...NO_RETRY, statuses: new Set([429]), maxRetries: MAX_RETRIES - 1, delaySource: 'body', }; export class DiscordApiError extends ApiError { endpoint; discordError; payload; constructor(endpoint, status, discordError, payload) { super(`Discord API error (${endpoint}): ${discordError}`, status); this.endpoint = endpoint; this.discordError = discordError; this.payload = payload; this.name = 'DiscordApiError'; } } /** * Report a channel Discord will not show as permission denied. * * A 401 or 403 (Missing Access) is Discord declining to show what the path * names, never the channel saying it is empty, so the path answers the way a * directory the caller may not open does: ls names it and fails, find reports * it and walks on. A 404 is the channel gone since it was listed. Any other * failure, a 429 that outlasted its retries among them, propagates. */ export async function refusalsDenied(pathSpec, run) { try { return await run(); } catch (err) { if (err instanceof ApiError && err.status !== null) { if (REFUSED_STATUSES.has(err.status)) throw eacces(pathSpec); if (err.status === GONE_STATUS) throw enoent(pathSpec); } throw err; } } function discordError(endpoint, response, body) { let parsed = null; try { parsed = body === '' ? null : JSON.parse(body); } catch { // a non-JSON error body: fall through to the bare status code } if (response.status === 429) { return new DiscordApiError(endpoint, 429, 'rate_limited', parsed ?? {}); } const message = parsed?.message ?? `http_${String(response.status)}`; return new DiscordApiError(endpoint, response.status, message, parsed); } export class HttpDiscordTransport { timeout; fetch = globalThis.fetch.bind(globalThis); constructor(timeout) { this.timeout = timeout; } async call(method, endpoint, params, body) { const base = this.baseUrl().replace(/\/$/, ''); const auth = await this.authHeaders(); const headers = { ...auth }; if (body !== undefined) headers['content-type'] = 'application/json'; return apiRequest(method, base + endpoint, { fetchFn: this.fetch, timeout: this.timeout, headers, ...(params !== undefined ? { params } : {}), ...(body !== undefined ? { json: body } : {}), retry: RATE_LIMIT_RETRY, errorOf: (response, text) => discordError(endpoint, response, text), }); } /** * POST a message with attachments, as Discord takes them: a multipart body * whose `payload_json` part is the JSON body and whose `files[n]` parts are * the files, each named by an `attachments` entry of the same id. */ async postFiles(endpoint, body, files) { const base = this.baseUrl().replace(/\/$/, ''); const form = new FormData(); const payload = { ...body, attachments: files.map((f, i) => ({ id: i, filename: f.name })) }; form.set('payload_json', new Blob([JSON.stringify(payload)], { type: 'application/json' })); files.forEach((f, i) => { form.set(`files[${String(i)}]`, new Blob([f.data], { type: mimeTypeFor(f.name) }), f.name); }); return apiRequest('POST', base + endpoint, { fetchFn: this.fetch, timeout: this.timeout, headers: await this.authHeaders(), body: form, errorOf: (response, text) => discordError(endpoint, response, text), }); } } export class NodeDiscordTransport extends HttpDiscordTransport { token; base; constructor(token, base, timeout) { super(timeout); this.token = token; this.base = base; } // base exists so the integ fake can stand in for discord.com; every // request must go through it, not the module constant. baseUrl() { return this.base ?? DISCORD_API; } authHeaders() { return { Authorization: `Bot ${this.token}` }; } } //# sourceMappingURL=client.js.map