@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
135 lines • 5.26 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 { 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