UNPKG

its-compiler-js

Version:

JavaScript/TypeScript implementation of the Instruction Template Specification (ITS) compiler

212 lines 7 kB
/** * Schema loading and caching for ITS Compiler */ import { promises as fs } from 'fs'; import { URL } from 'url'; import fetch from 'node-fetch'; import { ITSSecurityError } from './types.js'; import { SecurityValidator } from './security.js'; export class SchemaLoader { constructor(cacheTTL = 3600000, // 1 hour securityConfig, timeout = 10000 // 10 seconds ) { this.cache = {}; this.cacheTTL = cacheTTL; this.securityValidator = new SecurityValidator(securityConfig); this.timeout = timeout; } /** * Load a schema from URL or cache */ async loadSchema(schemaUrl, baseUrl) { // Resolve relative URLs const resolvedUrl = this.resolveUrl(schemaUrl, baseUrl); // Security validation this.securityValidator.validateSchemaUrl(resolvedUrl); // Check cache first const cached = this.getFromCache(resolvedUrl); if (cached) { return cached; } try { const schema = await this.loadFromUrl(resolvedUrl); this.saveToCache(resolvedUrl, schema); return schema; } catch (error) { throw new ITSSecurityError(`Failed to load schema from ${resolvedUrl}: ${error}`, 'schema_loading', 'SCHEMA_LOAD_FAILED'); } } /** * Resolve URL (handle relative URLs) */ resolveUrl(url, baseUrl) { if (!baseUrl || this.isAbsoluteUrl(url)) { return url; } try { const base = new URL(baseUrl); return new URL(url, base).toString(); } catch { // If URL resolution fails, return original URL return url; } } /** * Check if URL is absolute */ isAbsoluteUrl(url) { try { new URL(url); return true; } catch { return false; } } /** * Load schema from URL */ async loadFromUrl(url) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'its-compiler-js/1.0', Accept: 'application/json, text/plain', 'Cache-Control': 'no-cache', }, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Check content type const contentType = response.headers.get('content-type') || ''; if (!contentType.includes('application/json') && !contentType.includes('text/')) { throw new Error(`Invalid content type: ${contentType}`); } // Check content length const contentLength = response.headers.get('content-length'); if (contentLength) { const size = parseInt(contentLength, 10); if (size > 10 * 1024 * 1024) { // 10MB limit throw new Error(`Schema too large: ${size} bytes`); } } const text = await response.text(); // Limit response size even if no content-length header if (text.length > 10 * 1024 * 1024) { throw new Error(`Schema response too large: ${text.length} bytes`); } const schema = JSON.parse(text); // Validate schema structure this.validateSchemaStructure(schema); return schema; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Request timeout after ${this.timeout}ms`); } throw error; } } /** * Validate schema structure */ validateSchemaStructure(schema) { if (typeof schema !== 'object' || schema === null) { throw new Error('Schema must be a JSON object'); } // Validate instructionTypes if present if ('instructionTypes' in schema) { if (typeof schema.instructionTypes !== 'object' || schema.instructionTypes === null) { throw new Error('instructionTypes must be an object'); } for (const [typeName, typeDef] of Object.entries(schema.instructionTypes)) { this.validateInstructionType(typeName, typeDef); } } } /** * Validate individual instruction type definition */ validateInstructionType(typeName, typeDef) { if (typeof typeDef !== 'object' || typeDef === null) { throw new Error(`Instruction type '${typeName}' must be an object`); } if (!('template' in typeDef)) { throw new Error(`Instruction type '${typeName}' missing required 'template' field`); } if (typeof typeDef.template !== 'string') { throw new Error(`Instruction type '${typeName}' template must be a string`); } // Check template for dangerous patterns const dangerousPatterns = [/<script/i, /javascript:/i, /data:text\/html/i, /eval\(/i]; for (const pattern of dangerousPatterns) { if (pattern.test(typeDef.template)) { console.warn(`Warning: Potentially dangerous pattern in template: ${pattern}`); } } } /** * Get schema from cache */ getFromCache(url) { const cached = this.cache[url]; if (!cached) { return null; } if (Date.now() > cached.expiresAt) { delete this.cache[url]; return null; } return cached.schema; } /** * Save schema to cache */ saveToCache(url, schema) { this.cache[url] = { schema, cachedAt: Date.now(), expiresAt: Date.now() + this.cacheTTL, }; } /** * Clear cache */ clearCache() { this.cache = {}; } /** * Get cache statistics */ getCacheStats() { const urls = Object.keys(this.cache); return { size: urls.length, urls, }; } /** * Load schema from file (for testing) */ async loadSchemaFromFile(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); const schema = JSON.parse(content); this.validateSchemaStructure(schema); return schema; } catch (error) { throw new ITSSecurityError(`Failed to load schema from file ${filePath}: ${error}`, 'schema_loading', 'SCHEMA_LOAD_FAILED'); } } } //# sourceMappingURL=schema-loader.js.map