UNPKG

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
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-ptr-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-ptr-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 };