UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

349 lines • 14.1 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 { stallTimeout } from "../api/client.js"; function decodeEntities(s) { return s .replaceAll('&lt;', '<') .replaceAll('&gt;', '>') .replaceAll('&quot;', '"') .replaceAll('&apos;', "'") .replaceAll('&amp;', '&'); } function tagText(xml, tag) { const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`); const m = re.exec(xml); return m === null ? null : decodeEntities(m[1] ?? ''); } function tagBlocks(xml, tag) { const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, 'g'); const blocks = []; let m; while ((m = re.exec(xml)) !== null) blocks.push(m[1] ?? ''); return blocks; } function parseListObjectsV2(xml) { const body = tagText(xml, 'ListBucketResult') ?? xml; const truncated = (tagText(body, 'IsTruncated') ?? 'false').toLowerCase() === 'true'; const nextToken = tagText(body, 'NextContinuationToken') ?? undefined; const prefixes = []; for (const blk of tagBlocks(body, 'CommonPrefixes')) { const p = tagText(blk, 'Prefix'); if (p !== null && p !== '') prefixes.push({ Prefix: p }); } const contents = []; for (const blk of tagBlocks(body, 'Contents')) { const key = tagText(blk, 'Key'); if (key === null) continue; const sizeStr = tagText(blk, 'Size'); const size = sizeStr !== null ? Number.parseInt(sizeStr, 10) : 0; const lastMod = tagText(blk, 'LastModified'); const etagRaw = tagText(blk, 'ETag'); const etag = etagRaw !== null ? etagRaw.replaceAll('"', '') : ''; const entry = { Key: key, Size: Number.isFinite(size) ? size : 0, }; if (lastMod !== null) entry.LastModified = new Date(lastMod); if (etag !== '') entry.ETag = etag; contents.push(entry); } const out = { CommonPrefixes: prefixes, Contents: contents, IsTruncated: truncated, }; if (nextToken !== undefined) out.NextContinuationToken = nextToken; return out; } class BrowserCommand { __mirageTag; input; constructor(tag, input) { this.__mirageTag = tag; this.input = input; } } class GetObjectCommand extends BrowserCommand { constructor(input) { super('Get', input); } } class HeadObjectCommand extends BrowserCommand { constructor(input) { super('Head', input); } } class ListObjectsV2Command extends BrowserCommand { constructor(input) { super('List', input); } } class PutObjectCommand extends BrowserCommand { constructor(input) { super('Put', input); } } class DeleteObjectCommand extends BrowserCommand { constructor(input) { super('Delete', input); } } class DeleteObjectsCommand extends BrowserCommand { constructor(input) { super('Delete', input); } } class CopyObjectCommand extends BrowserCommand { constructor(input) { super('Copy', input); } } const stubS3ClientCtor = function stubS3Client() { throw new Error('BROWSER_S3_MODULE.S3Client is a stub — use createS3Client(config) to get the presigned-fetch client'); }; export const BROWSER_S3_MODULE = { S3Client: stubS3ClientCtor, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, CopyObjectCommand, }; async function bodyFromResponse(resp) { const stream = resp.body; if (stream === null) { const buf = new Uint8Array(await resp.arrayBuffer()); return (async function* () { await Promise.resolve(); yield buf; })(); } const reader = stream.getReader(); return (async function* () { let drained = false; try { for (;;) { const { value, done } = await reader.read(); if (done) { drained = true; return; } yield value; } } finally { // A consumer that stops early — a ranged read served by streaming — wants // the transfer to stop with it. Releasing the lock alone leaves the body // downloading, so the bytes it went out of its way not to ask for arrive // anyway; cancel() is what actually aborts it. if (!drained) await reader.cancel(); reader.releaseLock(); } })(); } function keyFromInput(input) { const k = input.Key; return typeof k === 'string' ? k : ''; } // A presigned URL is signed for a key, not for a version: the provider takes // `(path, operation, options)` and has no slot to carry one. Serving the // request anyway returns whichever version is current, which is the opposite // of what a pinned read asked for and is indistinguishable from a correct // answer — so refuse instead. Only fields that change the *answer* belong // here; a hint like MaxKeys, whose omission returns more data but the same // answer, must keep being ignored or `stat`'s List probe would break. function refuseVersionPin(op, key, input) { if (input.VersionId === undefined) return; throw new Error(`S3 ${op} ${key}: the presigned-fetch client cannot serve a version-pinned ` + 'read, so it would silently return the current object; a snapshot-pinned ' + 'read needs an SDK-backed config rather than presignedUrlProvider'); } function makeNotFound(path) { const e = new Error(`S3 object not found: ${path}`); e.$metadata = { httpStatusCode: 404 }; return e; } // The presigned fetch does not parse an error body, so a 403 takes the shape // the SDK gives a bodiless one (Unknown, status 403), which isDenied reads. function makeDenied(path) { const e = new Error(`S3 refused: ${path}`); e.name = 'Unknown'; e.$metadata = { httpStatusCode: 403 }; return e; } async function sendBrowserCommand(provider, defaultContentType, cmd, send) { const browserCmd = cmd; if (typeof browserCmd.__mirageTag !== 'string') { throw new Error('presigned-fetch client received a non-browser S3 command; check that you ' + 'did not mix AWS SDK command classes with a presigner-backed config'); } const tag = browserCmd.__mirageTag; switch (tag) { case 'Get': { const input = browserCmd.input; const key = typeof input.Key === 'string' ? input.Key : ''; refuseVersionPin('GET', key, input); // A presigned GET carries no Range: the header would make the request // non-simple and trip a CORS preflight presigned deployments generally // do not allow. Serving it anyway would return the whole object as if it // were the requested window, so refuse instead of answering wrongly — // core/s3/stream.ts streams the window on this path. if (input.Range !== undefined) { throw new Error(`S3 GET ${key}: the presigned-fetch client cannot serve a ranged ` + 'read; use core/s3/stream.ts readRange, which streams the window'); } const url = await provider(`/${key}`, 'GET'); const resp = await send(url); if (resp.status === 404) throw makeNotFound(key); if (resp.status === 403) throw makeDenied(key); if (!resp.ok) { throw new Error(`S3 GET ${key} failed: ${String(resp.status)} ${resp.statusText}`); } const body = await bodyFromResponse(resp); const lenHeader = resp.headers.get('content-length'); const ContentLength = lenHeader !== null ? Number.parseInt(lenHeader, 10) : undefined; const out = { Body: body }; if (ContentLength !== undefined && !Number.isNaN(ContentLength)) { out.ContentLength = ContentLength; } const etag = resp.headers.get('etag')?.replace(/^"|"$/g, '') ?? ''; if (etag !== '') out.ETag = etag; const lm = resp.headers.get('last-modified'); if (lm !== null) out.LastModified = new Date(lm); return out; } case 'Head': { const key = keyFromInput(browserCmd.input); refuseVersionPin('HEAD', key, browserCmd.input); const url = await provider(`/${key}`, 'HEAD'); const resp = await send(url, { method: 'HEAD' }); if (resp.status === 404) throw makeNotFound(key); if (resp.status === 403) throw makeDenied(key); if (!resp.ok) { throw new Error(`S3 HEAD ${key} failed: ${String(resp.status)} ${resp.statusText}`); } const lenHeader = resp.headers.get('content-length'); const ContentLength = lenHeader !== null ? Number.parseInt(lenHeader, 10) : undefined; const etag = resp.headers.get('etag')?.replace(/^"|"$/g, '') ?? ''; const lm = resp.headers.get('last-modified'); const out = {}; if (ContentLength !== undefined && !Number.isNaN(ContentLength)) { out.ContentLength = ContentLength; } if (etag !== '') out.ETag = etag; if (lm !== null) out.LastModified = new Date(lm); return out; } case 'List': { const input = browserCmd.input; const opts = { listPrefix: input.Prefix ?? '', }; if (input.Delimiter !== undefined) opts.listDelimiter = input.Delimiter; if (input.ContinuationToken !== undefined) opts.listContinuationToken = input.ContinuationToken; const url = await provider('/', 'LIST', opts); const resp = await send(url); if (resp.status === 403) throw makeDenied(opts.listPrefix); if (!resp.ok) { throw new Error(`S3 LIST failed: ${String(resp.status)} ${resp.statusText}`); } const xml = await resp.text(); return parseListObjectsV2(xml); } case 'Put': { const input = browserCmd.input; const key = typeof input.Key === 'string' ? input.Key : ''; const contentType = typeof input.ContentType === 'string' ? input.ContentType : (defaultContentType ?? 'application/octet-stream'); const url = await provider(`/${key}`, 'PUT', { contentType }); const body = input.Body; const resp = await send(url, { method: 'PUT', headers: { 'Content-Type': contentType }, body: body, }); if (!resp.ok) { const errBody = await resp.text().catch(() => ''); throw new Error(`S3 PUT ${key} failed: ${String(resp.status)} ${resp.statusText}${errBody !== '' ? ` — ${errBody.slice(0, 200)}` : ''}`); } return {}; } case 'Delete': { const input = browserCmd.input; if (typeof input.Key === 'string') { const url = await provider(`/${input.Key}`, 'DELETE'); const resp = await send(url, { method: 'DELETE' }); if (resp.status !== 404 && !resp.ok) { throw new Error(`S3 DELETE ${input.Key} failed: ${String(resp.status)} ${resp.statusText}`); } return {}; } const objects = input.Delete?.Objects ?? []; await Promise.all(objects.map(async (o) => { const url = await provider(`/${o.Key}`, 'DELETE'); await send(url, { method: 'DELETE' }); })); return { Deleted: objects.map((o) => ({ Key: o.Key })) }; } case 'Copy': { const input = browserCmd.input; const dstKey = typeof input.Key === 'string' ? input.Key : ''; const copySrcRaw = typeof input.CopySource === 'string' ? input.CopySource : ''; const srcKey = copySrcRaw.replace(/^\/?[^/]+\//, ''); const url = await provider(`/${dstKey}`, 'COPY', { copySource: srcKey }); const resp = await send(url, { method: 'PUT', headers: { 'x-amz-copy-source': `/${srcKey}` }, }); if (!resp.ok) { const errBody = await resp.text().catch(() => ''); throw new Error(`S3 COPY ${copySrcRaw} → ${dstKey} failed: ${String(resp.status)} ${resp.statusText}${errBody !== '' ? ` — ${errBody.slice(0, 200)}` : ''}`); } return {}; } } } export function createBrowserS3Client(config) { const provider = config.presignedUrlProvider; if (provider === undefined) { throw new Error('createBrowserS3Client called without presignedUrlProvider'); } const send = stallTimeout(config.timeout); return { send: (cmd) => sendBrowserCommand(provider, config.defaultContentType, cmd, send), }; } //# sourceMappingURL=client_browser.js.map