UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

176 lines • 7.07 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 } from "../../errors/fs.js"; import { windowFor } from "../../utils/ranges.js"; import { apiRequest, apiStream } from "../api/client.js"; import { ApiError } from "../api/errors.js"; export const REFUSED_ERRORS = new Set([ 'not_in_channel', 'channel_not_found', 'missing_scope', 'is_archived', 'not_authed', ]); export class SlackApiError extends ApiError { endpoint; slackError; needed; provided; constructor(endpoint, slackError, needed = null, provided = null, status = null) { super(formatSlackErrorMessage(endpoint, slackError, needed, provided), status); this.endpoint = endpoint; this.slackError = slackError; this.needed = needed; this.provided = provided; this.name = 'SlackApiError'; } } /** * Report a conversation Slack will not show as permission denied. * * `not_in_channel`, `missing_scope` and the rest are Slack declining to show * what the path names, never the conversation 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. */ export async function refusalsDenied(pathSpec, run) { try { return await run(); } catch (err) { if (err instanceof SlackApiError && REFUSED_ERRORS.has(err.slackError)) throw eacces(pathSpec); throw err; } } function formatSlackErrorMessage(endpoint, slackError, needed, provided) { const base = `Slack API error (${endpoint}): ${slackError}`; if (slackError !== 'missing_scope' || needed === null || needed === '') return base; const providedRepr = provided !== null && provided !== '' ? provided : '(none)'; return `${base} (needed: ${needed}; provided: ${providedRepr})`; } // Slack reports failures as ok:false payloads, usually with a 200; a // non-2xx that still carries one keeps Slack's own wording. function errorOf(endpoint, response, text) { let data = null; try { data = JSON.parse(text); } catch { data = null; } if (data !== null && typeof data === 'object') { const payload = data; return new SlackApiError(endpoint, payload.error ?? 'unknown_error', payload.needed ?? null, payload.provided ?? null, response.status); } return new SlackApiError(endpoint, `HTTP ${String(response.status)}`, null, null, response.status); } export class HttpSlackTransport { timeout; // Indirection so tests can inject a fake fetch without subclass plumbing. fetch = globalThis.fetch.bind(globalThis); constructor(timeout) { this.timeout = timeout; } async call(endpoint, params, body) { const base = this.baseUrl().replace(/\/$/, ''); const auth = await this.authHeaders(endpoint); const raw = await apiRequest(body === undefined ? 'GET' : 'POST', `${base}/${endpoint}`, { errorOf: (response, text) => errorOf(endpoint, response, text), headers: { 'Content-Type': 'application/json; charset=utf-8', ...auth }, ...(params === undefined ? {} : { params }), ...(body === undefined ? {} : { json: body }), fetchFn: this.fetch, timeout: this.timeout, }); const data = (raw ?? {}); if (!data.ok) { throw new SlackApiError(endpoint, data.error ?? 'unknown_error', data.needed ?? null, data.provided ?? null); } return data; } // Takes the window rather than a prepared header so the answer can be // checked against it: Slack serves files from a CDN, and a server is free // to ignore Range and reply 200 with the whole file, which the kit's // window handling then trims. async downloadFile(url, offset = 0, size = null) { const auth = await this.authHeaders(); const window = windowFor(offset, size); const data = await apiRequest('GET', url, { errorOf: (response) => new ApiError(`slack: download failed (${String(response.status)}): ${url}`, response.status), headers: auth, read: 'bytes', ...(window === undefined ? {} : { window }), fetchFn: this.fetch, timeout: this.timeout, }); return data; } // The URL files.getUploadURLExternal issues is presigned, so the request // carries no token. async uploadBytes(url, data) { await apiRequest('POST', url, { errorOf: (response) => new ApiError(`slack: upload failed (${String(response.status)}): ${url}`, response.status), body: data, read: 'none', fetchFn: this.fetch, timeout: this.timeout, }); } async *downloadFileStream(url, signal) { yield* apiStream(url, { errorOf: (response) => new ApiError(`slack: download failed (${String(response.status)}): ${url}`, response.status), headers: await this.authHeaders(), fetchFn: this.fetch, timeout: this.timeout, ...(signal === undefined ? {} : { signal }), }); } } export class NodeSlackTransport extends HttpSlackTransport { token; searchToken; baseUrlOverride; constructor(token, searchToken, baseUrlOverride, timeout) { super(timeout); this.token = token; this.searchToken = searchToken; this.baseUrlOverride = baseUrlOverride; } baseUrl() { return this.baseUrlOverride !== undefined && this.baseUrlOverride !== '' ? this.baseUrlOverride : 'https://slack.com/api'; } authHeaders(endpoint) { const token = endpoint?.startsWith('search.') === true && this.searchToken !== undefined && this.searchToken !== '' ? this.searchToken : this.token; return { Authorization: `Bearer ${token}` }; } searchAvailable() { if (this.searchToken !== undefined && this.searchToken !== '') return true; return this.token.startsWith('xoxp-'); } searcher() { if (this.searchToken === undefined || this.searchToken === '') return null; return new NodeSlackTransport(this.searchToken, undefined, this.baseUrlOverride, this.timeout); } } //# sourceMappingURL=client.js.map