@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
269 lines • 10.5 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 { toHex } from "./hex.js";
async function sha256(bytes) {
const digest = await crypto.subtle.digest('SHA-256', bytes);
return new Uint8Array(digest);
}
export async function sha256Hex(bytes) {
return toHex(await sha256(bytes));
}
async function subtleHex(algo, bytes) {
const digest = await crypto.subtle.digest(algo, bytes);
return toHex(new Uint8Array(digest));
}
export async function sha1Hex(bytes) {
if (typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined')
return subtleHex('SHA-1', bytes);
return yieldingDigest(new Sha1(), bytes);
}
/**
* The 8-byte SHA-1 length trailer: the message length in bits, big-endian
* across 64 bits. Split into high and low words, since the bit length passes
* 32 bits at 512 MiB and a 32-bit shift would wrap it. Exported so its test
* can cross that boundary without hashing 512 MiB.
*/
export function sha1LengthTrailer(byteLength) {
const out = new Uint8Array(8);
const view = new DataView(out.buffer);
view.setUint32(0, Math.floor(byteLength / 0x20000000) >>> 0);
view.setUint32(4, (byteLength * 8) >>> 0);
return out;
}
/**
* Incremental SHA-1, for bytes that arrive in chunks. WebCrypto hashes only
* a whole buffer, and a stream cannot be buffered whole to hash at the end:
* drains are bounded, so the copy would be unbounded and never cached. The
* partial block a chunk leaves is carried into the next `update`; padding
* happens only in `digest`.
*/
export class Sha1 {
state = new Uint32Array([
0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0,
]);
block = new Uint8Array(64);
words = new Uint32Array(80);
filled = 0;
length = 0;
update(chunk) {
this.length += chunk.byteLength;
let at = 0;
if (this.filled > 0) {
const take = Math.min(64 - this.filled, chunk.byteLength);
this.block.set(chunk.subarray(0, take), this.filled);
this.filled += take;
at = take;
if (this.filled < 64)
return;
this.compress(this.block, 0);
this.filled = 0;
}
for (; at + 64 <= chunk.byteLength; at += 64)
this.compress(chunk, at);
if (at < chunk.byteLength) {
this.block.set(chunk.subarray(at));
this.filled = chunk.byteLength - at;
}
}
digest() {
const pad = (this.filled < 56 ? 56 : 120) - this.filled;
const tail = new Uint8Array(this.filled + pad + 8);
tail.set(this.block.subarray(0, this.filled));
tail[this.filled] = 0x80;
tail.set(sha1LengthTrailer(this.length), tail.length - 8);
const state = Uint32Array.from(this.state);
for (let off = 0; off < tail.length; off += 64)
this.compress(tail, off, state);
const out = new Uint8Array(20);
const view = new DataView(out.buffer);
for (let i = 0; i < 5; i++)
view.setUint32(i * 4, state[i] ?? 0);
return toHex(out);
}
compress(bytes, off, state = this.state) {
const w = this.words;
const view = new DataView(bytes.buffer, bytes.byteOffset + off, 64);
for (let i = 0; i < 16; i++)
w[i] = view.getUint32(i * 4);
for (let i = 16; i < 80; i++) {
w[i] = rotl((w[i - 3] ?? 0) ^ (w[i - 8] ?? 0) ^ (w[i - 14] ?? 0) ^ (w[i - 16] ?? 0), 1);
}
let a = state[0] ?? 0;
let b = state[1] ?? 0;
let c = state[2] ?? 0;
let d = state[3] ?? 0;
let e = state[4] ?? 0;
for (let i = 0; i < 80; i++) {
let f;
let k;
if (i < 20) {
f = (b & c) | (~b & d);
k = 0x5a827999;
}
else if (i < 40) {
f = b ^ c ^ d;
k = 0x6ed9eba1;
}
else if (i < 60) {
f = (b & c) | (b & d) | (c & d);
k = 0x8f1bbcdc;
}
else {
f = b ^ c ^ d;
k = 0xca62c1d6;
}
const t = (rotl(a, 5) + f + e + k + (w[i] ?? 0)) >>> 0;
e = d;
d = c;
c = rotl(b, 30);
b = a;
a = t;
}
state[0] = ((state[0] ?? 0) + a) >>> 0;
state[1] = ((state[1] ?? 0) + b) >>> 0;
state[2] = ((state[2] ?? 0) + c) >>> 0;
state[3] = ((state[3] ?? 0) + d) >>> 0;
state[4] = ((state[4] ?? 0) + e) >>> 0;
}
}
const S = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14,
20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6,
10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
];
const K = [
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
];
function rotl(x, n) {
return ((x << n) | (x >>> (32 - n))) >>> 0;
}
/**
* Incremental MD5, for bytes that arrive in chunks, as `Sha1` is for SHA-1.
* Pure JS: WebCrypto has no MD5, and core cannot import node:crypto. The
* partial block a chunk leaves is carried into the next `update`; padding
* happens only in `digest`.
*/
export class Md5 {
state = new Uint32Array([0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]);
block = new Uint8Array(64);
blockView = new DataView(this.block.buffer);
words = new Uint32Array(16);
filled = 0;
length = 0;
update(chunk) {
this.length += chunk.byteLength;
let at = 0;
if (this.filled > 0) {
const take = Math.min(64 - this.filled, chunk.byteLength);
this.block.set(chunk.subarray(0, take), this.filled);
this.filled += take;
at = take;
if (this.filled < 64)
return;
this.compress(this.blockView, 0);
this.filled = 0;
}
const view = new DataView(chunk.buffer, chunk.byteOffset, chunk.byteLength);
for (; at + 64 <= chunk.byteLength; at += 64)
this.compress(view, at);
if (at < chunk.byteLength) {
this.block.set(chunk.subarray(at));
this.filled = chunk.byteLength - at;
}
}
digest() {
const pad = (this.filled < 56 ? 56 : 120) - this.filled;
const tail = new Uint8Array(this.filled + pad + 8);
tail.set(this.block.subarray(0, this.filled));
tail[this.filled] = 0x80;
const view = new DataView(tail.buffer);
const bits = this.length * 8;
view.setUint32(tail.length - 8, bits >>> 0, true);
view.setUint32(tail.length - 4, Math.floor(bits / 0x100000000) >>> 0, true);
const state = Uint32Array.from(this.state);
for (let off = 0; off < tail.length; off += 64)
this.compress(view, off, state);
const out = new Uint8Array(16);
const outView = new DataView(out.buffer);
for (let i = 0; i < 4; i++)
outView.setUint32(i * 4, state[i] ?? 0, true);
return toHex(out);
}
compress(view, off, state = this.state) {
const M = this.words;
for (let j = 0; j < 16; j++)
M[j] = view.getUint32(off + j * 4, true);
let A = state[0] ?? 0;
let B = state[1] ?? 0;
let C = state[2] ?? 0;
let D = state[3] ?? 0;
for (let i = 0; i < 64; i++) {
let F;
let g;
if (i < 16) {
F = (B & C) | (~B & D);
g = i;
}
else if (i < 32) {
F = (D & B) | (~D & C);
g = (5 * i + 1) % 16;
}
else if (i < 48) {
F = B ^ C ^ D;
g = (3 * i + 5) % 16;
}
else {
F = C ^ (B | ~D);
g = (7 * i) % 16;
}
F = (F + A + (K[i] ?? 0) + (M[g] ?? 0)) >>> 0;
A = D;
D = C;
C = B;
B = (B + rotl(F, S[i] ?? 0)) >>> 0;
}
state[0] = ((state[0] ?? 0) + A) >>> 0;
state[1] = ((state[1] ?? 0) + B) >>> 0;
state[2] = ((state[2] ?? 0) + C) >>> 0;
state[3] = ((state[3] ?? 0) + D) >>> 0;
}
}
export function md5Hex(bytes) {
const hash = new Md5();
hash.update(bytes);
return hash.digest();
}
// The yielding twin of md5Hex, for hashing bytes a caller just received.
// One long pure-JS hash would block the loop a TypeScript FUSE mount is
// served from, so this hands the loop back about every MiB.
export async function md5HexAsync(bytes) {
return yieldingDigest(new Md5(), bytes);
}
async function yieldingDigest(hash, bytes) {
const step = 1024 * 1024;
for (let offset = 0; offset < bytes.byteLength; offset += step) {
hash.update(bytes.subarray(offset, offset + step));
if (offset + step < bytes.byteLength)
await new Promise((resolve) => setTimeout(resolve, 0));
}
return hash.digest();
}
//# sourceMappingURL=hash.js.map