@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
349 lines • 14.1 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 { stallTimeout } from "../api/client.js";
function decodeEntities(s) {
return s
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll(''', "'")
.replaceAll('&', '&');
}
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