its-compiler-js
Version:
JavaScript/TypeScript implementation of the Instruction Template Specification (ITS) compiler
212 lines • 7 kB
JavaScript
/**
* 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