UNPKG

mlld

Version:

mlld: a modular prompt scripting language

339 lines (337 loc) 9.93 kB
import { TaintLevel } from './chunk-KYJC7SAY.mjs'; import { MlldResolutionError } from './chunk-YMCO2JI3.mjs'; import { __name, __publicField } from './chunk-OMKLS24H.mjs'; // core/resolvers/HTTPResolver.ts var _HTTPResolver = class _HTTPResolver { constructor() { __publicField(this, "name", "HTTP"); __publicField(this, "description", "Resolves modules from HTTP/HTTPS endpoints"); __publicField(this, "type", "input"); __publicField(this, "capabilities", { io: { read: true, write: false, list: false }, contexts: { import: true, path: true, output: false }, supportedContentTypes: [ "module", "data", "text" ], defaultContentType: "text", priority: 20, cache: { strategy: "persistent", ttl: { duration: 300 } // 5 minutes } }); __publicField(this, "cache", /* @__PURE__ */ new Map()); __publicField(this, "defaultTimeout", 3e4); __publicField(this, "defaultCacheTimeout", 3e5); } /** * Check if this resolver can handle the reference */ canResolve(ref, config) { return !!config?.baseUrl; } /** * Resolve a reference to HTTP content */ async resolve(ref, config) { if (!config?.baseUrl) { throw new MlldResolutionError("HTTPResolver requires baseUrl in configuration", { reference: ref }); } const url = this.buildUrl(ref, config); this.validateDomain(url, config); const cacheKey = url.toString(); const cached = this.getCached(cacheKey, config.cacheTimeout); if (cached) { const contentType = await this.detectContentType(url.pathname, cached.content); return { content: cached.content, contentType, metadata: { source: url.toString(), timestamp: /* @__PURE__ */ new Date(), taintLevel: TaintLevel.EXTERNAL, mimeType: cached.headers?.["content-type"] || "text/plain" } }; } try { const { content, etag, headers } = await this.fetchFromHttp(url, config, cached?.etag); this.cache.set(cacheKey, { content, timestamp: Date.now(), etag, headers }); const contentType = await this.detectContentType(url.pathname, content); return { content, contentType, metadata: { source: url.toString(), timestamp: /* @__PURE__ */ new Date(), taintLevel: TaintLevel.EXTERNAL, mimeType: headers["content-type"] || "text/plain", size: parseInt(headers["content-length"] || "0", 10) || void 0 } }; } catch (error) { if (error.status === 404) { throw new MlldResolutionError(`Resource not found: ${url}`, { reference: ref, url: url.toString() }); } throw new MlldResolutionError(`Failed to fetch from HTTP: ${error.message}`, { reference: ref, url: url.toString(), originalError: error }); } } /** * Validate configuration */ validateConfig(config) { const errors = []; if (!config?.baseUrl) { errors.push("baseUrl is required"); } else if (typeof config.baseUrl !== "string") { errors.push("baseUrl must be a string"); } else { try { new URL(config.baseUrl); } catch { errors.push("baseUrl must be a valid URL"); } } if (config.headers !== void 0) { if (typeof config.headers !== "object" || Array.isArray(config.headers)) { errors.push("headers must be an object"); } } if (config.allowedDomains !== void 0) { if (!Array.isArray(config.allowedDomains)) { errors.push("allowedDomains must be an array"); } else if (!config.allowedDomains.every((d) => typeof d === "string")) { errors.push("allowedDomains must contain only strings"); } } if (config.timeout !== void 0) { if (typeof config.timeout !== "number" || config.timeout < 0) { errors.push("timeout must be a non-negative number"); } } if (config.followRedirects !== void 0 && typeof config.followRedirects !== "boolean") { errors.push("followRedirects must be a boolean"); } if (config.maxRedirects !== void 0) { if (typeof config.maxRedirects !== "number" || config.maxRedirects < 0) { errors.push("maxRedirects must be a non-negative number"); } } if (config.validateSSL !== void 0 && typeof config.validateSSL !== "boolean") { errors.push("validateSSL must be a boolean"); } return errors; } /** * Check access - HTTP resolver is read-only */ async checkAccess(ref, operation, config) { if (operation === "write") { return false; } if (!config?.baseUrl) { return false; } try { const url = this.buildUrl(ref, config); this.validateDomain(url, config); return true; } catch { return false; } } /** * Build the full URL from reference and config */ buildUrl(ref, config) { const baseUrl = config.baseUrl.endsWith("/") ? config.baseUrl : config.baseUrl + "/"; const cleanRef = ref.startsWith("/") ? ref.slice(1) : ref; try { return new URL(cleanRef, baseUrl); } catch (error) { throw new MlldResolutionError(`Invalid URL: ${baseUrl}${cleanRef}`, { reference: ref, baseUrl: config.baseUrl }); } } /** * Validate that the URL domain is allowed */ validateDomain(url, config) { const baseUrl = new URL(config.baseUrl); const allowedDomains = config.allowedDomains || [ baseUrl.hostname ]; if (!allowedDomains.includes(url.hostname)) { throw new MlldResolutionError(`Domain not allowed: ${url.hostname}. Allowed domains: ${allowedDomains.join(", ")}`, { url: url.toString(), allowedDomains }); } if (url.protocol !== "https:" && config.validateSSL !== false) { throw new MlldResolutionError("Only HTTPS URLs are allowed for security reasons", { url: url.toString() }); } } /** * Get cached content if available */ getCached(key, timeout) { const cached = this.cache.get(key); if (!cached) return null; const maxAge = timeout ?? this.defaultCacheTimeout; if (Date.now() - cached.timestamp > maxAge) { this.cache.delete(key); return null; } return { content: cached.content, etag: cached.etag, headers: cached.headers }; } /** * Fetch content from HTTP endpoint */ async fetchFromHttp(url, config, etag) { const controller = new AbortController(); const timeout = config.timeout || this.defaultTimeout; const timeoutId = setTimeout(() => controller.abort(), timeout); try { const headers = { "User-Agent": "mlld-http-resolver", "Accept": "text/plain, text/*, application/json", ...config.headers }; if (etag) { headers["If-None-Match"] = etag; } const response = await fetch(url.toString(), { headers, signal: controller.signal, redirect: config.followRedirects === false ? "manual" : "follow" }); clearTimeout(timeoutId); if (response.status === 304) { const cached = this.cache.get(url.toString()); if (cached) { return { content: cached.content, etag: cached.etag, headers: cached.headers || {} }; } } if (!response.ok) { const error = new Error(`HTTP error: ${response.status} ${response.statusText}`); error.status = response.status; throw error; } const contentType = response.headers.get("content-type") || ""; if (!this.isTextContent(contentType)) { throw new Error(`Unsupported content type: ${contentType}`); } const content = await response.text(); const responseHeaders = {}; response.headers.forEach((value, key) => { responseHeaders[key.toLowerCase()] = value; }); return { content, etag: response.headers.get("etag") || void 0, headers: responseHeaders }; } catch (error) { if (error.name === "AbortError") { throw new Error(`Request timeout after ${timeout}ms`); } throw error; } finally { clearTimeout(timeoutId); } } /** * Check if content type is text-based */ isTextContent(contentType) { const textTypes = [ "text/", "application/json", "application/xml", "application/javascript", "application/x-yaml", "application/x-mlld" ]; return textTypes.some((type) => contentType.toLowerCase().includes(type)); } /** * Detect content type based on file extension and content */ async detectContentType(filePath, content) { if (filePath.endsWith(".mld") || filePath.endsWith(".mlld")) { return "module"; } if (filePath.endsWith(".json")) { return "data"; } try { const { parse } = await import('./parser-X2XXTU6D.mjs'); const result = await parse(content); if (result.success && this.hasModuleExports(result.ast)) { return "module"; } } catch { } try { JSON.parse(content); return "data"; } catch { } return "text"; } /** * Check if AST has module exports */ hasModuleExports(ast) { if (!ast || !Array.isArray(ast)) return false; return ast.some((node) => node && node.type === "Directive" && [ "var", "exe", "path" ].includes(node.kind)); } }; __name(_HTTPResolver, "HTTPResolver"); var HTTPResolver = _HTTPResolver; export { HTTPResolver }; //# sourceMappingURL=chunk-7T5PMNS2.mjs.map //# sourceMappingURL=chunk-7T5PMNS2.mjs.map