mcp-web-validator 1.3.14 → 1.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to this project are documented here. The project follows [Se
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.3.15] - 2026-09-22
8
+
9
+ ### Fixed
10
+
11
+ - Pinned Chromium screenshot HTTP(S) connections to DNS answers revalidated as public at proxy connection time, closing a DNS-rebinding/TOCTOU gap while preserving the original hostname for TLS/SNI validation.
12
+ - Scoped Open Graph title/image checks to the document head so body-only metadata lookalikes cannot suppress missing social-preview guidance.
13
+
7
14
  ## [1.3.14] - 2026-09-21
8
15
 
9
16
  ### Fixed
package/dist/network.d.ts CHANGED
@@ -18,10 +18,22 @@ export interface PublicHttpResult {
18
18
  response: Response;
19
19
  url: URL;
20
20
  }
21
+ export interface ResolvedPublicHttpUrl {
22
+ url: URL;
23
+ addresses: Array<{
24
+ address: string;
25
+ family: number;
26
+ }>;
27
+ }
21
28
  export declare class PublicUrlError extends Error {
22
29
  readonly name = "PublicUrlError";
23
30
  }
24
31
  export declare function getErrorMessage(error: unknown): string;
32
+ /**
33
+ * Parses an HTTP(S) URL and rejects hostnames that resolve to local, private,
34
+ * documentation, multicast, or otherwise non-public address space.
35
+ */
36
+ export declare function resolvePublicHttpUrl(input: string | URL): Promise<ResolvedPublicHttpUrl>;
25
37
  export declare function assertPublicHttpUrl(input: string | URL): Promise<URL>;
26
38
  export declare function cancelResponseBody(response: Response): Promise<void>;
27
39
  export declare function readResponseText(response: Response, maxBytes: number, encoding?: string, sniffHtmlEncoding?: boolean): Promise<string>;
