UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

461 lines (460 loc) 19.4 kB
import { h as resolveSecretInputString } from "./types.secrets-_0JOMGE5.js"; import { _ as resolvePinnedHostnameWithPolicy, c as isBlockedHostnameOrIp, t as SsrFBlockedError, u as isPrivateIpAddress } from "./ssrf-CvPEXMGn.js"; import { n as normalizeSecretInput } from "./normalize-secret-input-OwRUfByL.js"; import { a as wrapWebContent, i as wrapExternalContent } from "./external-content-pX-Pk1Iu.js"; import "./security-runtime-CQm7DD1u.js"; import { a as truncateText, r as markdownToText } from "./web-fetch-utils-CGeEaxjc.js"; import { a as readResponseText, i as readCache, o as resolveCacheTtlMs, r as normalizeCacheKey, s as resolvePositiveTimeoutSeconds, u as writeCache } from "./web-shared-TPeHhehr.js"; import "./secret-input-DVCzFGxN.js"; import "./ssrf-runtime-BOGN5pUi.js"; import { n as withSelfHostedWebToolsEndpoint, r as withStrictWebToolsEndpoint } from "./web-guarded-fetch-DYAWPWNo.js"; import { i as canResolveEnvSecretRefInReadOnlyPath } from "./extension-shared-B8fkO3TV.js"; import "./provider-web-fetch-vmFzBVVG.js"; const DEFAULT_FIRECRAWL_MAX_AGE_MS = 1728e5; const FIRECRAWL_API_KEY_ENV_VAR = "FIRECRAWL_API_KEY"; function resolveSearchConfig(cfg) { const search = cfg?.tools?.web?.search; if (!search || typeof search !== "object") return; return search; } function resolveFetchConfig(cfg) { const fetch = cfg?.tools?.web?.fetch; if (!fetch || typeof fetch !== "object") return; return fetch; } function resolveFirecrawlSearchConfig(cfg) { const pluginWebSearch = (cfg?.plugins?.entries?.firecrawl?.config)?.webSearch; if (pluginWebSearch && typeof pluginWebSearch === "object" && !Array.isArray(pluginWebSearch)) return pluginWebSearch; const search = resolveSearchConfig(cfg); if (!search || typeof search !== "object") return; const firecrawl = "firecrawl" in search ? search.firecrawl : void 0; if (!firecrawl || typeof firecrawl !== "object") return; return firecrawl; } function resolveFirecrawlFetchConfig(cfg) { const pluginWebFetch = (cfg?.plugins?.entries?.firecrawl?.config)?.webFetch; if (pluginWebFetch && typeof pluginWebFetch === "object" && !Array.isArray(pluginWebFetch)) return pluginWebFetch; const fetch = resolveFetchConfig(cfg); if (!fetch || typeof fetch !== "object") return; const firecrawl = "firecrawl" in fetch ? fetch.firecrawl : void 0; if (!firecrawl || typeof firecrawl !== "object") return; return firecrawl; } function resolveConfiguredSecret(value, path, cfg) { const resolved = resolveSecretInputString({ value, path, defaults: cfg?.secrets?.defaults, mode: "inspect" }); if (resolved.status === "available") { const normalized = normalizeSecretInput(resolved.value); return normalized ? { status: "available", value: normalized } : { status: "missing" }; } if (resolved.status === "missing") return { status: "missing" }; if (resolved.ref.source !== "env") return { status: "blocked" }; const envVarName = resolved.ref.id.trim(); if (envVarName !== FIRECRAWL_API_KEY_ENV_VAR) return { status: "blocked" }; if (!canResolveEnvSecretRefInReadOnlyPath({ cfg, provider: resolved.ref.provider, id: envVarName })) return { status: "blocked" }; const envValue = normalizeSecretInput(process.env[envVarName]); return envValue ? { status: "available", value: envValue } : { status: "missing" }; } function resolveFirecrawlApiKey(cfg) { const pluginConfig = cfg?.plugins?.entries?.firecrawl?.config; const search = resolveFirecrawlSearchConfig(cfg); const fetch = resolveFirecrawlFetchConfig(cfg); const configuredCandidates = [ { value: pluginConfig?.webFetch?.apiKey, path: "plugins.entries.firecrawl.config.webFetch.apiKey" }, { value: search?.apiKey, path: "plugins.entries.firecrawl.config.webSearch.apiKey" }, { value: search?.apiKey, path: "tools.web.search.firecrawl.apiKey" }, { value: fetch?.apiKey, path: "tools.web.fetch.firecrawl.apiKey" } ]; let blockedConfiguredSecret = false; for (const candidate of configuredCandidates) { const resolved = resolveConfiguredSecret(candidate.value, candidate.path, cfg); if (resolved.status === "available") return resolved.value; if (resolved.status === "blocked") blockedConfiguredSecret = true; } if (blockedConfiguredSecret) return; return normalizeSecretInput(process.env[FIRECRAWL_API_KEY_ENV_VAR]) || void 0; } function resolveFirecrawlBaseUrl(cfg) { const search = resolveFirecrawlSearchConfig(cfg); const fetch = resolveFirecrawlFetchConfig(cfg); return (typeof search?.baseUrl === "string" ? search.baseUrl.trim() : "") || (typeof fetch?.baseUrl === "string" ? fetch.baseUrl.trim() : "") || normalizeSecretInput(process.env.FIRECRAWL_BASE_URL) || "https://api.firecrawl.dev"; } function resolveFirecrawlOnlyMainContent(cfg, override) { if (typeof override === "boolean") return override; const fetch = resolveFirecrawlFetchConfig(cfg); if (typeof fetch?.onlyMainContent === "boolean") return fetch.onlyMainContent; return true; } function resolveFirecrawlMaxAgeMs(cfg, override) { if (typeof override === "number" && Number.isFinite(override) && override >= 0) return Math.floor(override); const fetch = resolveFirecrawlFetchConfig(cfg); if (typeof fetch?.maxAgeMs === "number" && Number.isFinite(fetch.maxAgeMs) && fetch.maxAgeMs >= 0) return Math.floor(fetch.maxAgeMs); return DEFAULT_FIRECRAWL_MAX_AGE_MS; } function resolveFirecrawlScrapeTimeoutSeconds(cfg, override) { return resolvePositiveTimeoutSeconds(override, resolvePositiveTimeoutSeconds(resolveFirecrawlFetchConfig(cfg)?.timeoutSeconds, 60)); } function resolveFirecrawlSearchTimeoutSeconds(override) { return resolvePositiveTimeoutSeconds(override, 30); } //#endregion //#region extensions/firecrawl/src/firecrawl-client.ts const SEARCH_CACHE = /* @__PURE__ */ new Map(); const SCRAPE_CACHE = /* @__PURE__ */ new Map(); const DEFAULT_SEARCH_COUNT = 5; const DEFAULT_SCRAPE_MAX_CHARS = 5e4; const ALLOWED_FIRECRAWL_HOSTS = new Set(["api.firecrawl.dev"]); const FIRECRAWL_SELF_HOSTED_PRIVATE_ERROR = "Firecrawl custom baseUrl must target a private or internal self-hosted endpoint."; const FIRECRAWL_HTTP_PRIVATE_ERROR = "Firecrawl HTTP baseUrl must target a private or internal self-hosted endpoint. Use https:// for public hosts."; async function readFirecrawlJsonResponse(response, label) { try { return await response.json(); } catch (cause) { throw new Error(`${label}: malformed JSON response`, { cause }); } } function assertFirecrawlScrapeTargetAllowed(url) { let parsed; try { parsed = new URL(url); } catch { throw new SsrFBlockedError("Invalid URL supplied to Firecrawl scrape"); } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new SsrFBlockedError(`Blocked non-HTTP(S) protocol in Firecrawl scrape URL: ${parsed.protocol}`); if (isBlockedHostnameOrIp(parsed.hostname)) throw new SsrFBlockedError(`Blocked hostname or private/internal IP in Firecrawl scrape URL: ${parsed.hostname}`); } function isOfficialFirecrawlEndpoint(url) { return url.protocol === "https:" && ALLOWED_FIRECRAWL_HOSTS.has(url.hostname); } async function firecrawlEndpointTargetsPrivateNetwork(url, lookupFn) { if (isBlockedHostnameOrIp(url.hostname)) return true; try { return (await resolvePinnedHostnameWithPolicy(url.hostname, { lookupFn, policy: { allowPrivateNetwork: true } })).addresses.every((address) => isPrivateIpAddress(address)); } catch { return false; } } async function validateFirecrawlBaseUrl(baseUrl, lookupFn) { let url; try { url = new URL(baseUrl.trim() || "https://api.firecrawl.dev"); } catch { throw new Error("Firecrawl baseUrl must be a valid http:// or https:// URL."); } if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("Firecrawl baseUrl must use http:// or https://."); if (isOfficialFirecrawlEndpoint(url)) return "strict"; if (await firecrawlEndpointTargetsPrivateNetwork(url, lookupFn)) return "selfHosted"; if (url.protocol === "http:") throw new Error(FIRECRAWL_HTTP_PRIVATE_ERROR); throw new Error(`${FIRECRAWL_SELF_HOSTED_PRIVATE_ERROR} Host: ${url.hostname}`); } async function resolveEndpoint(baseUrl, pathname, lookupFn) { const url = new URL(baseUrl.trim() || "https://api.firecrawl.dev"); const mode = await validateFirecrawlBaseUrl(url.toString(), lookupFn); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; url.pathname = pathname; return { url: url.toString(), mode }; } async function postFirecrawlJson(params, parse) { const apiKey = normalizeSecretInput(params.apiKey); return await ((params.mode ?? await validateFirecrawlBaseUrl(params.url)) === "selfHosted" ? withSelfHostedWebToolsEndpoint : withStrictWebToolsEndpoint)({ url: params.url, timeoutSeconds: params.timeoutSeconds, init: { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify(params.body) } }, async ({ response }) => { if (!response.ok) { let detail = typeof response.statusText === "string" && response.statusText.trim() ? response.statusText.trim() : "request failed"; const readJsonPayload = async () => { const candidate = response; const jsonResponse = typeof candidate.clone === "function" ? candidate.clone() : response; if (typeof jsonResponse.json !== "function") return null; try { const payload = await jsonResponse.json(); return payload && typeof payload === "object" && !Array.isArray(payload) ? payload : null; } catch { return null; } }; const payload = await readJsonPayload(); if (payload) detail = typeof payload.error === "string" ? payload.error : typeof payload.message === "string" ? payload.message : detail; else { const errorBody = await readResponseText(response, { maxBytes: 64e3 }); if (errorBody.text) detail = errorBody.text; } const safeDetail = wrapWebContent(detail.slice(0, 1e3), "web_fetch"); throw new Error(`${params.errorLabel} API error (${response.status}): ${safeDetail}`); } return await parse(response); }); } function resolveSiteName(urlRaw) { try { return new URL(urlRaw).hostname.replace(/^www\./, "") || void 0; } catch { return; } } function resolveSearchItems(payload) { const rawItems = [ payload.data, payload.results, payload.data?.results, payload.data?.data, payload.data?.web, payload.web?.results ].find((candidate) => Array.isArray(candidate)); if (!Array.isArray(rawItems)) return []; const items = []; for (const entry of rawItems) { if (!entry || typeof entry !== "object") continue; const record = entry; const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : void 0; const url = typeof record.url === "string" && record.url || typeof record.sourceURL === "string" && record.sourceURL || typeof record.sourceUrl === "string" && record.sourceUrl || typeof metadata?.sourceURL === "string" && metadata.sourceURL || ""; if (!url) continue; const title = typeof record.title === "string" && record.title || typeof metadata?.title === "string" && metadata.title || ""; const description = typeof record.description === "string" && record.description || typeof record.snippet === "string" && record.snippet || typeof record.summary === "string" && record.summary || void 0; const content = typeof record.markdown === "string" && record.markdown || typeof record.content === "string" && record.content || typeof record.text === "string" && record.text || void 0; const published = typeof record.publishedDate === "string" && record.publishedDate || typeof record.published === "string" && record.published || typeof metadata?.publishedTime === "string" && metadata.publishedTime || typeof metadata?.publishedDate === "string" && metadata.publishedDate || void 0; items.push({ title, url, description, content, published, siteName: resolveSiteName(url) }); } return items; } function buildSearchPayload(params) { return { query: params.query, provider: params.provider, count: params.items.length, tookMs: params.tookMs, externalContent: { untrusted: true, source: "web_search", provider: params.provider, wrapped: true }, results: params.items.map((entry) => ({ title: entry.title ? wrapWebContent(entry.title, "web_search") : "", url: entry.url, description: entry.description ? wrapWebContent(entry.description, "web_search") : "", ...entry.published ? { published: entry.published } : {}, ...entry.siteName ? { siteName: entry.siteName } : {}, ...params.scrapeResults && entry.content ? { content: wrapWebContent(entry.content, "web_search") } : {} })) }; } async function runFirecrawlSearch(params) { const apiKey = resolveFirecrawlApiKey(params.cfg); if (!apiKey) throw new Error("web_search (firecrawl) needs a Firecrawl API key. Set FIRECRAWL_API_KEY in the Gateway environment, or configure plugins.entries.firecrawl.config.webSearch.apiKey."); const count = typeof params.count === "number" && Number.isFinite(params.count) ? Math.max(1, Math.min(10, Math.floor(params.count))) : DEFAULT_SEARCH_COUNT; const timeoutSeconds = resolveFirecrawlSearchTimeoutSeconds(params.timeoutSeconds); const scrapeResults = params.scrapeResults === true; const sources = Array.isArray(params.sources) ? params.sources.filter(Boolean) : []; const categories = Array.isArray(params.categories) ? params.categories.filter(Boolean) : []; const baseUrl = resolveFirecrawlBaseUrl(params.cfg); const cacheKey = normalizeCacheKey(JSON.stringify({ type: "firecrawl-search", q: params.query, count, baseUrl, sources, categories, scrapeResults })); const cached = readCache(SEARCH_CACHE, cacheKey); if (cached) return { ...cached.value, cached: true }; const body = { query: params.query, limit: count }; if (sources.length > 0) body.sources = sources; if (categories.length > 0) body.categories = categories; if (scrapeResults) body.scrapeOptions = { formats: ["markdown"] }; const start = Date.now(); const endpoint = await resolveEndpoint(baseUrl, "/v2/search"); const payload = await postFirecrawlJson({ url: endpoint.url, mode: endpoint.mode, timeoutSeconds, apiKey, body, errorLabel: "Firecrawl Search" }, async (response) => { const payloadValue = await readFirecrawlJsonResponse(response, "Firecrawl Search API error"); if (payloadValue.success === false) { const error = typeof payloadValue.error === "string" ? payloadValue.error : typeof payloadValue.message === "string" ? payloadValue.message : "unknown error"; throw new Error(`Firecrawl Search API error: ${error}`); } return payloadValue; }); const result = buildSearchPayload({ query: params.query, provider: "firecrawl", items: resolveSearchItems(payload), tookMs: Date.now() - start, scrapeResults }); writeCache(SEARCH_CACHE, cacheKey, result, resolveCacheTtlMs(void 0, 15)); return result; } function resolveScrapeData(payload) { const data = payload.data; if (data && typeof data === "object") return data; return {}; } function parseFirecrawlScrapePayload(params) { const data = resolveScrapeData(params.payload); const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : void 0; const markdown = typeof data.markdown === "string" && data.markdown || typeof data.content === "string" && data.content || ""; if (!markdown) throw new Error("Firecrawl scrape returned no content."); const rawText = params.extractMode === "text" ? markdownToText(markdown) : markdown; const truncated = truncateText(rawText, params.maxChars); return { url: params.url, finalUrl: typeof metadata?.sourceURL === "string" && metadata.sourceURL || typeof data.url === "string" && data.url || params.url, status: typeof metadata?.statusCode === "number" && metadata.statusCode || typeof data.statusCode === "number" && data.statusCode || void 0, title: typeof metadata?.title === "string" && metadata.title ? wrapExternalContent(metadata.title, { source: "web_fetch", includeWarning: false }) : void 0, extractor: "firecrawl", extractMode: params.extractMode, externalContent: { untrusted: true, source: "web_fetch", wrapped: true }, truncated: truncated.truncated, rawLength: rawText.length, wrappedLength: wrapExternalContent(truncated.text, { source: "web_fetch", includeWarning: false }).length, text: wrapExternalContent(truncated.text, { source: "web_fetch", includeWarning: false }), warning: typeof params.payload.warning === "string" && params.payload.warning ? wrapExternalContent(params.payload.warning, { source: "web_fetch", includeWarning: false }) : void 0 }; } async function runFirecrawlScrape(params) { assertFirecrawlScrapeTargetAllowed(params.url); const apiKey = resolveFirecrawlApiKey(params.cfg); if (!apiKey) throw new Error("firecrawl_scrape needs a Firecrawl API key. Set FIRECRAWL_API_KEY in the Gateway environment, or configure plugins.entries.firecrawl.config.webFetch.apiKey."); const baseUrl = resolveFirecrawlBaseUrl(params.cfg); const timeoutSeconds = resolveFirecrawlScrapeTimeoutSeconds(params.cfg, params.timeoutSeconds); const onlyMainContent = resolveFirecrawlOnlyMainContent(params.cfg, params.onlyMainContent); const maxAgeMs = resolveFirecrawlMaxAgeMs(params.cfg, params.maxAgeMs); const proxy = params.proxy ?? "auto"; const storeInCache = params.storeInCache ?? true; const maxChars = typeof params.maxChars === "number" && Number.isFinite(params.maxChars) && params.maxChars > 0 ? Math.floor(params.maxChars) : DEFAULT_SCRAPE_MAX_CHARS; const cacheKey = normalizeCacheKey(JSON.stringify({ type: "firecrawl-scrape", url: params.url, extractMode: params.extractMode, baseUrl, onlyMainContent, maxAgeMs, proxy, storeInCache, maxChars })); const cached = readCache(SCRAPE_CACHE, cacheKey); if (cached) return { ...cached.value, cached: true }; const endpoint = await resolveEndpoint(baseUrl, "/v2/scrape"); const result = parseFirecrawlScrapePayload({ payload: await postFirecrawlJson({ url: endpoint.url, mode: endpoint.mode, timeoutSeconds, apiKey, errorLabel: "Firecrawl", body: { url: params.url, formats: ["markdown"], onlyMainContent, timeout: timeoutSeconds * 1e3, maxAge: maxAgeMs, proxy, storeInCache } }, async (response) => { const payloadLocal = await readFirecrawlJsonResponse(response, "Firecrawl fetch failed"); if (payloadLocal.success === false) { const detail = typeof payloadLocal.error === "string" ? payloadLocal.error : typeof payloadLocal.message === "string" ? payloadLocal.message : response.statusText; throw new Error(`Firecrawl fetch failed (${response.status}): ${wrapWebContent(detail, "web_fetch")}`.trim()); } return payloadLocal; }), url: params.url, extractMode: params.extractMode, maxChars }); writeCache(SCRAPE_CACHE, cacheKey, result, resolveCacheTtlMs(void 0, 15)); return result; } const testing = { assertFirecrawlScrapeTargetAllowed, parseFirecrawlScrapePayload, postFirecrawlJson, resolveEndpoint, validateFirecrawlBaseUrl, resolveSearchItems }; //#endregion export { testing as a, runFirecrawlSearch as i, parseFirecrawlScrapePayload as n, runFirecrawlScrape as r, assertFirecrawlScrapeTargetAllowed as t };