mlld
Version:
mlld: a modular prompt scripting language
339 lines (337 loc) • 9.93 kB
JavaScript
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