mcard-js
Version:
MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers
1,137 lines (1,119 loc) • 33.7 kB
JavaScript
import {
validateHandle
} from "./chunk-U3FWUPD3.js";
import {
MCard
} from "./chunk-PW4XS7M3.js";
// src/storage/IndexedDBEngine.ts
import { openDB } from "idb";
var IndexedDBEngine = class {
db = null;
dbName;
constructor(dbName = "mcard-db") {
this.dbName = dbName;
}
/**
* Initialize the database connection
*/
async init() {
this.db = await openDB(this.dbName, 1, {
upgrade(db) {
if (!db.objectStoreNames.contains("cards")) {
db.createObjectStore("cards", { keyPath: "hash" });
}
if (!db.objectStoreNames.contains("handles")) {
const handleStore = db.createObjectStore("handles", { keyPath: "handle" });
handleStore.createIndex("by-hash", "currentHash");
}
if (!db.objectStoreNames.contains("handleHistory")) {
const historyStore = db.createObjectStore("handleHistory", {
keyPath: "id",
autoIncrement: true
});
historyStore.createIndex("by-handle", "handle");
}
}
});
}
ensureDb() {
if (!this.db) {
throw new Error("Database not initialized. Call init() first.");
}
return this.db;
}
// =========== Card Operations ===========
async add(card) {
const db = this.ensureDb();
await db.put("cards", {
hash: card.hash,
content: card.content,
g_time: card.g_time
});
return card.hash;
}
async get(hash) {
const db = this.ensureDb();
const record = await db.get("cards", hash);
if (!record) return null;
return MCard.fromData(record.content, record.hash, record.g_time);
}
async delete(hash) {
const db = this.ensureDb();
await db.delete("cards", hash);
}
async getPage(pageNumber, pageSize) {
const db = this.ensureDb();
const totalItems = await db.count("cards");
const totalPages = Math.ceil(totalItems / pageSize);
const allCards = await db.getAll("cards");
const start = (pageNumber - 1) * pageSize;
const pageRecords = allCards.slice(start, start + pageSize);
const items = pageRecords.map((r) => MCard.fromData(r.content, r.hash, r.g_time));
return {
items,
totalItems,
pageNumber,
pageSize,
totalPages,
hasNext: pageNumber < totalPages,
hasPrevious: pageNumber > 1
};
}
async count() {
const db = this.ensureDb();
return db.count("cards");
}
async searchByHash(hashPrefix) {
const db = this.ensureDb();
const start = hashPrefix;
const end = hashPrefix + "\uFFFF";
const range = IDBKeyRange.bound(start, end);
const records = await db.getAll("cards", range);
return records.map((r) => MCard.fromData(r.content, r.hash, r.g_time));
}
async search(query, pageNumber, pageSize) {
const db = this.ensureDb();
const records = await db.getAll("cards");
const decoder = new TextDecoder();
const filtered = records.filter((r) => {
try {
const text = decoder.decode(r.content);
return text.includes(query);
} catch {
return false;
}
});
const totalItems = filtered.length;
const totalPages = Math.ceil(totalItems / pageSize);
const start = (pageNumber - 1) * pageSize;
const pageItems = filtered.slice(start, start + pageSize).map((r) => MCard.fromData(r.content, r.hash, r.g_time));
return {
items: pageItems,
totalItems,
pageNumber,
pageSize,
totalPages,
hasNext: pageNumber < totalPages,
hasPrevious: pageNumber > 1
};
}
async getAll() {
const db = this.ensureDb();
const records = await db.getAll("cards");
return records.map((r) => MCard.fromData(r.content, r.hash, r.g_time));
}
async clear() {
const db = this.ensureDb();
await db.clear("cards");
await db.clear("handles");
await db.clear("handleHistory");
}
// =========== Handle Operations ===========
async registerHandle(handle, hash) {
const db = this.ensureDb();
const normalized = validateHandle(handle);
const existing = await db.get("handles", normalized);
if (existing) {
throw new Error(`Handle '${handle}' already exists.`);
}
const now = (/* @__PURE__ */ new Date()).toISOString();
await db.put("handles", {
handle: normalized,
currentHash: hash,
createdAt: now,
updatedAt: now
});
}
async resolveHandle(handle) {
const db = this.ensureDb();
const normalized = validateHandle(handle);
const record = await db.get("handles", normalized);
return record?.currentHash ?? null;
}
async getByHandle(handle) {
const hash = await this.resolveHandle(handle);
if (!hash) return null;
return this.get(hash);
}
async updateHandle(handle, newHash) {
const db = this.ensureDb();
const normalized = validateHandle(handle);
const existing = await db.get("handles", normalized);
if (!existing) {
throw new Error(`Handle '${handle}' not found.`);
}
const previousHash = existing.currentHash;
const now = (/* @__PURE__ */ new Date()).toISOString();
await db.add("handleHistory", {
handle: normalized,
previousHash,
changedAt: now
});
await db.put("handles", {
...existing,
currentHash: newHash,
updatedAt: now
});
return previousHash;
}
async getHandleHistory(handle) {
const db = this.ensureDb();
const normalized = validateHandle(handle);
const records = await db.getAllFromIndex("handleHistory", "by-handle", normalized);
return records.map((r) => ({ previousHash: r.previousHash, changedAt: r.changedAt })).reverse();
}
};
// src/model/hash/algorithms/LocalSHA256.ts
async function computeHash(content) {
let contentStr;
if (typeof content === "string") {
contentStr = content;
} else {
contentStr = JSON.stringify(content);
}
const encoder = new TextEncoder();
const data = encoder.encode(contentStr);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
return hashHex;
}
// src/monads/Reader.ts
var Reader = class _Reader {
constructor(run) {
this.run = run;
}
/**
* Lift a pure value into Reader
*/
static pure(value) {
return new _Reader((_) => value);
}
/**
* Get the environment
*/
static ask() {
return new _Reader((env) => env);
}
/**
* Monadic bind (flatMap)
*/
bind(fn) {
return new _Reader((env) => {
const value = this.run(env);
return fn(value).run(env);
});
}
/**
* Map over the result
*/
map(fn) {
return this.bind((value) => _Reader.pure(fn(value)));
}
/**
* Execute the Reader logic with an environment
*/
evaluate(env) {
return this.run(env);
}
};
// src/monads/Writer.ts
var Writer = class _Writer {
constructor(run) {
this.run = run;
}
/**
* Lift a pure value into Writer (empty log)
*/
static pure(value) {
return new _Writer(() => [value, []]);
}
/**
* Write to log
*/
static tell(log) {
return new _Writer(() => [void 0, log]);
}
/**
* Monadic bind
*/
bind(fn) {
return new _Writer(() => {
const [val1, log1] = this.run();
const writer2 = fn(val1);
const [val2, log2] = writer2.evaluate();
return [val2, [...log1, ...log2]];
});
}
/**
* Map
*/
map(fn) {
return this.bind((val) => _Writer.pure(fn(val)));
}
/**
* Run the writer
*/
evaluate() {
return this.run();
}
};
// src/monads/State.ts
var State = class _State {
constructor(run) {
this.run = run;
}
/**
* Lift pure value
*/
static pure(value) {
return new _State((state) => [value, state]);
}
/**
* Get current state
*/
static get() {
return new _State((state) => [state, state]);
}
/**
* Set new state
*/
static put(newState) {
return new _State((_) => [void 0, newState]);
}
/**
* Monadic bind
*/
bind(fn) {
return new _State((state) => {
const [val, newState] = this.run(state);
return fn(val).run(newState);
});
}
/**
* Map
*/
map(fn) {
return this.bind((val) => _State.pure(fn(val)));
}
/**
* Execute state transition
*/
evaluate(initialState) {
return this.run(initialState);
}
};
// src/ptr/LensProtocol.ts
var LensProtocol = class {
static idCounter = 0;
/**
* Create an execute request
*/
static createExecuteRequest(pcard_hash, target_hash, context) {
return {
jsonrpc: "2.0",
id: ++this.idCounter,
method: "pcard.execute",
params: { pcard_hash, target_hash, context }
};
}
/**
* Create a verify request
*/
static createVerifyRequest(pcard_hash, target_hash, context) {
return {
jsonrpc: "2.0",
id: ++this.idCounter,
method: "pcard.verify",
params: { pcard_hash, target_hash, context }
};
}
/**
* Create a reveal request
*/
static createRevealRequest(card_hash, aspect) {
return {
jsonrpc: "2.0",
id: ++this.idCounter,
method: "lens.reveal",
params: { card_hash, aspect }
};
}
/**
* Create a system status request
*/
static createStatusRequest() {
return {
jsonrpc: "2.0",
id: ++this.idCounter,
method: "system.status"
};
}
/**
* Create a system health request
*/
static createHealthRequest() {
return {
jsonrpc: "2.0",
id: ++this.idCounter,
method: "system.health"
};
}
/**
* Create a success response
*/
static createSuccessResponse(id, result) {
return { jsonrpc: "2.0", id, result };
}
/**
* Create an error response
*/
static createErrorResponse(id, code, message, data) {
return { jsonrpc: "2.0", id, error: { code, message, data } };
}
/**
* Parse a JSON-RPC response
*/
static parseResponse(response) {
if (response.error) {
throw new Error(`JSON-RPC Error ${response.error.code}: ${response.error.message}`);
}
return response.result;
}
};
var ErrorCodes = {
PARSE_ERROR: -32700,
INVALID_REQUEST: -32600,
METHOD_NOT_FOUND: -32601,
INVALID_PARAMS: -32602,
INTERNAL_ERROR: -32603,
// Custom PTR errors
EXECUTION_ERROR: -32e3,
VERIFICATION_FAILED: -32001,
TIMEOUT: -32002
};
// src/ptr/common_types.ts
var VerificationStatus = /* @__PURE__ */ ((VerificationStatus2) => {
VerificationStatus2["PENDING"] = "pending";
VerificationStatus2["VERIFIED"] = "verified";
VerificationStatus2["FAILED"] = "failed";
VerificationStatus2["SKIPPED"] = "skipped";
return VerificationStatus2;
})(VerificationStatus || {});
// src/ptr/browser/storage/MCardStore.ts
import { openDB as openDB2 } from "idb";
var MCardStore = class {
dbPromise;
constructor(dbName = "mcard-store") {
this.dbPromise = openDB2(dbName, 8, {
upgrade(db, oldVersion, newVersion, transaction) {
if (db.objectStoreNames.contains("mcards")) db.deleteObjectStore("mcards");
if (db.objectStoreNames.contains("handles")) db.deleteObjectStore("handles");
let cardStore;
if (!db.objectStoreNames.contains("card")) {
cardStore = db.createObjectStore("card", { keyPath: "hash" });
} else {
cardStore = transaction.objectStore("card");
}
if (!cardStore.indexNames.contains("by_g_time")) {
cardStore.createIndex("by_g_time", "g_time");
}
let registryStore;
if (!db.objectStoreNames.contains("handle_registry")) {
registryStore = db.createObjectStore("handle_registry", { keyPath: "handle" });
} else {
registryStore = transaction.objectStore("handle_registry");
}
if (!registryStore.indexNames.contains("by_current_hash")) {
registryStore.createIndex("by_current_hash", "current_hash");
}
if (!registryStore.indexNames.contains("by_updated_at")) {
registryStore.createIndex("by_updated_at", "updated_at");
}
let historyStore;
if (!db.objectStoreNames.contains("handle_history")) {
historyStore = db.createObjectStore("handle_history", { keyPath: "id", autoIncrement: true });
} else {
historyStore = transaction.objectStore("handle_history");
}
if (!historyStore.indexNames.contains("by_handle")) {
historyStore.createIndex("by_handle", "handle");
}
if (!historyStore.indexNames.contains("by_previous_hash")) {
historyStore.createIndex("by_previous_hash", "previous_hash");
}
if (!historyStore.indexNames.contains("by_changed_at")) {
historyStore.createIndex("by_changed_at", "changed_at");
}
if (!db.objectStoreNames.contains("schema_version")) {
const versionStore = db.createObjectStore("schema_version", { keyPath: "version" });
versionStore.add({
version: "3.0.2",
applied_at: (/* @__PURE__ */ new Date()).toISOString(),
description: "Monadic Core Schema (Strict Compliance)"
});
}
}
});
}
// ═══════════════════════════════════════════════════════════════
// CARD OPERATIONS
// ═══════════════════════════════════════════════════════════════
async putCard(hash, content) {
const db = await this.dbPromise;
const g_time = (/* @__PURE__ */ new Date()).toISOString();
await db.put("card", {
hash,
content,
g_time
});
}
async getCard(hash) {
const db = await this.dbPromise;
return db.get("card", hash);
}
// ═══════════════════════════════════════════════════════════════
// HANDLE OPERATIONS (Transaction-safe)
// ═══════════════════════════════════════════════════════════════
async setHandle(handle, newHash) {
const db = await this.dbPromise;
const tx = db.transaction(["handle_registry", "handle_history"], "readwrite");
const registry = tx.objectStore("handle_registry");
const history = tx.objectStore("handle_history");
const now = (/* @__PURE__ */ new Date()).toISOString();
const existing = await registry.get(handle);
if (existing) {
await history.add({
handle,
previous_hash: existing.current_hash,
changed_at: now
});
await registry.put({
handle,
current_hash: newHash,
created_at: existing.created_at,
updated_at: now
});
} else {
await registry.put({
handle,
current_hash: newHash,
created_at: now,
updated_at: now
});
}
await tx.done;
}
async resolveHandle(handle) {
const db = await this.dbPromise;
const tx = db.transaction(["handle_registry", "card"], "readonly");
const registry = tx.objectStore("handle_registry");
const entry = await registry.get(handle);
if (!entry) return void 0;
const cardStore = tx.objectStore("card");
return cardStore.get(entry.current_hash);
}
// ═══════════════════════════════════════════════════════════════
// QUERY HELPERS (using Indices)
// ═══════════════════════════════════════════════════════════════
async getHandleHistory(handle) {
const db = await this.dbPromise;
return db.getAllFromIndex("handle_history", "by_handle", handle);
}
async getHandlesByHash(hash) {
const db = await this.dbPromise;
return db.getAllFromIndex("handle_registry", "by_current_hash", hash);
}
// ═══════════════════════════════════════════════════════════════
// UI HELPERS (For Browser Demo - keeping these for compatibility but implementing via new methods where possible)
// ═══════════════════════════════════════════════════════════════
async getAllCards() {
const db = await this.dbPromise;
return db.getAll("card");
}
async getAllHandles() {
const db = await this.dbPromise;
return db.getAll("handle_registry");
}
async getAllHistory() {
const db = await this.dbPromise;
return db.getAll("handle_history");
}
};
// src/ptr/browser/network/WebSocketClient.ts
var WebSocketClient = class {
ws = null;
url;
messageHandler = null;
reconnectInterval = 3e3;
constructor(url) {
this.url = url;
}
connect() {
console.log(`[BrowserPTR] Connecting to ${this.url}`);
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log("[BrowserPTR] WebSocket connected");
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (this.messageHandler) {
this.messageHandler(data);
}
} catch (e) {
console.error("[BrowserPTR] Failed to parse message:", e);
}
};
this.ws.onclose = () => {
console.log("[BrowserPTR] WebSocket closed. Reconnecting...");
this.ws = null;
setTimeout(() => this.connect(), this.reconnectInterval);
};
this.ws.onerror = (error) => {
console.error("[BrowserPTR] WebSocket error:", error);
};
}
setMessageHandler(handler) {
this.messageHandler = handler;
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
console.warn("[BrowserPTR] WebSocket not ready. Message dropped:", data);
}
}
};
// src/ptr/browser/worker/ServiceWorkerPTR.ts
var ServiceWorkerPTR = class {
store;
ws;
constructor(serverUrl, wasmUrl, storeName = "mcard-store") {
this.store = new MCardStore(storeName);
this.ws = new WebSocketClient(serverUrl);
this.ws.setMessageHandler(this.onMeshMessage.bind(this));
}
start() {
this.ws.connect();
self.addEventListener("message", (event) => {
if (event.data) {
if (event.data.type === "clm_execute") {
this.handleLocalRequest(event);
} else if (event.data.type === "EXECUTE_CLM") {
this.handleDirectExecution(event);
}
}
});
console.log("[ServiceWorkerPTR] Started");
}
getStore() {
return this.store;
}
async onMeshMessage(msg) {
console.log("[ServiceWorkerPTR] Received from mesh:", msg);
if (msg.type === "clm_execute") {
await this.executeCLM(msg.clm_hash, msg.input_hash, msg.request_id);
} else if (msg.type === "clm_result") {
const clients = await self.clients.matchAll();
for (const client of clients) {
client.postMessage(msg);
}
}
}
async handleLocalRequest(event) {
const { clm_hash, input_hash, request_id } = event.data;
console.log("[ServiceWorkerPTR] Local request:", request_id);
this.ws.send({
type: "clm_execute",
clm_hash,
input_hash,
request_id,
origin: "browser"
});
}
async handleDirectExecution(event) {
const { code } = event.data;
const port = event.ports[0];
console.log("[ServiceWorkerPTR] Direct Execution Request");
try {
const input = { content: { count: 0 } };
const result = await this.executeJavaScript(code, input, {});
if (port) {
port.postMessage({
result,
logs: ["[ServiceWorkerPTR] Executed successfully (Client Mode)"]
});
}
} catch (e) {
console.error("[ServiceWorkerPTR] Execution error:", e);
if (port) {
port.postMessage({
logs: [`[ServiceWorkerPTR] Error: ${e.message}`]
});
}
}
}
async executeCLM(clmHash, inputHash, requestId) {
console.log(`[ServiceWorkerPTR] Executing CLM ${clmHash} with input ${inputHash}`);
const input = await this.store.getCard(inputHash);
try {
const code = clmHash.includes("return") ? clmHash : "return input.content.count + 1;";
const result = await this.executeJavaScript(code, input || { content: { count: 0 } }, {});
const resultHash = `sha256:result_${requestId}`;
await this.store.putCard(resultHash, result);
this.ws.send({
type: "clm_result",
request_id: requestId,
output_hash: resultHash,
success: true
});
} catch (e) {
console.error("[ServiceWorkerPTR] Execution error:", e);
this.ws.send({
type: "clm_result",
request_id: requestId,
output_hash: "",
success: false,
error: e.message
});
}
}
// --- Execution Logic Ported from SandboxWorker ---
async executeJavaScript(code, input, context) {
try {
const fn = new Function("input", "context", code);
return fn(input, context || {});
} catch (e) {
throw e;
}
}
// Pyodide support placeholder
// Implementing full Pyodide loading requires handling caching and CDN usage
// similar to SandboxWorker.ts but adapted for ServiceWorker environment.
async executePython(code, input, context) {
throw new Error("Python execution not fully implemented yet in ServiceWorkerPTR");
}
};
// src/ptr/FaroSidecar.ts
import {
initializeFaro,
getWebInstrumentations
} from "@grafana/faro-web-sdk";
import { TracingInstrumentation } from "@grafana/faro-web-tracing";
var FaroSidecar = class _FaroSidecar {
static instance = null;
faro = null;
constructor() {
}
/**
* Get the singleton instance of FaroSidecar
*/
static getInstance() {
if (!_FaroSidecar.instance) {
_FaroSidecar.instance = new _FaroSidecar();
}
return _FaroSidecar.instance;
}
/**
* Initialize the Faro SDK
*
* @param config Configuration options
* @returns The initialized Faro instance or null if not in a browser environment
*/
initialize(config) {
if (typeof window === "undefined") {
console.warn("[FaroSidecar] Not initializing Grafana Faro: Non-browser environment detected.");
return null;
}
if (this.faro) {
console.warn("[FaroSidecar] Grafana Faro is already initialized.");
return this.faro;
}
const {
url,
apiKey,
appName,
appVersion,
enableTracing = true,
namespace = "ptr_runtime",
additionalInstrumentations = []
} = config;
const instrumentations = [
...getWebInstrumentations(),
...additionalInstrumentations
];
if (enableTracing) {
instrumentations.push(new TracingInstrumentation());
}
const faroOptions = {
url,
apiKey,
app: {
name: appName,
version: appVersion,
namespace
},
instrumentations
};
try {
this.faro = initializeFaro(faroOptions);
console.log(`[FaroSidecar] Grafana Faro initialized for ${appName}@${appVersion}`);
this.faro.api.pushLog([`PTR Observability Sidecar started for ${appName}`]);
} catch (error) {
console.error("[FaroSidecar] Failed to initialize Grafana Faro:", error);
}
return this.faro;
}
/**
* Get the underlying Faro instance
*/
getFaro() {
return this.faro;
}
/**
* Manually push an error to Faro
*/
pushError(error, context) {
if (this.faro) {
this.faro.api.pushError(error, { context });
}
}
/**
* Manually push a log message to Faro
*/
pushLog(message, context) {
if (this.faro) {
this.faro.api.pushLog([message], { context });
}
}
/**
* Push a custom event
*/
pushEvent(name, attributes) {
if (this.faro) {
this.faro.api.pushEvent(name, attributes);
}
}
};
// src/model/validators/BaseValidator.ts
var ValidationError = class extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
};
var BaseValidator = class {
};
// src/model/validators/TextValidator.ts
var TextValidator = class _TextValidator extends BaseValidator {
static TEXT_MIME_TYPES = /* @__PURE__ */ new Set([
"text/plain",
"application/json",
"application/xml",
"text/xml",
"image/svg+xml",
"text/html",
"text/markdown"
]);
canValidate(mimeType) {
return _TextValidator.TEXT_MIME_TYPES.has(mimeType);
}
validate(content, mimeType) {
const textContent = this.ensureString(content);
if (mimeType === "text/plain") {
this.validatePlainText(textContent);
} else if (mimeType === "application/json") {
this.validateJson(textContent);
} else if (["application/xml", "text/xml", "image/svg+xml"].includes(mimeType)) {
this.validateXml(textContent);
}
}
ensureString(content) {
if (typeof content === "string") {
return content;
}
return new TextDecoder().decode(content);
}
validatePlainText(content) {
const trimmed = content.trim();
if (!trimmed) {
throw new ValidationError("Invalid content: empty text");
}
if (trimmed.length < 3) {
throw new ValidationError("Invalid content: too short");
}
if (!/\s/.test(content)) {
if (content.split(/\s+/).length === 1 && content.length > 20) {
throw new ValidationError("Invalid content: likely not plain text");
}
}
}
validateJson(content) {
try {
const lines = content.split("\n");
if (lines.some((line) => line.trim().startsWith("//"))) {
throw new ValidationError("Invalid JSON content: contains comments");
}
JSON.parse(content);
} catch (e) {
if (e instanceof ValidationError) throw e;
throw new ValidationError("Invalid JSON content");
}
}
validateXml(content) {
const trimmed = content.trim();
if (!trimmed.startsWith("<") || !trimmed.endsWith(">")) {
throw new ValidationError("Invalid XML content");
}
}
};
// src/model/detectors/BinarySignatureDetector.ts
var BinarySignatureDetector = class _BinarySignatureDetector {
// Dictionary of binary signatures mapped to MIME types
// Note: In TS, we use numbers or number arrays for bytes
static SIGNATURES = {
// Images
"89504e470d0a1a0a": "image/png",
// \x89PNG\r\n\x1a\n
"ffd8ff": "image/jpeg",
"474946383761": "image/gif",
// GIF87a
"474946383961": "image/gif",
// GIF89a
"424d": "image/bmp",
// BM
"00000100": "image/x-icon",
"00000200": "image/x-icon",
// MP4
"00000018667479706d703432": "video/mp4",
// ...ftypmp42
"000000186674797069736f6d": "video/mp4",
// ...ftypisom
// ... (simplified for key common ones)
// Documents
"25504446": "application/pdf",
// %PDF
// Archives
"504b0304": "application/zip",
// PK\x03\x04
"1f8b08": "application/gzip",
"526172211a0700": "application/x-rar-compressed",
// Rar!
"377abcaf271c": "application/x-7z-compressed",
// 7z...
// Database
"53514c69746520666f726d6174203300": "application/x-sqlite3"
// SQLite format 3\0
};
static OLE_SIGNATURE = "d0cf11e0a1b11ae1";
/**
* Helper to convert hex string to Uint8Array for easy comparison if needed,
* but we will convert input bytes to hex for lookup.
*/
detect(content) {
return this.detectFromBytes(content);
}
detectFromBytes(content) {
if (this.startsWithAscii(content, "RIFF")) {
return this.detectRiffFormat(content);
}
const hexHeader = this.toHex(content.slice(0, 32));
for (const [signature, mimeType] of Object.entries(_BinarySignatureDetector.SIGNATURES)) {
if (hexHeader.startsWith(signature)) {
if (signature === _BinarySignatureDetector.OLE_SIGNATURE) {
return "application/oleobject";
}
if (signature === "504b0304") {
return "application/zip";
}
return mimeType;
}
}
return "application/octet-stream";
}
detectRiffFormat(content) {
if (content.length < 12) return "application/octet-stream";
const formatType = this.toAscii(content.slice(8, 12));
if (formatType === "WAVE") return "audio/wav";
if (formatType === "WEBP") return "image/webp";
return "application/octet-stream";
}
startsWithAscii(content, str) {
if (content.length < str.length) return false;
for (let i = 0; i < str.length; i++) {
if (content[i] !== str.charCodeAt(i)) return false;
}
return true;
}
toAscii(content) {
return Array.from(content).map((b) => String.fromCharCode(b)).join("");
}
toHex(content) {
return Array.from(content).map((b) => b.toString(16).padStart(2, "0")).join("");
}
};
// src/model/validators/BinaryValidator.ts
var BinaryValidator = class _BinaryValidator extends BaseValidator {
static BINARY_MIME_TYPES = /* @__PURE__ */ new Set([
"image/png",
"image/jpeg",
"image/gif",
"image/bmp",
"application/pdf",
"application/zip",
"video/mp4",
"audio/wav",
"application/octet-stream"
]);
detector = new BinarySignatureDetector();
canValidate(mimeType) {
return _BinaryValidator.BINARY_MIME_TYPES.has(mimeType) || mimeType.startsWith("image/") || mimeType.startsWith("audio/") || mimeType.startsWith("video/");
}
validate(content, mimeType) {
const contentBytes = this.ensureBytes(content);
if (mimeType.startsWith("image/")) {
this.validateImage(contentBytes, mimeType);
} else if (mimeType === "application/pdf") {
this.validatePdf(contentBytes);
} else if (mimeType === "application/zip") {
this.validateZip(contentBytes);
}
}
ensureBytes(content) {
if (content instanceof Uint8Array) {
return content;
}
return new TextEncoder().encode(content);
}
validateImage(content, mimeType) {
if (mimeType === "image/png" && content.length <= 8) {
throw new ValidationError("Invalid PNG content: truncated file");
} else if (mimeType === "image/jpeg" && content.length <= 3) {
throw new ValidationError("Invalid JPEG content: truncated file");
} else if (mimeType === "image/gif" && content.length <= 6) {
throw new ValidationError("Invalid GIF content: truncated file");
}
const signatures = BinarySignatureDetector.SIGNATURES;
let expectedSig = null;
for (const [sig, mime] of Object.entries(signatures)) {
if (mime === mimeType) {
expectedSig = sig;
}
}
if (expectedSig) {
const contentHex = this.toHex(content.slice(0, expectedSig.length / 2));
if (contentHex !== expectedSig) {
let hasMatch = false;
for (const [sig, mime] of Object.entries(signatures)) {
if (mime === mimeType) {
const currentHex = this.toHex(content.slice(0, sig.length / 2));
if (currentHex === sig) {
hasMatch = true;
break;
}
}
}
if (!hasMatch) {
throw new ValidationError(`Invalid ${mimeType} content: missing proper header`);
}
}
}
}
validatePdf(content) {
if (!this.startsWithAscii(content, "%PDF-")) {
throw new ValidationError("Invalid PDF content");
}
}
validateZip(content) {
if (content.length <= 4) {
throw new ValidationError("Invalid ZIP content");
}
}
// Helpers
toHex(content) {
return Array.from(content).map((b) => b.toString(16).padStart(2, "0")).join("");
}
startsWithAscii(content, str) {
if (content.length < str.length) return false;
for (let i = 0; i < str.length; i++) {
if (content[i] !== str.charCodeAt(i)) return false;
}
return true;
}
};
// src/model/validators/ValidationRegistry.ts
var ValidationRegistry = class {
validators;
constructor() {
this.validators = [
new TextValidator(),
new BinaryValidator()
];
}
/**
* Validate content using appropriate validator.
*
* @param content The content to validate
* @param mimeType The detected MIME type
* @throws ValidationError If content is invalid
*/
validate(content, mimeType) {
if (!content || content instanceof Uint8Array && content.length === 0) {
throw new ValidationError("Empty content");
}
if (typeof content === "string" && !content) {
throw new ValidationError("Empty content");
}
for (const validator of this.validators) {
if (validator.canValidate(mimeType)) {
validator.validate(content, mimeType);
return;
}
}
this.basicValidation(content);
}
basicValidation(content) {
if (content instanceof Uint8Array) {
if (content.length === 0) {
throw new ValidationError("Invalid content: empty byte array");
}
}
}
};
var validationRegistry = new ValidationRegistry();
export {
IndexedDBEngine,
computeHash,
Reader,
Writer,
State,
LensProtocol,
ErrorCodes,
VerificationStatus,
MCardStore,
ServiceWorkerPTR,
FaroSidecar,
ValidationRegistry,
validationRegistry
};