package/dist/network.js CHANGED
@@ -114,7 +114,7 @@ async function withTimeout(promise, timeoutMs, label) {
114
114
  * Parses an HTTP(S) URL and rejects hostnames that resolve to local, private,
115
115
  * documentation, multicast, or otherwise non-public address space.
116
116
  */
117
- async function resolvePublicHttpUrl(input) {
117
+ export async function resolvePublicHttpUrl(input) {
118
118
  const rawUrl = input instanceof URL ? input.href : input;
119
119
  if (typeof rawUrl !== "string" || rawUrl.length === 0 || rawUrl.length > MAX_URL_LENGTH) {
120
120
  throw new PublicUrlError(`URL must contain between 1 and ${MAX_URL_LENGTH} characters`);
@@ -1 +1 @@
1
- {"version":3,"file":"network.js","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"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","sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport * as fs from \"node:fs/promises\";\nimport { BlockList, isIP } from \"node:net\";\nimport * as path from \"node:path\";\nimport { getEncoding } from \"encoding-sniffer/sniffer\";\nimport { Agent } from \"undici\";\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_REDIRECTS = 5;\nconst DNS_TIMEOUT_MS = 5_000;\nconst MAX_URL_LENGTH = 8_192;\nconst HTML_ENCODING_SNIFF_BYTES = 1_024;\n\nconst blockedAddresses = new BlockList();\nfor (const [network, prefix] of [\n [\"0.0.0.0\", 8],\n [\"10.0.0.0\", 8],\n [\"100.64.0.0\", 10],\n [\"127.0.0.0\", 8],\n [\"169.254.0.0\", 16],\n [\"172.16.0.0\", 12],\n [\"192.0.0.0\", 24],\n [\"192.0.2.0\", 24],\n [\"192.88.99.0\", 24],\n [\"192.168.0.0\", 16],\n [\"198.18.0.0\", 15],\n [\"198.51.100.0\", 24],\n [\"203.0.113.0\", 24],\n [\"224.0.0.0\", 4],\n [\"240.0.0.0\", 4],\n] as const) {\n blockedAddresses.addSubnet(network, prefix, \"ipv4\");\n}\n\nfor (const [network, prefix] of [\n [\"::\", 96],\n [\"fc00::\", 7],\n [\"fe80::\", 10],\n [\"ff00::\", 8],\n [\"2001::\", 32],\n [\"2001:2::\", 48],\n [\"2001:10::\", 28],\n [\"2001:db8::\", 32],\n [\"2002::\", 16],\n] as const) {\n blockedAddresses.addSubnet(network, prefix, \"ipv6\");\n}\n\nconst publicIpv6Addresses = new BlockList();\npublicIpv6Addresses.addSubnet(\"2000::\", 3, \"ipv6\");\n\nconst blockedHostnames = new Set([\n \"localhost\",\n \"localhost.localdomain\",\n \"ip6-localhost\",\n \"ip6-loopback\",\n]);\n\nconst blockedHostnameSuffixes = [\n \".localhost\",\n \".local\",\n \".internal\",\n \".home.arpa\",\n \".test\",\n \".invalid\",\n \".example\",\n \".onion\",\n];\n\nconst redirectStatuses = new Set([301, 302, 303, 307, 308]);\n\nexport interface PublicFetchOptions {\n method?: \"GET\" | \"HEAD\";\n headers?: Record<string, string>;\n timeoutMs?: number;\n maxRedirects?: number;\n}\n\nexport interface PublicTextOptions extends PublicFetchOptions {\n maxBytes?: number;\n acceptedContentTypes?: readonly string[];\n}\n\nexport interface PublicTextResult {\n text: string;\n url: string;\n status: number;\n contentType: string | null;\n}\n\nexport interface PublicHttpResult {\n response: Response;\n url: URL;\n}\n\ninterface ResolvedPublicHttpUrl {\n url: URL;\n addresses: Array<{ address: string; family: number }>;\n}\n\nexport class PublicUrlError extends Error {\n override readonly name = \"PublicUrlError\";\n}\n\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n if (error.name === \"TimeoutError\" || error.name === \"AbortError\") {\n return \"Request timed out\";\n }\n return error.message || error.name;\n }\n\n return typeof error === \"string\" && error.trim() ? error : \"Unknown error\";\n}\n\nfunction assertPositiveInteger(value: number, label: string): void {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n}\n\nfunction normalizeHostname(hostname: string): string {\n const withoutBrackets = hostname.startsWith(\"[\") && hostname.endsWith(\"]\")\n ? hostname.slice(1, -1)\n : hostname;\n return withoutBrackets.replace(/\\.$/, \"\").toLowerCase();\n}\n\nfunction isPublicAddress(address: string, family: number): boolean {\n if (family === 4) {\n return !blockedAddresses.check(address, \"ipv4\");\n }\n\n if (family === 6) {\n return publicIpv6Addresses.check(address, \"ipv6\")\n && !blockedAddresses.check(address, \"ipv6\");\n }\n\n return false;\n}\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {\n let timeout: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<T>((_, reject) => {\n timeout = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) {\n clearTimeout(timeout);\n }\n }\n}\n\n/**\n * Parses an HTTP(S) URL and rejects hostnames that resolve to local, private,\n * documentation, multicast, or otherwise non-public address space.\n */\nasync function resolvePublicHttpUrl(input: string | URL): Promise<ResolvedPublicHttpUrl> {\n const rawUrl = input instanceof URL ? input.href : input;\n if (typeof rawUrl !== \"string\" || rawUrl.length === 0 || rawUrl.length > MAX_URL_LENGTH) {\n throw new PublicUrlError(`URL must contain between 1 and ${MAX_URL_LENGTH} characters`);\n }\n\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n throw new PublicUrlError(\"URL must be an absolute HTTP or HTTPS URL\");\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new PublicUrlError(\"Only HTTP and HTTPS URLs are allowed\");\n }\n if (parsed.username || parsed.password) {\n throw new PublicUrlError(\"URLs containing credentials are not allowed\");\n }\n // URL normalizes matching defaults (HTTP :80 / HTTPS :443) to an empty port.\n // Any remaining port is custom or mismatched to the scheme and must be rejected.\n if (parsed.port) {\n throw new PublicUrlError(\"Only default HTTP and HTTPS ports are allowed\");\n }\n\n parsed.hash = \"\";\n const hostname = normalizeHostname(parsed.hostname);\n if (!hostname) {\n throw new PublicUrlError(\"URL hostname is required\");\n }\n if (\n blockedHostnames.has(hostname)\n || blockedHostnameSuffixes.some((suffix) => hostname.endsWith(suffix))\n ) {\n throw new PublicUrlError(`URL hostname \"${hostname}\" is not public`);\n }\n\n const literalFamily = isIP(hostname);\n const resolvedAddresses = literalFamily\n ? [{ address: hostname, family: literalFamily }]\n : await withTimeout(\n lookup(hostname, { all: true, verbatim: true }),\n DNS_TIMEOUT_MS,\n \"DNS lookup\",\n ).catch((error: unknown) => {\n throw new PublicUrlError(`Unable to resolve URL hostname: ${getErrorMessage(error)}`);\n });\n\n if (resolvedAddresses.length === 0) {\n throw new PublicUrlError(\"URL hostname did not resolve to an address\");\n }\n\n for (const record of resolvedAddresses) {\n if (!isPublicAddress(record.address, record.family)) {\n throw new PublicUrlError(`URL hostname resolves to non-public address ${record.address}`);\n }\n }\n\n return { url: parsed, addresses: resolvedAddresses };\n}\n\nexport async function assertPublicHttpUrl(input: string | URL): Promise<URL> {\n return (await resolvePublicHttpUrl(input)).url;\n}\n\nexport async function cancelResponseBody(response: Response): Promise<void> {\n if (!response.body) {\n return;\n }\n\n try {\n await response.body.cancel();\n } catch {\n // The stream may already be closed or consumed; there is nothing left to release.\n }\n}\n\nfunction getDeclaredCharacterEncoding(contentType: string | null): string | undefined {\n if (!contentType) {\n return undefined;\n }\n\n const match = /(?:^|;)\\s*charset\\s*=\\s*(?:\"([^\"]*)\"|([^;\\s]*))/i.exec(contentType);\n if (!match) {\n return undefined;\n }\n return (match[1] ?? match[2] ?? \"\").trim();\n}\n\nexport async function readResponseText(\n response: Response,\n maxBytes: number,\n encoding?: string,\n sniffHtmlEncoding = false,\n): Promise<string> {\n assertPositiveInteger(maxBytes, \"maxBytes\");\n\n const declaredLength = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {\n await cancelResponseBody(response);\n throw new Error(`Response exceeds the ${maxBytes}-byte limit`);\n }\n\n if (!response.body) {\n return \"\";\n }\n\n const reader = response.body.getReader();\n let totalBytes = 0;\n let text = \"\";\n let decoder: TextDecoder | undefined;\n let pendingChunks: Uint8Array[] = [];\n let pendingBytes = 0;\n\n const createDecoder = async (encodingLabel: string): Promise<TextDecoder> => {\n try {\n return new TextDecoder(encodingLabel);\n } catch {\n await reader.cancel();\n throw new Error(`Unsupported response character encoding \"${encodingLabel}\"`);\n }\n };\n\n const startDecoder = async (): Promise<void> => {\n if (decoder) return;\n let encodingLabel = encoding ?? \"utf-8\";\n if (encoding === undefined && sniffHtmlEncoding) {\n const sniffLength = Math.min(pendingBytes, HTML_ENCODING_SNIFF_BYTES);\n const sniffBytes = new Uint8Array(sniffLength);\n let copied = 0;\n for (const chunk of pendingChunks) {\n if (copied >= sniffLength) break;\n const length = Math.min(chunk.byteLength, sniffLength - copied);\n sniffBytes.set(chunk.subarray(0, length), copied);\n copied += length;\n }\n encodingLabel = getEncoding(sniffBytes, {\n maxBytes: HTML_ENCODING_SNIFF_BYTES,\n defaultEncoding: \"utf-8\",\n });\n }\n decoder = await createDecoder(encodingLabel);\n for (const chunk of pendingChunks) {\n text += decoder.decode(chunk, { stream: true });\n }\n pendingChunks = [];\n pendingBytes = 0;\n };\n\n if (encoding !== undefined || !sniffHtmlEncoding) {\n await startDecoder();\n }\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n totalBytes += value.byteLength;\n if (totalBytes > maxBytes) {\n await reader.cancel();\n throw new Error(`Response exceeds the ${maxBytes}-byte limit`);\n }\n\n if (!decoder) {\n pendingChunks.push(value);\n pendingBytes += value.byteLength;\n if (pendingBytes >= HTML_ENCODING_SNIFF_BYTES) {\n await startDecoder();\n }\n } else {\n text += decoder.decode(value, { stream: true });\n }\n }\n await startDecoder();\n if (!decoder) {\n throw new Error(\"Response decoder was not initialized\");\n }\n text += decoder.decode();\n return text;\n } finally {\n reader.releaseLock();\n }\n}\n\nfunction createPinnedDispatcher(\n resolvedAddresses: ReadonlyArray<{ address: string; family: number }>,\n): Agent {\n const addresses = resolvedAddresses.map((record) => ({ ...record }));\n\n return new Agent({\n // Every validated request gets its own non-reused connection so a later DNS\n // answer cannot replace the address set approved for this hop.\n pipelining: 0,\n connect: {\n lookup: (_hostname, options, callback) => {\n const requestedFamily = options.family === 4 || options.family === 6 ? options.family : 0;\n const candidates = requestedFamily\n ? addresses.filter((record) => record.family === requestedFamily)\n : addresses;\n\n if (candidates.length === 0) {\n const error = new Error(\"No validated address is available for the requested IP family\") as NodeJS.ErrnoException;\n error.code = \"ENOTFOUND\";\n callback(error, []);\n return;\n }\n\n if (options.all) {\n callback(null, candidates);\n return;\n }\n\n const selected = candidates[0];\n callback(null, selected.address, selected.family);\n },\n },\n });\n}\n\nfunction beginClosingDispatcher(dispatcher: Agent): void {\n // close() waits for the active response stream to finish, so starting it here\n // releases the request-scoped Agent without interrupting callers reading the body.\n void dispatcher.close().catch(() => {});\n}\n\n/** Fetches a public HTTP(S) URL while validating every redirect target. */\nexport async function fetchPublicHttp(\n input: string | URL,\n options: PublicFetchOptions = {},\n): Promise<PublicHttpResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n assertPositiveInteger(timeoutMs, \"timeoutMs\");\n if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0 || maxRedirects > 10) {\n throw new Error(\"maxRedirects must be an integer between 0 and 10\");\n }\n\n let currentTarget = await resolvePublicHttpUrl(input);\n for (let redirectCount = 0; ; redirectCount += 1) {\n const dispatcher = createPinnedDispatcher(currentTarget.addresses);\n let response: Response;\n try {\n response = await fetch(currentTarget.url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n redirect: \"manual\",\n signal: AbortSignal.timeout(timeoutMs),\n dispatcher,\n } as RequestInit & { dispatcher: Agent });\n } catch (error) {\n await dispatcher.close();\n throw error;\n }\n\n if (!redirectStatuses.has(response.status)) {\n beginClosingDispatcher(dispatcher);\n return { response, url: currentTarget.url };\n }\n\n const location = response.headers.get(\"location\");\n if (!location) {\n beginClosingDispatcher(dispatcher);\n return { response, url: currentTarget.url };\n }\n\n if (redirectCount >= maxRedirects) {\n beginClosingDispatcher(dispatcher);\n return { response, url: currentTarget.url };\n }\n\n await cancelResponseBody(response);\n await dispatcher.close();\n let redirectUrl: URL;\n try {\n redirectUrl = new URL(location, currentTarget.url);\n } catch {\n throw new PublicUrlError(\"Redirect target is not a valid URL\");\n }\n currentTarget = await resolvePublicHttpUrl(redirectUrl);\n }\n}\n\n/** Fetches bounded text from a public URL and rejects non-success responses. */\nexport async function fetchPublicText(\n input: string | URL,\n options: PublicTextOptions = {},\n): Promise<PublicTextResult> {\n const maxBytes = options.maxBytes ?? 2_000_000;\n assertPositiveInteger(maxBytes, \"maxBytes\");\n\n const { response, url } = await fetchPublicHttp(input, {\n method: \"GET\",\n headers: options.headers,\n timeoutMs: options.timeoutMs,\n maxRedirects: options.maxRedirects,\n });\n\n if (!response.ok) {\n const status = response.status;\n await cancelResponseBody(response);\n throw new Error(`URL returned HTTP status ${status}`);\n }\n\n const contentTypeHeader = response.headers.get(\"content-type\");\n const contentType = contentTypeHeader?.split(\";\", 1)[0]?.trim().toLowerCase() || null;\n if (\n options.acceptedContentTypes\n && !options.acceptedContentTypes.some((accepted) => accepted.trim().toLowerCase() === contentType)\n ) {\n await cancelResponseBody(response);\n throw new Error(`URL returned unsupported content type ${contentType ?? \"missing\"}`);\n }\n\n const declaredEncoding = getDeclaredCharacterEncoding(contentTypeHeader);\n return {\n text: await readResponseText(\n response,\n maxBytes,\n declaredEncoding,\n declaredEncoding === undefined && contentType === \"text/html\",\n ),\n url: url.href,\n status: response.status,\n contentType: contentTypeHeader,\n };\n}\n\n/** Reads a UTF-8 file only when it is a regular file within the requested cap. */\nexport async function readTextFile(filePath: string, maxBytes: number): Promise<string> {\n assertPositiveInteger(maxBytes, \"maxBytes\");\n const resolvedPath = path.resolve(filePath);\n const stats = await fs.stat(resolvedPath);\n if (!stats.isFile()) {\n throw new Error(`Path is not a regular file: ${resolvedPath}`);\n }\n if (stats.size > maxBytes) {\n throw new Error(`File exceeds the ${maxBytes}-byte limit: ${resolvedPath}`);\n }\n\n const contents = await fs.readFile(resolvedPath);\n if (contents.byteLength > maxBytes) {\n throw new Error(`File exceeds the ${maxBytes}-byte limit: ${resolvedPath}`);\n }\n return contents.toString(\"utf8\");\n}\n"]}
1
+ {"version":3,"file":"network.js","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"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","sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport * as fs from \"node:fs/promises\";\nimport { BlockList, isIP } from \"node:net\";\nimport * as path from \"node:path\";\nimport { getEncoding } from \"encoding-sniffer/sniffer\";\nimport { Agent } from \"undici\";\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_REDIRECTS = 5;\nconst DNS_TIMEOUT_MS = 5_000;\nconst MAX_URL_LENGTH = 8_192;\nconst HTML_ENCODING_SNIFF_BYTES = 1_024;\n\nconst blockedAddresses = new BlockList();\nfor (const [network, prefix] of [\n [\"0.0.0.0\", 8],\n [\"10.0.0.0\", 8],\n [\"100.64.0.0\", 10],\n [\"127.0.0.0\", 8],\n [\"169.254.0.0\", 16],\n [\"172.16.0.0\", 12],\n [\"192.0.0.0\", 24],\n [\"192.0.2.0\", 24],\n [\"192.88.99.0\", 24],\n [\"192.168.0.0\", 16],\n [\"198.18.0.0\", 15],\n [\"198.51.100.0\", 24],\n [\"203.0.113.0\", 24],\n [\"224.0.0.0\", 4],\n [\"240.0.0.0\", 4],\n] as const) {\n blockedAddresses.addSubnet(network, prefix, \"ipv4\");\n}\n\nfor (const [network, prefix] of [\n [\"::\", 96],\n [\"fc00::\", 7],\n [\"fe80::\", 10],\n [\"ff00::\", 8],\n [\"2001::\", 32],\n [\"2001:2::\", 48],\n [\"2001:10::\", 28],\n [\"2001:db8::\", 32],\n [\"2002::\", 16],\n] as const) {\n blockedAddresses.addSubnet(network, prefix, \"ipv6\");\n}\n\nconst publicIpv6Addresses = new BlockList();\npublicIpv6Addresses.addSubnet(\"2000::\", 3, \"ipv6\");\n\nconst blockedHostnames = new Set([\n \"localhost\",\n \"localhost.localdomain\",\n \"ip6-localhost\",\n \"ip6-loopback\",\n]);\n\nconst blockedHostnameSuffixes = [\n \".localhost\",\n \".local\",\n \".internal\",\n \".home.arpa\",\n \".test\",\n \".invalid\",\n \".example\",\n \".onion\",\n];\n\nconst redirectStatuses = new Set([301, 302, 303, 307, 308]);\n\nexport interface PublicFetchOptions {\n method?: \"GET\" | \"HEAD\";\n headers?: Record<string, string>;\n timeoutMs?: number;\n maxRedirects?: number;\n}\n\nexport interface PublicTextOptions extends PublicFetchOptions {\n maxBytes?: number;\n acceptedContentTypes?: readonly string[];\n}\n\nexport interface PublicTextResult {\n text: string;\n url: string;\n status: number;\n contentType: string | null;\n}\n\nexport interface PublicHttpResult {\n response: Response;\n url: URL;\n}\n\nexport interface ResolvedPublicHttpUrl {\n url: URL;\n addresses: Array<{ address: string; family: number }>;\n}\n\nexport class PublicUrlError extends Error {\n override readonly name = \"PublicUrlError\";\n}\n\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n if (error.name === \"TimeoutError\" || error.name === \"AbortError\") {\n return \"Request timed out\";\n }\n return error.message || error.name;\n }\n\n return typeof error === \"string\" && error.trim() ? error : \"Unknown error\";\n}\n\nfunction assertPositiveInteger(value: number, label: string): void {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n}\n\nfunction normalizeHostname(hostname: string): string {\n const withoutBrackets = hostname.startsWith(\"[\") && hostname.endsWith(\"]\")\n ? hostname.slice(1, -1)\n : hostname;\n return withoutBrackets.replace(/\\.$/, \"\").toLowerCase();\n}\n\nfunction isPublicAddress(address: string, family: number): boolean {\n if (family === 4) {\n return !blockedAddresses.check(address, \"ipv4\");\n }\n\n if (family === 6) {\n return publicIpv6Addresses.check(address, \"ipv6\")\n && !blockedAddresses.check(address, \"ipv6\");\n }\n\n return false;\n}\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {\n let timeout: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<T>((_, reject) => {\n timeout = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) {\n clearTimeout(timeout);\n }\n }\n}\n\n/**\n * Parses an HTTP(S) URL and rejects hostnames that resolve to local, private,\n * documentation, multicast, or otherwise non-public address space.\n */\nexport async function resolvePublicHttpUrl(input: string | URL): Promise<ResolvedPublicHttpUrl> {\n const rawUrl = input instanceof URL ? input.href : input;\n if (typeof rawUrl !== \"string\" || rawUrl.length === 0 || rawUrl.length > MAX_URL_LENGTH) {\n throw new PublicUrlError(`URL must contain between 1 and ${MAX_URL_LENGTH} characters`);\n }\n\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n throw new PublicUrlError(\"URL must be an absolute HTTP or HTTPS URL\");\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new PublicUrlError(\"Only HTTP and HTTPS URLs are allowed\");\n }\n if (parsed.username || parsed.password) {\n throw new PublicUrlError(\"URLs containing credentials are not allowed\");\n }\n // URL normalizes matching defaults (HTTP :80 / HTTPS :443) to an empty port.\n // Any remaining port is custom or mismatched to the scheme and must be rejected.\n if (parsed.port) {\n throw new PublicUrlError(\"Only default HTTP and HTTPS ports are allowed\");\n }\n\n parsed.hash = \"\";\n const hostname = normalizeHostname(parsed.hostname);\n if (!hostname) {\n throw new PublicUrlError(\"URL hostname is required\");\n }\n if (\n blockedHostnames.has(hostname)\n || blockedHostnameSuffixes.some((suffix) => hostname.endsWith(suffix))\n ) {\n throw new PublicUrlError(`URL hostname \"${hostname}\" is not public`);\n }\n\n const literalFamily = isIP(hostname);\n const resolvedAddresses = literalFamily\n ? [{ address: hostname, family: literalFamily }]\n : await withTimeout(\n lookup(hostname, { all: true, verbatim: true }),\n DNS_TIMEOUT_MS,\n \"DNS lookup\",\n ).catch((error: unknown) => {\n throw new PublicUrlError(`Unable to resolve URL hostname: ${getErrorMessage(error)}`);\n });\n\n if (resolvedAddresses.length === 0) {\n throw new PublicUrlError(\"URL hostname did not resolve to an address\");\n }\n\n for (const record of resolvedAddresses) {\n if (!isPublicAddress(record.address, record.family)) {\n throw new PublicUrlError(`URL hostname resolves to non-public address ${record.address}`);\n }\n }\n\n return { url: parsed, addresses: resolvedAddresses };\n}\n\nexport async function assertPublicHttpUrl(input: string | URL): Promise<URL> {\n return (await resolvePublicHttpUrl(input)).url;\n}\n\nexport async function cancelResponseBody(response: Response): Promise<void> {\n if (!response.body) {\n return;\n }\n\n try {\n await response.body.cancel();\n } catch {\n // The stream may already be closed or consumed; there is nothing left to release.\n }\n}\n\nfunction getDeclaredCharacterEncoding(contentType: string | null): string | undefined {\n if (!contentType) {\n return undefined;\n }\n\n const match = /(?:^|;)\\s*charset\\s*=\\s*(?:\"([^\"]*)\"|([^;\\s]*))/i.exec(contentType);\n if (!match) {\n return undefined;\n }\n return (match[1] ?? match[2] ?? \"\").trim();\n}\n\nexport async function readResponseText(\n response: Response,\n maxBytes: number,\n encoding?: string,\n sniffHtmlEncoding = false,\n): Promise<string> {\n assertPositiveInteger(maxBytes, \"maxBytes\");\n\n const declaredLength = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {\n await cancelResponseBody(response);\n throw new Error(`Response exceeds the ${maxBytes}-byte limit`);\n }\n\n if (!response.body) {\n return \"\";\n }\n\n const reader = response.body.getReader();\n let totalBytes = 0;\n let text = \"\";\n let decoder: TextDecoder | undefined;\n let pendingChunks: Uint8Array[] = [];\n let pendingBytes = 0;\n\n const createDecoder = async (encodingLabel: string): Promise<TextDecoder> => {\n try {\n return new TextDecoder(encodingLabel);\n } catch {\n await reader.cancel();\n throw new Error(`Unsupported response character encoding \"${encodingLabel}\"`);\n }\n };\n\n const startDecoder = async (): Promise<void> => {\n if (decoder) return;\n let encodingLabel = encoding ?? \"utf-8\";\n if (encoding === undefined && sniffHtmlEncoding) {\n const sniffLength = Math.min(pendingBytes, HTML_ENCODING_SNIFF_BYTES);\n const sniffBytes = new Uint8Array(sniffLength);\n let copied = 0;\n for (const chunk of pendingChunks) {\n if (copied >= sniffLength) break;\n const length = Math.min(chunk.byteLength, sniffLength - copied);\n sniffBytes.set(chunk.subarray(0, length), copied);\n copied += length;\n }\n encodingLabel = getEncoding(sniffBytes, {\n maxBytes: HTML_ENCODING_SNIFF_BYTES,\n defaultEncoding: \"utf-8\",\n });\n }\n decoder = await createDecoder(encodingLabel);\n for (const chunk of pendingChunks) {\n text += decoder.decode(chunk, { stream: true });\n }\n pendingChunks = [];\n pendingBytes = 0;\n };\n\n if (encoding !== undefined || !sniffHtmlEncoding) {\n await startDecoder();\n }\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n totalBytes += value.byteLength;\n if (totalBytes > maxBytes) {\n await reader.cancel();\n throw new Error(`Response exceeds the ${maxBytes}-byte limit`);\n }\n\n if (!decoder) {\n pendingChunks.push(value);\n pendingBytes += value.byteLength;\n if (pendingBytes >= HTML_ENCODING_SNIFF_BYTES) {\n await startDecoder();\n }\n } else {\n text += decoder.decode(value, { stream: true });\n }\n }\n await startDecoder();\n if (!decoder) {\n throw new Error(\"Response decoder was not initialized\");\n }\n text += decoder.decode();\n return text;\n } finally {\n reader.releaseLock();\n }\n}\n\nfunction createPinnedDispatcher(\n resolvedAddresses: ReadonlyArray<{ address: string; family: number }>,\n): Agent {\n const addresses = resolvedAddresses.map((record) => ({ ...record }));\n\n return new Agent({\n // Every validated request gets its own non-reused connection so a later DNS\n // answer cannot replace the address set approved for this hop.\n pipelining: 0,\n connect: {\n lookup: (_hostname, options, callback) => {\n const requestedFamily = options.family === 4 || options.family === 6 ? options.family : 0;\n const candidates = requestedFamily\n ? addresses.filter((record) => record.family === requestedFamily)\n : addresses;\n\n if (candidates.length === 0) {\n const error = new Error(\"No validated address is available for the requested IP family\") as NodeJS.ErrnoException;\n error.code = \"ENOTFOUND\";\n callback(error, []);\n return;\n }\n\n if (options.all) {\n callback(null, candidates);\n return;\n }\n\n const selected = candidates[0];\n callback(null, selected.address, selected.family);\n },\n },\n });\n}\n\nfunction beginClosingDispatcher(dispatcher: Agent): void {\n // close() waits for the active response stream to finish, so starting it here\n // releases the request-scoped Agent without interrupting callers reading the body.\n void dispatcher.close().catch(() => {});\n}\n\n/** Fetches a public HTTP(S) URL while validating every redirect target. */\nexport async function fetchPublicHttp(\n input: string | URL,\n options: PublicFetchOptions = {},\n): Promise<PublicHttpResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n assertPositiveInteger(timeoutMs, \"timeoutMs\");\n if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0 || maxRedirects > 10) {\n throw new Error(\"maxRedirects must be an integer between 0 and 10\");\n }\n\n let currentTarget = await resolvePublicHttpUrl(input);\n for (let redirectCount = 0; ; redirectCount += 1) {\n const dispatcher = createPinnedDispatcher(currentTarget.addresses);\n let response: Response;\n try {\n response = await fetch(currentTarget.url, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n redirect: \"manual\",\n signal: AbortSignal.timeout(timeoutMs),\n dispatcher,\n } as RequestInit & { dispatcher: Agent });\n } catch (error) {\n await dispatcher.close();\n throw error;\n }\n\n if (!redirectStatuses.has(response.status)) {\n beginClosingDispatcher(dispatcher);\n return { response, url: currentTarget.url };\n }\n\n const location = response.headers.get(\"location\");\n if (!location) {\n beginClosingDispatcher(dispatcher);\n return { response, url: currentTarget.url };\n }\n\n if (redirectCount >= maxRedirects) {\n beginClosingDispatcher(dispatcher);\n return { response, url: currentTarget.url };\n }\n\n await cancelResponseBody(response);\n await dispatcher.close();\n let redirectUrl: URL;\n try {\n redirectUrl = new URL(location, currentTarget.url);\n } catch {\n throw new PublicUrlError(\"Redirect target is not a valid URL\");\n }\n currentTarget = await resolvePublicHttpUrl(redirectUrl);\n }\n}\n\n/** Fetches bounded text from a public URL and rejects non-success responses. */\nexport async function fetchPublicText(\n input: string | URL,\n options: PublicTextOptions = {},\n): Promise<PublicTextResult> {\n const maxBytes = options.maxBytes ?? 2_000_000;\n assertPositiveInteger(maxBytes, \"maxBytes\");\n\n const { response, url } = await fetchPublicHttp(input, {\n method: \"GET\",\n headers: options.headers,\n timeoutMs: options.timeoutMs,\n maxRedirects: options.maxRedirects,\n });\n\n if (!response.ok) {\n const status = response.status;\n await cancelResponseBody(response);\n throw new Error(`URL returned HTTP status ${status}`);\n }\n\n const contentTypeHeader = response.headers.get(\"content-type\");\n const contentType = contentTypeHeader?.split(\";\", 1)[0]?.trim().toLowerCase() || null;\n if (\n options.acceptedContentTypes\n && !options.acceptedContentTypes.some((accepted) => accepted.trim().toLowerCase() === contentType)\n ) {\n await cancelResponseBody(response);\n throw new Error(`URL returned unsupported content type ${contentType ?? \"missing\"}`);\n }\n\n const declaredEncoding = getDeclaredCharacterEncoding(contentTypeHeader);\n return {\n text: await readResponseText(\n response,\n maxBytes,\n declaredEncoding,\n declaredEncoding === undefined && contentType === \"text/html\",\n ),\n url: url.href,\n status: response.status,\n contentType: contentTypeHeader,\n };\n}\n\n/** Reads a UTF-8 file only when it is a regular file within the requested cap. */\nexport async function readTextFile(filePath: string, maxBytes: number): Promise<string> {\n assertPositiveInteger(maxBytes, \"maxBytes\");\n const resolvedPath = path.resolve(filePath);\n const stats = await fs.stat(resolvedPath);\n if (!stats.isFile()) {\n throw new Error(`Path is not a regular file: ${resolvedPath}`);\n }\n if (stats.size > maxBytes) {\n throw new Error(`File exceeds the ${maxBytes}-byte limit: ${resolvedPath}`);\n }\n\n const contents = await fs.readFile(resolvedPath);\n if (contents.byteLength > maxBytes) {\n throw new Error(`File exceeds the ${maxBytes}-byte limit: ${resolvedPath}`);\n }\n return contents.toString(\"utf8\");\n}\n"]}
@@ -0,0 +1,5 @@
1
+ export interface ScreenshotTransportProxy {
2
+ url: string;
3
+ close(): Promise<void>;
4
+ }
5
+ export declare function startScreenshotTransportProxy(onBlocked: (message: string) => void): Promise<ScreenshotTransportProxy>;
@@ -0,0 +1,226 @@
1
+ import * as http from "node:http";
2
+ import * as https from "node:https";
3
+ import * as net from "node:net";
4
+ import { getErrorMessage, resolvePublicHttpUrl, } from "./network.js";
5
+ const PROXY_CONNECT_TIMEOUT_MS = 5_000;
6
+ const LOOPBACK_HOST = "127.0.0.1";
7
+ function createPinnedLookup(addresses) {
8
+ const pinned = addresses.map((record) => ({ ...record }));
9
+ return (_hostname, options, callback) => {
10
+ const requestedFamily = options.family === 4 || options.family === 6 ? options.family : 0;
11
+ const candidates = requestedFamily
12
+ ? pinned.filter((record) => record.family === requestedFamily)
13
+ : pinned;
14
+ if (candidates.length === 0) {
15
+ const error = new Error("No validated address is available for the requested IP family");
16
+ error.code = "ENOTFOUND";
17
+ callback(error, []);
18
+ return;
19
+ }
20
+ if (options.all) {
21
+ callback(null, candidates);
22
+ return;
23
+ }
24
+ const selected = candidates[0];
25
+ callback(null, selected.address, selected.family);
26
+ };
27
+ }
28
+ async function connectPinnedTcp(addresses, port) {
29
+ let lastError;
30
+ for (const record of addresses) {
31
+ try {
32
+ return await new Promise((resolve, reject) => {
33
+ const socket = net.connect({
34
+ host: record.address,
35
+ port,
36
+ family: record.family === 6 ? 6 : 4,
37
+ });
38
+ let settled = false;
39
+ let timer;
40
+ const finish = (error) => {
41
+ if (settled)
42
+ return;
43
+ settled = true;
44
+ if (timer)
45
+ clearTimeout(timer);
46
+ socket.off("error", onError);
47
+ if (error) {
48
+ socket.destroy();
49
+ reject(error);
50
+ }
51
+ else {
52
+ resolve(socket);
53
+ }
54
+ };
55
+ const onError = (error) => finish(error);
56
+ timer = setTimeout(() => finish(new Error("Pinned screenshot proxy connection timed out")), PROXY_CONNECT_TIMEOUT_MS);
57
+ socket.once("error", onError);
58
+ socket.once("connect", () => finish());
59
+ });
60
+ }
61
+ catch (error) {
62
+ lastError = error;
63
+ }
64
+ }
65
+ throw lastError ?? new Error("No validated address was available for the screenshot proxy connection");
66
+ }
67
+ function sendProxyFailure(response) {
68
+ if (!response.headersSent) {
69
+ response.writeHead(502, {
70
+ Connection: "close",
71
+ "Content-Length": "0",
72
+ });
73
+ }
74
+ response.end();
75
+ }
76
+ function sendTunnelFailure(socket) {
77
+ if (socket.destroyed)
78
+ return;
79
+ socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
80
+ }
81
+ function proxyRequestUrl(request) {
82
+ const requestUrl = request.url?.trim();
83
+ if (!requestUrl) {
84
+ throw new Error("Blocked empty screenshot proxy request URL");
85
+ }
86
+ if (/^https?:\/\//i.test(requestUrl)) {
87
+ return requestUrl;
88
+ }
89
+ const host = request.headers.host?.trim();
90
+ if (!host) {
91
+ throw new Error("Blocked screenshot proxy request without a Host header");
92
+ }
93
+ return `http://${host}${requestUrl.startsWith("/") ? requestUrl : `/${requestUrl}`}`;
94
+ }
95
+ async function forwardHttpRequest(request, response, onBlocked) {
96
+ let target;
97
+ try {
98
+ target = await resolvePublicHttpUrl(proxyRequestUrl(request));
99
+ }
100
+ catch (error) {
101
+ onBlocked(getErrorMessage(error));
102
+ sendProxyFailure(response);
103
+ return;
104
+ }
105
+ const headers = { ...request.headers };
106
+ delete headers["proxy-authorization"];
107
+ delete headers["proxy-connection"];
108
+ headers.host = target.url.host;
109
+ const handleResponse = (upstreamResponse) => {
110
+ response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
111
+ upstreamResponse.pipe(response);
112
+ };
113
+ const requestOptions = {
114
+ hostname: target.url.hostname,
115
+ port: target.url.protocol === "https:" ? 443 : 80,
116
+ method: request.method,
117
+ path: `${target.url.pathname}${target.url.search}`,
118
+ headers,
119
+ lookup: createPinnedLookup(target.addresses),
120
+ agent: false,
121
+ };
122
+ let upstreamRequest;
123
+ if (target.url.protocol === "https:") {
124
+ const httpsOptions = {
125
+ ...requestOptions,
126
+ servername: target.url.hostname,
127
+ };
128
+ upstreamRequest = https.request(httpsOptions, handleResponse);
129
+ }
130
+ else {
131
+ upstreamRequest = http.request(requestOptions, handleResponse);
132
+ }
133
+ upstreamRequest.once("error", () => sendProxyFailure(response));
134
+ request.once("aborted", () => upstreamRequest.destroy());
135
+ request.pipe(upstreamRequest);
136
+ }
137
+ async function forwardConnect(request, clientSocket, head, onBlocked, trackUpstream) {
138
+ const authority = request.url?.trim();
139
+ if (!authority) {
140
+ onBlocked("Blocked empty screenshot proxy tunnel target");
141
+ sendTunnelFailure(clientSocket);
142
+ return;
143
+ }
144
+ let target;
145
+ try {
146
+ target = await resolvePublicHttpUrl(`https://${authority}/`);
147
+ }
148
+ catch (error) {
149
+ onBlocked(getErrorMessage(error));
150
+ sendTunnelFailure(clientSocket);
151
+ return;
152
+ }
153
+ let upstreamSocket;
154
+ try {
155
+ upstreamSocket = await connectPinnedTcp(target.addresses, 443);
156
+ }
157
+ catch {
158
+ sendTunnelFailure(clientSocket);
159
+ return;
160
+ }
161
+ trackUpstream(upstreamSocket);
162
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
163
+ if (head.length > 0)
164
+ upstreamSocket.write(head);
165
+ clientSocket.once("error", () => upstreamSocket.destroy());
166
+ upstreamSocket.once("error", () => clientSocket.destroy());
167
+ clientSocket.once("close", () => upstreamSocket.destroy());
168
+ upstreamSocket.once("close", () => clientSocket.destroy());
169
+ clientSocket.pipe(upstreamSocket);
170
+ upstreamSocket.pipe(clientSocket);
171
+ }
172
+ export async function startScreenshotTransportProxy(onBlocked) {
173
+ const clientSockets = new Set();
174
+ const upstreamSockets = new Set();
175
+ const server = http.createServer((request, response) => {
176
+ void forwardHttpRequest(request, response, onBlocked).catch(() => {
177
+ sendProxyFailure(response);
178
+ });
179
+ });
180
+ server.on("connection", (socket) => {
181
+ clientSockets.add(socket);
182
+ socket.once("close", () => clientSockets.delete(socket));
183
+ });
184
+ server.on("connect", (request, socket, head) => {
185
+ void forwardConnect(request, socket, head, onBlocked, (upstreamSocket) => {
186
+ upstreamSockets.add(upstreamSocket);
187
+ upstreamSocket.once("close", () => upstreamSockets.delete(upstreamSocket));
188
+ }).catch(() => sendTunnelFailure(socket));
189
+ });
190
+ server.on("clientError", (_error, socket) => {
191
+ if (!socket.destroyed) {
192
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
193
+ }
194
+ });
195
+ await new Promise((resolve, reject) => {
196
+ const onError = (error) => reject(error);
197
+ server.once("error", onError);
198
+ server.listen(0, LOOPBACK_HOST, () => {
199
+ server.off("error", onError);
200
+ resolve();
201
+ });
202
+ });
203
+ const address = server.address();
204
+ if (!address || typeof address === "string") {
205
+ server.close();
206
+ throw new Error("Screenshot transport proxy did not expose a loopback TCP address");
207
+ }
208
+ let closed = false;
209
+ return {
210
+ url: `http://${LOOPBACK_HOST}:${address.port}`,
211
+ async close() {
212
+ if (closed)
213
+ return;
214
+ closed = true;
215
+ const closePromise = new Promise((resolve) => {
216
+ server.close(() => resolve());
217
+ });
218
+ for (const socket of clientSockets)
219
+ socket.destroy();
220
+ for (const socket of upstreamSockets)
221
+ socket.destroy();
222
+ await closePromise;
223
+ },
224
+ };
225
+ }
226
+ //# sourceMappingURL=screenshot-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-proxy.js","sourceRoot":"","sources":["../src/screenshot-proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as net from \"node:net\";\nimport {\n getErrorMessage,\n resolvePublicHttpUrl,\n type ResolvedPublicHttpUrl,\n} from \"./network.js\";\n\nconst PROXY_CONNECT_TIMEOUT_MS = 5_000;\nconst LOOPBACK_HOST = \"127.0.0.1\";\n\ntype ResolvedAddress = ResolvedPublicHttpUrl[\"addresses\"][number];\n\nexport interface ScreenshotTransportProxy {\n url: string;\n close(): Promise<void>;\n}\n\nfunction createPinnedLookup(addresses: readonly ResolvedAddress[]): net.LookupFunction {\n const pinned = addresses.map((record) => ({ ...record }));\n return (_hostname, options, callback): void => {\n const requestedFamily = options.family === 4 || options.family === 6 ? options.family : 0;\n const candidates = requestedFamily\n ? pinned.filter((record) => record.family === requestedFamily)\n : pinned;\n if (candidates.length === 0) {\n const error = new Error(\"No validated address is available for the requested IP family\") as NodeJS.ErrnoException;\n error.code = \"ENOTFOUND\";\n callback(error, []);\n return;\n }\n if (options.all) {\n callback(null, candidates);\n return;\n }\n const selected = candidates[0];\n callback(null, selected.address, selected.family);\n };\n}\n\nasync function connectPinnedTcp(\n addresses: readonly ResolvedAddress[],\n port: number,\n): Promise<net.Socket> {\n let lastError: unknown;\n for (const record of addresses) {\n try {\n return await new Promise<net.Socket>((resolve, reject) => {\n const socket = net.connect({\n host: record.address,\n port,\n family: record.family === 6 ? 6 : 4,\n });\n let settled = false;\n let timer: NodeJS.Timeout | undefined;\n const finish = (error?: unknown): void => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n socket.off(\"error\", onError);\n if (error) {\n socket.destroy();\n reject(error);\n } else {\n resolve(socket);\n }\n };\n const onError = (error: Error): void => finish(error);\n timer = setTimeout(\n () => finish(new Error(\"Pinned screenshot proxy connection timed out\")),\n PROXY_CONNECT_TIMEOUT_MS,\n );\n socket.once(\"error\", onError);\n socket.once(\"connect\", () => finish());\n });\n } catch (error) {\n lastError = error;\n }\n }\n throw lastError ?? new Error(\"No validated address was available for the screenshot proxy connection\");\n}\n\nfunction sendProxyFailure(response: http.ServerResponse): void {\n if (!response.headersSent) {\n response.writeHead(502, {\n Connection: \"close\",\n \"Content-Length\": \"0\",\n });\n }\n response.end();\n}\n\nfunction sendTunnelFailure(socket: net.Socket): void {\n if (socket.destroyed) return;\n socket.end(\n \"HTTP/1.1 502 Bad Gateway\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n\",\n );\n}\n\nfunction proxyRequestUrl(request: http.IncomingMessage): string {\n const requestUrl = request.url?.trim();\n if (!requestUrl) {\n throw new Error(\"Blocked empty screenshot proxy request URL\");\n }\n if (/^https?:\\/\\//i.test(requestUrl)) {\n return requestUrl;\n }\n const host = request.headers.host?.trim();\n if (!host) {\n throw new Error(\"Blocked screenshot proxy request without a Host header\");\n }\n return `http://${host}${requestUrl.startsWith(\"/\") ? requestUrl : `/${requestUrl}`}`;\n}\n\nasync function forwardHttpRequest(\n request: http.IncomingMessage,\n response: http.ServerResponse,\n onBlocked: (message: string) => void,\n): Promise<void> {\n let target: ResolvedPublicHttpUrl;\n try {\n target = await resolvePublicHttpUrl(proxyRequestUrl(request));\n } catch (error) {\n onBlocked(getErrorMessage(error));\n sendProxyFailure(response);\n return;\n }\n\n const headers: http.OutgoingHttpHeaders = { ...request.headers };\n delete headers[\"proxy-authorization\"];\n delete headers[\"proxy-connection\"];\n headers.host = target.url.host;\n\n const handleResponse = (upstreamResponse: http.IncomingMessage): void => {\n response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);\n upstreamResponse.pipe(response);\n };\n const requestOptions: http.RequestOptions = {\n hostname: target.url.hostname,\n port: target.url.protocol === \"https:\" ? 443 : 80,\n method: request.method,\n path: `${target.url.pathname}${target.url.search}`,\n headers,\n lookup: createPinnedLookup(target.addresses),\n agent: false,\n };\n let upstreamRequest: http.ClientRequest;\n if (target.url.protocol === \"https:\") {\n const httpsOptions: https.RequestOptions = {\n ...requestOptions,\n servername: target.url.hostname,\n };\n upstreamRequest = https.request(httpsOptions, handleResponse);\n } else {\n upstreamRequest = http.request(requestOptions, handleResponse);\n }\n\n upstreamRequest.once(\"error\", () => sendProxyFailure(response));\n request.once(\"aborted\", () => upstreamRequest.destroy());\n request.pipe(upstreamRequest);\n}\n\nasync function forwardConnect(\n request: http.IncomingMessage,\n clientSocket: net.Socket,\n head: Buffer,\n onBlocked: (message: string) => void,\n trackUpstream: (socket: net.Socket) => void,\n): Promise<void> {\n const authority = request.url?.trim();\n if (!authority) {\n onBlocked(\"Blocked empty screenshot proxy tunnel target\");\n sendTunnelFailure(clientSocket);\n return;\n }\n\n let target: ResolvedPublicHttpUrl;\n try {\n target = await resolvePublicHttpUrl(`https://${authority}/`);\n } catch (error) {\n onBlocked(getErrorMessage(error));\n sendTunnelFailure(clientSocket);\n return;\n }\n\n let upstreamSocket: net.Socket;\n try {\n upstreamSocket = await connectPinnedTcp(target.addresses, 443);\n } catch {\n sendTunnelFailure(clientSocket);\n return;\n }\n trackUpstream(upstreamSocket);\n\n clientSocket.write(\"HTTP/1.1 200 Connection Established\\r\\n\\r\\n\");\n if (head.length > 0) upstreamSocket.write(head);\n\n clientSocket.once(\"error\", () => upstreamSocket.destroy());\n upstreamSocket.once(\"error\", () => clientSocket.destroy());\n clientSocket.once(\"close\", () => upstreamSocket.destroy());\n upstreamSocket.once(\"close\", () => clientSocket.destroy());\n clientSocket.pipe(upstreamSocket);\n upstreamSocket.pipe(clientSocket);\n}\n\nexport async function startScreenshotTransportProxy(\n onBlocked: (message: string) => void,\n): Promise<ScreenshotTransportProxy> {\n const clientSockets = new Set<net.Socket>();\n const upstreamSockets = new Set<net.Socket>();\n const server = http.createServer((request, response) => {\n void forwardHttpRequest(request, response, onBlocked).catch(() => {\n sendProxyFailure(response);\n });\n });\n\n server.on(\"connection\", (socket) => {\n clientSockets.add(socket);\n socket.once(\"close\", () => clientSockets.delete(socket));\n });\n server.on(\"connect\", (request, socket, head) => {\n void forwardConnect(\n request,\n socket as net.Socket,\n head,\n onBlocked,\n (upstreamSocket) => {\n upstreamSockets.add(upstreamSocket);\n upstreamSocket.once(\"close\", () => upstreamSockets.delete(upstreamSocket));\n },\n ).catch(() => sendTunnelFailure(socket as net.Socket));\n });\n server.on(\"clientError\", (_error, socket) => {\n if (!socket.destroyed) {\n socket.end(\"HTTP/1.1 400 Bad Request\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n\");\n }\n });\n\n await new Promise<void>((resolve, reject) => {\n const onError = (error: Error): void => reject(error);\n server.once(\"error\", onError);\n server.listen(0, LOOPBACK_HOST, () => {\n server.off(\"error\", onError);\n resolve();\n });\n });\n\n const address = server.address();\n if (!address || typeof address === \"string\") {\n server.close();\n throw new Error(\"Screenshot transport proxy did not expose a loopback TCP address\");\n }\n\n let closed = false;\n return {\n url: `http://${LOOPBACK_HOST}:${address.port}`,\n async close(): Promise<void> {\n if (closed) return;\n closed = true;\n const closePromise = new Promise<void>((resolve) => {\n server.close(() => resolve());\n });\n for (const socket of clientSockets) socket.destroy();\n for (const socket of upstreamSockets) socket.destroy();\n await closePromise;\n },\n };\n}\n"]}
@@ -19,7 +19,7 @@ type BrowserInstaller = (options: InstallOptions & {
19
19
  executablePath: string;
20
20
  }>;
21
21
  export interface ScreenshotBrowserRuntime {
22
- launch(): Promise<PuppeteerBrowser>;
22
+ launch(proxyServer: string): Promise<PuppeteerBrowser>;
23
23
  }
24
24
  export declare function resolveScreenshotBrowserExecutable(options?: {
25
25
  cacheDir?: string;
@@ -5,6 +5,7 @@ import * as fs from "fs/promises";
5
5
  import * as os from "node:os";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { assertPublicHttpUrl, getErrorMessage } from "./network.js";
8
+ import { startScreenshotTransportProxy } from "./screenshot-proxy.js";
8
9
  const DEFAULT_VIEWPORTS = [
9
10
  { name: "desktop", width: 1440, height: 900 },
10
11
  { name: "tablet", width: 768, height: 1024 },
@@ -54,7 +55,7 @@ export async function resolveScreenshotBrowserExecutable(options = {}) {
54
55
  }
55
56
  let screenshotBrowserExecutablePromise;
56
57
  const defaultScreenshotBrowserRuntime = {
57
- async launch() {
58
+ async launch(proxyServer) {
58
59
  screenshotBrowserExecutablePromise ??= resolveScreenshotBrowserExecutable();
59
60
  let executablePath;
60
61
  try {
@@ -67,6 +68,10 @@ const defaultScreenshotBrowserRuntime = {
67
68
  return puppeteer.launch({
68
69
  executablePath,
69
70
  headless: SCREENSHOT_HEADLESS_MODE,
71
+ args: [
72
+ `--proxy-server=${proxyServer}`,
73
+ "--proxy-bypass-list=<-loopback>",
74
+ ],
70
75
  });
71
76
  },
72
77
  };
@@ -197,12 +202,16 @@ export async function captureScreenshots(targetPath, outputDirectory, customView
197
202
  }
198
203
  // Ensure output directory exists
199
204
  await fs.mkdir(resolvedOutputDirectory, { recursive: true });
200
- const browser = await browserRuntime.launch();
201
205
  const results = [];
206
+ let blockedRequestMessage;
207
+ const transportProxy = await startScreenshotTransportProxy((message) => {
208
+ blockedRequestMessage ??= message;
209
+ });
210
+ let browser;
202
211
  try {
212
+ browser = await browserRuntime.launch(transportProxy.url);
203
213
  const page = await browser.newPage();
204
214
  page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS);
205
- let blockedRequestMessage;
206
215
  await page.setRequestInterception(true);
207
216
  page.on("request", (request) => {
208
217
  void resolveScreenshotRequest(request, localDirectory, (message) => {
@@ -247,7 +256,10 @@ export async function captureScreenshots(targetPath, outputDirectory, customView
247
256
  }
248
257
  }
249
258
  finally {
250
- await browser.close();
259
+ if (browser) {
260
+ await browser.close();
261
+ }
262
+ await transportProxy.close();
251
263
  }
252
264
  return results;
253
265
  }
@@ -1 +1 @@
1
- {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../src/screenshot.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Browser as ManagedBrowser,\n computeExecutablePath,\n detectBrowserPlatform,\n install as installBrowser,\n type BrowserPlatform,\n type InstallOptions,\n} from \"@puppeteer/browsers\";\nimport puppeteer, { type Browser as PuppeteerBrowser } from \"puppeteer-core\";\nimport * as path from \"path\";\nimport * as fs from \"fs/promises\";\nimport * as os from \"node:os\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { assertPublicHttpUrl, getErrorMessage } from \"./network.js\";\n\nexport interface ViewportConfig {\n name: string;\n width: number;\n height: number;\n}\n\nexport interface ScreenshotResult {\n viewportName: string;\n width: number;\n height: number;\n outputPath: string;\n}\n\nconst DEFAULT_VIEWPORTS: ViewportConfig[] = [\n { name: \"desktop\", width: 1440, height: 900 },\n { name: \"tablet\", width: 768, height: 1024 },\n { name: \"mobile\", width: 375, height: 812 }\n];\n\nconst MAX_VIEWPORTS = 10;\nconst MIN_VIEWPORT_DIMENSION = 100;\nconst MAX_VIEWPORT_DIMENSION = 7_680;\nconst SAFE_VIEWPORT_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\nconst NAVIGATION_TIMEOUT_MS = 30_000;\n\nexport const SCREENSHOT_HEADLESS_MODE = \"shell\" as const;\nexport const SCREENSHOT_BROWSER_BUILD_ID = \"152.0.7977.42\";\n\ntype BrowserInstaller = (options: InstallOptions & { unpack?: true }) => Promise<{ executablePath: string }>;\n\nexport interface ScreenshotBrowserRuntime {\n launch(): Promise<PuppeteerBrowser>;\n}\n\nexport async function resolveScreenshotBrowserExecutable(options: {\n cacheDir?: string;\n platform?: BrowserPlatform;\n installer?: BrowserInstaller;\n} = {}): Promise<string> {\n const platform = options.platform ?? detectBrowserPlatform();\n if (!platform) {\n throw new Error(\"Screenshot capture is not supported on this operating system\");\n }\n\n const cacheDir = options.cacheDir\n ?? process.env.PUPPETEER_CACHE_DIR\n ?? path.join(os.homedir(), \".cache\", \"puppeteer\");\n const browser = ManagedBrowser.CHROMEHEADLESSSHELL;\n const executablePath = computeExecutablePath({\n browser,\n buildId: SCREENSHOT_BROWSER_BUILD_ID,\n cacheDir,\n platform,\n });\n\n try {\n await fs.access(executablePath);\n return executablePath;\n } catch {\n // First screenshot use installs only the verified headless shell, not full Chrome.\n }\n\n try {\n const installed = await (options.installer ?? installBrowser)({\n browser,\n buildId: SCREENSHOT_BROWSER_BUILD_ID,\n cacheDir,\n platform,\n });\n return installed.executablePath;\n } catch (cause: unknown) {\n throw new Error(\n `Screenshot browser setup failed: ${getErrorMessage(cause)}`,\n { cause },\n );\n }\n}\n\nlet screenshotBrowserExecutablePromise: Promise<string> | undefined;\n\nconst defaultScreenshotBrowserRuntime: ScreenshotBrowserRuntime = {\n async launch(): Promise<PuppeteerBrowser> {\n screenshotBrowserExecutablePromise ??= resolveScreenshotBrowserExecutable();\n let executablePath: string;\n try {\n executablePath = await screenshotBrowserExecutablePromise;\n } catch (cause) {\n screenshotBrowserExecutablePromise = undefined;\n throw cause;\n }\n return puppeteer.launch({\n executablePath,\n headless: SCREENSHOT_HEADLESS_MODE,\n });\n },\n};\n\ninterface InterceptedRequest {\n url(): string;\n isInterceptResolutionHandled(): boolean;\n continue(): Promise<unknown>;\n abort(errorCode?: string): Promise<unknown>;\n}\n\nfunction validateViewports(viewports: ViewportConfig[]): ViewportConfig[] {\n if (viewports.length === 0 || viewports.length > MAX_VIEWPORTS) {\n throw new Error(`Between 1 and ${MAX_VIEWPORTS} viewports are required`);\n }\n\n const outputNames = new Set<string>();\n return viewports.map((viewport, index) => {\n if (!viewport || typeof viewport !== \"object\") {\n throw new Error(`Viewport #${index + 1} must be an object`);\n }\n if (typeof viewport.name !== \"string\" || !SAFE_VIEWPORT_NAME.test(viewport.name)) {\n throw new Error(\n `Viewport #${index + 1} name must match ${SAFE_VIEWPORT_NAME.source}`,\n );\n }\n for (const [label, value] of [[\"width\", viewport.width], [\"height\", viewport.height]] as const) {\n if (\n !Number.isSafeInteger(value)\n || value < MIN_VIEWPORT_DIMENSION\n || value > MAX_VIEWPORT_DIMENSION\n ) {\n throw new Error(\n `Viewport #${index + 1} ${label} must be an integer between ${MIN_VIEWPORT_DIMENSION} and ${MAX_VIEWPORT_DIMENSION}`,\n );\n }\n }\n\n const outputName = `${viewport.name}_${viewport.width}x${viewport.height}.png`.toLowerCase();\n if (outputNames.has(outputName)) {\n throw new Error(`Duplicate viewport output filename: ${outputName}`);\n }\n outputNames.add(outputName);\n return { name: viewport.name, width: viewport.width, height: viewport.height };\n });\n}\n\nfunction resolveContainedOutputPath(outputDirectory: string, fileName: string): string {\n const outputPath = path.resolve(outputDirectory, fileName);\n const relativePath = path.relative(outputDirectory, outputPath);\n if (relativePath.startsWith(\"..\") || path.isAbsolute(relativePath)) {\n throw new Error(\"Screenshot output path escapes the requested output directory\");\n }\n return outputPath;\n}\n\nfunction isPathWithinDirectory(directory: string, filePath: string): boolean {\n const relativePath = path.relative(directory, filePath);\n return relativePath === \"\" || (!relativePath.startsWith(\"..\") && !path.isAbsolute(relativePath));\n}\n\nasync function assertLocalScreenshotRequestAllowed(requestUrl: URL, localDirectory: string): Promise<void> {\n if (requestUrl.hostname) {\n throw new Error(\"Blocked local file request with a host name\");\n }\n\n let resolvedPath: string;\n try {\n resolvedPath = await fs.realpath(fileURLToPath(requestUrl));\n } catch (error: unknown) {\n throw new Error(`Blocked local file request: ${getErrorMessage(error)}`);\n }\n\n if (!isPathWithinDirectory(localDirectory, resolvedPath)) {\n throw new Error(\"Blocked local file request outside the selected file directory\");\n }\n}\n\nasync function assertScreenshotRequestAllowed(requestUrl: string, localDirectory?: string): Promise<void> {\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(requestUrl);\n } catch {\n throw new Error(\"Blocked invalid page request URL\");\n }\n\n if (parsedUrl.protocol === \"http:\" || parsedUrl.protocol === \"https:\") {\n await assertPublicHttpUrl(parsedUrl);\n return;\n }\n\n if (parsedUrl.protocol === \"data:\" || parsedUrl.protocol === \"blob:\") {\n return;\n }\n\n if (parsedUrl.protocol === \"file:\" && localDirectory) {\n await assertLocalScreenshotRequestAllowed(parsedUrl, localDirectory);\n return;\n }\n\n throw new Error(`Blocked unsupported page request protocol ${parsedUrl.protocol}`);\n}\n\nasync function resolveScreenshotRequest(\n request: InterceptedRequest,\n localDirectory: string | undefined,\n onBlocked: (message: string) => void,\n): Promise<void> {\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n\n try {\n await assertScreenshotRequestAllowed(request.url(), localDirectory);\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n await request.continue();\n } catch (error: unknown) {\n onBlocked(getErrorMessage(error));\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n await request.abort(\"blockedbyclient\");\n }\n}\n\n/**\n * Captures screenshots of a local HTML file or remote URL at different viewport sizes.\n * @param targetPath Local file path or HTTP(S) URL\n * @param outputDirectory Absolute path to save screenshots\n * @param customViewports Optional custom viewport settings\n */\nexport async function captureScreenshots(\n targetPath: string,\n outputDirectory: string,\n customViewports?: ViewportConfig[],\n browserRuntime: ScreenshotBrowserRuntime = defaultScreenshotBrowserRuntime,\n): Promise<ScreenshotResult[]> {\n if (typeof targetPath !== \"string\" || targetPath.trim() === \"\") {\n throw new Error(\"targetPath must be a non-empty string\");\n }\n if (typeof outputDirectory !== \"string\" || outputDirectory.trim() === \"\") {\n throw new Error(\"outputDirectory must be a non-empty string\");\n }\n\n const viewports = validateViewports(\n customViewports && customViewports.length > 0 ? customViewports : DEFAULT_VIEWPORTS,\n );\n const resolvedOutputDirectory = path.resolve(outputDirectory);\n\n // Resolve target to file:// URL if it is a local file path\n let targetUrl: string;\n let localDirectory: string | undefined;\n if (/^https?:\\/\\//i.test(targetPath)) {\n targetUrl = (await assertPublicHttpUrl(targetPath)).href;\n } else {\n const absolutePath = path.resolve(targetPath);\n const resolvedPath = await fs.realpath(absolutePath);\n const stats = await fs.stat(resolvedPath);\n if (!stats.isFile()) {\n throw new Error(`Screenshot target is not a regular file: ${resolvedPath}`);\n }\n localDirectory = path.dirname(resolvedPath);\n targetUrl = pathToFileURL(resolvedPath).href;\n }\n\n // Ensure output directory exists\n await fs.mkdir(resolvedOutputDirectory, { recursive: true });\n\n const browser = await browserRuntime.launch();\n\n const results: ScreenshotResult[] = [];\n\n try {\n const page = await browser.newPage();\n page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS);\n\n let blockedRequestMessage: string | undefined;\n await page.setRequestInterception(true);\n page.on(\"request\", (request) => {\n void resolveScreenshotRequest(request, localDirectory, (message) => {\n blockedRequestMessage ??= message;\n }).catch(() => {\n // Puppeteer will surface navigation failures; avoid an unhandled listener rejection.\n });\n });\n\n for (const vp of viewports) {\n await page.setViewport({\n width: vp.width,\n height: vp.height,\n deviceScaleFactor: 1\n });\n\n // Load URL (wait until network is idle or DOM loaded)\n try {\n await page.goto(targetUrl, {\n waitUntil: \"domcontentloaded\",\n timeout: NAVIGATION_TIMEOUT_MS,\n });\n } catch (error: unknown) {\n if (blockedRequestMessage) {\n throw new Error(`Screenshot navigation blocked: ${blockedRequestMessage}`);\n }\n throw new Error(`Screenshot navigation failed: ${getErrorMessage(error)}`);\n }\n\n // Short wait for any animations or layout adjustments to settle\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n const fileName = `${vp.name}_${vp.width}x${vp.height}.png`;\n const outputPath = resolveContainedOutputPath(resolvedOutputDirectory, fileName);\n\n await page.screenshot({\n path: outputPath,\n fullPage: false // Captures above the fold viewport\n });\n\n results.push({\n viewportName: vp.name,\n width: vp.width,\n height: vp.height,\n outputPath\n });\n }\n } finally {\n await browser.close();\n }\n\n return results;\n}\n"]}
1
+ {"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../src/screenshot.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Browser as ManagedBrowser,\n computeExecutablePath,\n detectBrowserPlatform,\n install as installBrowser,\n type BrowserPlatform,\n type InstallOptions,\n} from \"@puppeteer/browsers\";\nimport puppeteer, { type Browser as PuppeteerBrowser } from \"puppeteer-core\";\nimport * as path from \"path\";\nimport * as fs from \"fs/promises\";\nimport * as os from \"node:os\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport { assertPublicHttpUrl, getErrorMessage } from \"./network.js\";\nimport { startScreenshotTransportProxy } from \"./screenshot-proxy.js\";\n\nexport interface ViewportConfig {\n name: string;\n width: number;\n height: number;\n}\n\nexport interface ScreenshotResult {\n viewportName: string;\n width: number;\n height: number;\n outputPath: string;\n}\n\nconst DEFAULT_VIEWPORTS: ViewportConfig[] = [\n { name: \"desktop\", width: 1440, height: 900 },\n { name: \"tablet\", width: 768, height: 1024 },\n { name: \"mobile\", width: 375, height: 812 }\n];\n\nconst MAX_VIEWPORTS = 10;\nconst MIN_VIEWPORT_DIMENSION = 100;\nconst MAX_VIEWPORT_DIMENSION = 7_680;\nconst SAFE_VIEWPORT_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;\nconst NAVIGATION_TIMEOUT_MS = 30_000;\n\nexport const SCREENSHOT_HEADLESS_MODE = \"shell\" as const;\nexport const SCREENSHOT_BROWSER_BUILD_ID = \"152.0.7977.42\";\n\ntype BrowserInstaller = (options: InstallOptions & { unpack?: true }) => Promise<{ executablePath: string }>;\n\nexport interface ScreenshotBrowserRuntime {\n launch(proxyServer: string): Promise<PuppeteerBrowser>;\n}\n\nexport async function resolveScreenshotBrowserExecutable(options: {\n cacheDir?: string;\n platform?: BrowserPlatform;\n installer?: BrowserInstaller;\n} = {}): Promise<string> {\n const platform = options.platform ?? detectBrowserPlatform();\n if (!platform) {\n throw new Error(\"Screenshot capture is not supported on this operating system\");\n }\n\n const cacheDir = options.cacheDir\n ?? process.env.PUPPETEER_CACHE_DIR\n ?? path.join(os.homedir(), \".cache\", \"puppeteer\");\n const browser = ManagedBrowser.CHROMEHEADLESSSHELL;\n const executablePath = computeExecutablePath({\n browser,\n buildId: SCREENSHOT_BROWSER_BUILD_ID,\n cacheDir,\n platform,\n });\n\n try {\n await fs.access(executablePath);\n return executablePath;\n } catch {\n // First screenshot use installs only the verified headless shell, not full Chrome.\n }\n\n try {\n const installed = await (options.installer ?? installBrowser)({\n browser,\n buildId: SCREENSHOT_BROWSER_BUILD_ID,\n cacheDir,\n platform,\n });\n return installed.executablePath;\n } catch (cause: unknown) {\n throw new Error(\n `Screenshot browser setup failed: ${getErrorMessage(cause)}`,\n { cause },\n );\n }\n}\n\nlet screenshotBrowserExecutablePromise: Promise<string> | undefined;\n\nconst defaultScreenshotBrowserRuntime: ScreenshotBrowserRuntime = {\n async launch(proxyServer: string): Promise<PuppeteerBrowser> {\n screenshotBrowserExecutablePromise ??= resolveScreenshotBrowserExecutable();\n let executablePath: string;\n try {\n executablePath = await screenshotBrowserExecutablePromise;\n } catch (cause) {\n screenshotBrowserExecutablePromise = undefined;\n throw cause;\n }\n return puppeteer.launch({\n executablePath,\n headless: SCREENSHOT_HEADLESS_MODE,\n args: [\n `--proxy-server=${proxyServer}`,\n \"--proxy-bypass-list=<-loopback>\",\n ],\n });\n },\n};\n\ninterface InterceptedRequest {\n url(): string;\n isInterceptResolutionHandled(): boolean;\n continue(): Promise<unknown>;\n abort(errorCode?: string): Promise<unknown>;\n}\n\nfunction validateViewports(viewports: ViewportConfig[]): ViewportConfig[] {\n if (viewports.length === 0 || viewports.length > MAX_VIEWPORTS) {\n throw new Error(`Between 1 and ${MAX_VIEWPORTS} viewports are required`);\n }\n\n const outputNames = new Set<string>();\n return viewports.map((viewport, index) => {\n if (!viewport || typeof viewport !== \"object\") {\n throw new Error(`Viewport #${index + 1} must be an object`);\n }\n if (typeof viewport.name !== \"string\" || !SAFE_VIEWPORT_NAME.test(viewport.name)) {\n throw new Error(\n `Viewport #${index + 1} name must match ${SAFE_VIEWPORT_NAME.source}`,\n );\n }\n for (const [label, value] of [[\"width\", viewport.width], [\"height\", viewport.height]] as const) {\n if (\n !Number.isSafeInteger(value)\n || value < MIN_VIEWPORT_DIMENSION\n || value > MAX_VIEWPORT_DIMENSION\n ) {\n throw new Error(\n `Viewport #${index + 1} ${label} must be an integer between ${MIN_VIEWPORT_DIMENSION} and ${MAX_VIEWPORT_DIMENSION}`,\n );\n }\n }\n\n const outputName = `${viewport.name}_${viewport.width}x${viewport.height}.png`.toLowerCase();\n if (outputNames.has(outputName)) {\n throw new Error(`Duplicate viewport output filename: ${outputName}`);\n }\n outputNames.add(outputName);\n return { name: viewport.name, width: viewport.width, height: viewport.height };\n });\n}\n\nfunction resolveContainedOutputPath(outputDirectory: string, fileName: string): string {\n const outputPath = path.resolve(outputDirectory, fileName);\n const relativePath = path.relative(outputDirectory, outputPath);\n if (relativePath.startsWith(\"..\") || path.isAbsolute(relativePath)) {\n throw new Error(\"Screenshot output path escapes the requested output directory\");\n }\n return outputPath;\n}\n\nfunction isPathWithinDirectory(directory: string, filePath: string): boolean {\n const relativePath = path.relative(directory, filePath);\n return relativePath === \"\" || (!relativePath.startsWith(\"..\") && !path.isAbsolute(relativePath));\n}\n\nasync function assertLocalScreenshotRequestAllowed(requestUrl: URL, localDirectory: string): Promise<void> {\n if (requestUrl.hostname) {\n throw new Error(\"Blocked local file request with a host name\");\n }\n\n let resolvedPath: string;\n try {\n resolvedPath = await fs.realpath(fileURLToPath(requestUrl));\n } catch (error: unknown) {\n throw new Error(`Blocked local file request: ${getErrorMessage(error)}`);\n }\n\n if (!isPathWithinDirectory(localDirectory, resolvedPath)) {\n throw new Error(\"Blocked local file request outside the selected file directory\");\n }\n}\n\nasync function assertScreenshotRequestAllowed(requestUrl: string, localDirectory?: string): Promise<void> {\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(requestUrl);\n } catch {\n throw new Error(\"Blocked invalid page request URL\");\n }\n\n if (parsedUrl.protocol === \"http:\" || parsedUrl.protocol === \"https:\") {\n await assertPublicHttpUrl(parsedUrl);\n return;\n }\n\n if (parsedUrl.protocol === \"data:\" || parsedUrl.protocol === \"blob:\") {\n return;\n }\n\n if (parsedUrl.protocol === \"file:\" && localDirectory) {\n await assertLocalScreenshotRequestAllowed(parsedUrl, localDirectory);\n return;\n }\n\n throw new Error(`Blocked unsupported page request protocol ${parsedUrl.protocol}`);\n}\n\nasync function resolveScreenshotRequest(\n request: InterceptedRequest,\n localDirectory: string | undefined,\n onBlocked: (message: string) => void,\n): Promise<void> {\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n\n try {\n await assertScreenshotRequestAllowed(request.url(), localDirectory);\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n await request.continue();\n } catch (error: unknown) {\n onBlocked(getErrorMessage(error));\n if (request.isInterceptResolutionHandled()) {\n return;\n }\n await request.abort(\"blockedbyclient\");\n }\n}\n\n/**\n * Captures screenshots of a local HTML file or remote URL at different viewport sizes.\n * @param targetPath Local file path or HTTP(S) URL\n * @param outputDirectory Absolute path to save screenshots\n * @param customViewports Optional custom viewport settings\n */\nexport async function captureScreenshots(\n targetPath: string,\n outputDirectory: string,\n customViewports?: ViewportConfig[],\n browserRuntime: ScreenshotBrowserRuntime = defaultScreenshotBrowserRuntime,\n): Promise<ScreenshotResult[]> {\n if (typeof targetPath !== \"string\" || targetPath.trim() === \"\") {\n throw new Error(\"targetPath must be a non-empty string\");\n }\n if (typeof outputDirectory !== \"string\" || outputDirectory.trim() === \"\") {\n throw new Error(\"outputDirectory must be a non-empty string\");\n }\n\n const viewports = validateViewports(\n customViewports && customViewports.length > 0 ? customViewports : DEFAULT_VIEWPORTS,\n );\n const resolvedOutputDirectory = path.resolve(outputDirectory);\n\n // Resolve target to file:// URL if it is a local file path\n let targetUrl: string;\n let localDirectory: string | undefined;\n if (/^https?:\\/\\//i.test(targetPath)) {\n targetUrl = (await assertPublicHttpUrl(targetPath)).href;\n } else {\n const absolutePath = path.resolve(targetPath);\n const resolvedPath = await fs.realpath(absolutePath);\n const stats = await fs.stat(resolvedPath);\n if (!stats.isFile()) {\n throw new Error(`Screenshot target is not a regular file: ${resolvedPath}`);\n }\n localDirectory = path.dirname(resolvedPath);\n targetUrl = pathToFileURL(resolvedPath).href;\n }\n\n // Ensure output directory exists\n await fs.mkdir(resolvedOutputDirectory, { recursive: true });\n\n const results: ScreenshotResult[] = [];\n let blockedRequestMessage: string | undefined;\n const transportProxy = await startScreenshotTransportProxy((message) => {\n blockedRequestMessage ??= message;\n });\n let browser: PuppeteerBrowser | undefined;\n\n try {\n browser = await browserRuntime.launch(transportProxy.url);\n const page = await browser.newPage();\n page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS);\n\n await page.setRequestInterception(true);\n page.on(\"request\", (request) => {\n void resolveScreenshotRequest(request, localDirectory, (message) => {\n blockedRequestMessage ??= message;\n }).catch(() => {\n // Puppeteer will surface navigation failures; avoid an unhandled listener rejection.\n });\n });\n\n for (const vp of viewports) {\n await page.setViewport({\n width: vp.width,\n height: vp.height,\n deviceScaleFactor: 1\n });\n\n // Load URL (wait until network is idle or DOM loaded)\n try {\n await page.goto(targetUrl, {\n waitUntil: \"domcontentloaded\",\n timeout: NAVIGATION_TIMEOUT_MS,\n });\n } catch (error: unknown) {\n if (blockedRequestMessage) {\n throw new Error(`Screenshot navigation blocked: ${blockedRequestMessage}`);\n }\n throw new Error(`Screenshot navigation failed: ${getErrorMessage(error)}`);\n }\n\n // Short wait for any animations or layout adjustments to settle\n await new Promise((resolve) => setTimeout(resolve, 500));\n\n const fileName = `${vp.name}_${vp.width}x${vp.height}.png`;\n const outputPath = resolveContainedOutputPath(resolvedOutputDirectory, fileName);\n\n await page.screenshot({\n path: outputPath,\n fullPage: false // Captures above the fold viewport\n });\n\n results.push({\n viewportName: vp.name,\n width: vp.width,\n height: vp.height,\n outputPath\n });\n }\n } finally {\n if (browser) {\n await browser.close();\n }\n await transportProxy.close();\n }\n\n return results;\n}\n"]}
@@ -196,8 +196,8 @@ export function auditSeoMetadataDetailed(htmlContent) {
196
196
  }
197
197
  });
198
198
  // --- Open Graph / Social Tags ---
199
- const ogTitle = $('meta[property="og:title"]');
200
- const ogImage = $('meta[property="og:image"]');
199
+ const ogTitle = $('head > meta[property="og:title"]');
200
+ const ogImage = $('head > meta[property="og:image"]');
201
201
  if (ogTitle.length === 0 || ogImage.length === 0) {
202
202
  add({
203
203
  code: "seo.open_graph.missing",
@@ -1 +1 @@
1
- {"version":3,"file":"seo-auditor.js","sourceRoot":"","sources":["../src/seo-auditor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,cAAc,GACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AACpC,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AACrC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AACjC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AACpC,MAAM,qBAAqB,GAAG,qBAAqB,eAAe,0CAA0C,CAAC;AAuB7G,SAAS,QAAQ,CAAC,MAAkB,EAAE,KAAe;IACnD,IAAI,MAAM,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC;AAED,SAAS,oBAAoB;IAK3B,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,OAAO;QACL,MAAM;QACN,GAAG,CAAC,KAAK;YACP,WAAW,IAAI,CAAC,CAAC;YACjB,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC;QACD,OAAO;YACL,OAAO;gBACL,MAAM;gBACN,WAAW;gBACX,SAAS,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM;aACvC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAwB;IAClD,OAAO,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,qBAAqB,CAAC;AAC/E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,WAAmB;IAC1D,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,oBAAoB,EAAE,CAAC;IACzC,MAAM,EAAE,GAAG,EAAE,GAAG,SAAS,CAAC;IAE1B,2BAA2B;IAC3B,MAAM,QAAQ,GAAG,CAAC,CAAC,cAAc,CAAC,CAAC;IACnC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,GAAG,CAAC;YACF,IAAI,EAAE,4BAA4B;YAClC,QAAQ,EAAE,OAAO;YACjB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,qGAAqG;SAC/G,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;QACzC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,GAAG,CAAC;gBACF,IAAI,EAAE,4BAA4B;gBAClC,QAAQ,EAAE,OAAO;gBACjB,QAAQ,EAAE,KAAK;gBACf,OAAO,EAAE,2BAA2B;aACrC,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,SAAS,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;YACjC,GAAG,CAAC;gBACF,IAAI,EAAE,kBAAkB;gBACxB,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,KAAK;gBACf,OAAO,EAAE,YAAY,SAAS,CAAC,MAAM,qHAAqH;gBAC1J,OAAO,EAAE,UAAU,SAAS,UAAU;aACvC,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,SAAS,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;YACjC,GAAG,CAAC;gBACF,IAAI,EAAE,kBAAkB;gBACxB,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,KAAK;gBACf,OAAO,EAAE,YAAY,SAAS,CAAC,MAAM,wJAAwJ;gBAC7L,OAAO,EAAE,UAAU,SAAS,UAAU;aACvC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,kCAAkC;IAClC,MAAM,eAAe,GAAG,CAAC,CAAC,mCAAmC,CAAC,CAAC;IAC/D,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC;YACF,IAAI,EAAE,uCAAuC;YAC7C,QAAQ,EAAE,OAAO;YACjB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,mJAAmJ;SAC7J,CAAC,CAAC;IACL,CAAC;SAAM,CAAC;QACN,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAC/D,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC;gBACF,IAAI,EAAE,uCAAuC;gBAC7C,QAAQ,EAAE,OAAO;gBACjB,QAAQ,EAAE,KAAK;gBACf,OAAO,EAAE,8CAA8C;aACxD,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACjC,GAAG,CAAC;gBACF,IAAI,EAAE,6BAA6B;gBACnC,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,KAAK;gBACf,OAAO,EAAE,uBAAuB,QAAQ,CAAC,MAAM,yHAAyH;gBACxK,OAAO,EAAE,qCAAqC,QAAQ,IAAI;aAC3D,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACjC,GAAG,CAAC;gBACF,IAAI,EAAE,6BAA6B;gBACnC,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,KAAK;gBACf,OAAO,EAAE,uBAAuB,QAAQ,CAAC,MAAM,6IAA6I;gBAC5L,OAAO,EAAE,qCAAqC,QAAQ,IAAI;aAC3D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,MAAM,cAAc,GAAG,CAAC,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE;QACjE,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACzC,OAAO,GAAG;aACP,IAAI,EAAE;aACN,KAAK,CAAC,cAAc,CAAC;aACrB,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,WAAW,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,MAAM,eAAe,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACxG,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,GAAG,CAAC;YACF,IAAI,EAAE,uBAAuB;YAC7B,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,0HAA0H;SACpI,CAAC,CAAC;IACL,CAAC;SAAM,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxC,GAAG,CAAC;YACF,IAAI,EAAE,uBAAuB;YAC7B,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,+GAA+G;SACzH,CAAC,CAAC;IACL,CAAC;IAED,oDAAoD;IACpD,MAAM,QAAQ,GAAG,CAAC,CAAC,gCAAgC,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,GAAG,CAAC;YACF,IAAI,EAAE,sBAAsB;YAC5B,QAAQ,EAAE,OAAO;YACjB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,mIAAmI;SAC7I,CAAC,CAAC;IACL,CAAC;IAED,4BAA4B;IAC5B,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,GAAG,CAAC;YACF,IAAI,EAAE,gBAAgB;YACtB,QAAQ,EAAE,SAAS;YACnB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,6GAA6G;SACvH,CAAC,CAAC;IACL,CAAC;SAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,GAAG,CAAC;YACF,IAAI,EAAE,iBAAiB;YACvB,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,mBAAmB,MAAM,CAAC,MAAM,+IAA+I;SACzL,CAAC,CAAC;IACL,CAAC;IAED,gDAAgD;IAChD,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE;QAC3B,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;QACvB,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,gBAAgB,CAAC;QAChD,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE5B,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,GAAG,CAAC;gBACF,IAAI,EAAE,iCAAiC;gBACvC,QAAQ,EAAE,OAAO;gBACjB,QAAQ,EAAE,eAAe;gBACzB,OAAO,EAAE,iFAAiF;gBAC1F,OAAO,EAAE,aAAa,GAAG,IAAI;aAC9B,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC7B,0EAA0E;YAC1E,GAAG,CAAC;gBACF,IAAI,EAAE,+BAA+B;gBACrC,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,eAAe;gBACzB,OAAO,EAAE,sGAAsG;gBAC/G,OAAO,EAAE,aAAa,GAAG,WAAW;aACrC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,mCAAmC;IACnC,MAAM,OAAO,GAAG,CAAC,CAAC,2BAA2B,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,CAAC,CAAC,2BAA2B,CAAC,CAAC;IAC/C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC;YACF,IAAI,EAAE,wBAAwB;YAC9B,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,gIAAgI;SAC1I,CAAC,CAAC;IACL,CAAC;IAED,OAAO,SAAS,CAAC,OAAO,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAClD,OAAO,wBAAwB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;AACtD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,4BAA4B,CAAC,WAAmB;IAC9D,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,oBAAoB,EAAE,CAAC;IACzC,MAAM,EAAE,GAAG,EAAE,GAAG,SAAS,CAAC;IAC1B,MAAM,MAAM,GAAG,CAAC,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAErG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QAC7B,MAAM,UAAU,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QAC3C,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC7B,GAAG,CAAC;gBACF,IAAI,EAAE,qBAAqB;gBAC3B,QAAQ,EAAE,SAAS;gBACnB,QAAQ,EAAE,QAAQ;gBAClB,OAAO,EAAE,kBAAkB,KAAK,GAAG,CAAC,YAAY;aACjD,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,GAAG,CAAC;gBACF,IAAI,EAAE,4BAA4B;gBAClC,QAAQ,EAAE,OAAO;gBACjB,QAAQ,EAAE,QAAQ;gBAClB,OAAO,EAAE,kCAAkC,eAAe,CAAC,KAAK,CAAC,EAAE;gBACnE,OAAO,EAAE,iDAAiD;aAC3D,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC,OAAO,EAAE,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,WAAmB;IACtD,OAAO,4BAA4B,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;AAC1D,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,WAAmB,EACnB,OAAgB,EAChB,QAAQ,GAAG,kBAAkB;IAE7B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,kBAAkB,CAAC,CAAC;IACzD,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACpC,MAAM,eAAe,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACjF,IAAI,aAAa,GAAG,eAAe,CAAC;IACpC,MAAM,gBAAgB,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;QACnC,IAAI,oBAAqC,CAAC;QAC1C,IAAI,CAAC;YACH,oBAAoB,GAAG,eAAe;gBACpC,CAAC,CAAC,IAAI,GAAG,CAAC,gBAAgB,EAAE,eAAe,CAAC;gBAC5C,CAAC,CAAC,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,aAAa,GAAG,SAAS,CAAC;QAC5B,CAAC;QACD,IAAI,oBAAoB,EAAE,CAAC;YACzB,IAAI,CAAC;gBACH,aAAa,GAAG,MAAM,mBAAmB,CAAC,oBAAoB,CAAC,CAAC;YAClE,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACxB,IAAI,CAAC,CAAC,KAAK,YAAY,cAAc,CAAC,EAAE,CAAC;oBACvC,MAAM,KAAK,CAAC;gBACd,CAAC;gBACD,aAAa,GAAG,SAAS,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IAEnC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE;QACzB,IAAI,IAAI,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC;YAC7B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;QAC7C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAClC,OAAO;QACT,CAAC;QAED,IAAI,WAAgB,CAAC;QACrB,IAAI,CAAC;YACH,WAAW,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC7E,CAAC;QAAC,MAAM,CAAC;YACP,wFAAwF;YACxF,OAAO;QACT,CAAC;QAED,IAAI,WAAW,CAAC,QAAQ,KAAK,OAAO,IAAI,WAAW,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC1E,OAAO;QACT,CAAC;QAED,WAAW,CAAC,IAAI,GAAG,EAAE,CAAC;QACtB,MAAM,aAAa,GAAG,WAAW,CAAC,IAAI,CAAC;QACvC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;YACjC,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,IAAI,KAAK,CAAa,IAAI,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,UAAU,SAAS,CAAC,GAAW;QAClC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE;gBAC5C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,YAAY,EAAE,qBAAqB,EAAE;gBAChD,SAAS,EAAE,qBAAqB;gBAChC,YAAY,EAAE,CAAC;aAChB,CAAC,CAAC;YACH,MAAM,UAAU,GAAG,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC9C,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC;YACzC,MAAM,kBAAkB,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YAE9C,yFAAyF;YACzF,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;gBACnE,IAAI,SAAS,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE;oBACzC,MAAM,EAAE,KAAK;oBACb,OAAO,EAAE;wBACP,KAAK,EAAE,WAAW;wBAClB,YAAY,EAAE,qBAAqB;qBACpC;oBACD,SAAS,EAAE,qBAAqB;oBAChC,YAAY,EAAE,CAAC;iBAChB,CAAC,CAAC;gBACH,IAAI,SAAS,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBACtC,MAAM,kBAAkB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;oBAC7C,SAAS,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE;wBACrC,MAAM,EAAE,KAAK;wBACb,OAAO,EAAE,EAAE,YAAY,EAAE,qBAAqB,EAAE;wBAChD,SAAS,EAAE,qBAAqB;wBAChC,YAAY,EAAE,CAAC;qBAChB,CAAC,CAAC;gBACL,CAAC;gBAED,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC;gBACzC,MAAM,EAAE,GAAG,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC;gBACzC,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;gBACpC,MAAM,kBAAkB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;gBAC7C,OAAO;oBACL,GAAG;oBACH,MAAM;oBACN,EAAE;oBACF,OAAO,EACL,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG;wBAC3B,CAAC,CAAC,uBAAuB;wBACzB,CAAC,CAAC,QAAQ,KAAK,GAAG;4BAChB,CAAC,CAAC,iBAAiB,QAAQ,EAAE;4BAC7B,CAAC,CAAC,SAAS;iBAClB,CAAC;YACJ,CAAC;YAED,OAAO;gBACL,GAAG;gBACH,MAAM,EAAE,UAAU;gBAClB,EAAE,EAAE,UAAU,IAAI,GAAG,IAAI,UAAU,GAAG,GAAG;gBACzC,OAAO,EACL,UAAU,IAAI,GAAG,IAAI,UAAU,GAAG,GAAG;oBACnC,CAAC,CAAC,uBAAuB;oBACzB,CAAC,CAAC,YAAY,KAAK,GAAG;wBACpB,CAAC,CAAC,iBAAiB,YAAY,EAAE;wBACjC,CAAC,CAAC,SAAS;aAClB,CAAC;QACJ,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,OAAO;gBACL,GAAG;gBACH,MAAM,EAAE,KAAK,YAAY,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ;gBAC9D,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,eAAe,CAAC,KAAK,CAAC;aAChC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,UAAU,MAAM;QACnB,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,KAAK,GAAG,SAAS,CAAC;YACxB,SAAS,IAAI,CAAC,CAAC;YACf,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBACzB,OAAO;YACT,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,sBAAsB,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IAClE,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACvE,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["import * as cheerio from \"cheerio\";\nimport {\n assertPublicHttpUrl,\n cancelResponseBody,\n fetchPublicHttp,\n getErrorMessage,\n PublicUrlError,\n} from \"./network.js\";\nimport { PACKAGE_VERSION } from \"./version.js\";\n\nexport const MAX_AUDIT_ISSUES = 200;\nexport const MAX_LINKS_TO_CHECK = 25;\nconst LINK_CHECK_CONCURRENCY = 5;\nconst LINK_CHECK_TIMEOUT_MS = 5_000;\nconst LINK_CHECK_USER_AGENT = `mcp-web-validator/${PACKAGE_VERSION} (+https://digestseo.com/validator-mcp/)`;\n\nexport interface SEOIssue {\n code: string;\n severity: \"error\" | \"warning\" | \"info\";\n category: \"SEO\" | \"Schema\" | \"BrokenLinks\" | \"Accessibility\";\n message: string;\n element?: string;\n}\n\nexport interface LinkStatus {\n url: string;\n status: number | string;\n ok: boolean;\n message?: string;\n}\n\nexport interface AuditDetails {\n issues: SEOIssue[];\n totalIssues: number;\n truncated: boolean;\n}\n\nfunction addIssue(issues: SEOIssue[], issue: SEOIssue): void {\n if (issues.length < MAX_AUDIT_ISSUES) {\n issues.push(issue);\n }\n}\n\nfunction createIssueCollector(): {\n issues: SEOIssue[];\n add: (issue: SEOIssue) => void;\n details: () => AuditDetails;\n} {\n const issues: SEOIssue[] = [];\n let totalIssues = 0;\n return {\n issues,\n add(issue) {\n totalIssues += 1;\n addIssue(issues, issue);\n },\n details() {\n return {\n issues,\n totalIssues,\n truncated: totalIssues > issues.length,\n };\n },\n };\n}\n\nfunction isJsonLdScriptType(type: string | undefined): boolean {\n return type?.split(\";\", 1)[0].trim().toLowerCase() === \"application/ld+json\";\n}\n\n/**\n * Audits technical SEO and accessibility basics on HTML content using Cheerio\n */\nexport function auditSeoMetadataDetailed(htmlContent: string): AuditDetails {\n const $ = cheerio.load(htmlContent);\n const collector = createIssueCollector();\n const { add } = collector;\n\n // --- Title Tag Audits ---\n const titleTag = $(\"head > title\");\n if (titleTag.length === 0) {\n add({\n code: \"seo.title.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <title> tag. Add a concise, descriptive title to help represent the page in search results.\",\n });\n } else {\n const titleText = titleTag.text().trim();\n if (titleText.length === 0) {\n add({\n code: \"seo.title.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"The <title> tag is empty.\",\n });\n } else if (titleText.length < 30) {\n add({\n code: \"seo.title.length\",\n severity: \"info\",\n category: \"SEO\",\n message: `Title is ${titleText.length} characters. This is shorter than the audit's common editorial range; review whether it describes the page clearly.`,\n element: `<title>${titleText}</title>`,\n });\n } else if (titleText.length > 60) {\n add({\n code: \"seo.title.length\",\n severity: \"info\",\n category: \"SEO\",\n message: `Title is ${titleText.length} characters. This is longer than the audit's common editorial range; Google title links may be shortened or rewritten depending on context and device.`,\n element: `<title>${titleText}</title>`,\n });\n }\n }\n\n // --- Meta Description Audits ---\n const metaDescription = $('head > meta[name=\"description\" i]');\n if (metaDescription.length === 0) {\n add({\n code: \"seo.meta_description.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <meta name=\\\"description\\\">. Add a concise, accurate page summary; Google may use page content or this description to generate a snippet.\",\n });\n } else {\n const descText = metaDescription.attr(\"content\")?.trim() || \"\";\n if (descText.length === 0) {\n add({\n code: \"seo.meta_description.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Meta description content attribute is empty.\",\n });\n } else if (descText.length < 120) {\n add({\n code: \"seo.meta_description.length\",\n severity: \"info\",\n category: \"SEO\",\n message: `Meta description is ${descText.length} characters. This is shorter than the audit's common editorial range; review whether it provides a useful page summary.`,\n element: `<meta name=\"description\" content=\"${descText}\">`,\n });\n } else if (descText.length > 160) {\n add({\n code: \"seo.meta_description.length\",\n severity: \"info\",\n category: \"SEO\",\n message: `Meta description is ${descText.length} characters. This is longer than the audit's common editorial range; displayed snippets may be shortened depending on the query and device.`,\n element: `<meta name=\"description\" content=\"${descText}\">`,\n });\n }\n }\n\n // --- Canonical Link ---\n const canonicalLinks = $(\"head > link[rel]\").filter((_, element) => {\n const rel = $(element).attr(\"rel\") ?? \"\";\n return rel\n .trim()\n .split(/[\\t\\n\\f\\r ]+/)\n .some((token) => token.toLowerCase() === \"canonical\");\n });\n const usableCanonical = canonicalLinks.filter((_, element) => Boolean($(element).attr(\"href\")?.trim()));\n if (canonicalLinks.length === 0) {\n add({\n code: \"seo.canonical.missing\",\n severity: \"info\",\n category: \"SEO\",\n message: \"No rel=\\\"canonical\\\" preference is declared. This is optional unless the page needs an explicit canonicalization signal.\",\n });\n } else if (usableCanonical.length === 0) {\n add({\n code: \"seo.canonical.missing\",\n severity: \"warning\",\n category: \"SEO\",\n message: \"Canonical link is present but its href is empty. Provide a usable canonical target or remove the declaration.\",\n });\n }\n\n // --- Viewport Meta Tag (Mobile Responsiveness) ---\n const viewport = $('head > meta[name=\"viewport\" i]');\n if (viewport.length === 0) {\n add({\n code: \"seo.viewport.missing\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <meta name=\\\"viewport\\\"> tag. Review mobile rendering; this tag helps browsers size and scale the page on mobile devices.\",\n });\n }\n\n // --- Heading Structure ---\n const h1Tags = $(\"h1\");\n if (h1Tags.length === 0) {\n add({\n code: \"seo.h1.missing\",\n severity: \"warning\",\n category: \"SEO\",\n message: \"No <h1> heading found. Review whether the page has a clear main heading and a meaningful heading hierarchy.\",\n });\n } else if (h1Tags.length > 1) {\n add({\n code: \"seo.h1.multiple\",\n severity: \"info\",\n category: \"SEO\",\n message: `Found multiple (${h1Tags.length}) <h1> headings. Multiple H1s are not inherently an SEO error; ensure the heading hierarchy is meaningful and the main visual title is clear.`,\n });\n }\n\n // --- Images Alt Tags (SEO + Accessibility) ---\n $(\"img\").each((_, element) => {\n const img = $(element);\n const src = img.attr(\"src\") || \"unknown-source\";\n const alt = img.attr(\"alt\");\n\n if (alt === undefined) {\n add({\n code: \"accessibility.image_alt.missing\",\n severity: \"error\",\n category: \"Accessibility\",\n message: \"Missing 'alt' attribute on image. This makes it inaccessible to screen readers.\",\n element: `<img src=\"${src}\">`,\n });\n } else if (alt.trim() === \"\") {\n // Empty alt is acceptable for purely decorative images, but worth warning\n add({\n code: \"accessibility.image_alt.empty\",\n severity: \"info\",\n category: \"Accessibility\",\n message: \"Empty 'alt' attribute found. Ensure this image is purely decorative, otherwise add descriptive text.\",\n element: `<img src=\"${src}\" alt=\"\">`,\n });\n }\n });\n\n // --- Open Graph / Social Tags ---\n const ogTitle = $('meta[property=\"og:title\"]');\n const ogImage = $('meta[property=\"og:image\"]');\n if (ogTitle.length === 0 || ogImage.length === 0) {\n add({\n code: \"seo.open_graph.missing\",\n severity: \"info\",\n category: \"SEO\",\n message: \"Missing Open Graph social metadata (og:title / og:image). Add these to control preview cards on platforms like LinkedIn and X.\",\n });\n }\n\n return collector.details();\n}\n\nexport function auditSeoMetadata(htmlContent: string): SEOIssue[] {\n return auditSeoMetadataDetailed(htmlContent).issues;\n}\n\n/**\n * Parses and validates JSON-LD Schema markup\n */\nexport function validateSchemaMarkupDetailed(htmlContent: string): AuditDetails {\n const $ = cheerio.load(htmlContent);\n const collector = createIssueCollector();\n const { add } = collector;\n const blocks = $(\"script[type]\").filter((_, element) => isJsonLdScriptType($(element).attr(\"type\")));\n\n blocks.each((index, element) => {\n const scriptText = $(element).html() || \"\";\n if (scriptText.trim() === \"\") {\n add({\n code: \"schema.jsonld.empty\",\n severity: \"warning\",\n category: \"Schema\",\n message: `JSON-LD block #${index + 1} is empty.`,\n });\n return;\n }\n\n try {\n JSON.parse(scriptText);\n } catch (error: unknown) {\n add({\n code: \"schema.jsonld.invalid_json\",\n severity: \"error\",\n category: \"Schema\",\n message: `Invalid JSON-LD schema syntax: ${getErrorMessage(error)}`,\n element: `<script type=\"application/ld+json\">...</script>`,\n });\n }\n });\n\n return collector.details();\n}\n\nexport function validateSchemaMarkup(htmlContent: string): SEOIssue[] {\n return validateSchemaMarkupDetailed(htmlContent).issues;\n}\n\n/**\n * Extracts and tests links, treating 3xx responses as reachable redirects and 4xx/5xx as broken.\n */\nexport async function checkBrokenLinks(\n htmlContent: string,\n baseUrl?: string,\n maxLinks = MAX_LINKS_TO_CHECK,\n): Promise<LinkStatus[]> {\n if (!Number.isSafeInteger(maxLinks) || maxLinks <= 0) {\n throw new Error(\"maxLinks must be a positive integer\");\n }\n const linkLimit = Math.min(maxLinks, MAX_LINKS_TO_CHECK);\n const $ = cheerio.load(htmlContent);\n const fallbackBaseUrl = baseUrl ? await assertPublicHttpUrl(baseUrl) : undefined;\n let parsedBaseUrl = fallbackBaseUrl;\n const documentBaseHref = $(\"base[href]\").first().attr(\"href\");\n if (documentBaseHref !== undefined) {\n let resolvedDocumentBase: URL | undefined;\n try {\n resolvedDocumentBase = fallbackBaseUrl\n ? new URL(documentBaseHref, fallbackBaseUrl)\n : new URL(documentBaseHref);\n } catch {\n parsedBaseUrl = undefined;\n }\n if (resolvedDocumentBase) {\n try {\n parsedBaseUrl = await assertPublicHttpUrl(resolvedDocumentBase);\n } catch (error: unknown) {\n if (!(error instanceof PublicUrlError)) {\n throw error;\n }\n parsedBaseUrl = undefined;\n }\n }\n }\n const urls: string[] = [];\n const seenUrls = new Set<string>();\n\n $(\"a\").each((_, element) => {\n if (urls.length >= linkLimit) {\n return false;\n }\n\n const href = $(element).attr(\"href\")?.trim();\n if (!href || href.startsWith(\"#\")) {\n return;\n }\n\n let resolvedUrl: URL;\n try {\n resolvedUrl = parsedBaseUrl ? new URL(href, parsedBaseUrl) : new URL(href);\n } catch {\n // Relative links require a public base URL; unsupported or malformed links are skipped.\n return;\n }\n\n if (resolvedUrl.protocol !== \"http:\" && resolvedUrl.protocol !== \"https:\") {\n return;\n }\n\n resolvedUrl.hash = \"\";\n const normalizedUrl = resolvedUrl.href;\n if (!seenUrls.has(normalizedUrl)) {\n seenUrls.add(normalizedUrl);\n urls.push(normalizedUrl);\n }\n });\n\n const results = new Array<LinkStatus>(urls.length);\n let nextIndex = 0;\n\n async function checkLink(url: string): Promise<LinkStatus> {\n try {\n const headResult = await fetchPublicHttp(url, {\n method: \"HEAD\",\n headers: { \"User-Agent\": LINK_CHECK_USER_AGENT },\n timeoutMs: LINK_CHECK_TIMEOUT_MS,\n maxRedirects: 0,\n });\n const headStatus = headResult.response.status;\n const headFinalUrl = headResult.url.href;\n await cancelResponseBody(headResult.response);\n\n // Some sites reject or do not implement HEAD even when the linked resource is available.\n if (headStatus === 405 || headStatus === 403 || headStatus === 501) {\n let getResult = await fetchPublicHttp(url, {\n method: \"GET\",\n headers: {\n Range: \"bytes=0-0\",\n \"User-Agent\": LINK_CHECK_USER_AGENT,\n },\n timeoutMs: LINK_CHECK_TIMEOUT_MS,\n maxRedirects: 0,\n });\n if (getResult.response.status === 416) {\n await cancelResponseBody(getResult.response);\n getResult = await fetchPublicHttp(url, {\n method: \"GET\",\n headers: { \"User-Agent\": LINK_CHECK_USER_AGENT },\n timeoutMs: LINK_CHECK_TIMEOUT_MS,\n maxRedirects: 0,\n });\n }\n\n const status = getResult.response.status;\n const ok = status >= 200 && status < 400;\n const finalUrl = getResult.url.href;\n await cancelResponseBody(getResult.response);\n return {\n url,\n status,\n ok,\n message:\n status >= 300 && status < 400\n ? \"Redirect not followed\"\n : finalUrl !== url\n ? `Redirected to ${finalUrl}`\n : undefined,\n };\n }\n\n return {\n url,\n status: headStatus,\n ok: headStatus >= 200 && headStatus < 400,\n message:\n headStatus >= 300 && headStatus < 400\n ? \"Redirect not followed\"\n : headFinalUrl !== url\n ? `Redirected to ${headFinalUrl}`\n : undefined,\n };\n } catch (error: unknown) {\n return {\n url,\n status: error instanceof PublicUrlError ? \"blocked\" : \"failed\",\n ok: false,\n message: getErrorMessage(error),\n };\n }\n }\n\n async function worker(): Promise<void> {\n while (true) {\n const index = nextIndex;\n nextIndex += 1;\n if (index >= urls.length) {\n return;\n }\n results[index] = await checkLink(urls[index]);\n }\n }\n\n const workerCount = Math.min(LINK_CHECK_CONCURRENCY, urls.length);\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n}\n"]}
1
+ {"version":3,"file":"seo-auditor.js","sourceRoot":"","sources":["../src/seo-auditor.ts"],"names":[],"mappings":"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","sourcesContent":["import * as cheerio from \"cheerio\";\nimport {\n assertPublicHttpUrl,\n cancelResponseBody,\n fetchPublicHttp,\n getErrorMessage,\n PublicUrlError,\n} from \"./network.js\";\nimport { PACKAGE_VERSION } from \"./version.js\";\n\nexport const MAX_AUDIT_ISSUES = 200;\nexport const MAX_LINKS_TO_CHECK = 25;\nconst LINK_CHECK_CONCURRENCY = 5;\nconst LINK_CHECK_TIMEOUT_MS = 5_000;\nconst LINK_CHECK_USER_AGENT = `mcp-web-validator/${PACKAGE_VERSION} (+https://digestseo.com/validator-mcp/)`;\n\nexport interface SEOIssue {\n code: string;\n severity: \"error\" | \"warning\" | \"info\";\n category: \"SEO\" | \"Schema\" | \"BrokenLinks\" | \"Accessibility\";\n message: string;\n element?: string;\n}\n\nexport interface LinkStatus {\n url: string;\n status: number | string;\n ok: boolean;\n message?: string;\n}\n\nexport interface AuditDetails {\n issues: SEOIssue[];\n totalIssues: number;\n truncated: boolean;\n}\n\nfunction addIssue(issues: SEOIssue[], issue: SEOIssue): void {\n if (issues.length < MAX_AUDIT_ISSUES) {\n issues.push(issue);\n }\n}\n\nfunction createIssueCollector(): {\n issues: SEOIssue[];\n add: (issue: SEOIssue) => void;\n details: () => AuditDetails;\n} {\n const issues: SEOIssue[] = [];\n let totalIssues = 0;\n return {\n issues,\n add(issue) {\n totalIssues += 1;\n addIssue(issues, issue);\n },\n details() {\n return {\n issues,\n totalIssues,\n truncated: totalIssues > issues.length,\n };\n },\n };\n}\n\nfunction isJsonLdScriptType(type: string | undefined): boolean {\n return type?.split(\";\", 1)[0].trim().toLowerCase() === \"application/ld+json\";\n}\n\n/**\n * Audits technical SEO and accessibility basics on HTML content using Cheerio\n */\nexport function auditSeoMetadataDetailed(htmlContent: string): AuditDetails {\n const $ = cheerio.load(htmlContent);\n const collector = createIssueCollector();\n const { add } = collector;\n\n // --- Title Tag Audits ---\n const titleTag = $(\"head > title\");\n if (titleTag.length === 0) {\n add({\n code: \"seo.title.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <title> tag. Add a concise, descriptive title to help represent the page in search results.\",\n });\n } else {\n const titleText = titleTag.text().trim();\n if (titleText.length === 0) {\n add({\n code: \"seo.title.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"The <title> tag is empty.\",\n });\n } else if (titleText.length < 30) {\n add({\n code: \"seo.title.length\",\n severity: \"info\",\n category: \"SEO\",\n message: `Title is ${titleText.length} characters. This is shorter than the audit's common editorial range; review whether it describes the page clearly.`,\n element: `<title>${titleText}</title>`,\n });\n } else if (titleText.length > 60) {\n add({\n code: \"seo.title.length\",\n severity: \"info\",\n category: \"SEO\",\n message: `Title is ${titleText.length} characters. This is longer than the audit's common editorial range; Google title links may be shortened or rewritten depending on context and device.`,\n element: `<title>${titleText}</title>`,\n });\n }\n }\n\n // --- Meta Description Audits ---\n const metaDescription = $('head > meta[name=\"description\" i]');\n if (metaDescription.length === 0) {\n add({\n code: \"seo.meta_description.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <meta name=\\\"description\\\">. Add a concise, accurate page summary; Google may use page content or this description to generate a snippet.\",\n });\n } else {\n const descText = metaDescription.attr(\"content\")?.trim() || \"\";\n if (descText.length === 0) {\n add({\n code: \"seo.meta_description.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Meta description content attribute is empty.\",\n });\n } else if (descText.length < 120) {\n add({\n code: \"seo.meta_description.length\",\n severity: \"info\",\n category: \"SEO\",\n message: `Meta description is ${descText.length} characters. This is shorter than the audit's common editorial range; review whether it provides a useful page summary.`,\n element: `<meta name=\"description\" content=\"${descText}\">`,\n });\n } else if (descText.length > 160) {\n add({\n code: \"seo.meta_description.length\",\n severity: \"info\",\n category: \"SEO\",\n message: `Meta description is ${descText.length} characters. This is longer than the audit's common editorial range; displayed snippets may be shortened depending on the query and device.`,\n element: `<meta name=\"description\" content=\"${descText}\">`,\n });\n }\n }\n\n // --- Canonical Link ---\n const canonicalLinks = $(\"head > link[rel]\").filter((_, element) => {\n const rel = $(element).attr(\"rel\") ?? \"\";\n return rel\n .trim()\n .split(/[\\t\\n\\f\\r ]+/)\n .some((token) => token.toLowerCase() === \"canonical\");\n });\n const usableCanonical = canonicalLinks.filter((_, element) => Boolean($(element).attr(\"href\")?.trim()));\n if (canonicalLinks.length === 0) {\n add({\n code: \"seo.canonical.missing\",\n severity: \"info\",\n category: \"SEO\",\n message: \"No rel=\\\"canonical\\\" preference is declared. This is optional unless the page needs an explicit canonicalization signal.\",\n });\n } else if (usableCanonical.length === 0) {\n add({\n code: \"seo.canonical.missing\",\n severity: \"warning\",\n category: \"SEO\",\n message: \"Canonical link is present but its href is empty. Provide a usable canonical target or remove the declaration.\",\n });\n }\n\n // --- Viewport Meta Tag (Mobile Responsiveness) ---\n const viewport = $('head > meta[name=\"viewport\" i]');\n if (viewport.length === 0) {\n add({\n code: \"seo.viewport.missing\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <meta name=\\\"viewport\\\"> tag. Review mobile rendering; this tag helps browsers size and scale the page on mobile devices.\",\n });\n }\n\n // --- Heading Structure ---\n const h1Tags = $(\"h1\");\n if (h1Tags.length === 0) {\n add({\n code: \"seo.h1.missing\",\n severity: \"warning\",\n category: \"SEO\",\n message: \"No <h1> heading found. Review whether the page has a clear main heading and a meaningful heading hierarchy.\",\n });\n } else if (h1Tags.length > 1) {\n add({\n code: \"seo.h1.multiple\",\n severity: \"info\",\n category: \"SEO\",\n message: `Found multiple (${h1Tags.length}) <h1> headings. Multiple H1s are not inherently an SEO error; ensure the heading hierarchy is meaningful and the main visual title is clear.`,\n });\n }\n\n // --- Images Alt Tags (SEO + Accessibility) ---\n $(\"img\").each((_, element) => {\n const img = $(element);\n const src = img.attr(\"src\") || \"unknown-source\";\n const alt = img.attr(\"alt\");\n\n if (alt === undefined) {\n add({\n code: \"accessibility.image_alt.missing\",\n severity: \"error\",\n category: \"Accessibility\",\n message: \"Missing 'alt' attribute on image. This makes it inaccessible to screen readers.\",\n element: `<img src=\"${src}\">`,\n });\n } else if (alt.trim() === \"\") {\n // Empty alt is acceptable for purely decorative images, but worth warning\n add({\n code: \"accessibility.image_alt.empty\",\n severity: \"info\",\n category: \"Accessibility\",\n message: \"Empty 'alt' attribute found. Ensure this image is purely decorative, otherwise add descriptive text.\",\n element: `<img src=\"${src}\" alt=\"\">`,\n });\n }\n });\n\n // --- Open Graph / Social Tags ---\n const ogTitle = $('head > meta[property=\"og:title\"]');\n const ogImage = $('head > meta[property=\"og:image\"]');\n if (ogTitle.length === 0 || ogImage.length === 0) {\n add({\n code: \"seo.open_graph.missing\",\n severity: \"info\",\n category: \"SEO\",\n message: \"Missing Open Graph social metadata (og:title / og:image). Add these to control preview cards on platforms like LinkedIn and X.\",\n });\n }\n\n return collector.details();\n}\n\nexport function auditSeoMetadata(htmlContent: string): SEOIssue[] {\n return auditSeoMetadataDetailed(htmlContent).issues;\n}\n\n/**\n * Parses and validates JSON-LD Schema markup\n */\nexport function validateSchemaMarkupDetailed(htmlContent: string): AuditDetails {\n const $ = cheerio.load(htmlContent);\n const collector = createIssueCollector();\n const { add } = collector;\n const blocks = $(\"script[type]\").filter((_, element) => isJsonLdScriptType($(element).attr(\"type\")));\n\n blocks.each((index, element) => {\n const scriptText = $(element).html() || \"\";\n if (scriptText.trim() === \"\") {\n add({\n code: \"schema.jsonld.empty\",\n severity: \"warning\",\n category: \"Schema\",\n message: `JSON-LD block #${index + 1} is empty.`,\n });\n return;\n }\n\n try {\n JSON.parse(scriptText);\n } catch (error: unknown) {\n add({\n code: \"schema.jsonld.invalid_json\",\n severity: \"error\",\n category: \"Schema\",\n message: `Invalid JSON-LD schema syntax: ${getErrorMessage(error)}`,\n element: `<script type=\"application/ld+json\">...</script>`,\n });\n }\n });\n\n return collector.details();\n}\n\nexport function validateSchemaMarkup(htmlContent: string): SEOIssue[] {\n return validateSchemaMarkupDetailed(htmlContent).issues;\n}\n\n/**\n * Extracts and tests links, treating 3xx responses as reachable redirects and 4xx/5xx as broken.\n */\nexport async function checkBrokenLinks(\n htmlContent: string,\n baseUrl?: string,\n maxLinks = MAX_LINKS_TO_CHECK,\n): Promise<LinkStatus[]> {\n if (!Number.isSafeInteger(maxLinks) || maxLinks <= 0) {\n throw new Error(\"maxLinks must be a positive integer\");\n }\n const linkLimit = Math.min(maxLinks, MAX_LINKS_TO_CHECK);\n const $ = cheerio.load(htmlContent);\n const fallbackBaseUrl = baseUrl ? await assertPublicHttpUrl(baseUrl) : undefined;\n let parsedBaseUrl = fallbackBaseUrl;\n const documentBaseHref = $(\"base[href]\").first().attr(\"href\");\n if (documentBaseHref !== undefined) {\n let resolvedDocumentBase: URL | undefined;\n try {\n resolvedDocumentBase = fallbackBaseUrl\n ? new URL(documentBaseHref, fallbackBaseUrl)\n : new URL(documentBaseHref);\n } catch {\n parsedBaseUrl = undefined;\n }\n if (resolvedDocumentBase) {\n try {\n parsedBaseUrl = await assertPublicHttpUrl(resolvedDocumentBase);\n } catch (error: unknown) {\n if (!(error instanceof PublicUrlError)) {\n throw error;\n }\n parsedBaseUrl = undefined;\n }\n }\n }\n const urls: string[] = [];\n const seenUrls = new Set<string>();\n\n $(\"a\").each((_, element) => {\n if (urls.length >= linkLimit) {\n return false;\n }\n\n const href = $(element).attr(\"href\")?.trim();\n if (!href || href.startsWith(\"#\")) {\n return;\n }\n\n let resolvedUrl: URL;\n try {\n resolvedUrl = parsedBaseUrl ? new URL(href, parsedBaseUrl) : new URL(href);\n } catch {\n // Relative links require a public base URL; unsupported or malformed links are skipped.\n return;\n }\n\n if (resolvedUrl.protocol !== \"http:\" && resolvedUrl.protocol !== \"https:\") {\n return;\n }\n\n resolvedUrl.hash = \"\";\n const normalizedUrl = resolvedUrl.href;\n if (!seenUrls.has(normalizedUrl)) {\n seenUrls.add(normalizedUrl);\n urls.push(normalizedUrl);\n }\n });\n\n const results = new Array<LinkStatus>(urls.length);\n let nextIndex = 0;\n\n async function checkLink(url: string): Promise<LinkStatus> {\n try {\n const headResult = await fetchPublicHttp(url, {\n method: \"HEAD\",\n headers: { \"User-Agent\": LINK_CHECK_USER_AGENT },\n timeoutMs: LINK_CHECK_TIMEOUT_MS,\n maxRedirects: 0,\n });\n const headStatus = headResult.response.status;\n const headFinalUrl = headResult.url.href;\n await cancelResponseBody(headResult.response);\n\n // Some sites reject or do not implement HEAD even when the linked resource is available.\n if (headStatus === 405 || headStatus === 403 || headStatus === 501) {\n let getResult = await fetchPublicHttp(url, {\n method: \"GET\",\n headers: {\n Range: \"bytes=0-0\",\n \"User-Agent\": LINK_CHECK_USER_AGENT,\n },\n timeoutMs: LINK_CHECK_TIMEOUT_MS,\n maxRedirects: 0,\n });\n if (getResult.response.status === 416) {\n await cancelResponseBody(getResult.response);\n getResult = await fetchPublicHttp(url, {\n method: \"GET\",\n headers: { \"User-Agent\": LINK_CHECK_USER_AGENT },\n timeoutMs: LINK_CHECK_TIMEOUT_MS,\n maxRedirects: 0,\n });\n }\n\n const status = getResult.response.status;\n const ok = status >= 200 && status < 400;\n const finalUrl = getResult.url.href;\n await cancelResponseBody(getResult.response);\n return {\n url,\n status,\n ok,\n message:\n status >= 300 && status < 400\n ? \"Redirect not followed\"\n : finalUrl !== url\n ? `Redirected to ${finalUrl}`\n : undefined,\n };\n }\n\n return {\n url,\n status: headStatus,\n ok: headStatus >= 200 && headStatus < 400,\n message:\n headStatus >= 300 && headStatus < 400\n ? \"Redirect not followed\"\n : headFinalUrl !== url\n ? `Redirected to ${headFinalUrl}`\n : undefined,\n };\n } catch (error: unknown) {\n return {\n url,\n status: error instanceof PublicUrlError ? \"blocked\" : \"failed\",\n ok: false,\n message: getErrorMessage(error),\n };\n }\n }\n\n async function worker(): Promise<void> {\n while (true) {\n const index = nextIndex;\n nextIndex += 1;\n if (index >= urls.length) {\n return;\n }\n results[index] = await checkLink(urls[index]);\n }\n }\n\n const workerCount = Math.min(LINK_CHECK_CONCURRENCY, urls.length);\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-web-validator",
3
- "version": "1.3.14",
3
+ "version": "1.3.15",
4
4
  "description": "W3C HTML/CSS Validator and Technical SEO Audit MCP Server for AI coding assistants.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {