playwright-archaeologist 0.1.1
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/LICENSE +21 -0
- package/README.md +392 -0
- package/bin/cli.js +2 -0
- package/dist/chunk-7ZQGW5OV.js +255 -0
- package/dist/chunk-7ZQGW5OV.js.map +1 -0
- package/dist/chunk-F5WCXM7I.js +4469 -0
- package/dist/chunk-F5WCXM7I.js.map +1 -0
- package/dist/chunk-RWPEKZOW.js +118 -0
- package/dist/chunk-RWPEKZOW.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +310 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1948 -0
- package/dist/index.js +789 -0
- package/dist/index.js.map +1 -0
- package/dist/page-scanner-Q76HROEW.js +8 -0
- package/dist/page-scanner-Q76HROEW.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types/config.ts","../src/types/errors.ts","../src/utils/logger.ts","../src/crawl/frontier.ts","../src/security/output-sanitizer.ts","../src/collectors/screenshot-capturer.ts","../src/collectors/form-prober.ts","../src/security/credential-scrubber.ts","../src/collectors/network-logger.ts","../src/assembler/api-grouper.ts","../src/assembler/flow-graph.ts","../src/report/openapi-output.ts","../src/report/html/escape.ts","../src/report/html/template.ts","../src/crawl/orchestrator.ts","../src/security/network-guard.ts","../src/security/browser-hardening.ts","../src/diff/diff-engine.ts","../src/diff/diff-report.ts","../src/bundle/bundle-creator.ts"],"sourcesContent":["/**\n * playwright-archaeologist configuration schema.\n *\n * Validated with Zod at CLI entry before any crawl work begins.\n * ConfigError is thrown for all validation failures with a clear\n * user-facing message indicating which field failed and why.\n *\n * Validation rules and edge cases:\n * --depth 0 Valid (crawl only the entry URL)\n * --depth -1 Invalid: must be >= 0\n * --depth 999999 Valid but warn: clamped to 100 with warning\n * --concurrency 0 Invalid: must be >= 1\n * --concurrency 100 Valid but warn: clamped to 20 with warning\n * --viewport invalid Invalid: must match /^\\d+x\\d+$/\n * --viewport 100x100 Valid\n * --output /nonexistent Validated at startup: parent dir must exist and be writable\n * --auth missing.ts AuthError('script_not_found')\n * --cookies bad.json AuthError('cookie_file_malformed')\n * URL without protocol Auto-prepend https:// with warning\n * --timeout 0 Invalid: must be >= 1000\n * --max-time 0 Invalid: must be >= 10\n * --delay -1 Invalid: must be >= 0\n * --max-pages 0 Invalid: must be >= 1\n *\n * The Zod schema produces a strongly-typed CrawlConfig object that is\n * passed to the Orchestrator and threaded through to all collectors.\n */\n\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// Viewport\n// ---------------------------------------------------------------------------\n\nexport const ViewportSchema = z.object({\n width: z.number().int().min(320, 'Viewport width must be >= 320').max(7680, 'Viewport width must be <= 7680'),\n height: z.number().int().min(240, 'Viewport height must be >= 240').max(4320, 'Viewport height must be <= 4320'),\n});\n\nexport type Viewport = z.infer<typeof ViewportSchema>;\n\n/**\n * Parse a \"WxH\" string into a Viewport object.\n * Returns null if the format is invalid.\n */\nexport function parseViewport(input: string): Viewport | null {\n const match = input.match(/^(\\d+)x(\\d+)$/);\n if (!match) return null;\n const width = parseInt(match[1], 10);\n const height = parseInt(match[2], 10);\n const result = ViewportSchema.safeParse({ width, height });\n return result.success ? result.data : null;\n}\n\n// ---------------------------------------------------------------------------\n// Output format\n// ---------------------------------------------------------------------------\n\nexport const OutputFormatSchema = z.enum(['html', 'json', 'openapi', 'both']);\nexport type OutputFormat = z.infer<typeof OutputFormatSchema>;\n\n// ---------------------------------------------------------------------------\n// URL normalization for the entry URL\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize the user-provided entry URL.\n * - Prepend https:// if no protocol is provided\n * - Validate it is http or https\n * Returns { url, warnings } where warnings contains any auto-corrections.\n */\nexport function normalizeEntryUrl(raw: string): { url: string; warnings: string[] } {\n const warnings: string[] = [];\n let url = raw.trim();\n\n // Strip trailing slash for consistency\n // (the crawler will add it back for the root)\n\n // Auto-prepend https:// if no protocol\n if (!/^https?:\\/\\//i.test(url)) {\n if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {\n // Has a protocol but it's not http/https -- reject\n throw new Error(`Unsupported protocol in URL: ${url}. Only http and https are allowed.`);\n }\n url = `https://${url}`;\n warnings.push(`No protocol specified. Using https://${raw.trim()}`);\n }\n\n // Validate the URL can be parsed\n try {\n const parsed = new URL(url);\n // Block non-http(s) protocols\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`Unsupported protocol \"${parsed.protocol}\". Only http: and https: are allowed.`);\n }\n // Block obviously invalid hostnames\n if (!parsed.hostname || parsed.hostname.length === 0) {\n throw new Error('URL has no hostname.');\n }\n } catch (err) {\n if (err instanceof TypeError) {\n throw new Error(`Invalid URL: ${url}`);\n }\n throw err;\n }\n\n return { url, warnings };\n}\n\n// ---------------------------------------------------------------------------\n// Main config schema\n// ---------------------------------------------------------------------------\n\nexport const CrawlConfigSchema = z.object({\n // ---- Target ----\n /** Entry URL to start crawling. Must be http or https. */\n targetUrl: z.string().url('Target URL must be a valid URL'),\n\n // ---- Discovery ----\n /** Maximum crawl depth from the entry URL. 0 = entry URL only. */\n depth: z\n .number()\n .int()\n .min(0, 'Depth must be >= 0')\n .max(100, 'Depth is clamped to 100')\n .default(5),\n\n /** Maximum total pages to visit. */\n maxPages: z\n .number()\n .int()\n .min(1, 'Max pages must be >= 1')\n .max(100_000, 'Max pages is clamped to 100,000')\n .default(1000),\n\n /** URL glob patterns to include. Only URLs matching at least one pattern are crawled. */\n include: z.array(z.string()).default([]),\n\n /** URL glob patterns to exclude. URLs matching any pattern are skipped. */\n exclude: z.array(z.string()).default([]),\n\n /** Whether to follow links to external origins. Default: false (same-origin only). */\n followExternal: z.boolean().default(false),\n\n /** Enable Tier 3 clicking: click non-link interactive elements to discover SPA states. */\n deepClick: z.boolean().default(false),\n\n /** Crawl cross-origin iframe content. Default: false (record URL only). */\n includeIframes: z.boolean().default(false),\n\n // ---- Performance ----\n /** Number of parallel browser contexts. */\n concurrency: z\n .number()\n .int()\n .min(1, 'Concurrency must be >= 1')\n .max(20, 'Concurrency is clamped to 20')\n .default(3),\n\n /** Delay in milliseconds between page visits per context. */\n delay: z\n .number()\n .int()\n .min(0, 'Delay must be >= 0')\n .max(60_000, 'Delay must be <= 60000ms')\n .default(0),\n\n /** Per-page navigation timeout in milliseconds. */\n timeout: z\n .number()\n .int()\n .min(1000, 'Timeout must be >= 1000ms')\n .max(300_000, 'Timeout must be <= 300000ms')\n .default(30_000),\n\n /** Global crawl timeout in seconds. 0 = no limit. */\n maxTime: z\n .number()\n .int()\n .min(0, 'Max time must be >= 0')\n .max(86_400, 'Max time must be <= 86400s (24 hours)')\n .default(3600),\n\n // ---- Auth ----\n /** Path to auth script (.ts or .js) exporting a default async function. */\n authScript: z.string().optional(),\n\n /** Path to cookies JSON file for session injection. */\n cookiesFile: z.string().optional(),\n\n /** Include cookies/auth headers in output (default: scrubbed). */\n includeCookies: z.boolean().default(false),\n\n // ---- Output ----\n /** Output directory path. Created if it does not exist. Parent must exist. */\n outputDir: z.string().default('.archaeologist'),\n\n /** Output format(s). */\n format: OutputFormatSchema.default('both'),\n\n /** Skip screenshot capture entirely. */\n noScreenshots: z.boolean().default(false),\n\n /** Skip HAR recording entirely. */\n noHar: z.boolean().default(false),\n\n // ---- Display ----\n /** Primary viewport dimensions. */\n viewport: ViewportSchema.default({ width: 1280, height: 720 }),\n\n /** Additional viewports for responsive screenshots (empty = primary only). */\n additionalViewports: z.array(ViewportSchema).default([]),\n\n // ---- Resume ----\n /** Resume from last checkpoint in the output directory. */\n resume: z.boolean().default(false),\n\n /** Skip confirmation prompts (auth script execution, etc.). */\n yes: z.boolean().default(false),\n\n // ---- Security ----\n /** Allow crawling private/internal IP ranges (SSRF protection bypass). */\n allowPrivate: z.boolean().default(false),\n\n // ---- Diff (for `pa diff` subcommand) ----\n /** Pixel diff threshold (0-1 scale, lower = more sensitive). */\n diffThreshold: z\n .number()\n .min(0, 'Diff threshold must be >= 0')\n .max(1, 'Diff threshold must be <= 1')\n .default(0.1),\n\n /** Maximum diff ratio (0-100%) before a screenshot is considered \"changed\". */\n diffMaxRatio: z\n .number()\n .min(0, 'Diff max ratio must be >= 0')\n .max(100, 'Diff max ratio must be <= 100')\n .default(0.5),\n\n /** Fields to ignore during API diff (comma-separated or array). */\n diffIgnoreFields: z.array(z.string()).default([\n 'timestamp', 'createdAt', 'updatedAt', 'date',\n 'requestId', 'traceId', 'correlationId',\n 'token', 'nonce', 'csrf',\n 'etag', 'lastModified',\n ]),\n});\n\nexport type CrawlConfig = z.infer<typeof CrawlConfigSchema>;\n\n// ---------------------------------------------------------------------------\n// Diff subcommand config\n// ---------------------------------------------------------------------------\n\nexport const DiffConfigSchema = z.object({\n /** Path to the \"old\" .archaeologist bundle. */\n oldBundle: z.string(),\n\n /** Path to the \"new\" .archaeologist bundle. */\n newBundle: z.string(),\n\n /** Output directory for diff report and artifacts. */\n outputDir: z.string().default('.archaeologist'),\n\n /** Pixel diff threshold. */\n diffThreshold: CrawlConfigSchema.shape.diffThreshold,\n\n /** Maximum diff ratio. */\n diffMaxRatio: CrawlConfigSchema.shape.diffMaxRatio,\n\n /** Fields to ignore during API response diff. */\n diffIgnoreFields: CrawlConfigSchema.shape.diffIgnoreFields,\n\n /** Normalize dynamic values (UUIDs, timestamps, JWTs) before diffing. */\n normalizeDynamicValues: z.boolean().default(false),\n\n /** Show detailed value-level diffs (default: schema-level only). */\n detailed: z.boolean().default(false),\n\n /** Output formats for the diff report. */\n outputFormats: z.object({\n html: z.string().optional(),\n json: z.string().optional(),\n junit: z.string().optional(),\n markdown: z.string().optional(),\n }).default({}),\n});\n\nexport type DiffConfig = z.infer<typeof DiffConfigSchema>;\n\n// ---------------------------------------------------------------------------\n// Resolved runtime config (after validation, path resolution, etc.)\n// ---------------------------------------------------------------------------\n\n/**\n * ResolvedConfig is the fully validated and resolved config passed to\n * the Orchestrator. All paths are absolute, all defaults are applied,\n * and the entry URL is normalized.\n */\nexport interface ResolvedConfig extends CrawlConfig {\n /** Absolute path to the resolved output directory. */\n outputDir: string;\n /** Absolute path to the auth script, if provided. */\n authScript?: string;\n /** Absolute path to the cookies file, if provided. */\n cookiesFile?: string;\n /** Warnings generated during config resolution (e.g. auto-prepended https://). */\n warnings: string[];\n}\n","/**\n * playwright-archaeologist error types.\n *\n * Error hierarchy:\n * ArchaeologistError (base)\n * +-- ConfigError Invalid CLI options or config file\n * +-- AuthError Auth script loading, execution, or validation failure\n * +-- CrawlError (base) Any error during the crawl phase\n * | +-- NavigationError page.goto failure (timeout, network, HTTP >= 400)\n * | +-- CollectorError A collector (scanner/prober/logger/capturer) failed on a page\n * | +-- BrowserContextError Context crashed or was killed\n * | +-- FrontierError URL queue corruption or checkpoint I/O failure\n * | +-- SecurityBlockError SSRF or protocol violation blocked a request\n * +-- AssemblerError Data merging or dedup failure\n * +-- ReportError Report generation failure (HTML, JSON, OpenAPI, Mermaid)\n * +-- DiffError Bundle loading, comparison, or diff report failure\n * +-- BundleError .archaeologist bundle I/O or integrity failure\n *\n * Error propagation rules:\n * - NavigationError on one page: log + skip page, crawl continues\n * - CollectorError on one page: log + partial data, crawl continues\n * - BrowserContextError: recycle context, re-enqueue URL, crawl continues\n * - FrontierError (checkpoint): warn, crawl continues without checkpointing\n * - SecurityBlockError: log + skip URL, crawl continues\n * - ConfigError: abort with user-facing message before crawl starts\n * - AuthError: abort with user-facing message before crawl starts\n * - AssemblerError: abort, partial output may exist\n * - ReportError: degrade gracefully (skip failed section, warn)\n * - DiffError: abort with exit code 2\n * - BundleError: abort with exit code 2\n */\n\n// ---------------------------------------------------------------------------\n// Base error\n// ---------------------------------------------------------------------------\n\nexport class ArchaeologistError extends Error {\n /** Machine-readable error code for programmatic handling. */\n readonly code: string;\n\n constructor(message: string, code: string, options?: ErrorOptions) {\n super(message, options);\n this.name = 'ArchaeologistError';\n this.code = code;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Configuration errors (detected before crawl starts)\n// ---------------------------------------------------------------------------\n\nexport class ConfigError extends ArchaeologistError {\n /** The config field that failed validation (e.g. \"depth\", \"viewport\"). */\n readonly field: string;\n /** The invalid value the user supplied. */\n readonly value: unknown;\n\n constructor(field: string, message: string, value?: unknown) {\n super(message, 'ERR_CONFIG');\n this.name = 'ConfigError';\n this.field = field;\n this.value = value;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Auth errors (detected before or at the start of crawl)\n// ---------------------------------------------------------------------------\n\nexport type AuthErrorReason =\n | 'script_not_found'\n | 'script_import_failed'\n | 'script_no_default_export'\n | 'script_execution_failed'\n | 'script_dangerous_import'\n | 'script_user_declined'\n | 'cookie_file_not_found'\n | 'cookie_file_malformed'\n | 'auth_verification_failed';\n\nexport class AuthError extends ArchaeologistError {\n readonly reason: AuthErrorReason;\n readonly scriptPath?: string;\n\n constructor(reason: AuthErrorReason, message: string, scriptPath?: string, options?: ErrorOptions) {\n super(message, 'ERR_AUTH', options);\n this.name = 'AuthError';\n this.reason = reason;\n this.scriptPath = scriptPath;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Crawl errors (per-page or per-context failures during crawl)\n// ---------------------------------------------------------------------------\n\nexport class CrawlError extends ArchaeologistError {\n /** The URL being processed when the error occurred. */\n readonly url: string;\n\n constructor(message: string, code: string, url: string, options?: ErrorOptions) {\n super(message, code, options);\n this.name = 'CrawlError';\n this.url = url;\n }\n}\n\nexport type NavigationStatus =\n | 'timeout'\n | 'network_error'\n | 'http_error'\n | 'no_response'\n | 'aborted'\n | 'redirect_loop';\n\nexport class NavigationError extends CrawlError {\n readonly status: NavigationStatus;\n /** HTTP status code, if applicable (e.g. 404, 500). */\n readonly httpStatus?: number;\n\n constructor(url: string, status: NavigationStatus, message: string, httpStatus?: number, options?: ErrorOptions) {\n super(message, 'ERR_NAVIGATION', url, options);\n this.name = 'NavigationError';\n this.status = status;\n this.httpStatus = httpStatus;\n }\n}\n\nexport type CollectorName = 'page-scanner' | 'form-prober' | 'network-logger' | 'screenshot-capturer';\n\nexport class CollectorError extends CrawlError {\n readonly collector: CollectorName;\n\n constructor(collector: CollectorName, url: string, message: string, options?: ErrorOptions) {\n super(message, 'ERR_COLLECTOR', url, options);\n this.name = 'CollectorError';\n this.collector = collector;\n }\n}\n\nexport class BrowserContextError extends CrawlError {\n readonly contextId: number;\n\n constructor(contextId: number, url: string, message: string, options?: ErrorOptions) {\n super(message, 'ERR_BROWSER_CONTEXT', url, options);\n this.name = 'BrowserContextError';\n this.contextId = contextId;\n }\n}\n\nexport class FrontierError extends ArchaeologistError {\n readonly operation: 'enqueue' | 'dequeue' | 'checkpoint_write' | 'checkpoint_read';\n\n constructor(operation: FrontierError['operation'], message: string, options?: ErrorOptions) {\n super(message, 'ERR_FRONTIER', options);\n this.name = 'FrontierError';\n this.operation = operation;\n }\n}\n\nexport type SecurityBlockReason =\n | 'private_ip'\n | 'blocked_protocol'\n | 'dns_rebinding'\n | 'metadata_endpoint'\n | 'redirect_to_private';\n\nexport class SecurityBlockError extends CrawlError {\n readonly reason: SecurityBlockReason;\n /** The resolved IP address that triggered the block, if applicable. */\n readonly resolvedIp?: string;\n\n constructor(url: string, reason: SecurityBlockReason, message: string, resolvedIp?: string, options?: ErrorOptions) {\n super(message, 'ERR_SECURITY_BLOCK', url, options);\n this.name = 'SecurityBlockError';\n this.reason = reason;\n this.resolvedIp = resolvedIp;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Assembler errors (post-crawl data merging)\n// ---------------------------------------------------------------------------\n\nexport class AssemblerError extends ArchaeologistError {\n readonly phase: 'merge' | 'deduplicate' | 'route_tree' | 'api_grouping' | 'flow_graph';\n\n constructor(phase: AssemblerError['phase'], message: string, options?: ErrorOptions) {\n super(message, 'ERR_ASSEMBLER', options);\n this.name = 'AssemblerError';\n this.phase = phase;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Report errors (output generation failures)\n// ---------------------------------------------------------------------------\n\nexport type ReportSection =\n | 'html_template'\n | 'sitemap'\n | 'forms'\n | 'api_map'\n | 'screenshots'\n | 'flow_graph'\n | 'mermaid_render'\n | 'json_output'\n | 'openapi_output'\n | 'thumbnail_generation';\n\nexport class ReportError extends ArchaeologistError {\n readonly section: ReportSection;\n /**\n * Whether this section failure is recoverable (report can still be produced\n * without it). Mermaid render failure is recoverable; html_template is not.\n */\n readonly recoverable: boolean;\n\n constructor(section: ReportSection, message: string, recoverable: boolean, options?: ErrorOptions) {\n super(message, 'ERR_REPORT', options);\n this.name = 'ReportError';\n this.section = section;\n this.recoverable = recoverable;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Diff errors\n// ---------------------------------------------------------------------------\n\nexport type DiffErrorReason =\n | 'bundle_not_found'\n | 'bundle_corrupt'\n | 'manifest_invalid'\n | 'incompatible_config'\n | 'screenshot_size_mismatch'\n | 'diff_generation_failed';\n\nexport class DiffError extends ArchaeologistError {\n readonly reason: DiffErrorReason;\n\n constructor(reason: DiffErrorReason, message: string, options?: ErrorOptions) {\n super(message, 'ERR_DIFF', options);\n this.name = 'DiffError';\n this.reason = reason;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Bundle errors (.archaeologist ZIP I/O)\n// ---------------------------------------------------------------------------\n\nexport type BundleErrorReason =\n | 'create_failed'\n | 'extract_failed'\n | 'integrity_check_failed'\n | 'missing_manifest'\n | 'unsupported_version'\n | 'output_dir_not_writable';\n\nexport class BundleError extends ArchaeologistError {\n readonly reason: BundleErrorReason;\n readonly bundlePath?: string;\n\n constructor(reason: BundleErrorReason, message: string, bundlePath?: string, options?: ErrorOptions) {\n super(message, 'ERR_BUNDLE', options);\n this.name = 'BundleError';\n this.reason = reason;\n this.bundlePath = bundlePath;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Error classification helpers\n// ---------------------------------------------------------------------------\n\n/** Errors that should stop the entire crawl. */\nexport function isFatalError(err: unknown): boolean {\n if (err instanceof ConfigError) return true;\n if (err instanceof AuthError) return true;\n if (err instanceof AssemblerError) return true;\n if (err instanceof BundleError && err.reason === 'output_dir_not_writable') return true;\n return false;\n}\n\n/** Errors that allow the crawl to continue after logging. */\nexport function isRecoverableError(err: unknown): boolean {\n if (err instanceof NavigationError) return true;\n if (err instanceof CollectorError) return true;\n if (err instanceof SecurityBlockError) return true;\n if (err instanceof BrowserContextError) return true;\n if (err instanceof FrontierError && err.operation === 'checkpoint_write') return true;\n if (err instanceof ReportError && err.recoverable) return true;\n return false;\n}\n\n/** Errors that require context recycling (close and recreate browser context). */\nexport function requiresContextRecycle(err: unknown): boolean {\n return err instanceof BrowserContextError;\n}\n\n// ---------------------------------------------------------------------------\n// Crawl error log entry (for structured logging and report error summary)\n// ---------------------------------------------------------------------------\n\nexport interface CrawlErrorEntry {\n timestamp: string;\n url: string;\n code: string;\n message: string;\n collector?: CollectorName;\n status?: NavigationStatus;\n httpStatus?: number;\n securityReason?: SecurityBlockReason;\n}\n\nexport function toCrawlErrorEntry(err: CrawlError): CrawlErrorEntry {\n const entry: CrawlErrorEntry = {\n timestamp: new Date().toISOString(),\n url: err.url,\n code: err.code,\n message: err.message,\n };\n if (err instanceof CollectorError) entry.collector = err.collector;\n if (err instanceof NavigationError) {\n entry.status = err.status;\n entry.httpStatus = err.httpStatus;\n }\n if (err instanceof SecurityBlockError) entry.securityReason = err.reason;\n return entry;\n}\n","/**\n * Leveled logger for playwright-archaeologist.\n *\n * Provides debug, info, warn, error, and success log methods\n * with timestamps and ANSI color output. Debug messages are\n * suppressed unless verbose mode is enabled.\n */\n\n// ANSI color codes — no external dependency needed.\nconst RESET = '\\x1b[0m';\nconst DIM = '\\x1b[2m';\nconst RED = '\\x1b[31m';\nconst YELLOW = '\\x1b[33m';\nconst GREEN = '\\x1b[32m';\nconst CYAN = '\\x1b[36m';\nconst BOLD = '\\x1b[1m';\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n};\n\nfunction timestamp(): string {\n const now = new Date();\n const h = String(now.getHours()).padStart(2, '0');\n const m = String(now.getMinutes()).padStart(2, '0');\n const s = String(now.getSeconds()).padStart(2, '0');\n const ms = String(now.getMilliseconds()).padStart(3, '0');\n return `${h}:${m}:${s}.${ms}`;\n}\n\nexport class Logger {\n private level: LogLevel;\n\n constructor(level: LogLevel = 'info') {\n this.level = level;\n }\n\n /** Update the minimum log level at runtime. */\n setLevel(level: LogLevel): void {\n this.level = level;\n }\n\n /** Get the current minimum log level. */\n getLevel(): LogLevel {\n return this.level;\n }\n\n /** Check whether a given level would be emitted. */\n isLevelEnabled(level: LogLevel): boolean {\n return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.level];\n }\n\n debug(message: string, ...args: unknown[]): void {\n if (!this.isLevelEnabled('debug')) return;\n const ts = `${DIM}${timestamp()}${RESET}`;\n const prefix = `${CYAN}DEBUG${RESET}`;\n // eslint-disable-next-line no-console\n console.error(`${ts} ${prefix} ${message}`, ...args);\n }\n\n info(message: string, ...args: unknown[]): void {\n if (!this.isLevelEnabled('info')) return;\n const ts = `${DIM}${timestamp()}${RESET}`;\n const prefix = `${BOLD}INFO${RESET}`;\n // eslint-disable-next-line no-console\n console.error(`${ts} ${prefix} ${message}`, ...args);\n }\n\n warn(message: string, ...args: unknown[]): void {\n if (!this.isLevelEnabled('warn')) return;\n const ts = `${DIM}${timestamp()}${RESET}`;\n const prefix = `${YELLOW}WARN${RESET}`;\n // eslint-disable-next-line no-console\n console.error(`${ts} ${prefix} ${message}`, ...args);\n }\n\n error(message: string, ...args: unknown[]): void {\n if (!this.isLevelEnabled('error')) return;\n const ts = `${DIM}${timestamp()}${RESET}`;\n const prefix = `${RED}ERROR${RESET}`;\n // eslint-disable-next-line no-console\n console.error(`${ts} ${prefix} ${message}`, ...args);\n }\n\n /** Success message — always shown at info level or below. */\n success(message: string, ...args: unknown[]): void {\n if (!this.isLevelEnabled('info')) return;\n const ts = `${DIM}${timestamp()}${RESET}`;\n const prefix = `${GREEN}OK${RESET}`;\n // eslint-disable-next-line no-console\n console.error(`${ts} ${prefix} ${message}`, ...args);\n }\n}\n\n/**\n * Shared singleton logger instance.\n * The CLI sets its level to 'debug' when --verbose is passed.\n */\nexport const logger = new Logger('info');\n","/**\n * URL Frontier -- BFS queue with deduplication.\n *\n * Maintains a queue of URLs to visit with:\n * - BFS ordering (FIFO)\n * - Deduplication using normalized URLs\n * - Depth tracking per URL\n *\n * Uses a dequeue pointer instead of Array.shift() to avoid O(n)\n * cost on large queues. Compacts the internal array when more than\n * half of its entries have been consumed.\n */\n\nimport { normalizeUrl } from './url-utils';\n\nexport interface FrontierEntry {\n url: string;\n depth: number;\n referrer?: string;\n}\n\nexport class Frontier {\n private queue: FrontierEntry[] = [];\n private head: number = 0;\n private seen: Set<string> = new Set();\n private maxDepth: number;\n\n constructor(options: { maxDepth?: number } = {}) {\n this.maxDepth = options.maxDepth ?? Infinity;\n }\n\n /**\n * Add a URL to the queue. Returns false if already seen or exceeds max depth.\n */\n enqueue(entry: FrontierEntry): boolean {\n if (entry.depth > this.maxDepth) {\n return false;\n }\n\n const normalized = normalizeUrl(entry.url);\n\n if (this.seen.has(normalized)) {\n return false;\n }\n\n this.seen.add(normalized);\n this.queue.push(entry);\n return true;\n }\n\n /**\n * Remove and return the next URL from the queue (FIFO).\n * Returns undefined if queue is empty.\n */\n dequeue(): FrontierEntry | undefined {\n if (this.head >= this.queue.length) {\n return undefined;\n }\n\n const entry = this.queue[this.head++];\n\n // Compact when more than half the array is consumed\n if (this.head > this.queue.length / 2) {\n this.queue = this.queue.slice(this.head);\n this.head = 0;\n }\n\n return entry;\n }\n\n /**\n * Check if a URL has been seen (ever enqueued, whether still queued or already dequeued).\n */\n hasSeen(url: string): boolean {\n const normalized = normalizeUrl(url);\n return this.seen.has(normalized);\n }\n\n /**\n * Get the number of URLs remaining in the queue.\n */\n get size(): number {\n return this.queue.length - this.head;\n }\n\n /**\n * Get the total number of URLs seen (visited + queued).\n */\n get totalSeen(): number {\n return this.seen.size;\n }\n\n /**\n * Check if the queue is empty.\n */\n get isEmpty(): boolean {\n return this.head >= this.queue.length;\n }\n}\n","/**\n * Output Sanitizer\n *\n * Prevents path traversal, sanitizes filenames from URLs,\n * and validates output paths.\n */\n\nimport { resolve, basename, extname, isAbsolute, normalize, sep } from 'node:path';\nimport { accessSync, constants } from 'node:fs';\nimport { platform } from 'node:os';\n\n// -------------------------------------------------------------------------\n// Windows reserved device names\n// -------------------------------------------------------------------------\nconst WINDOWS_RESERVED = new Set([\n 'CON', 'PRN', 'AUX', 'NUL',\n 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',\n 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',\n]);\n\n// -------------------------------------------------------------------------\n// System directories that must never be used as output directories\n// -------------------------------------------------------------------------\nconst UNIX_SYSTEM_DIRS = new Set([\n '/etc',\n '/usr',\n '/bin',\n '/sbin',\n '/var',\n '/boot',\n '/dev',\n '/proc',\n '/sys',\n '/lib',\n '/lib64',\n '/root',\n]);\n\nconst WINDOWS_SYSTEM_DIRS_LOWER = new Set([\n 'c:\\\\windows',\n 'c:\\\\program files',\n 'c:\\\\program files (x86)',\n 'c:\\\\system32',\n]);\n\n/**\n * Check if a path is a blocked system directory.\n * Checks both the resolved path AND the original input for Unix system paths\n * (to correctly block /etc/passwd even on Windows where it resolves to C:\\etc\\passwd).\n */\nfunction isSystemDirectory(absolutePath: string, originalInput?: string): boolean {\n const normalized = normalize(absolutePath);\n\n // Unix system dirs — check resolved path\n for (const dir of UNIX_SYSTEM_DIRS) {\n if (normalized === dir || normalized.startsWith(dir + '/')) {\n return true;\n }\n }\n\n // Also check the original input for Unix-style system paths (cross-platform safety)\n if (originalInput) {\n const cleanInput = originalInput.replace(/\\\\/g, '/');\n for (const dir of UNIX_SYSTEM_DIRS) {\n if (cleanInput === dir || cleanInput.startsWith(dir + '/')) {\n return true;\n }\n }\n }\n\n // Windows system dirs (case-insensitive)\n const lower = normalized.toLowerCase().replace(/\\//g, '\\\\');\n for (const dir of WINDOWS_SYSTEM_DIRS_LOWER) {\n if (lower === dir || lower.startsWith(dir + '\\\\')) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Sanitize a filename derived from user input or URL path.\n * - Removes path traversal sequences (../, ..\\)\n * - Removes null bytes\n * - Replaces Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)\n * - Truncates very long filenames (preserving extension)\n * - Handles Unicode\n * - Replaces special characters with safe alternatives\n * - Collapses consecutive underscores\n */\nexport function sanitizeFilename(input: string): string {\n if (!input) return '_unnamed';\n\n let result = input;\n\n // Decode percent-encoding\n try {\n result = decodeURIComponent(result);\n } catch {\n // If decoding fails, continue with the raw string\n }\n\n // Remove null bytes\n result = result.replace(/\\0/g, '');\n\n // Remove path traversal sequences\n result = result.replace(/\\.\\.\\//g, '');\n result = result.replace(/\\.\\.\\\\/g, '');\n result = result.replace(/\\.\\./g, '');\n\n // Replace characters not allowed in filenames: / \\ : * ? \" < > | and control chars\n // Keep letters, digits, dots, hyphens, underscores, and Unicode word characters\n result = result.replace(/[/\\\\:*?\"<>|\\x00-\\x1F\\x7F]/g, '_');\n\n // Remove any remaining potentially dangerous characters for strict mode\n // Allow: alphanumeric, dot, hyphen, underscore\n result = result.replace(/[^a-zA-Z0-9._-]/g, '_');\n\n // Collapse consecutive underscores\n result = result.replace(/_{2,}/g, '_');\n\n // Remove leading/trailing underscores and dots (except a single dot before extension)\n result = result.replace(/^[_]+/, '');\n result = result.replace(/[_]+$/, '');\n\n // If empty after sanitization, use fallback\n if (!result || result === '.' || result === '..') {\n return '_unnamed';\n }\n\n // Handle Windows reserved names\n const ext = extname(result);\n const nameWithoutExt = ext ? result.slice(0, -ext.length) : result;\n if (WINDOWS_RESERVED.has(nameWithoutExt.toUpperCase())) {\n result = `_${nameWithoutExt}${ext}`;\n }\n\n // Truncate to 255 chars (preserving extension)\n const MAX_LENGTH = 255;\n if (result.length > MAX_LENGTH) {\n const extension = extname(result);\n if (extension && extension.length < 20) {\n const maxBase = MAX_LENGTH - extension.length;\n result = result.slice(0, maxBase) + extension;\n } else {\n result = result.slice(0, MAX_LENGTH);\n }\n }\n\n return result;\n}\n\n/**\n * Validate and resolve an output path.\n * For relative paths: resolves against the base directory (outputDir/cwd).\n * For absolute paths: accepts if not a system directory or path-traversal.\n * Rejects null bytes, empty paths, and paths to system directories.\n * Returns the resolved absolute path.\n */\nexport function validateOutputPath(\n requestedPath: string,\n outputDir: string\n): string {\n // Reject empty paths\n if (!requestedPath) {\n throw new Error('Output path must not be empty');\n }\n\n // Reject null bytes\n if (requestedPath.includes('\\0')) {\n throw new Error('Output path must not contain null bytes');\n }\n\n // Resolve the path\n let resolved: string;\n if (isAbsolute(requestedPath)) {\n resolved = resolve(requestedPath);\n } else {\n resolved = resolve(outputDir, requestedPath);\n }\n\n // Normalize\n resolved = normalize(resolved);\n\n // Check for system directory\n if (isSystemDirectory(resolved, requestedPath)) {\n throw new Error(`Output path '${resolved}' is a protected system directory`);\n }\n\n // For relative paths, verify the resolved path doesn't escape the base directory\n if (!isAbsolute(requestedPath)) {\n const normalizedBase = normalize(resolve(outputDir));\n if (!resolved.startsWith(normalizedBase)) {\n throw new Error(\n `Path traversal detected: '${requestedPath}' resolves outside '${normalizedBase}'`\n );\n }\n }\n\n // For absolute paths that are outside the cwd, check they're not escaping\n // via traversal in the original string\n if (isAbsolute(requestedPath)) {\n // Check if it's a system directory (already done above)\n // Also check permissions on Unix if the directory exists\n if (platform() !== 'win32') {\n try {\n accessSync(resolved, constants.W_OK);\n } catch (err: any) {\n if (err?.code === 'EACCES') {\n throw new Error(\n `Output directory '${resolved}' is not writable — check permission`\n );\n }\n // ENOENT is OK — directory will be created later\n }\n }\n }\n\n return resolved;\n}\n\n/**\n * Convert a URL to a safe filename for screenshots/artifacts.\n * Produces a deterministic, human-readable filename from a URL.\n */\nexport function urlToFilename(url: string): string {\n let result: string;\n\n try {\n const parsed = new URL(url);\n // Combine host and path for a meaningful filename\n result = parsed.hostname + parsed.pathname;\n if (parsed.search) {\n result += parsed.search;\n }\n } catch {\n // If URL parsing fails, use the raw string\n result = url;\n }\n\n // Sanitize through the filename sanitizer\n return sanitizeFilename(result);\n}\n","/**\n * ScreenshotCapturer collector — captures full-page and viewport screenshots.\n *\n * Receives a Playwright Page that has already navigated to the target URL.\n * Takes two screenshots per page:\n * 1. Full-page screenshot (captures entire scrollable area)\n * 2. Viewport-only screenshot (captures visible area only)\n *\n * Before capturing, performs limited scrolling (up to 3 viewport increments)\n * to trigger lazy-loaded content, then scrolls back to the top.\n *\n * Detects visible modals/dialogs on the page for metadata.\n * Gracefully degrades: returns an empty result if screenshots fail.\n */\n\nimport type { Page } from 'playwright';\nimport type { Viewport } from '../types/config.js';\nimport type { ScreenshotResult } from '../types/artifacts.js';\nimport { urlToFilename } from '../security/output-sanitizer.js';\nimport { createHash } from 'node:crypto';\nimport { mkdir, readFile, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_VIEWPORT: Viewport = { width: 1280, height: 720 };\nconst MAX_SCROLL_INCREMENTS = 3;\nconst SCROLL_SETTLE_MS = 200;\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Capture full-page and viewport screenshots of a page.\n *\n * @param page Playwright Page already navigated to the target URL.\n * @param baseUrl The crawl's starting URL (unused here, kept for collector interface consistency).\n * @param outputDir Directory where screenshot files will be saved.\n * @param viewport Viewport dimensions used for capture. Defaults to 1280x720.\n * @returns ScreenshotResult with paths, hashes, and metadata.\n */\nexport async function captureScreenshots(\n page: Page,\n baseUrl: string,\n outputDir: string,\n viewport?: Viewport,\n): Promise<ScreenshotResult> {\n const effectiveViewport = viewport ?? DEFAULT_VIEWPORT;\n const pageUrl = page.url();\n\n // Build deterministic file paths from the page URL\n const baseName = urlToFilename(pageUrl);\n const fullPageFilename = `${baseName}_full.png`;\n const viewportFilename = `${baseName}_viewport.png`;\n const fullPagePath = join(outputDir, fullPageFilename);\n const viewportPath = join(outputDir, viewportFilename);\n\n try {\n // Ensure the output directory exists\n await mkdir(outputDir, { recursive: true });\n\n // Scroll to trigger lazy-loaded content, then scroll back to top\n await triggerLazyContent(page, effectiveViewport);\n\n // Detect modals/dialogs before taking screenshots\n const modalDetected = await detectModals(page);\n\n // Take full-page screenshot\n const fullPageBuffer = await page.screenshot({ fullPage: true, path: fullPagePath });\n\n // Take viewport-only screenshot\n const viewportBuffer = await page.screenshot({ path: viewportPath });\n\n // Compute SHA-256 hashes\n const fullPageHash = computeSha256(fullPageBuffer);\n const viewportHash = computeSha256(viewportBuffer);\n\n // Extract dimensions from the full-page PNG buffer\n const dimensions = extractPngDimensions(fullPageBuffer);\n\n // Get file size of the full-page screenshot\n const fileSizeBytes = await getFileSize(fullPagePath, fullPageBuffer);\n\n return {\n pageUrl,\n fullPagePath,\n viewportPath,\n viewport: effectiveViewport,\n fullPageHash,\n viewportHash,\n dimensions,\n fileSizeBytes,\n modalDetected,\n };\n } catch {\n // Graceful failure: return a result with empty paths and zero hashes\n return {\n pageUrl,\n fullPagePath: '',\n viewportPath: '',\n viewport: effectiveViewport,\n fullPageHash: '0'.repeat(64),\n viewportHash: '0'.repeat(64),\n dimensions: { width: 0, height: 0 },\n fileSizeBytes: 0,\n modalDetected: false,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Lazy-content scrolling\n// ---------------------------------------------------------------------------\n\n/**\n * Scroll down in viewport-sized increments (up to MAX_SCROLL_INCREMENTS)\n * to trigger lazy-loaded images and content, then scroll back to top.\n */\nasync function triggerLazyContent(page: Page, viewport: Viewport): Promise<void> {\n const scrollHeight = await page.evaluate(() => document.documentElement.scrollHeight);\n const viewportHeight = viewport.height;\n const maxScrollDistance = viewportHeight * MAX_SCROLL_INCREMENTS;\n const targetScroll = Math.min(scrollHeight, maxScrollDistance);\n\n for (let scrolled = viewportHeight; scrolled <= targetScroll; scrolled += viewportHeight) {\n await page.evaluate((y: number) => window.scrollTo(0, y), scrolled);\n // Brief wait for lazy content to begin loading\n await page.evaluate(\n (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)),\n SCROLL_SETTLE_MS,\n );\n }\n\n // Scroll back to top\n await page.evaluate(() => window.scrollTo(0, 0));\n}\n\n// ---------------------------------------------------------------------------\n// Modal/dialog detection\n// ---------------------------------------------------------------------------\n\n/**\n * Check if any modal or dialog overlay is currently visible on the page.\n */\nasync function detectModals(page: Page): Promise<boolean> {\n const modalSelector = 'dialog[open], [role=\"dialog\"], .modal.show, .modal.active';\n const locator = page.locator(modalSelector);\n try {\n const count = await locator.count();\n if (count === 0) return false;\n\n // Check if any matched element is actually visible\n for (let i = 0; i < count; i++) {\n const isVisible = await locator.nth(i).isVisible();\n if (isVisible) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Hashing\n// ---------------------------------------------------------------------------\n\n/**\n * Compute SHA-256 hex digest of a buffer.\n */\nfunction computeSha256(buffer: Buffer): string {\n return createHash('sha256').update(buffer).digest('hex');\n}\n\n// ---------------------------------------------------------------------------\n// PNG dimension extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract width and height from a PNG file buffer by reading the IHDR chunk.\n *\n * PNG structure: 8-byte signature, then IHDR chunk.\n * IHDR data starts at byte 16:\n * - Bytes 16-19: width (big-endian uint32)\n * - Bytes 20-23: height (big-endian uint32)\n */\nfunction extractPngDimensions(buffer: Buffer): { width: number; height: number } {\n if (buffer.length < 24) {\n return { width: 0, height: 0 };\n }\n\n const width = buffer.readUInt32BE(16);\n const height = buffer.readUInt32BE(20);\n\n return { width, height };\n}\n\n// ---------------------------------------------------------------------------\n// File size\n// ---------------------------------------------------------------------------\n\n/**\n * Get the file size. Uses stat if possible, falls back to buffer length.\n */\nasync function getFileSize(filePath: string, buffer: Buffer): Promise<number> {\n try {\n const stats = await stat(filePath);\n return stats.size;\n } catch {\n return buffer.length;\n }\n}\n","/// <reference lib=\"dom\" />\n\n/**\n * FormProber collector — extracts form metadata from a page.\n *\n * Receives a Playwright Page that has already navigated to the target URL.\n * Does NOT submit forms or trigger validation — it only reads the current DOM state.\n *\n * Extracts:\n * - Explicit <form> elements with all fields, options, and attributes\n * - Implicit forms (orphaned input groups outside any <form> wrapper)\n * - Submit button text\n * - Field labels, validation attributes, and default values\n * - Password field values are redacted\n */\n\nimport type { Page } from 'playwright';\nimport type {\n FormProbeResult,\n FormInfo,\n FormField,\n FormFieldOption,\n} from '../types/artifacts.js';\n\n// ---------------------------------------------------------------------------\n// Types for the raw data returned by page.evaluate inside a form context\n// ---------------------------------------------------------------------------\n\ninterface RawFormData {\n action: string;\n method: string;\n id: string | null;\n name: string | null;\n enctype: string | null;\n ariaLabel: string | null;\n fields: RawFieldData[];\n submitButtonText: string | null;\n}\n\ninterface RawFieldData {\n name: string;\n type: string;\n required: boolean;\n pattern: string | null;\n placeholder: string | null;\n label: string | null;\n options: FormFieldOption[] | null;\n min: string | null;\n max: string | null;\n step: string | null;\n maxLength: number | null;\n multiple: boolean;\n accept: string | null;\n defaultValue: string | null;\n isPassword: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Probe all forms on the page and extract their metadata.\n *\n * @param page Playwright Page already navigated to the target URL.\n * @param pageUrl The URL of the page being probed.\n */\nexport async function probeForms(\n page: Page,\n pageUrl: string,\n): Promise<FormProbeResult> {\n const forms: FormInfo[] = [];\n\n // 1. Extract explicit <form> elements using locators\n const formLocators = await page.locator('form').all();\n\n for (const formLocator of formLocators) {\n try {\n const rawForm = await formLocator.evaluate(\n (formEl: HTMLFormElement, currentPageUrl: string): RawFormData => {\n // --- Form-level attributes ---\n const rawAction = formEl.getAttribute('action');\n let action = '';\n if (rawAction !== null && rawAction !== '') {\n // Resolve relative action URLs to absolute using the browser's URL resolution\n try {\n action = new URL(rawAction, currentPageUrl).href;\n } catch {\n action = rawAction;\n }\n }\n\n const method = (formEl.getAttribute('method') ?? 'GET').toUpperCase();\n const id = formEl.getAttribute('id');\n const name = formEl.getAttribute('name');\n const enctype = formEl.getAttribute('enctype');\n const ariaLabel = formEl.getAttribute('aria-label');\n\n // --- Fields ---\n const fieldEls = formEl.querySelectorAll('input, select, textarea');\n const fields: RawFieldData[] = [];\n const processedRadioGroups = new Set<string>();\n\n fieldEls.forEach((el) => {\n const tagName = el.tagName.toLowerCase();\n const fieldName = el.getAttribute('name') ?? '';\n let fieldType: string;\n\n if (tagName === 'select') {\n fieldType = 'select';\n } else if (tagName === 'textarea') {\n fieldType = 'textarea';\n } else {\n fieldType = (el as HTMLInputElement).type ?? 'text';\n }\n\n // For radio/checkbox groups, collect all options under the same name\n if ((fieldType === 'radio' || fieldType === 'checkbox') && fieldName) {\n if (processedRadioGroups.has(fieldName)) return;\n processedRadioGroups.add(fieldName);\n\n const groupEls = formEl.querySelectorAll(\n `input[name=\"${CSS.escape(fieldName)}\"]`,\n );\n const options: FormFieldOption[] = [];\n groupEls.forEach((groupEl) => {\n const inp = groupEl as HTMLInputElement;\n // Derive label from associated label element or adjacent text\n const groupLabel = deriveLabel(groupEl);\n options.push({\n value: inp.value ?? '',\n label: groupLabel ?? inp.value ?? '',\n selected: inp.checked,\n });\n });\n\n const firstEl = groupEls[0] as HTMLInputElement;\n fields.push({\n name: fieldName,\n type: fieldType,\n required:\n firstEl.required ||\n firstEl.getAttribute('aria-required') === 'true',\n pattern: null,\n placeholder: null,\n label: deriveLabel(firstEl),\n options,\n min: null,\n max: null,\n step: null,\n maxLength: null,\n multiple: false,\n accept: null,\n defaultValue: null,\n isPassword: false,\n });\n return;\n }\n\n // Skip individual radio/checkbox elements that belong to a group\n // (they are handled above)\n if (fieldType === 'radio' || fieldType === 'checkbox') {\n // standalone radio/checkbox without a name — treat as individual field\n if (fieldName && processedRadioGroups.has(fieldName)) return;\n }\n\n // Regular field extraction\n const required =\n (el as HTMLInputElement).required ||\n el.getAttribute('aria-required') === 'true';\n const pattern = el.getAttribute('pattern');\n const placeholder = el.getAttribute('placeholder');\n const label = deriveLabel(el);\n\n // Select options\n let options: FormFieldOption[] | null = null;\n if (tagName === 'select') {\n const selectEl = el as HTMLSelectElement;\n options = [];\n const optionEls = selectEl.querySelectorAll('option');\n optionEls.forEach((opt) => {\n options!.push({\n value: opt.value,\n label: opt.textContent?.trim() ?? opt.value,\n selected: opt.selected,\n });\n });\n }\n\n // Numeric attributes\n const min = el.getAttribute('min');\n const max = el.getAttribute('max');\n const step = el.getAttribute('step');\n\n // MaxLength\n const maxLengthAttr = el.getAttribute('maxlength');\n const maxLength =\n maxLengthAttr !== null ? parseInt(maxLengthAttr, 10) : null;\n\n // Multiple\n const multiple =\n (el as HTMLInputElement).multiple ||\n (el as HTMLSelectElement).multiple ||\n false;\n\n // Accept (file inputs)\n const accept = el.getAttribute('accept');\n\n // Default value\n const isPassword = fieldType === 'password';\n let defaultValue: string | null = null;\n if (tagName === 'textarea') {\n defaultValue = (el as HTMLTextAreaElement).value || null;\n } else if (tagName === 'select') {\n const selectedOpt = (el as HTMLSelectElement).selectedOptions[0];\n defaultValue = selectedOpt?.value ?? null;\n } else {\n defaultValue = (el as HTMLInputElement).value || null;\n }\n\n fields.push({\n name: fieldName,\n type: fieldType,\n required,\n pattern,\n placeholder,\n label,\n options,\n min,\n max,\n step,\n maxLength: maxLength !== null && !isNaN(maxLength) ? maxLength : null,\n multiple,\n accept,\n defaultValue,\n isPassword,\n });\n });\n\n // --- Submit button ---\n let submitButtonText: string | null = null;\n const submitBtn =\n formEl.querySelector<HTMLButtonElement>('button[type=\"submit\"]') ??\n formEl.querySelector<HTMLInputElement>('input[type=\"submit\"]');\n if (submitBtn) {\n if (submitBtn.tagName.toLowerCase() === 'input') {\n submitButtonText = (submitBtn as HTMLInputElement).value || null;\n } else {\n submitButtonText = submitBtn.textContent?.trim() || null;\n }\n } else {\n // Fall back to first button inside the form\n const firstButton =\n formEl.querySelector<HTMLButtonElement>('button');\n if (firstButton) {\n submitButtonText = firstButton.textContent?.trim() || null;\n }\n }\n\n return {\n action,\n method,\n id,\n name,\n enctype,\n ariaLabel,\n fields,\n submitButtonText,\n };\n\n // --- Helper: derive label for a form element ---\n function deriveLabel(element: Element): string | null {\n // 1. Check aria-label\n const ariaLbl = element.getAttribute('aria-label');\n if (ariaLbl) return ariaLbl;\n\n // 2. Check aria-labelledby\n const labelledBy = element.getAttribute('aria-labelledby');\n if (labelledBy) {\n const labelEl = document.getElementById(labelledBy);\n if (labelEl) {\n const text = labelEl.textContent?.trim();\n if (text) return text;\n }\n }\n\n // 3. Check for associated <label> element\n const elId = element.getAttribute('id');\n if (elId) {\n const labelEl = document.querySelector<HTMLLabelElement>(\n `label[for=\"${CSS.escape(elId)}\"]`,\n );\n if (labelEl) {\n const text = labelEl.textContent?.trim();\n if (text) return text;\n }\n }\n\n // 4. Check for wrapping <label> element\n const parentLabel = element.closest('label');\n if (parentLabel) {\n const text = parentLabel.textContent?.trim();\n if (text) return text;\n }\n\n return null;\n }\n },\n pageUrl,\n );\n\n forms.push(convertRawFormToFormInfo(rawForm, false));\n } catch {\n // If extracting a single form fails, skip it (graceful degradation)\n continue;\n }\n }\n\n // 2. Detect implicit forms (orphaned inputs outside any <form> wrapper)\n try {\n const implicitForms = await page.evaluate(\n (currentPageUrl: string): RawFormData[] => {\n // Find all inputs/selects/textareas not inside a <form>\n const allFields = document.querySelectorAll(\n 'input, select, textarea',\n );\n const orphanedFields: Element[] = [];\n\n allFields.forEach((field) => {\n if (!field.closest('form')) {\n // Exclude hidden inputs and submit/button types\n const tagName = field.tagName.toLowerCase();\n const type = (field as HTMLInputElement).type;\n if (tagName === 'input' && (type === 'hidden' || type === 'submit' || type === 'button')) {\n return;\n }\n orphanedFields.push(field);\n }\n });\n\n if (orphanedFields.length === 0) return [];\n\n // Group orphaned fields — treat all orphaned fields as one implicit form\n const fields: RawFieldData[] = orphanedFields.map((el) => {\n const tagName = el.tagName.toLowerCase();\n const fieldName = el.getAttribute('name') ?? '';\n let fieldType: string;\n\n if (tagName === 'select') {\n fieldType = 'select';\n } else if (tagName === 'textarea') {\n fieldType = 'textarea';\n } else {\n fieldType = (el as HTMLInputElement).type ?? 'text';\n }\n\n const required =\n (el as HTMLInputElement).required ||\n el.getAttribute('aria-required') === 'true';\n\n // Select options\n let options: FormFieldOption[] | null = null;\n if (tagName === 'select') {\n options = [];\n const optionEls = (el as HTMLSelectElement).querySelectorAll('option');\n optionEls.forEach((opt) => {\n options!.push({\n value: opt.value,\n label: opt.textContent?.trim() ?? opt.value,\n selected: opt.selected,\n });\n });\n }\n\n const isPassword = fieldType === 'password';\n let defaultValue: string | null = null;\n if (tagName === 'textarea') {\n defaultValue = (el as HTMLTextAreaElement).value || null;\n } else if (tagName === 'select') {\n const selectedOpt = (el as HTMLSelectElement).selectedOptions[0];\n defaultValue = selectedOpt?.value ?? null;\n } else {\n defaultValue = (el as HTMLInputElement).value || null;\n }\n\n // Derive label\n let label: string | null = null;\n const ariaLbl = el.getAttribute('aria-label');\n if (ariaLbl) {\n label = ariaLbl;\n } else {\n const elId = el.getAttribute('id');\n if (elId) {\n const labelEl = document.querySelector<HTMLLabelElement>(\n `label[for=\"${CSS.escape(elId)}\"]`,\n );\n if (labelEl) {\n label = labelEl.textContent?.trim() ?? null;\n }\n }\n if (!label) {\n const parentLabel = el.closest('label');\n if (parentLabel) {\n label = parentLabel.textContent?.trim() ?? null;\n }\n }\n }\n\n return {\n name: fieldName,\n type: fieldType,\n required,\n pattern: el.getAttribute('pattern'),\n placeholder: el.getAttribute('placeholder'),\n label,\n options,\n min: el.getAttribute('min'),\n max: el.getAttribute('max'),\n step: el.getAttribute('step'),\n maxLength: el.getAttribute('maxlength')\n ? parseInt(el.getAttribute('maxlength')!, 10)\n : null,\n multiple: (el as HTMLInputElement).multiple || false,\n accept: el.getAttribute('accept'),\n defaultValue,\n isPassword,\n };\n });\n\n // Look for a submit button near the orphaned fields\n let submitButtonText: string | null = null;\n const submitBtns = document.querySelectorAll(\n 'button[type=\"submit\"], input[type=\"submit\"]',\n );\n submitBtns.forEach((btn) => {\n if (!btn.closest('form') && !submitButtonText) {\n if (btn.tagName.toLowerCase() === 'input') {\n submitButtonText = (btn as HTMLInputElement).value || null;\n } else {\n submitButtonText = btn.textContent?.trim() || null;\n }\n }\n });\n\n return [\n {\n action: '',\n method: 'GET',\n id: null,\n name: null,\n enctype: null,\n ariaLabel: null,\n fields,\n submitButtonText,\n },\n ];\n },\n pageUrl,\n );\n\n for (const rawForm of implicitForms) {\n forms.push(convertRawFormToFormInfo(rawForm, true));\n }\n } catch {\n // If implicit form detection fails, skip it (graceful degradation)\n }\n\n return {\n pageUrl,\n forms,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert raw form data from page.evaluate into a FormInfo object.\n * Strips null values into undefined and redacts password defaults.\n */\nfunction convertRawFormToFormInfo(\n raw: RawFormData,\n isImplicit: boolean,\n): FormInfo {\n const fields: FormField[] = raw.fields.map((f) => {\n const field: FormField = {\n name: f.name,\n type: f.type,\n required: f.required,\n };\n\n if (f.pattern != null) field.pattern = f.pattern;\n if (f.placeholder != null) field.placeholder = f.placeholder;\n if (f.label != null) field.label = f.label;\n if (f.options != null) field.options = f.options;\n if (f.min != null) field.min = f.min;\n if (f.max != null) field.max = f.max;\n if (f.step != null) field.step = f.step;\n if (f.maxLength != null) field.maxLength = f.maxLength;\n if (f.multiple) field.multiple = f.multiple;\n if (f.accept != null) field.accept = f.accept;\n\n // Redact password field default values\n if (f.isPassword && f.defaultValue != null) {\n field.defaultValue = '[REDACTED]';\n } else if (f.defaultValue != null) {\n field.defaultValue = f.defaultValue;\n }\n\n return field;\n });\n\n const formInfo: FormInfo = {\n action: raw.action,\n method: raw.method,\n isImplicit,\n fields,\n validationMessages: [],\n wasHidden: false,\n };\n\n if (raw.id != null) formInfo.id = raw.id;\n if (raw.name != null) formInfo.name = raw.name;\n if (raw.submitButtonText != null) formInfo.submitButtonText = raw.submitButtonText;\n if (raw.enctype != null) formInfo.enctype = raw.enctype;\n if (raw.ariaLabel != null) formInfo.ariaLabel = raw.ariaLabel;\n\n return formInfo;\n}\n","/**\n * Credential Scrubber\n *\n * Strips sensitive data from captured network traffic before output:\n * - Authorization headers (Bearer, Basic, Proxy-Authorization)\n * - Cookie / Set-Cookie headers\n * - Custom auth headers (X-API-Key, X-Auth-Token, X-Session-ID, X-CSRF-Token, X-XSRF-Token)\n * - Bearer tokens in URLs\n * - API keys in query parameters and URL fragments\n * - JWTs\n * - Password, secret, token fields in request/response bodies\n * - Credit card numbers\n * - AWS access keys\n * - Environment variable values in output text\n */\n\nexport interface ScrubOptions {\n includeCookies?: boolean;\n additionalPatterns?: RegExp[];\n}\n\nexport interface HeaderMap {\n [key: string]: string;\n}\n\n// -------------------------------------------------------------------------\n// Sensitive header patterns (case-insensitive matching)\n// -------------------------------------------------------------------------\n\n/** Headers that are always scrubbed (regardless of includeCookies). */\nconst ALWAYS_SCRUB_HEADERS = new Set([\n 'authorization',\n 'proxy-authorization',\n 'x-api-key',\n 'x-auth-token',\n 'x-session-id',\n 'x-csrf-token',\n 'x-xsrf-token',\n]);\n\n/** Cookie-related headers (scrubbed by default, preserved when includeCookies is true). */\nconst COOKIE_HEADERS = new Set([\n 'cookie',\n 'set-cookie',\n]);\n\n// -------------------------------------------------------------------------\n// Sensitive URL query parameter names\n// -------------------------------------------------------------------------\nconst SENSITIVE_PARAMS = new Set([\n 'token',\n 'api_key',\n 'apikey',\n 'key',\n 'secret',\n 'password',\n 'passwd',\n 'access_token',\n 'auth',\n 'id_token',\n]);\n\n// -------------------------------------------------------------------------\n// Sensitive JSON body field names (case-insensitive matching)\n// -------------------------------------------------------------------------\nconst SENSITIVE_BODY_FIELDS = new Set([\n 'password',\n 'passwd',\n 'secret',\n 'client_secret',\n 'token',\n 'access_token',\n 'refresh_token',\n 'apikey',\n 'api_key',\n 'aws_access_key',\n 'aws_secret_key',\n 'sessiontoken',\n]);\n\n// -------------------------------------------------------------------------\n// Credit card regex (Visa, Mastercard, Amex, Discover, etc.)\n// Matches 13-19 digit card numbers with optional spaces or dashes\n// -------------------------------------------------------------------------\nconst CREDIT_CARD_RE = /\\b(?:\\d[ -]*?){13,19}\\b/g;\n\n/**\n * Validate a potential credit card number using the Luhn algorithm.\n */\nfunction isLuhnValid(digits: string): boolean {\n const nums = digits.replace(/\\D/g, '');\n if (nums.length < 13 || nums.length > 19) return false;\n let sum = 0;\n let alternate = false;\n for (let i = nums.length - 1; i >= 0; i--) {\n let n = parseInt(nums[i], 10);\n if (alternate) {\n n *= 2;\n if (n > 9) n -= 9;\n }\n sum += n;\n alternate = !alternate;\n }\n return sum % 10 === 0;\n}\n\n// -------------------------------------------------------------------------\n// AWS key patterns\n// -------------------------------------------------------------------------\nconst AWS_ACCESS_KEY_RE = /\\bAKIA[0-9A-Z]{16}\\b/g;\nconst AWS_SECRET_KEY_RE = /\\b[A-Za-z0-9/+=]{40}\\b/g;\n\n// -------------------------------------------------------------------------\n// JWT pattern: eyJ<base64>.<base64>.<signature>\n// -------------------------------------------------------------------------\nconst JWT_RE = /eyJ[A-Za-z0-9_-]+\\.eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/g;\n\n// -------------------------------------------------------------------------\n// Public API\n// -------------------------------------------------------------------------\n\n/**\n * Scrub sensitive headers from a header map.\n * Replaces sensitive header values with [REDACTED].\n * Removes Cookie/Set-Cookie by default, preserves when includeCookies is true.\n */\nexport function scrubHeaders(\n headers: HeaderMap,\n options?: ScrubOptions\n): HeaderMap {\n const result: HeaderMap = {};\n const includeCookies = options?.includeCookies ?? false;\n\n for (const [key, value] of Object.entries(headers)) {\n const lower = key.toLowerCase();\n\n if (ALWAYS_SCRUB_HEADERS.has(lower)) {\n result[key] = '[REDACTED]';\n continue;\n }\n\n if (COOKIE_HEADERS.has(lower)) {\n if (includeCookies) {\n result[key] = value;\n } else {\n result[key] = '[REDACTED]';\n }\n continue;\n }\n\n result[key] = value;\n }\n\n return result;\n}\n\n/**\n * Scrub sensitive values from a URL (query parameters and fragment).\n * Masks: token, api_key, apiKey, key, secret, password, access_token, auth, id_token\n */\nexport function scrubUrl(url: string): string {\n try {\n const parsed = new URL(url);\n\n // Build scrubbed query string manually to avoid percent-encoding of [REDACTED]\n const params = Array.from(parsed.searchParams.entries());\n if (params.length > 0) {\n const scrubbedParams = params.map(([key, value]) => {\n if (SENSITIVE_PARAMS.has(key.toLowerCase())) {\n return `${key}=[REDACTED]`;\n }\n return `${key}=${value}`;\n });\n // Replace the search portion manually\n const baseUrl = parsed.origin + parsed.pathname;\n let result = baseUrl + '?' + scrubbedParams.join('&');\n\n // Scrub fragment\n if (parsed.hash) {\n result += scrubFragment(parsed.hash);\n }\n\n result = maskJwt(result);\n return result;\n }\n\n // No query params — handle fragment only\n let result = parsed.origin + parsed.pathname;\n if (parsed.hash) {\n result += scrubFragment(parsed.hash);\n }\n\n result = maskJwt(result);\n return result;\n } catch {\n // If URL parsing fails, do best-effort string replacement\n return maskJwt(url);\n }\n}\n\n/**\n * Scrub sensitive params from a URL fragment string (e.g., #access_token=xxx).\n */\nfunction scrubFragment(hash: string): string {\n if (!hash || hash === '#') return hash;\n\n const fragment = hash.substring(1); // remove #\n try {\n const fragmentParams = new URLSearchParams(fragment);\n const entries = Array.from(fragmentParams.entries());\n if (entries.length === 0) return hash;\n\n let hasChanges = false;\n const scrubbedParts = entries.map(([key, value]) => {\n if (SENSITIVE_PARAMS.has(key.toLowerCase())) {\n hasChanges = true;\n return `${key}=[REDACTED]`;\n }\n return `${key}=${value}`;\n });\n\n if (hasChanges) {\n return '#' + scrubbedParts.join('&');\n }\n } catch {\n // Not parseable as params\n }\n return hash;\n}\n\n/**\n * Detect and mask JWT tokens in a string.\n */\nexport function maskJwt(value: string): string {\n return value.replace(JWT_RE, '[JWT REDACTED]');\n}\n\n/**\n * Scrub sensitive fields from a JSON object, recursively.\n */\nfunction scrubJsonObject(obj: any): any {\n if (obj === null || obj === undefined) return obj;\n if (typeof obj !== 'object') return obj;\n\n if (Array.isArray(obj)) {\n return obj.map(item => scrubJsonObject(item));\n }\n\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (SENSITIVE_BODY_FIELDS.has(key.toLowerCase())) {\n result[key] = '[REDACTED]';\n } else if (typeof value === 'object' && value !== null) {\n result[key] = scrubJsonObject(value);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Scrub credit card numbers from text.\n */\nfunction scrubCreditCards(text: string): string {\n return text.replace(CREDIT_CARD_RE, (match) => {\n const digits = match.replace(/\\D/g, '');\n if (digits.length >= 13 && digits.length <= 19 && isLuhnValid(digits)) {\n return '[CARD REDACTED]';\n }\n return match;\n });\n}\n\n/**\n * Scrub AWS access keys from text.\n */\nfunction scrubAwsKeys(text: string): string {\n return text.replace(AWS_ACCESS_KEY_RE, '[AWS_KEY REDACTED]');\n}\n\n/**\n * Scrub form-encoded body sensitive fields.\n */\nfunction scrubFormBody(body: string): string {\n try {\n const params = new URLSearchParams(body);\n let changed = false;\n for (const [key] of params) {\n if (SENSITIVE_BODY_FIELDS.has(key.toLowerCase())) {\n params.set(key, '[REDACTED]');\n changed = true;\n }\n }\n return changed ? params.toString() : body;\n } catch {\n return body;\n }\n}\n\n/**\n * Scrub sensitive fields from a request/response body string.\n * Handles JSON bodies with password, secret, token, apiKey fields.\n * Handles form-encoded bodies.\n * Handles credit card patterns and AWS keys.\n * Handles non-JSON gracefully.\n */\nexport function scrubBody(body: string): string {\n if (!body) return body;\n\n let result = body;\n\n // Try JSON parse\n try {\n const parsed = JSON.parse(body);\n if (typeof parsed === 'object' && parsed !== null) {\n const scrubbed = scrubJsonObject(parsed);\n result = JSON.stringify(scrubbed);\n // Still apply pattern-based scrubbing for credit cards and AWS keys\n result = scrubCreditCards(result);\n result = scrubAwsKeys(result);\n return result;\n }\n } catch {\n // Not valid JSON — try other formats\n }\n\n // Try form-encoded\n if (body.includes('=') && !body.includes(' ') && !body.startsWith('{')) {\n result = scrubFormBody(body);\n result = scrubCreditCards(result);\n result = scrubAwsKeys(result);\n return result;\n }\n\n // Plain text — apply pattern-based scrubbing\n result = scrubCreditCards(result);\n result = scrubAwsKeys(result);\n\n // Best-effort regex scrub for password/secret patterns in malformed JSON\n result = result.replace(\n /(\"(?:password|passwd|secret|client_secret|token|access_token|refresh_token|apiKey|api_key|aws_access_key|aws_secret_key|SessionToken)\")\\s*:\\s*\"([^\"]*?)\"/gi,\n '$1: \"[REDACTED]\"'\n );\n\n return result;\n}\n\n/**\n * Alias for scrubBody — used by security tests as scrubResponseBody.\n */\nexport function scrubResponseBody(body: string): string {\n if (!body) return body;\n\n let result = body;\n\n // Try JSON parse\n try {\n const parsed = JSON.parse(body);\n if (typeof parsed === 'object' && parsed !== null) {\n const scrubbed = scrubJsonObject(parsed);\n result = JSON.stringify(scrubbed);\n result = scrubCreditCards(result);\n result = scrubAwsKeys(result);\n return result;\n }\n } catch {\n // Not valid JSON\n }\n\n // Try form-encoded (detect by presence of & and = without spaces before first &)\n if (body.includes('=') && /^[^{<]/.test(body)) {\n const maybeForm = scrubFormBody(body);\n if (maybeForm !== body) {\n result = maybeForm;\n result = scrubCreditCards(result);\n result = scrubAwsKeys(result);\n return result;\n }\n }\n\n // Plain text — apply pattern-based scrubbing\n result = scrubCreditCards(result);\n result = scrubAwsKeys(result);\n\n // Best-effort regex scrub for sensitive field patterns in malformed/partial JSON\n result = result.replace(\n /(\"(?:password|passwd|secret|client_secret|token|access_token|refresh_token|apiKey|api_key|aws_access_key|aws_secret_key|SessionToken)\")\\s*:\\s*\"([^\"]*?)\"/gi,\n '$1:\"[REDACTED]\"'\n );\n\n return result;\n}\n\n/**\n * Scrub known environment variable values from text.\n * Only scrubs values longer than 3 characters to avoid false positives.\n */\nexport function scrubEnvValues(text: string, envVarNames: string[]): string {\n let result = text;\n\n for (const name of envVarNames) {\n const value = process.env[name];\n if (!value || value.length <= 3) continue;\n\n // Escape regex special characters in the value\n const escaped = value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const re = new RegExp(escaped, 'g');\n result = result.replace(re, `[ENV:${name}]`);\n }\n\n return result;\n}\n\n/**\n * Scrub all sensitive data from a captured network request.\n */\nexport function scrubRequest(\n request: {\n url: string;\n headers: HeaderMap;\n body?: string;\n },\n options?: ScrubOptions\n): {\n url: string;\n headers: HeaderMap;\n body?: string;\n} {\n return {\n url: scrubUrl(request.url),\n headers: scrubHeaders(request.headers, options),\n body: request.body ? scrubBody(request.body) : undefined,\n };\n}\n","/**\n * NetworkLogger collector — captures all network activity on a page.\n *\n * Attaches to Playwright Page events to record:\n * - HTTP requests and responses (with header scrubbing)\n * - Failed requests (network errors, CORS, timeouts)\n * - GraphQL operations (detected from POST to /graphql endpoints)\n * - WebSocket connections and sample messages\n *\n * Usage:\n * const logger = createNetworkLogger(page, pageUrl);\n * logger.start();\n * // ... navigate, interact ...\n * const result = logger.stop();\n */\n\nimport type { Page, Request, Response, WebSocket } from 'playwright';\nimport type {\n NetworkLogResult,\n CapturedRequest,\n CapturedResponse,\n FailedRequest,\n GraphQLOperation,\n WebSocketConnection,\n RequestClassification,\n} from '../types/artifacts.js';\nimport { scrubHeaders } from '../security/credential-scrubber.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Maximum response body size to capture (100 KB). */\nconst MAX_BODY_SIZE = 100 * 1024;\n\n/** Maximum sample messages per direction for WebSocket connections. */\nconst MAX_WS_MESSAGES_PER_DIRECTION = 5;\n\n/** Known analytics domains (partial match against hostname). */\nconst ANALYTICS_DOMAINS = [\n 'google-analytics.com',\n 'googletagmanager.com',\n 'analytics.google.com',\n 'segment.io',\n 'segment.com',\n 'cdn.segment.com',\n 'api.segment.io',\n 'mixpanel.com',\n 'hotjar.com',\n 'fullstory.com',\n 'heap.io',\n 'heapanalytics.com',\n 'amplitude.com',\n 'plausible.io',\n 'clarity.ms',\n 'newrelic.com',\n 'nr-data.net',\n 'sentry.io',\n 'datadog',\n 'pendo.io',\n];\n\n/** Static resource file extensions. */\nconst STATIC_EXTENSIONS = new Set([\n '.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg',\n '.woff', '.woff2', '.ttf', '.eot', '.otf',\n '.ico', '.webp', '.avif', '.mp4', '.webm', '.mp3',\n '.ogg', '.wav', '.map',\n]);\n\n/** Playwright resource types considered static. */\nconst STATIC_RESOURCE_TYPES = new Set([\n 'stylesheet', 'image', 'font', 'media',\n]);\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface NetworkLoggerOptions {\n includeCookies?: boolean;\n}\n\nexport interface NetworkLogger {\n start(): void;\n stop(): NetworkLogResult;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a NetworkLogger that captures all network activity on a page.\n *\n * @param page Playwright Page to attach listeners to.\n * @param pageUrl The page URL (used for origin comparison and GraphQL context).\n * @param options Optional configuration (e.g. whether to include cookies).\n */\nexport function createNetworkLogger(\n page: Page,\n pageUrl: string,\n options?: NetworkLoggerOptions,\n): NetworkLogger {\n const includeCookies = options?.includeCookies ?? false;\n\n // Internal state\n let counter = 0;\n const requestIdMap = new Map<Request, string>();\n const requestTimings = new Map<string, number>();\n const requests: CapturedRequest[] = [];\n const responses: CapturedResponse[] = [];\n const failedRequests: FailedRequest[] = [];\n const graphqlOperations: GraphQLOperation[] = [];\n const webSocketConnections: WebSocketConnection[] = [];\n\n // Parse the page origin for third-party classification\n let pageOrigin = '';\n try {\n pageOrigin = new URL(pageUrl).origin;\n } catch {\n // Invalid pageUrl — all requests will be classified as 'other'\n }\n\n // -----------------------------------------------------------------------\n // Classification helpers\n // -----------------------------------------------------------------------\n\n function classifyRequest(url: string, resourceType: string, contentType?: string): RequestClassification {\n if (resourceType === 'websocket') {\n return 'websocket';\n }\n\n // Check analytics domains\n try {\n const hostname = new URL(url).hostname;\n for (const domain of ANALYTICS_DOMAINS) {\n if (hostname.includes(domain)) {\n return 'analytics';\n }\n }\n } catch {\n // Invalid URL — fall through\n }\n\n // Check API patterns\n if (\n url.includes('/api/') ||\n url.includes('/graphql') ||\n (contentType && contentType.includes('application/json'))\n ) {\n return 'api';\n }\n\n // Check static resources\n if (STATIC_RESOURCE_TYPES.has(resourceType)) {\n return 'static';\n }\n\n // Check static file extensions\n try {\n const pathname = new URL(url).pathname;\n const dotIndex = pathname.lastIndexOf('.');\n if (dotIndex !== -1) {\n const ext = pathname.slice(dotIndex).toLowerCase();\n if (STATIC_EXTENSIONS.has(ext)) {\n return 'static';\n }\n }\n } catch {\n // Invalid URL — fall through\n }\n\n // Check third-party (different origin)\n if (pageOrigin) {\n try {\n const requestOrigin = new URL(url).origin;\n if (requestOrigin !== pageOrigin) {\n return 'third-party';\n }\n } catch {\n // Invalid URL — fall through\n }\n }\n\n return 'other';\n }\n\n function mapInitiator(resourceType: string): CapturedRequest['initiator'] {\n switch (resourceType) {\n case 'document':\n return 'navigation';\n case 'script':\n case 'stylesheet':\n return 'script';\n case 'fetch':\n return 'fetch';\n case 'xhr':\n return 'xhr';\n default:\n return 'other';\n }\n }\n\n // -----------------------------------------------------------------------\n // GraphQL detection\n // -----------------------------------------------------------------------\n\n function tryDetectGraphQL(\n request: Request,\n requestId: string,\n url: string,\n body: string | undefined,\n ): void {\n if (request.method() !== 'POST') return;\n if (!url.includes('graphql')) return;\n if (!body) return;\n\n try {\n const parsed = JSON.parse(body);\n const operationName = parsed.operationName ?? 'unknown';\n const query: string = parsed.query ?? '';\n const variables = parsed.variables;\n\n // Determine operation type from query string\n let operationType: GraphQLOperation['operationType'] = 'query';\n const trimmedQuery = query.trimStart();\n if (trimmedQuery.startsWith('mutation')) {\n operationType = 'mutation';\n } else if (trimmedQuery.startsWith('subscription')) {\n operationType = 'subscription';\n }\n\n const op: GraphQLOperation = {\n pageUrl,\n endpointUrl: url,\n operationName,\n operationType,\n query,\n };\n\n if (variables !== undefined && variables !== null) {\n op.variables = variables;\n }\n\n graphqlOperations.push(op);\n } catch {\n // Body is not valid JSON — skip GraphQL detection\n }\n }\n\n // -----------------------------------------------------------------------\n // Event handlers (stored as named functions for removal)\n // -----------------------------------------------------------------------\n\n function onRequest(request: Request): void {\n const id = `req_${counter++}`;\n requestIdMap.set(request, id);\n requestTimings.set(id, Date.now());\n\n const url = request.url();\n const resourceType = request.resourceType();\n const method = request.method();\n const rawHeaders = request.headers();\n const postData = request.postData() ?? undefined;\n const contentType = rawHeaders['content-type'] ?? undefined;\n\n const classification = classifyRequest(url, resourceType, contentType);\n const initiator = mapInitiator(resourceType);\n\n const captured: CapturedRequest = {\n requestId: id,\n url,\n method,\n headers: scrubHeaders(rawHeaders, { includeCookies }),\n resourceType,\n classification,\n initiator,\n };\n\n if (postData !== undefined) {\n captured.body = postData;\n }\n if (contentType !== undefined) {\n captured.contentType = contentType;\n }\n\n requests.push(captured);\n\n // GraphQL detection\n tryDetectGraphQL(request, id, url, postData);\n }\n\n async function onResponse(response: Response): Promise<void> {\n const request = response.request();\n const id = requestIdMap.get(request);\n if (!id) return;\n\n const startTime = requestTimings.get(id) ?? Date.now();\n const timing = Date.now() - startTime;\n\n const rawHeaders = response.headers();\n const statusCode = response.status();\n const contentType = rawHeaders['content-type'] ?? undefined;\n\n // Attempt to capture body for text-based responses\n let body: string | undefined;\n let bodySize = 0;\n\n try {\n const buffer = await response.body();\n bodySize = buffer.length;\n\n // Only capture text-based content types\n if (contentType && isTextContentType(contentType)) {\n const text = buffer.toString('utf-8');\n body = text.length > MAX_BODY_SIZE ? text.slice(0, MAX_BODY_SIZE) : text;\n }\n } catch {\n // Body may not be available (e.g. redirects, streaming)\n }\n\n const captured: CapturedResponse = {\n requestId: id,\n statusCode,\n headers: scrubHeaders(rawHeaders, { includeCookies }),\n bodySize,\n timing,\n };\n\n if (body !== undefined) {\n captured.body = body;\n }\n if (contentType !== undefined) {\n captured.contentType = contentType;\n }\n\n responses.push(captured);\n\n // Attach response data to GraphQL operations\n if (body) {\n const gqlOp = graphqlOperations.find(op => {\n // Match by the request that produced this response\n const matchingReq = requests.find(r => r.requestId === id);\n return matchingReq && op.endpointUrl === matchingReq.url;\n });\n if (gqlOp && gqlOp.responseData === undefined) {\n try {\n gqlOp.responseData = JSON.parse(body);\n } catch {\n // Non-JSON response\n }\n }\n }\n }\n\n function onRequestFailed(request: Request): void {\n const id = requestIdMap.get(request) ?? `req_${counter++}`;\n const url = request.url();\n const method = request.method();\n const resourceType = request.resourceType();\n const classification = classifyRequest(url, resourceType);\n\n const failure = request.failure();\n const errorText = failure?.errorText ?? 'Unknown error';\n\n failedRequests.push({\n requestId: id,\n url,\n method,\n errorText,\n classification,\n });\n }\n\n function onWebSocket(ws: WebSocket): void {\n const wsUrl = ws.url();\n let sentCount = 0;\n let receivedCount = 0;\n\n const connection: WebSocketConnection = {\n url: wsUrl,\n pageUrl,\n connected: true,\n messageCount: 0,\n sampleMessages: [],\n };\n\n webSocketConnections.push(connection);\n\n ws.on('framereceived', (data: { payload: string | Buffer }) => {\n connection.messageCount++;\n if (receivedCount < MAX_WS_MESSAGES_PER_DIRECTION) {\n receivedCount++;\n connection.sampleMessages.push({\n direction: 'received',\n data: typeof data.payload === 'string' ? data.payload : data.payload.toString('utf-8'),\n timestamp: new Date().toISOString(),\n });\n }\n });\n\n ws.on('framesent', (data: { payload: string | Buffer }) => {\n connection.messageCount++;\n if (sentCount < MAX_WS_MESSAGES_PER_DIRECTION) {\n sentCount++;\n connection.sampleMessages.push({\n direction: 'sent',\n data: typeof data.payload === 'string' ? data.payload : data.payload.toString('utf-8'),\n timestamp: new Date().toISOString(),\n });\n }\n });\n\n ws.on('close', () => {\n connection.connected = false;\n });\n }\n\n // -----------------------------------------------------------------------\n // Public interface\n // -----------------------------------------------------------------------\n\n return {\n start(): void {\n page.on('request', onRequest);\n page.on('response', onResponse);\n page.on('requestfailed', onRequestFailed);\n page.on('websocket', onWebSocket);\n },\n\n stop(): NetworkLogResult {\n page.off('request', onRequest);\n page.off('response', onResponse);\n page.off('requestfailed', onRequestFailed);\n page.off('websocket', onWebSocket);\n\n return {\n pageUrl,\n requests,\n responses,\n failedRequests,\n graphqlOperations,\n webSocketConnections,\n cookieMutations: [],\n };\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Determine if a content-type is text-based and worth capturing the body.\n */\nfunction isTextContentType(contentType: string): boolean {\n const lower = contentType.toLowerCase();\n return (\n lower.includes('text/') ||\n lower.includes('application/json') ||\n lower.includes('application/xml') ||\n lower.includes('application/javascript') ||\n lower.includes('application/xhtml') ||\n lower.includes('application/graphql') ||\n lower.includes('+json') ||\n lower.includes('+xml')\n );\n}\n","/**\n * API Endpoint Grouper\n *\n * Groups discovered API endpoints by URL pattern, replacing dynamic\n * segments with parameter placeholders:\n * - Numeric IDs: /users/123 -> /users/:id\n * - UUIDs: /items/550e8400-... -> /items/:id\n * - Slugs: /posts/hello-world -> /posts/:slug\n */\n\nexport interface ApiEndpoint {\n method: string;\n url: string;\n statusCode?: number;\n requestBody?: unknown;\n responseBody?: unknown;\n headers?: Record<string, string>;\n}\n\nexport interface ApiGroup {\n pattern: string;\n method: string;\n examples: ApiEndpoint[];\n parameterTypes: Record<string, 'id' | 'uuid' | 'slug' | 'unknown'>;\n}\n\n/** UUID v4 pattern: 8-4-4-4-12 hex digits, with or without dashes. */\nconst UUID_WITH_DASHES = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst UUID_WITHOUT_DASHES = /^[0-9a-f]{32}$/i;\n\n/**\n * Detect if a URL path segment is a numeric ID.\n */\nexport function isNumericId(segment: string): boolean {\n return /^\\d+$/.test(segment);\n}\n\n/**\n * Detect if a URL path segment is a UUID.\n */\nexport function isUuid(segment: string): boolean {\n return UUID_WITH_DASHES.test(segment) || UUID_WITHOUT_DASHES.test(segment);\n}\n\n/**\n * Detect if a URL path segment is a slug.\n * A slug contains at least one hyphen, is all lowercase alphanumeric + hyphens,\n * and has more than one \"word\" (segment between hyphens).\n * UUIDs are excluded since they are handled separately.\n */\nexport function isSlug(segment: string): boolean {\n if (!segment || !segment.includes('-')) return false;\n\n // Must be lowercase alphanumeric and hyphens only\n if (!/^[a-z0-9]+(-[a-z0-9]+)+$/.test(segment)) return false;\n\n // Exclude UUIDs\n if (isUuid(segment)) return false;\n\n return true;\n}\n\n/**\n * Convert a concrete URL path to a parameterized pattern.\n * /api/v2/users/123/posts/456 -> /api/v2/users/:id/posts/:id\n */\nexport function parameterizePath(path: string): string {\n // Strip query string before parameterizing\n const pathOnly = path.split('?')[0];\n\n const segments = pathOnly.split('/');\n\n const parameterized = segments.map((segment) => {\n if (!segment) return segment; // preserve empty segments from leading/trailing slashes\n\n if (isUuid(segment)) return ':id';\n if (isNumericId(segment)) return ':id';\n if (isSlug(segment)) return ':slug';\n\n return segment;\n });\n\n return parameterized.join('/');\n}\n\n/**\n * Determine the parameter type for a segment.\n */\nfunction getParameterType(segment: string): 'id' | 'uuid' | 'slug' | 'unknown' {\n if (isUuid(segment)) return 'uuid';\n if (isNumericId(segment)) return 'id';\n if (isSlug(segment)) return 'slug';\n return 'unknown';\n}\n\n/**\n * Build a parameterTypes map from a concrete path by comparing it\n * against its parameterized version.\n */\nfunction extractParameterTypes(path: string): Record<string, 'id' | 'uuid' | 'slug' | 'unknown'> {\n const pathOnly = path.split('?')[0];\n const segments = pathOnly.split('/');\n const result: Record<string, 'id' | 'uuid' | 'slug' | 'unknown'> = {};\n let idIndex = 0;\n let slugIndex = 0;\n\n for (const segment of segments) {\n if (!segment) continue;\n\n if (isUuid(segment)) {\n result[`:id_${idIndex++}`] = 'uuid';\n } else if (isNumericId(segment)) {\n result[`:id_${idIndex++}`] = 'id';\n } else if (isSlug(segment)) {\n result[`:slug_${slugIndex++}`] = 'slug';\n }\n }\n\n return result;\n}\n\n/**\n * Group a list of API endpoints by their parameterized pattern + method.\n */\nexport function groupEndpoints(endpoints: ApiEndpoint[]): ApiGroup[] {\n if (endpoints.length === 0) return [];\n\n const groupMap = new Map<string, ApiGroup>();\n\n for (const endpoint of endpoints) {\n const pattern = parameterizePath(endpoint.url);\n const key = `${endpoint.method} ${pattern}`;\n\n const existing = groupMap.get(key);\n if (existing) {\n existing.examples.push(endpoint);\n } else {\n groupMap.set(key, {\n pattern,\n method: endpoint.method,\n examples: [endpoint],\n parameterTypes: extractParameterTypes(endpoint.url),\n });\n }\n }\n\n return Array.from(groupMap.values());\n}\n\n/**\n * Detect GraphQL operations and group by operation name.\n */\nexport function groupGraphQLOperations(\n endpoints: ApiEndpoint[]\n): Map<string, ApiEndpoint[]> {\n const result = new Map<string, ApiEndpoint[]>();\n\n for (const endpoint of endpoints) {\n // Only consider POST requests to /graphql\n if (endpoint.method !== 'POST') continue;\n\n const pathOnly = endpoint.url.split('?')[0];\n if (!pathOnly.endsWith('/graphql') && pathOnly !== '/graphql') continue;\n\n const body = endpoint.requestBody as Record<string, unknown> | undefined;\n if (!body || typeof body !== 'object') continue;\n\n const operationName =\n typeof body.operationName === 'string' ? body.operationName : 'anonymous';\n\n const existing = result.get(operationName);\n if (existing) {\n existing.push(endpoint);\n } else {\n result.set(operationName, [endpoint]);\n }\n }\n\n return result;\n}\n","/**\n * Flow Graph Builder\n *\n * Takes navigation edges from a crawl and produces:\n * - A FlowGraph with node metadata, cycle detection, and clustering\n * - A Mermaid diagram definition for visualization\n *\n * Used by the Assembler to generate flow diagrams in the HTML report.\n */\n\nimport type { NavigationEdge, NavigationTrigger } from '../types/artifacts.js';\nimport { parameterizePath } from './api-grouper.js';\n\n// ---------------------------------------------------------------------------\n// Public interfaces\n// ---------------------------------------------------------------------------\n\nexport interface FlowGraphNode {\n url: string;\n /** Short label derived from URL path */\n label: string;\n /** Whether this is the entry point */\n isEntry: boolean;\n /** Whether this is a dead-end (no outgoing edges) */\n isExit: boolean;\n /** Number of incoming edges */\n inDegree: number;\n /** Number of outgoing edges */\n outDegree: number;\n /** Cluster/group based on URL path prefix */\n cluster?: string;\n}\n\nexport interface FlowGraph {\n nodes: FlowGraphNode[];\n edges: NavigationEdge[];\n /** Entry URL */\n entryUrl: string;\n /** Whether cycles were detected */\n hasCycles: boolean;\n /** URLs involved in cycles */\n cycleNodes: string[];\n /** Cluster groups (path prefix -> URLs) */\n clusters: Map<string, string[]>;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst MAX_LABEL_LENGTH = 30;\nconst DEFAULT_MAX_NODES = 50;\n\n// ---------------------------------------------------------------------------\n// urlToLabel\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a full URL to a short display label.\n * Strips the origin, parameterizes dynamic segments, and truncates.\n */\nexport function urlToLabel(url: string): string {\n let path: string;\n try {\n const parsed = new URL(url);\n path = parsed.pathname;\n } catch {\n // If it's not a valid URL, treat the whole string as a path\n path = url.split('?')[0];\n }\n\n if (path === '/' || path === '') {\n return '/';\n }\n\n // Strip leading slash for the label\n const stripped = path.startsWith('/') ? path.slice(1) : path;\n\n // Parameterize dynamic segments\n const parameterized = parameterizePath('/' + stripped).slice(1);\n\n // Truncate if too long\n if (parameterized.length > MAX_LABEL_LENGTH) {\n return parameterized.slice(0, MAX_LABEL_LENGTH - 3) + '...';\n }\n\n return parameterized;\n}\n\n// ---------------------------------------------------------------------------\n// detectCycles\n// ---------------------------------------------------------------------------\n\n/**\n * DFS-based cycle detection.\n * Returns an array of URLs that participate in at least one cycle.\n */\nexport function detectCycles(edges: NavigationEdge[]): string[] {\n if (edges.length === 0) return [];\n\n // Build adjacency list\n const adjacency = new Map<string, Set<string>>();\n const allNodes = new Set<string>();\n\n for (const edge of edges) {\n allNodes.add(edge.from);\n allNodes.add(edge.to);\n let neighbors = adjacency.get(edge.from);\n if (!neighbors) {\n neighbors = new Set<string>();\n adjacency.set(edge.from, neighbors);\n }\n neighbors.add(edge.to);\n }\n\n const cycleNodeSet = new Set<string>();\n\n // States: 0 = unvisited, 1 = in current path, 2 = fully processed\n const state = new Map<string, 0 | 1 | 2>();\n // Track the current DFS path for extracting cycle participants\n const pathStack: string[] = [];\n\n function dfs(node: string): void {\n state.set(node, 1);\n pathStack.push(node);\n\n const neighbors = adjacency.get(node);\n if (neighbors) {\n for (const neighbor of neighbors) {\n const neighborState = state.get(neighbor) ?? 0;\n\n if (neighborState === 1) {\n // Found a cycle - mark all nodes from the cycle start to current\n const cycleStartIndex = pathStack.indexOf(neighbor);\n if (cycleStartIndex >= 0) {\n for (let i = cycleStartIndex; i < pathStack.length; i++) {\n cycleNodeSet.add(pathStack[i]);\n }\n }\n } else if (neighborState === 0) {\n dfs(neighbor);\n }\n }\n }\n\n pathStack.pop();\n state.set(node, 2);\n }\n\n for (const node of allNodes) {\n if ((state.get(node) ?? 0) === 0) {\n dfs(node);\n }\n }\n\n return Array.from(cycleNodeSet);\n}\n\n// ---------------------------------------------------------------------------\n// clusterByPathPrefix\n// ---------------------------------------------------------------------------\n\n/**\n * Group URLs by their first path segment.\n * e.g., /api/users, /api/posts -> cluster \"api\"\n * /docs/intro -> cluster \"docs\"\n * / -> cluster \"/\"\n */\nexport function clusterByPathPrefix(urls: string[]): Map<string, string[]> {\n const clusters = new Map<string, string[]>();\n\n for (const url of urls) {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n path = url.split('?')[0];\n }\n\n // Extract first path segment\n const segments = path.split('/').filter(Boolean);\n const prefix = segments.length > 0 ? segments[0] : '/';\n\n let group = clusters.get(prefix);\n if (!group) {\n group = [];\n clusters.set(prefix, group);\n }\n group.push(url);\n }\n\n return clusters;\n}\n\n// ---------------------------------------------------------------------------\n// buildFlowGraph\n// ---------------------------------------------------------------------------\n\n/**\n * Build a FlowGraph from navigation edges and an entry URL.\n * Deduplicates edges, computes node metadata, detects cycles, and clusters URLs.\n */\nexport function buildFlowGraph(edges: NavigationEdge[], entryUrl: string): FlowGraph {\n // Deduplicate edges by (from, to, trigger)\n const edgeKeys = new Set<string>();\n const dedupedEdges: NavigationEdge[] = [];\n\n for (const edge of edges) {\n const key = `${edge.from}\\0${edge.to}\\0${edge.trigger}`;\n if (!edgeKeys.has(key)) {\n edgeKeys.add(key);\n dedupedEdges.push(edge);\n }\n }\n\n // Collect all unique URLs\n const urlSet = new Set<string>();\n urlSet.add(entryUrl);\n for (const edge of dedupedEdges) {\n urlSet.add(edge.from);\n urlSet.add(edge.to);\n }\n\n // Compute in-degree and out-degree\n const inDegree = new Map<string, number>();\n const outDegree = new Map<string, number>();\n for (const url of urlSet) {\n inDegree.set(url, 0);\n outDegree.set(url, 0);\n }\n for (const edge of dedupedEdges) {\n inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);\n outDegree.set(edge.from, (outDegree.get(edge.from) ?? 0) + 1);\n }\n\n // Detect cycles\n const cycleNodes = detectCycles(dedupedEdges);\n const hasCycles = cycleNodes.length > 0;\n\n // Cluster by path prefix\n const allUrls = Array.from(urlSet);\n const clusters = clusterByPathPrefix(allUrls);\n\n // Build URL-to-cluster lookup\n const urlToCluster = new Map<string, string>();\n for (const [prefix, urls] of clusters) {\n for (const url of urls) {\n urlToCluster.set(url, prefix);\n }\n }\n\n // Build nodes\n const nodes: FlowGraphNode[] = allUrls.map((url) => ({\n url,\n label: urlToLabel(url),\n isEntry: url === entryUrl,\n isExit: (outDegree.get(url) ?? 0) === 0,\n inDegree: inDegree.get(url) ?? 0,\n outDegree: outDegree.get(url) ?? 0,\n cluster: urlToCluster.get(url),\n }));\n\n return {\n nodes,\n edges: dedupedEdges,\n entryUrl,\n hasCycles,\n cycleNodes,\n clusters,\n };\n}\n\n// ---------------------------------------------------------------------------\n// generateMermaidDefinition\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitize a string for use as a Mermaid node label.\n * Escapes quotes and brackets that break Mermaid syntax.\n */\nfunction sanitizeMermaidLabel(label: string): string {\n return label\n .replace(/\"/g, '#quot;')\n .replace(/\\[/g, '#lsqb;')\n .replace(/\\]/g, '#rsqb;')\n .replace(/\\(/g, '#lpar;')\n .replace(/\\)/g, '#rpar;')\n .replace(/\\{/g, '#lcub;')\n .replace(/\\}/g, '#rcub;')\n .replace(/</g, '#lt;')\n .replace(/>/g, '#gt;');\n}\n\n/**\n * Create a stable node ID from a URL for use in Mermaid definitions.\n */\nfunction nodeId(url: string, index: number): string {\n return `N${index}`;\n}\n\n/**\n * Get the Mermaid edge style for a navigation trigger.\n * - Solid (-->) for link, click, form-submit\n * - Dashed (-.->) for redirect, meta-refresh, js-redirect\n * - Dotted (-.->) for pushState, replaceState, navigation-api\n */\nfunction edgeStyle(trigger: NavigationTrigger): string {\n switch (trigger) {\n case 'link':\n case 'click':\n case 'form-submit':\n return '-->';\n case 'redirect':\n case 'meta-refresh':\n case 'js-redirect':\n return '-.->';\n case 'pushState':\n case 'replaceState':\n case 'navigation-api':\n return '-.->';\n default:\n return '-->';\n }\n}\n\n/**\n * Generate a Mermaid flowchart definition from a FlowGraph.\n *\n * If the graph exceeds maxNodes (default 50), URLs are auto-clustered\n * by first path segment and rendered as Mermaid subgraphs.\n */\nexport function generateMermaidDefinition(\n graph: FlowGraph,\n options?: { maxNodes?: number },\n): string {\n const maxNodes = options?.maxNodes ?? DEFAULT_MAX_NODES;\n\n if (graph.nodes.length === 0) {\n return 'flowchart LR\\n';\n }\n\n // Build URL -> index mapping for stable node IDs\n const urlIndex = new Map<string, number>();\n graph.nodes.forEach((node, i) => {\n urlIndex.set(node.url, i);\n });\n\n const lines: string[] = ['flowchart LR'];\n\n const useSubgraphs = graph.nodes.length > maxNodes;\n\n if (useSubgraphs) {\n // Group nodes by cluster\n const clusterNodes = new Map<string, FlowGraphNode[]>();\n for (const node of graph.nodes) {\n const cluster = node.cluster ?? '/';\n let group = clusterNodes.get(cluster);\n if (!group) {\n group = [];\n clusterNodes.set(cluster, group);\n }\n group.push(node);\n }\n\n for (const [cluster, nodes] of clusterNodes) {\n const sanitizedCluster = sanitizeMermaidLabel(cluster);\n lines.push(` subgraph ${sanitizedCluster}`);\n for (const node of nodes) {\n const id = nodeId(node.url, urlIndex.get(node.url)!);\n const label = sanitizeMermaidLabel(node.label);\n if (node.isEntry) {\n lines.push(` ${id}([\"${label}\"])`);\n } else if (node.isExit) {\n lines.push(` ${id}[/\"${label}\"/]`);\n } else {\n lines.push(` ${id}[\"${label}\"]`);\n }\n }\n lines.push(' end');\n }\n } else {\n // Flat node declarations\n for (const node of graph.nodes) {\n const id = nodeId(node.url, urlIndex.get(node.url)!);\n const label = sanitizeMermaidLabel(node.label);\n if (node.isEntry) {\n lines.push(` ${id}([\"${label}\"])`);\n } else if (node.isExit) {\n lines.push(` ${id}[/\"${label}\"/]`);\n } else {\n lines.push(` ${id}[\"${label}\"]`);\n }\n }\n }\n\n // Edge declarations\n for (const edge of graph.edges) {\n const fromIdx = urlIndex.get(edge.from);\n const toIdx = urlIndex.get(edge.to);\n if (fromIdx === undefined || toIdx === undefined) continue;\n\n const fromId = nodeId(edge.from, fromIdx);\n const toId = nodeId(edge.to, toIdx);\n const style = edgeStyle(edge.trigger);\n const triggerLabel = edge.triggerText\n ? ` |${sanitizeMermaidLabel(edge.triggerText)}|`\n : '';\n\n lines.push(` ${fromId} ${style}${triggerLabel} ${toId}`);\n }\n\n return lines.join('\\n') + '\\n';\n}\n","/**\n * OpenAPI 3.0.3 Specification Generator\n *\n * Converts discovered ApiEndpointGroup[] into a valid OpenAPI 3.0.3 spec.\n * Used to produce machine-readable API documentation from crawl results.\n */\n\nimport { writeFile, mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type { ApiEndpointGroup } from '../types/artifacts.js';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface OpenApiOptions {\n /** Title for the API specification. */\n title: string;\n /** Base URL of the crawled site (used for servers[].url). */\n targetUrl: string;\n /** API version string. Defaults to \"1.0.0\". */\n version?: string;\n /** Optional human-readable description. */\n description?: string;\n}\n\nexport interface OpenApiSpec {\n openapi: '3.0.3';\n info: { title: string; version: string; description?: string };\n servers: Array<{ url: string }>;\n paths: Record<string, Record<string, OpenApiOperation>>;\n}\n\nexport interface OpenApiOperation {\n summary: string;\n operationId: string;\n parameters?: OpenApiParameter[];\n requestBody?: {\n content: Record<string, { schema: unknown; example?: unknown }>;\n };\n responses: Record<\n string,\n {\n description: string;\n content?: Record<string, { schema: unknown; example?: unknown }>;\n }\n >;\n tags?: string[];\n}\n\nexport interface OpenApiParameter {\n name: string;\n in: 'path' | 'query';\n required: boolean;\n schema: { type: string };\n}\n\n// ---------------------------------------------------------------------------\n// Status code descriptions\n// ---------------------------------------------------------------------------\n\nconst STATUS_DESCRIPTIONS: Record<number, string> = {\n 200: 'Success',\n 201: 'Created',\n 204: 'No Content',\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Not Found',\n 500: 'Internal Server Error',\n};\n\n// ---------------------------------------------------------------------------\n// Schema inference (max 2 levels deep)\n// ---------------------------------------------------------------------------\n\n/**\n * Infer a JSON Schema from a value. Limited to 2 levels of nesting\n * to keep generated specs manageable.\n */\nexport function inferSchema(value: unknown, depth = 0): Record<string, unknown> {\n if (value === null || value === undefined) {\n return { type: 'string', nullable: true };\n }\n\n if (Array.isArray(value)) {\n if (value.length === 0 || depth >= 2) {\n return { type: 'array', items: {} };\n }\n return { type: 'array', items: inferSchema(value[0], depth + 1) };\n }\n\n if (typeof value === 'object') {\n if (depth >= 2) {\n return { type: 'object' };\n }\n const properties: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\n properties[key] = inferSchema(val, depth + 1);\n }\n return { type: 'object', properties };\n }\n\n if (typeof value === 'number') {\n return Number.isInteger(value) ? { type: 'integer' } : { type: 'number' };\n }\n\n if (typeof value === 'boolean') {\n return { type: 'boolean' };\n }\n\n return { type: 'string' };\n}\n\n// ---------------------------------------------------------------------------\n// Path conversion helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert colon-style path parameters to OpenAPI curly-brace style.\n * `/api/users/:id` becomes `/api/users/{id}`.\n */\nexport function toOpenApiPath(pattern: string): string {\n return pattern.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '{$1}');\n}\n\n/**\n * Extract parameter names from a colon-style pattern.\n * `/api/users/:id/posts/:postId` returns `['id', 'postId']`.\n */\nexport function extractPathParams(pattern: string): string[] {\n const params: string[] = [];\n const re = /:([a-zA-Z_][a-zA-Z0-9_]*)/g;\n let match: RegExpExecArray | null;\n while ((match = re.exec(pattern)) !== null) {\n params.push(match[1]);\n }\n return params;\n}\n\n// ---------------------------------------------------------------------------\n// Operation ID generation\n// ---------------------------------------------------------------------------\n\n/**\n * Generate an operationId from HTTP method and URL pattern.\n * `GET /api/users/:id` -> `getApiUsersById`\n */\nexport function generateOperationId(method: string, pattern: string): string {\n const segments = pattern\n .split('/')\n .filter(Boolean)\n .map((seg) => {\n if (seg.startsWith(':')) {\n // :userId -> ByUserId\n const paramName = seg.slice(1);\n return 'By' + paramName.charAt(0).toUpperCase() + paramName.slice(1);\n }\n return seg.charAt(0).toUpperCase() + seg.slice(1);\n });\n\n return method.toLowerCase() + segments.join('');\n}\n\n// ---------------------------------------------------------------------------\n// Tag extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Extract a tag from the endpoint pattern.\n * Uses the first meaningful path segment (skips common prefixes like \"api\").\n */\nexport function extractTag(pattern: string): string | undefined {\n const segments = pattern.split('/').filter(Boolean);\n // Skip common API prefixes\n const prefixes = new Set(['api', 'v1', 'v2', 'v3', 'v4', 'rest']);\n for (const seg of segments) {\n if (seg.startsWith(':')) continue;\n if (prefixes.has(seg.toLowerCase())) continue;\n return seg;\n }\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Methods that typically carry a request body\n// ---------------------------------------------------------------------------\n\nconst BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);\n\n// ---------------------------------------------------------------------------\n// Main generator\n// ---------------------------------------------------------------------------\n\n/**\n * Generate an OpenAPI 3.0.3 specification from discovered API endpoint groups.\n */\nexport function generateOpenApiSpec(\n endpoints: ApiEndpointGroup[],\n options: OpenApiOptions,\n): OpenApiSpec {\n if (!options.title) {\n throw new Error('OpenAPI title is required');\n }\n if (!options.targetUrl) {\n throw new Error('OpenAPI targetUrl is required');\n }\n\n const info: OpenApiSpec['info'] = {\n title: options.title,\n version: options.version ?? '1.0.0',\n };\n if (options.description) {\n info.description = options.description;\n }\n\n // Strip trailing slash from server URL\n const serverUrl = options.targetUrl.replace(/\\/+$/, '');\n\n const paths: OpenApiSpec['paths'] = {};\n\n for (const endpoint of endpoints) {\n const openApiPath = toOpenApiPath(endpoint.pattern);\n const methodKey = endpoint.method.toLowerCase();\n\n if (!paths[openApiPath]) {\n paths[openApiPath] = {};\n }\n\n const operation = buildOperation(endpoint);\n paths[openApiPath][methodKey] = operation;\n }\n\n return {\n openapi: '3.0.3',\n info,\n servers: [{ url: serverUrl }],\n paths,\n };\n}\n\n/**\n * Build a single OpenAPI operation from an endpoint group.\n */\nfunction buildOperation(endpoint: ApiEndpointGroup): OpenApiOperation {\n const operationId = generateOperationId(endpoint.method, endpoint.pattern);\n const summary = `${endpoint.method} ${endpoint.pattern}`;\n\n const operation: OpenApiOperation = {\n summary,\n operationId,\n responses: {},\n };\n\n // Path parameters\n const paramNames = extractPathParams(endpoint.pattern);\n if (paramNames.length > 0) {\n operation.parameters = paramNames.map((name) => ({\n name,\n in: 'path' as const,\n required: true,\n schema: { type: 'string' },\n }));\n }\n\n // Tags\n const tag = extractTag(endpoint.pattern);\n if (tag) {\n operation.tags = [tag];\n }\n\n // Request body (for POST, PUT, PATCH)\n if (BODY_METHODS.has(endpoint.method.toUpperCase()) && endpoint.exampleRequest.body) {\n const contentType = endpoint.exampleRequest.contentType ?? 'application/json';\n let parsed: unknown;\n try {\n parsed = JSON.parse(endpoint.exampleRequest.body);\n } catch {\n parsed = endpoint.exampleRequest.body;\n }\n operation.requestBody = {\n content: {\n [contentType]: {\n schema: inferSchema(parsed),\n example: parsed,\n },\n },\n };\n }\n\n // Responses\n for (const statusCode of endpoint.observedStatusCodes) {\n const description =\n STATUS_DESCRIPTIONS[statusCode] ?? 'Response';\n\n const responseEntry: { description: string; content?: Record<string, { schema: unknown; example?: unknown }> } = {\n description,\n };\n\n // Attach response body schema only for the example response status code\n if (\n statusCode === endpoint.exampleResponse.statusCode &&\n endpoint.exampleResponse.body\n ) {\n const contentType = endpoint.exampleResponse.contentType ?? 'application/json';\n let parsed: unknown;\n try {\n parsed = JSON.parse(endpoint.exampleResponse.body);\n } catch {\n parsed = endpoint.exampleResponse.body;\n }\n responseEntry.content = {\n [contentType]: {\n schema: inferSchema(parsed),\n example: parsed,\n },\n };\n }\n\n operation.responses[String(statusCode)] = responseEntry;\n }\n\n // If no status codes observed, add a default 200\n if (endpoint.observedStatusCodes.length === 0) {\n operation.responses['200'] = { description: 'Success' };\n }\n\n return operation;\n}\n\n// ---------------------------------------------------------------------------\n// File writer\n// ---------------------------------------------------------------------------\n\n/**\n * Write an OpenAPI spec to disk as JSON with 2-space indentation.\n */\nexport async function writeOpenApiSpec(\n spec: OpenApiSpec,\n outputPath: string,\n): Promise<void> {\n if (!outputPath) {\n throw new Error('Output path is required');\n }\n await mkdir(dirname(outputPath), { recursive: true });\n const json = JSON.stringify(spec, null, 2) + '\\n';\n await writeFile(outputPath, json, 'utf-8');\n}\n","/**\n * HTML Escaping — XSS Prevention\n *\n * All data from the target site MUST be HTML-entity-encoded\n * before embedding in reports. This is a critical security boundary.\n */\n\n/**\n * Escape a string for safe inclusion in HTML content.\n * Encodes: & < > \" '\n * Strips null bytes.\n * Preserves Unicode characters.\n */\nexport function escapeHtml(input: string): string {\n // Strip null bytes first\n const stripped = input.replace(/\\0/g, '');\n\n return stripped.replace(/[&<>\"']/g, (char) => {\n switch (char) {\n case '&':\n return '&';\n case '<':\n return '<';\n case '>':\n return '>';\n case '\"':\n return '"';\n case \"'\":\n return ''';\n default:\n return char;\n }\n });\n}\n\n/**\n * Escape a string for safe inclusion in an HTML attribute value.\n * More aggressive than content escaping.\n */\nexport function escapeAttribute(input: string): string {\n const htmlEscaped = escapeHtml(input);\n\n // Additionally encode / and backtick for attribute context\n return htmlEscaped\n .replace(/\\//g, '/')\n .replace(/`/g, '`');\n}\n\n/**\n * Sanitize a string for inclusion in a JSON block embedded in HTML.\n * Prevents </script> injection.\n */\nexport function escapeJsonInHtml(jsonString: string): string {\n return jsonString\n .replace(/<\\//g, '<\\\\/')\n .replace(/<!--/g, '<\\\\!--');\n}\n","/**\n * HTML Report Template\n *\n * Generates the self-contained HTML report from assembled artifacts.\n * All target-site data is HTML-entity-encoded via escape.ts.\n * Includes CSP meta tag for XSS prevention.\n */\n\nimport { escapeHtml, escapeAttribute, escapeJsonInHtml } from './escape.js';\n\n// ---------------------------------------------------------------------------\n// Interfaces\n// ---------------------------------------------------------------------------\n\nexport interface ReportPage {\n url: string;\n title: string;\n status: number;\n depth?: number;\n contentHash?: string;\n}\n\nexport interface ReportForm {\n url: string;\n action: string;\n method: string;\n fields: Array<{ name: string; type: string; required: boolean }>;\n}\n\nexport interface ReportApiEndpoint {\n url?: string;\n pattern?: string;\n method: string;\n status?: number;\n examples?: Array<{ url: string; status: number }>;\n}\n\nexport interface ReportScreenshot {\n url: string;\n thumbnailBase64: string;\n fullPath: string;\n}\n\nexport interface ReportInput {\n title?: string;\n targetUrl: string;\n crawlDate: string;\n duration: number;\n pagesVisited: number;\n errors: number;\n pages?: ReportPage[];\n sitemap?: ReportPage[];\n forms: ReportForm[];\n apiEndpoints: ReportApiEndpoint[];\n screenshots?: ReportScreenshot[];\n flowDiagramSvg?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst CSP_META = `<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:;\">`;\n\n// ---------------------------------------------------------------------------\n// CSS\n// ---------------------------------------------------------------------------\n\nfunction generateCss(): string {\n return `\n :root {\n --bg: #ffffff;\n --bg-surface: #f8f9fa;\n --bg-surface-hover: #f0f1f3;\n --bg-nav: #1a1a2e;\n --text: #1a1a2e;\n --text-muted: #6c757d;\n --text-nav: #ffffff;\n --border: #dee2e6;\n --border-light: #e9ecef;\n --accent: #4361ee;\n --accent-hover: #3a56d4;\n --status-2xx: #198754;\n --status-3xx: #0d6efd;\n --status-4xx: #e67700;\n --status-5xx: #dc3545;\n --method-get: #198754;\n --method-post: #0d6efd;\n --method-put: #e67700;\n --method-delete: #dc3545;\n --method-patch: #cc8800;\n --badge-bg: #e9ecef;\n --shadow: 0 1px 3px rgba(0,0,0,0.08);\n --radius: 6px;\n }\n\n @media (prefers-color-scheme: dark) {\n :root {\n --bg: #0d1117;\n --bg-surface: #161b22;\n --bg-surface-hover: #1c2129;\n --bg-nav: #010409;\n --text: #e6edf3;\n --text-muted: #8b949e;\n --text-nav: #e6edf3;\n --border: #30363d;\n --border-light: #21262d;\n --accent: #58a6ff;\n --accent-hover: #79b8ff;\n --status-2xx: #3fb950;\n --status-3xx: #58a6ff;\n --status-4xx: #d29922;\n --status-5xx: #f85149;\n --method-get: #3fb950;\n --method-post: #58a6ff;\n --method-put: #d29922;\n --method-delete: #f85149;\n --method-patch: #e3b341;\n --badge-bg: #21262d;\n --shadow: 0 1px 3px rgba(0,0,0,0.3);\n }\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n line-height: 1.6;\n color: var(--text);\n background: var(--bg);\n }\n\n /* Navigation */\n .nav {\n position: sticky;\n top: 0;\n z-index: 100;\n background: var(--bg-nav);\n color: var(--text-nav);\n display: flex;\n align-items: center;\n gap: 0.25rem;\n padding: 0 1.5rem;\n box-shadow: var(--shadow);\n flex-wrap: wrap;\n }\n .nav-brand {\n font-weight: 700;\n font-size: 0.95rem;\n padding: 0.75rem 0.5rem 0.75rem 0;\n margin-right: 1rem;\n white-space: nowrap;\n }\n .nav a {\n color: var(--text-nav);\n text-decoration: none;\n padding: 0.75rem 0.75rem;\n font-size: 0.875rem;\n opacity: 0.8;\n transition: opacity 0.15s;\n }\n .nav a:hover, .nav a:focus { opacity: 1; }\n\n /* Main */\n .container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 1.5rem;\n }\n\n /* Stats bar */\n .stats {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n gap: 1rem;\n margin-bottom: 2rem;\n }\n .stat-card {\n background: var(--bg-surface);\n border: 1px solid var(--border-light);\n border-radius: var(--radius);\n padding: 1rem 1.25rem;\n }\n .stat-label {\n font-size: 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-muted);\n margin-bottom: 0.25rem;\n }\n .stat-value {\n font-size: 1.5rem;\n font-weight: 700;\n }\n .stat-value a {\n color: var(--accent);\n text-decoration: none;\n }\n\n /* Search */\n .search-bar {\n margin-bottom: 1.5rem;\n }\n .search-bar input {\n width: 100%;\n max-width: 400px;\n padding: 0.5rem 0.75rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n font-size: 0.875rem;\n background: var(--bg);\n color: var(--text);\n }\n .search-bar input:focus {\n outline: 2px solid var(--accent);\n outline-offset: -1px;\n }\n\n /* Sections */\n .section {\n margin-bottom: 2.5rem;\n }\n .section-title {\n font-size: 1.25rem;\n font-weight: 700;\n margin-bottom: 1rem;\n padding-bottom: 0.5rem;\n border-bottom: 2px solid var(--border-light);\n }\n\n /* Details / Summary */\n details {\n margin: 0.75rem 0;\n border: 1px solid var(--border-light);\n border-radius: var(--radius);\n overflow: hidden;\n }\n summary {\n cursor: pointer;\n font-weight: 600;\n padding: 0.75rem 1rem;\n background: var(--bg-surface);\n user-select: none;\n }\n summary:hover { background: var(--bg-surface-hover); }\n details > :not(summary) { padding: 0 1rem; }\n details[open] > :not(summary) { padding: 0.75rem 1rem; }\n\n /* Tables */\n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.875rem;\n }\n th, td {\n padding: 0.625rem 0.75rem;\n text-align: left;\n border-bottom: 1px solid var(--border-light);\n }\n th {\n background: var(--bg-surface);\n font-weight: 600;\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n color: var(--text-muted);\n position: sticky;\n top: 0;\n }\n tbody tr:hover { background: var(--bg-surface-hover); }\n tr[data-searchable] { transition: none; }\n\n /* Badges */\n .badge {\n display: inline-block;\n padding: 0.15rem 0.5rem;\n border-radius: 3px;\n font-size: 0.75rem;\n font-weight: 600;\n font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;\n }\n .badge-get { background: var(--method-get); color: #fff; }\n .badge-post { background: var(--method-post); color: #fff; }\n .badge-put { background: var(--method-put); color: #fff; }\n .badge-delete { background: var(--method-delete); color: #fff; }\n .badge-patch { background: var(--method-patch); color: #fff; }\n .badge-default { background: var(--badge-bg); color: var(--text); }\n .badge-field {\n background: var(--badge-bg);\n color: var(--text);\n margin: 0.1rem;\n }\n .badge-required {\n background: var(--status-5xx);\n color: #fff;\n }\n\n /* Status colors */\n .status-2xx { color: var(--status-2xx); font-weight: 600; }\n .status-3xx { color: var(--status-3xx); font-weight: 600; }\n .status-4xx { color: var(--status-4xx); font-weight: 600; }\n .status-5xx { color: var(--status-5xx); font-weight: 600; }\n\n /* Sitemap tree */\n .sitemap-tree { list-style: none; padding-left: 0; }\n .sitemap-tree ul { list-style: none; padding-left: 1.25rem; }\n .sitemap-tree li { padding: 0.2rem 0; }\n .sitemap-tree .tree-label {\n font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;\n font-size: 0.85rem;\n }\n .sitemap-tree .tree-title {\n color: var(--text-muted);\n font-size: 0.8rem;\n margin-left: 0.5rem;\n }\n\n /* Screenshots grid */\n .screenshot-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: 1rem;\n }\n .screenshot-card {\n border: 1px solid var(--border-light);\n border-radius: var(--radius);\n overflow: hidden;\n cursor: pointer;\n transition: box-shadow 0.15s;\n }\n .screenshot-card:hover {\n box-shadow: 0 4px 12px rgba(0,0,0,0.12);\n }\n .screenshot-card img {\n width: 100%;\n height: auto;\n display: block;\n background: var(--bg-surface);\n }\n .screenshot-label {\n padding: 0.5rem 0.75rem;\n font-size: 0.8rem;\n color: var(--text-muted);\n background: var(--bg-surface);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n /* Dialog lightbox */\n dialog {\n border: none;\n border-radius: var(--radius);\n padding: 0;\n max-width: 90vw;\n max-height: 90vh;\n background: var(--bg);\n box-shadow: 0 8px 30px rgba(0,0,0,0.3);\n }\n dialog::backdrop {\n background: rgba(0,0,0,0.6);\n }\n .dialog-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 1rem;\n }\n .dialog-content img {\n max-width: 100%;\n max-height: 80vh;\n }\n .dialog-actions {\n display: flex;\n gap: 0.75rem;\n margin-top: 0.75rem;\n align-items: center;\n }\n .dialog-actions a, .dialog-actions button {\n font-size: 0.85rem;\n padding: 0.4rem 0.8rem;\n border-radius: var(--radius);\n cursor: pointer;\n }\n .dialog-actions button {\n border: 1px solid var(--border);\n background: var(--bg-surface);\n color: var(--text);\n }\n .dialog-actions a {\n background: var(--accent);\n color: #fff;\n text-decoration: none;\n }\n\n /* Flow diagram */\n .flow-svg-container { overflow-x: auto; }\n .flow-svg-container svg { max-width: 100%; height: auto; }\n\n /* Examples sub-rows */\n .examples-list {\n list-style: none;\n padding: 0;\n margin: 0;\n }\n .examples-list li {\n padding: 0.2rem 0;\n font-size: 0.8rem;\n font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;\n }\n\n /* Responsive */\n @media (max-width: 768px) {\n .nav { padding: 0 0.75rem; }\n .nav a { padding: 0.5rem 0.5rem; font-size: 0.8rem; }\n .container { padding: 1rem; }\n .stats { grid-template-columns: repeat(2, 1fr); }\n .screenshot-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }\n th, td { padding: 0.4rem 0.5rem; font-size: 0.8rem; }\n }\n\n /* Print */\n @media print {\n .nav { display: none; }\n .search-bar { display: none; }\n body { background: #fff; color: #000; }\n .container { max-width: none; padding: 0; }\n details { border: none; }\n details[open] > summary { background: none; border-bottom: 1px solid #ccc; }\n details { break-inside: avoid; }\n details > * { display: block !important; }\n details:not([open]) > :not(summary) { display: block !important; }\n .screenshot-card { break-inside: avoid; }\n dialog { display: none; }\n table { font-size: 0.75rem; }\n th { background: #f0f0f0 !important; }\n }\n `;\n}\n\n// ---------------------------------------------------------------------------\n// JS\n// ---------------------------------------------------------------------------\n\nfunction generateJs(hasScreenshots: boolean): string {\n return `\n (function() {\n // Search filter\n var searchInput = document.getElementById('report-search');\n if (searchInput) {\n searchInput.addEventListener('input', function(e) {\n var term = e.target.value.toLowerCase();\n var rows = document.querySelectorAll('tr[data-searchable]');\n for (var i = 0; i < rows.length; i++) {\n var text = rows[i].textContent.toLowerCase();\n rows[i].style.display = term && text.indexOf(term) === -1 ? 'none' : '';\n }\n var items = document.querySelectorAll('li[data-searchable]');\n for (var j = 0; j < items.length; j++) {\n var itemText = items[j].textContent.toLowerCase();\n items[j].style.display = term && itemText.indexOf(term) === -1 ? 'none' : '';\n }\n });\n }\n ${hasScreenshots ? `\n // Screenshot lightbox\n var dialog = document.getElementById('screenshot-dialog');\n var dialogImg = document.getElementById('dialog-img');\n var dialogLink = document.getElementById('dialog-link');\n var cards = document.querySelectorAll('.screenshot-card');\n for (var k = 0; k < cards.length; k++) {\n cards[k].addEventListener('click', function() {\n var src = this.getAttribute('data-full');\n var thumb = this.querySelector('img');\n if (dialogImg && thumb) dialogImg.src = thumb.src;\n if (dialogLink) dialogLink.href = src;\n if (dialog && dialog.showModal) dialog.showModal();\n });\n }\n var closeBtn = document.getElementById('dialog-close');\n if (closeBtn && dialog) {\n closeBtn.addEventListener('click', function() { dialog.close(); });\n }\n if (dialog) {\n dialog.addEventListener('click', function(e) {\n if (e.target === dialog) dialog.close();\n });\n }\n ` : ''}\n })();\n `;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction statusClass(status: number): string {\n if (status >= 200 && status < 300) return 'status-2xx';\n if (status >= 300 && status < 400) return 'status-3xx';\n if (status >= 400 && status < 500) return 'status-4xx';\n if (status >= 500) return 'status-5xx';\n return '';\n}\n\nfunction methodBadgeClass(method: string): string {\n const m = method.toUpperCase();\n switch (m) {\n case 'GET': return 'badge badge-get';\n case 'POST': return 'badge badge-post';\n case 'PUT': return 'badge badge-put';\n case 'DELETE': return 'badge badge-delete';\n case 'PATCH': return 'badge badge-patch';\n default: return 'badge badge-default';\n }\n}\n\nfunction formatDuration(seconds: number): string {\n if (seconds < 60) return `${seconds.toFixed(1)}s`;\n const mins = Math.floor(seconds / 60);\n const secs = seconds % 60;\n return `${mins}m ${secs.toFixed(0)}s`;\n}\n\n// ---------------------------------------------------------------------------\n// Sitemap tree builder\n// ---------------------------------------------------------------------------\n\ninterface TreeNode {\n segment: string;\n page?: ReportPage;\n children: Map<string, TreeNode>;\n}\n\nfunction buildTree(pages: ReportPage[]): TreeNode {\n const root: TreeNode = { segment: '', children: new Map() };\n\n for (const page of pages) {\n let path: string;\n try {\n const u = new URL(page.url);\n path = u.pathname;\n } catch {\n path = page.url;\n }\n\n const segments = path.split('/').filter(Boolean);\n let current = root;\n\n for (const seg of segments) {\n if (!current.children.has(seg)) {\n current.children.set(seg, { segment: seg, children: new Map() });\n }\n current = current.children.get(seg)!;\n }\n current.page = page;\n\n // Also handle root page (path = '/')\n if (segments.length === 0) {\n root.page = page;\n }\n }\n\n return root;\n}\n\nfunction renderTree(node: TreeNode, depth: number = 0): string {\n const lines: string[] = [];\n\n // Render current node if it has a page\n if (node.page) {\n const page = node.page;\n const label = node.segment || '/';\n const statusCls = statusClass(page.status);\n const titlePart = page.title ? ` <span class=\"tree-title\">${escapeHtml(page.title)}</span>` : '';\n lines.push(\n `<li data-searchable><span class=\"tree-label\">${escapeHtml(label)}</span>` +\n ` <span class=\"${statusCls}\">${page.status}</span>` +\n `${titlePart}</li>`\n );\n } else if (depth > 0) {\n // Directory node with no page\n lines.push(`<li><span class=\"tree-label\">${escapeHtml(node.segment)}/</span></li>`);\n }\n\n // Sort children and render\n const sortedChildren = [...node.children.entries()].sort((a, b) => a[0].localeCompare(b[0]));\n if (sortedChildren.length > 0) {\n lines.push('<ul>');\n for (const [, child] of sortedChildren) {\n lines.push(renderTree(child, depth + 1));\n }\n lines.push('</ul>');\n }\n\n return lines.join('\\n');\n}\n\n// ---------------------------------------------------------------------------\n// Section renderers\n// ---------------------------------------------------------------------------\n\nfunction renderNavigation(input: ReportInput): string {\n const hasScreenshots = input.screenshots && input.screenshots.length > 0;\n const hasFlow = !!input.flowDiagramSvg;\n const pages = input.pages ?? input.sitemap ?? [];\n\n const links: string[] = [];\n if (pages.length > 0) links.push('<a href=\"#sitemap\">Sitemap</a>');\n if (input.forms.length > 0) links.push('<a href=\"#forms\">Forms</a>');\n if (input.apiEndpoints.length > 0) links.push('<a href=\"#api\">API</a>');\n if (hasScreenshots) links.push('<a href=\"#screenshots\">Screenshots</a>');\n if (hasFlow) links.push('<a href=\"#flow\">Flow</a>');\n\n return `<nav class=\"nav\" role=\"navigation\" aria-label=\"Report sections\">\n <span class=\"nav-brand\">Archaeologist</span>\n ${links.join('\\n ')}\n </nav>`;\n}\n\nfunction renderStatsBar(input: ReportInput): string {\n return `<div class=\"stats\">\n <div class=\"stat-card\">\n <div class=\"stat-label\">Target</div>\n <div class=\"stat-value\"><a href=\"${escapeAttribute(input.targetUrl)}\">${escapeHtml(truncateUrl(input.targetUrl))}</a></div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Crawl Date</div>\n <div class=\"stat-value\">${escapeHtml(input.crawlDate)}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Duration</div>\n <div class=\"stat-value\">${escapeHtml(formatDuration(input.duration))}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Pages Visited</div>\n <div class=\"stat-value\">${input.pagesVisited}</div>\n </div>\n <div class=\"stat-card\">\n <div class=\"stat-label\">Errors</div>\n <div class=\"stat-value\">${input.errors}</div>\n </div>\n </div>`;\n}\n\nfunction truncateUrl(url: string): string {\n if (url.length <= 40) return url;\n try {\n const u = new URL(url);\n return u.hostname + (u.pathname.length > 1 ? u.pathname : '');\n } catch {\n return url;\n }\n}\n\nfunction renderSitemapSection(pages: ReportPage[]): string {\n if (pages.length === 0) return '';\n\n const tree = buildTree(pages);\n const treeHtml = renderTree(tree);\n\n return `<div class=\"section\" id=\"sitemap\">\n <h2 class=\"section-title\">Sitemap (${pages.length} pages)</h2>\n <details open>\n <summary>Page Tree</summary>\n <ul class=\"sitemap-tree\">\n ${treeHtml}\n </ul>\n </details>\n </div>`;\n}\n\nfunction renderFormsSection(forms: ReportForm[]): string {\n if (forms.length === 0) return '';\n\n const rows = forms.map((form) => {\n const fieldsHtml = form.fields.map((f) => {\n const reqClass = f.required ? 'badge badge-required' : 'badge badge-field';\n return `<span class=\"${reqClass}\">${escapeHtml(f.name)}: ${escapeHtml(f.type)}</span>`;\n }).join(' ');\n\n return `<tr data-searchable>\n <td>${escapeHtml(form.url)}</td>\n <td>${escapeHtml(form.action)}</td>\n <td><span class=\"${methodBadgeClass(form.method)}\">${escapeHtml(form.method.toUpperCase())}</span></td>\n <td>${fieldsHtml}</td>\n </tr>`;\n }).join('\\n');\n\n return `<div class=\"section\" id=\"forms\">\n <h2 class=\"section-title\">Forms (${forms.length})</h2>\n <table>\n <thead><tr><th>Page URL</th><th>Action</th><th>Method</th><th>Fields</th></tr></thead>\n <tbody>${rows}</tbody>\n </table>\n </div>`;\n}\n\nfunction renderApiSection(endpoints: ReportApiEndpoint[]): string {\n if (endpoints.length === 0) return '';\n\n const rows = endpoints.map((ep) => {\n const pattern = ep.pattern ?? ep.url ?? '';\n const examplesHtml = ep.examples && ep.examples.length > 0\n ? `<details><summary>${ep.examples.length} example(s)</summary><ul class=\"examples-list\">${\n ep.examples.map((ex) =>\n `<li>${escapeHtml(ex.url)} <span class=\"${statusClass(ex.status)}\">${ex.status}</span></li>`\n ).join('')\n }</ul></details>`\n : (ep.status != null ? `<span class=\"${statusClass(ep.status)}\">${ep.status}</span>` : '');\n\n return `<tr data-searchable>\n <td><span class=\"${methodBadgeClass(ep.method)}\">${escapeHtml(ep.method.toUpperCase())}</span></td>\n <td>${escapeHtml(pattern)}</td>\n <td>${examplesHtml}</td>\n </tr>`;\n }).join('\\n');\n\n return `<div class=\"section\" id=\"api\">\n <h2 class=\"section-title\">API Endpoints (${endpoints.length})</h2>\n <table>\n <thead><tr><th>Method</th><th>Pattern</th><th>Details</th></tr></thead>\n <tbody>${rows}</tbody>\n </table>\n </div>`;\n}\n\nfunction renderScreenshotsSection(screenshots: ReportScreenshot[]): string {\n if (screenshots.length === 0) return '';\n\n const cards = screenshots.map((ss) => {\n let label: string;\n try {\n const u = new URL(ss.url);\n label = u.pathname || '/';\n } catch {\n label = ss.url;\n }\n\n return `<div class=\"screenshot-card\" data-full=\"${escapeAttribute(ss.fullPath)}\" data-searchable>\n <img src=\"data:image/jpeg;base64,${escapeAttribute(ss.thumbnailBase64)}\" alt=\"Screenshot of ${escapeAttribute(label)}\" loading=\"lazy\">\n <div class=\"screenshot-label\">${escapeHtml(label)}</div>\n </div>`;\n }).join('\\n');\n\n return `<div class=\"section\" id=\"screenshots\">\n <h2 class=\"section-title\">Screenshots (${screenshots.length})</h2>\n <div class=\"screenshot-grid\">\n ${cards}\n </div>\n </div>\n\n <dialog id=\"screenshot-dialog\">\n <div class=\"dialog-content\">\n <img id=\"dialog-img\" src=\"\" alt=\"Full screenshot\">\n <div class=\"dialog-actions\">\n <a id=\"dialog-link\" href=\"\" target=\"_blank\" rel=\"noopener\">Open Full Image</a>\n <button id=\"dialog-close\" type=\"button\">Close</button>\n </div>\n </div>\n </dialog>`;\n}\n\nfunction renderFlowSection(svgContent: string): string {\n return `<div class=\"section\" id=\"flow\">\n <h2 class=\"section-title\">Flow Diagram</h2>\n <details open>\n <summary>Navigation Flow</summary>\n <div class=\"flow-svg-container\">\n ${svgContent}\n </div>\n </details>\n </div>`;\n}\n\n// ---------------------------------------------------------------------------\n// Main generator\n// ---------------------------------------------------------------------------\n\n/**\n * Generate the complete HTML report string.\n * All user-provided data is escaped to prevent XSS.\n */\nexport function generateReportHtml(input: ReportInput): string {\n const pages = input.pages ?? input.sitemap ?? [];\n const screenshots = input.screenshots ?? [];\n const hasScreenshots = screenshots.length > 0;\n const hasFlow = !!input.flowDiagramSvg;\n\n const reportTitle = input.title\n ? escapeHtml(input.title)\n : `Archaeologist Report: ${escapeHtml(input.targetUrl)}`;\n\n const reportData = {\n targetUrl: input.targetUrl,\n crawlDate: input.crawlDate,\n duration: input.duration,\n pagesVisited: input.pagesVisited,\n errors: input.errors,\n pageCount: pages.length,\n formCount: input.forms.length,\n apiEndpointCount: input.apiEndpoints.length,\n screenshotCount: screenshots.length,\n };\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n ${CSP_META}\n <title>${reportTitle}</title>\n <style>${generateCss()}</style>\n</head>\n<body>\n ${renderNavigation(input)}\n\n <div class=\"container\">\n <h1>${reportTitle}</h1>\n\n ${renderStatsBar(input)}\n\n <div class=\"search-bar\">\n <input type=\"text\" id=\"report-search\" placeholder=\"Filter pages, forms, endpoints...\" aria-label=\"Search report\">\n </div>\n\n ${renderSitemapSection(pages)}\n ${renderFormsSection(input.forms)}\n ${renderApiSection(input.apiEndpoints)}\n ${hasScreenshots ? renderScreenshotsSection(screenshots) : ''}\n ${hasFlow ? renderFlowSection(input.flowDiagramSvg!) : ''}\n </div>\n\n <script type=\"application/json\" id=\"report-data\">${escapeJsonInHtml(JSON.stringify(reportData))}</script>\n <script>${generateJs(hasScreenshots)}</script>\n</body>\n</html>`;\n}\n","/**\n * Crawl orchestrator — the heart of M1.\n *\n * Coordinates browser launch, secure context creation, BFS frontier loop,\n * per-page navigation and scanning, SSRF protection, graceful shutdown,\n * and output generation.\n *\n * Error strategy:\n * - Per-page errors are logged and the page is skipped (crawl continues).\n * - SIGINT triggers graceful shutdown: stop loop, write partial results, close browser.\n * - Fatal errors (browser crash, output dir unwritable) abort the crawl.\n */\n\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { chromium } from 'playwright';\nimport type { Browser, BrowserContext, Page, Response } from 'playwright';\n\nimport type { ResolvedConfig } from '../types/config.js';\nimport type {\n CrawlResult,\n DigResult,\n PageVisitResult,\n PageScanResult,\n PageVisitStatus,\n AssembledArtifacts,\n RouteNode,\n RouteInfo,\n ApiEndpointGroup,\n ScreenshotManifestEntry,\n} from '../types/artifacts.js';\nimport type { CrawlErrorEntry } from '../types/errors.js';\nimport type { FrontierEntry } from './frontier.js';\n\nimport { Frontier } from './frontier.js';\nimport { shouldCrawl } from './url-utils.js';\nimport { isBlockedUrl } from '../security/network-guard.js';\nimport {\n getSecureContextOptions,\n getSecureLaunchOptions,\n TOOL_VERSION,\n} from '../security/browser-hardening.js';\nimport { logger } from '../utils/logger.js';\n\n// Collectors\nimport { captureScreenshots } from '../collectors/screenshot-capturer.js';\nimport { probeForms } from '../collectors/form-prober.js';\nimport { createNetworkLogger } from '../collectors/network-logger.js';\n\n// Assemblers & report generators\nimport { buildFlowGraph, generateMermaidDefinition } from '../assembler/flow-graph.js';\nimport { parameterizePath } from '../assembler/api-grouper.js';\nimport { generateOpenApiSpec, writeOpenApiSpec } from '../report/openapi-output.js';\nimport { generateReportHtml } from '../report/html/template.js';\n\n\n// ---------------------------------------------------------------------------\n// Init script injected into every page context.\n// Provides DOM stability detection via MutationObserver.\n// ---------------------------------------------------------------------------\n\nconst INIT_SCRIPT = `\n(() => {\n // DOM stability detection — sets window.__pa_isDomStable when no mutations\n // have occurred for 500ms.\n let _paStableTimer = null;\n let _paIsStable = false;\n\n window.__pa_isDomStable = () => _paIsStable;\n\n const observer = new MutationObserver(() => {\n _paIsStable = false;\n if (_paStableTimer) clearTimeout(_paStableTimer);\n _paStableTimer = setTimeout(() => { _paIsStable = true; }, 500);\n });\n\n // Start observing once the DOM is ready.\n if (document.documentElement) {\n observer.observe(document.documentElement, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n } else {\n document.addEventListener('DOMContentLoaded', () => {\n observer.observe(document.documentElement, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n });\n }\n\n // Mark stable initially after a short delay (for static pages).\n _paStableTimer = setTimeout(() => { _paIsStable = true; }, 500);\n})();\n`;\n\n// ---------------------------------------------------------------------------\n// Types internal to the orchestrator\n// ---------------------------------------------------------------------------\n\ninterface VisitResult {\n status: PageVisitStatus;\n url: string;\n finalUrl: string;\n httpStatus?: number;\n response?: Response;\n error?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Sleep utility\n// ---------------------------------------------------------------------------\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ---------------------------------------------------------------------------\n// Per-page navigation\n// ---------------------------------------------------------------------------\n\nasync function visitPage(\n page: Page,\n entry: FrontierEntry,\n config: ResolvedConfig,\n): Promise<VisitResult> {\n try {\n // Use 'load' NOT 'networkidle' (per blocker #7).\n const response = await page.goto(entry.url, {\n waitUntil: 'load',\n timeout: config.timeout,\n });\n\n if (!response) {\n return { status: 'no_response', url: entry.url, finalUrl: entry.url };\n }\n\n const finalUrl = response.url();\n const httpStatus = response.status();\n\n if (httpStatus >= 400) {\n return {\n status: 'http_error',\n url: entry.url,\n finalUrl,\n httpStatus,\n response,\n };\n }\n\n // Wait for DOM stability (500ms quiet period from MutationObserver).\n // Non-fatal if this times out — we proceed with what we have.\n await page\n .waitForFunction('window.__pa_isDomStable && window.__pa_isDomStable()', {\n timeout: 5000,\n })\n .catch(() => {\n logger.debug(`DOM stability timeout on ${entry.url} — proceeding anyway`);\n });\n\n return {\n status: 'ok',\n url: entry.url,\n finalUrl,\n httpStatus,\n response,\n };\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n\n if (message.includes('net::ERR_')) {\n return {\n status: 'network_error',\n url: entry.url,\n finalUrl: entry.url,\n error: message,\n };\n }\n if (message.includes('Timeout') || message.includes('timeout')) {\n return {\n status: 'timeout',\n url: entry.url,\n finalUrl: entry.url,\n error: message,\n };\n }\n\n return {\n status: 'network_error',\n url: entry.url,\n finalUrl: entry.url,\n error: message,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Lightweight page scanner (inline for M1 — scanPage collector built in\n// parallel will replace this once available).\n//\n// We attempt to dynamically import the real scanner. If not available,\n// fall back to the built-in minimal implementation.\n// ---------------------------------------------------------------------------\n\nasync function scanPage(page: Page, targetUrl: string): Promise<PageScanResult> {\n // Try the real collector first.\n try {\n const mod = await import('../collectors/page-scanner.js');\n if (typeof mod.scanPage === 'function') {\n return await mod.scanPage(page, targetUrl);\n }\n } catch {\n // Collector not yet available — use inline fallback.\n }\n\n return inlineScanPage(page);\n}\n\n/**\n * Minimal inline page scanner used when the full collector module\n * is not yet built. Extracts links, title, and basic metadata.\n */\nasync function inlineScanPage(page: Page): Promise<PageScanResult> {\n const url = page.url();\n\n const data = await page.evaluate(() => {\n const title = document.title || '';\n\n // Collect links\n const anchors = Array.from(document.querySelectorAll('a[href]'));\n const links = anchors\n .map((a) => {\n const el = a as HTMLAnchorElement;\n const href = el.href; // already resolved to absolute by the browser\n if (!href || href.startsWith('javascript:') || href.startsWith('mailto:')) {\n return null;\n }\n return {\n href,\n text: (el.textContent || '').trim().slice(0, 200),\n isExternal: el.origin !== window.location.origin,\n rel: el.rel || undefined,\n };\n })\n .filter(Boolean) as Array<{\n href: string;\n text: string;\n isExternal: boolean;\n rel?: string;\n }>;\n\n // Collect headings\n const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')).map((h) => ({\n level: parseInt(h.tagName[1], 10) as 1 | 2 | 3 | 4 | 5 | 6,\n text: (h.textContent || '').trim().slice(0, 500),\n }));\n\n // Collect meta tags\n const metaTags = Array.from(document.querySelectorAll('meta[name], meta[property]')).map((m) => {\n const el = m as HTMLMetaElement;\n return {\n name: el.name || undefined,\n property: el.getAttribute('property') || undefined,\n content: el.content || '',\n };\n });\n\n // Collect landmarks\n const landmarkRoles = ['banner', 'navigation', 'main', 'complementary', 'contentinfo', 'search', 'form', 'region'];\n const landmarks = Array.from(document.querySelectorAll('[role]'))\n .filter((el) => landmarkRoles.includes((el.getAttribute('role') || '').toLowerCase()))\n .map((el) => ({\n role: el.getAttribute('role')!,\n tagName: el.tagName.toLowerCase(),\n label: el.getAttribute('aria-label') || undefined,\n }));\n\n // Simple content hash (hash of innerText length + first 1000 chars)\n const textContent = (document.body?.innerText || '').slice(0, 10000);\n let hash = 0;\n for (let i = 0; i < textContent.length; i++) {\n const char = textContent.charCodeAt(i);\n hash = ((hash << 5) - hash + char) | 0;\n }\n const contentHash = Math.abs(hash).toString(16).padStart(8, '0');\n\n // Timing\n const perfEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming | undefined;\n const timing = {\n loadTime: perfEntry ? Math.round(perfEntry.loadEventEnd - perfEntry.startTime) : 0,\n domContentLoaded: perfEntry ? Math.round(perfEntry.domContentLoadedEventEnd - perfEntry.startTime) : 0,\n firstContentfulPaint: undefined as number | undefined,\n };\n const fcp = performance.getEntriesByName('first-contentful-paint')[0];\n if (fcp) {\n timing.firstContentfulPaint = Math.round(fcp.startTime);\n }\n\n // Hash routing detection\n const hashRoutingDetected = window.location.hash.startsWith('#/') || window.location.hash.startsWith('#!/');\n\n return {\n title,\n links,\n headings,\n metaTags,\n landmarks,\n contentHash,\n timing,\n hashRoutingDetected,\n };\n });\n\n return {\n url,\n statusCode: 200,\n title: data.title,\n metaTags: data.metaTags,\n headings: data.headings,\n landmarks: data.landmarks,\n links: data.links,\n interactiveElements: [],\n timing: data.timing,\n contentHash: data.contentHash,\n hashRoutingDetected: data.hashRoutingDetected,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Build a minimal route tree from page visit results\n// ---------------------------------------------------------------------------\n\nfunction buildRouteTree(pages: PageVisitResult[], targetUrl: string): RouteNode {\n const root: RouteNode = {\n segment: '/',\n url: targetUrl,\n title: '',\n statusCode: 0,\n contentHash: '',\n headings: [],\n landmarks: [],\n depth: 0,\n children: [],\n formCount: 0,\n apiCallCount: 0,\n hasScreenshot: false,\n };\n\n // Simple flat tree: each page becomes a child of root.\n // The full Assembler will build a proper hierarchical tree.\n for (const page of pages) {\n if (page.url === targetUrl && page.pageScan) {\n // Fill root node from entry page.\n root.title = page.pageScan.title;\n root.statusCode = page.pageScan.statusCode;\n root.contentHash = page.pageScan.contentHash;\n root.headings = page.pageScan.headings;\n root.landmarks = page.pageScan.landmarks;\n root.hasScreenshot = !!page.screenshot;\n continue;\n }\n\n let segment: string;\n try {\n const parsed = new URL(page.url);\n segment = parsed.pathname.split('/').filter(Boolean).pop() || parsed.pathname;\n } catch {\n segment = page.url;\n }\n\n const node: RouteNode = {\n segment,\n url: page.url,\n title: page.pageScan?.title || '',\n statusCode: page.pageScan?.statusCode || page.httpStatus || 0,\n contentHash: page.pageScan?.contentHash || '',\n headings: page.pageScan?.headings || [],\n landmarks: page.pageScan?.landmarks || [],\n depth: page.depth,\n children: [],\n formCount: 0,\n apiCallCount: 0,\n hasScreenshot: !!page.screenshot,\n };\n root.children.push(node);\n }\n\n return root;\n}\n\nfunction pagesToRouteInfos(pages: PageVisitResult[]): RouteInfo[] {\n return pages\n .filter((p) => p.status === 'ok')\n .map((p) => ({\n url: p.url,\n title: p.pageScan?.title || '',\n statusCode: p.pageScan?.statusCode || p.httpStatus || 0,\n contentHash: p.pageScan?.contentHash || '',\n depth: p.depth,\n formCount: 0,\n apiCallCount: 0,\n hasScreenshot: !!p.screenshot,\n discoveryMethod: p.discoveryMethod,\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Helper: build ApiEndpointGroup[] from network logs across all pages\n// ---------------------------------------------------------------------------\n\nfunction buildApiEndpointGroupsFromPages(pages: PageVisitResult[]): ApiEndpointGroup[] {\n // Collect all API requests with their responses across all pages.\n const groupMap = new Map<string, {\n pattern: string;\n method: string;\n urls: string[];\n callingPages: Set<string>;\n statusCodes: Set<number>;\n contentTypes: Set<string>;\n isGraphQL: boolean;\n graphqlOps: Set<string>;\n firstRequest?: { url: string; headers: Record<string, string>; body?: string; contentType?: string };\n firstResponse?: { statusCode: number; headers: Record<string, string>; body?: string; contentType?: string; bodySize: number };\n }>();\n\n for (const page of pages) {\n if (!page.networkLog) continue;\n const log = page.networkLog;\n\n for (const req of log.requests) {\n if (req.classification !== 'api') continue;\n\n let urlPath: string;\n try {\n urlPath = new URL(req.url).pathname;\n } catch {\n urlPath = req.url;\n }\n\n const pattern = parameterizePath(urlPath);\n const key = `${req.method} ${pattern}`;\n\n let group = groupMap.get(key);\n if (!group) {\n group = {\n pattern,\n method: req.method,\n urls: [],\n callingPages: new Set(),\n statusCodes: new Set(),\n contentTypes: new Set(),\n isGraphQL: req.url.includes('graphql'),\n graphqlOps: new Set(),\n };\n groupMap.set(key, group);\n }\n\n group.urls.push(req.url);\n group.callingPages.add(page.url);\n\n const resp = log.responses.find(r => r.requestId === req.requestId);\n\n if (resp) {\n group.statusCodes.add(resp.statusCode);\n if (resp.contentType) group.contentTypes.add(resp.contentType);\n }\n\n // Store first request/response as example.\n if (!group.firstRequest) {\n group.firstRequest = {\n url: req.url,\n headers: req.headers,\n body: req.body,\n contentType: req.contentType,\n };\n }\n if (!group.firstResponse && resp) {\n group.firstResponse = {\n statusCode: resp.statusCode,\n headers: resp.headers,\n body: resp.body,\n contentType: resp.contentType,\n bodySize: resp.bodySize,\n };\n }\n }\n\n // Collect GraphQL operations.\n for (const gqlOp of log.graphqlOperations) {\n const key = `POST ${parameterizePath(new URL(gqlOp.endpointUrl).pathname)}`;\n const group = groupMap.get(key);\n if (group) {\n group.isGraphQL = true;\n group.graphqlOps.add(gqlOp.operationName);\n }\n }\n }\n\n // Convert to ApiEndpointGroup[].\n const result: ApiEndpointGroup[] = [];\n for (const g of groupMap.values()) {\n result.push({\n pattern: g.pattern,\n method: g.method,\n classification: 'api',\n observedUrls: g.urls,\n callCount: g.urls.length,\n callingPages: Array.from(g.callingPages),\n exampleRequest: g.firstRequest ?? {\n url: g.urls[0] ?? '',\n headers: {},\n },\n exampleResponse: g.firstResponse ?? {\n statusCode: 0,\n headers: {},\n bodySize: 0,\n },\n observedStatusCodes: Array.from(g.statusCodes),\n observedContentTypes: Array.from(g.contentTypes),\n isGraphQL: g.isGraphQL,\n graphqlOperations: g.graphqlOps.size > 0 ? Array.from(g.graphqlOps) : undefined,\n });\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Helper: build ScreenshotManifestEntry[] from pages\n// ---------------------------------------------------------------------------\n\nfunction buildScreenshotManifest(pages: PageVisitResult[], outputDir: string): ScreenshotManifestEntry[] {\n return pages\n .filter(p => p.screenshot && p.screenshot.fullPagePath)\n .map(p => {\n const ss = p.screenshot!;\n // Convert absolute paths to relative (from output dir).\n const fullPageRel = path.relative(outputDir, ss.fullPagePath);\n const viewportRel = path.relative(outputDir, ss.viewportPath);\n\n return {\n url: ss.pageUrl,\n title: p.pageScan?.title || '',\n fullPagePath: fullPageRel,\n viewportPath: viewportRel,\n viewport: ss.viewport,\n dimensions: ss.dimensions,\n fullPageHash: ss.fullPageHash,\n thumbnailBase64: '', // Thumbnail generation is a separate step (not in M1).\n };\n });\n}\n\n// ---------------------------------------------------------------------------\n// Build the assembled artifacts from crawl results\n// ---------------------------------------------------------------------------\n\nfunction buildAssembledArtifacts(\n crawlResult: CrawlResult,\n pages: PageVisitResult[],\n): AssembledArtifacts {\n const routeTree = buildRouteTree(pages, crawlResult.config.targetUrl);\n const routes = pagesToRouteInfos(pages);\n\n // Collect forms from all pages.\n const allForms = pages\n .filter(p => p.formProbe)\n .flatMap(p => p.formProbe!.forms);\n\n // Build API endpoint groups from network logs.\n const apiEndpoints = buildApiEndpointGroupsFromPages(pages);\n\n // Collect GraphQL operations from all pages.\n const allGraphqlOps = pages\n .filter(p => p.networkLog)\n .flatMap(p => p.networkLog!.graphqlOperations);\n\n // Collect WebSocket connections from all pages.\n const allWebSockets = pages\n .filter(p => p.networkLog)\n .flatMap(p => p.networkLog!.webSocketConnections);\n\n // Build flow graph from navigation edges.\n const allEdges = pages.flatMap(p => p.navigationEdges);\n const flowGraph = allEdges.length > 0\n ? buildFlowGraph(allEdges, crawlResult.config.targetUrl)\n : undefined;\n\n // Generate Mermaid definition.\n const mermaidDef = flowGraph ? generateMermaidDefinition(flowGraph) : '';\n\n // Build screenshot manifest.\n const screenshots = buildScreenshotManifest(pages, crawlResult.config.targetUrl);\n\n // Update route tree node counts.\n for (const node of [routeTree, ...routeTree.children]) {\n const pageForNode = pages.find(p => p.url === node.url);\n if (pageForNode) {\n node.formCount = pageForNode.formProbe?.forms.length ?? 0;\n node.apiCallCount = pageForNode.networkLog?.requests.filter(r => r.classification === 'api').length ?? 0;\n node.hasScreenshot = !!pageForNode.screenshot;\n }\n }\n\n // Update route info counts.\n const enrichedRoutes = routes.map(r => {\n const pageForRoute = pages.find(p => p.url === r.url);\n return {\n ...r,\n formCount: pageForRoute?.formProbe?.forms.length ?? 0,\n apiCallCount: pageForRoute?.networkLog?.requests.filter(req => req.classification === 'api').length ?? 0,\n hasScreenshot: !!pageForRoute?.screenshot,\n };\n });\n\n return {\n meta: {\n toolVersion: TOOL_VERSION,\n targetUrl: crawlResult.config.targetUrl,\n crawlDate: crawlResult.startedAt,\n duration: crawlResult.durationMs,\n pagesVisited: pages.length,\n pagesDiscovered: crawlResult.pages.length + crawlResult.unvisitedUrls.length,\n formsFound: allForms.length,\n apiEndpointsFound: apiEndpoints.length,\n screenshotsTaken: screenshots.length,\n errorCount: crawlResult.errors.length,\n completionStatus: crawlResult.completionStatus,\n viewport: crawlResult.config.viewport,\n },\n routeTree,\n routes: enrichedRoutes,\n forms: allForms,\n apiEndpoints,\n graphqlOperations: allGraphqlOps,\n webSocketConnections: allWebSockets,\n flowGraph: flowGraph\n ? {\n nodes: flowGraph.nodes.map(n => ({\n url: n.url,\n title: pages.find(p => p.url === n.url)?.pageScan?.title || '',\n cluster: n.cluster ?? '/',\n })),\n edges: flowGraph.edges,\n entryPoint: flowGraph.entryUrl,\n deadEnds: flowGraph.nodes.filter(n => n.isExit).map(n => n.url),\n cycles: flowGraph.cycleNodes.length > 0 ? [flowGraph.cycleNodes] : [],\n }\n : {\n nodes: pages\n .filter(p => p.status === 'ok')\n .map(p => ({\n url: p.url,\n title: p.pageScan?.title || '',\n cluster: '/',\n })),\n edges: [],\n entryPoint: crawlResult.config.targetUrl,\n deadEnds: [],\n cycles: [],\n },\n flowDiagrams: {\n overview: { definition: mermaidDef },\n sections: [],\n },\n screenshots,\n errors: crawlResult.errors,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Write output files\n// ---------------------------------------------------------------------------\n\nasync function writeOutputFiles(\n outputDir: string,\n crawlResult: CrawlResult,\n pages: PageVisitResult[],\n): Promise<{\n sitemapJson?: string;\n}> {\n const paths: { sitemapJson?: string } = {};\n\n // Write sitemap.json\n const sitemap = pages\n .filter((p) => p.status === 'ok')\n .map((p) => ({\n url: p.url,\n title: p.pageScan?.title || '',\n depth: p.depth,\n statusCode: p.httpStatus,\n links: p.pageScan?.links.length ?? 0,\n }));\n\n const sitemapPath = path.join(outputDir, 'sitemap.json');\n await fs.writeFile(sitemapPath, JSON.stringify(sitemap, null, 2), 'utf-8');\n paths.sitemapJson = sitemapPath;\n\n // Write crawl summary\n const summary = {\n targetUrl: crawlResult.config.targetUrl,\n startedAt: crawlResult.startedAt,\n finishedAt: crawlResult.finishedAt,\n durationMs: crawlResult.durationMs,\n pagesVisited: crawlResult.pages.length,\n pagesDiscovered: crawlResult.pages.length + crawlResult.unvisitedUrls.length,\n completionStatus: crawlResult.completionStatus,\n errors: crawlResult.errors.length,\n };\n\n const summaryPath = path.join(outputDir, 'crawl-summary.json');\n await fs.writeFile(summaryPath, JSON.stringify(summary, null, 2), 'utf-8');\n\n return paths;\n}\n\n// ---------------------------------------------------------------------------\n// Pre-flight checks\n// ---------------------------------------------------------------------------\n\nasync function ensureOutputDir(outputDir: string): Promise<void> {\n await fs.mkdir(outputDir, { recursive: true });\n}\n\nasync function checkBrowserInstallation(): Promise<void> {\n let browser: Browser | undefined;\n try {\n browser = await chromium.launch({ handleSIGINT: false, handleSIGTERM: false, handleSIGHUP: false });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `Chromium browser not found. Run \\`pa install\\` to install it.\\n\\nDetails: ${message}`,\n );\n } finally {\n if (browser) {\n await browser.close();\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main orchestrator\n// ---------------------------------------------------------------------------\n\n/**\n * Run a full crawl of the target URL.\n *\n * @param config - Fully validated and resolved crawl configuration.\n * @returns The assembled artifacts, output paths, and completion status.\n */\nexport async function dig(config: ResolvedConfig): Promise<DigResult> {\n const startedAt = new Date();\n const errors: CrawlErrorEntry[] = [];\n const pages: PageVisitResult[] = [];\n let completionStatus: CrawlResult['completionStatus'] = 'complete';\n let interrupted = false;\n let browser: Browser | undefined;\n let context: BrowserContext | undefined;\n\n // ------------------------------------------------------------------\n // SIGINT handler for graceful shutdown\n // ------------------------------------------------------------------\n const sigintHandler = () => {\n logger.warn('SIGINT received — shutting down gracefully...');\n interrupted = true;\n completionStatus = 'interrupted';\n };\n process.on('SIGINT', sigintHandler);\n\n try {\n // ----------------------------------------------------------------\n // 1. Pre-flight checks\n // ----------------------------------------------------------------\n logger.info(`Crawling ${config.targetUrl}...`);\n logger.info(` depth=${config.depth}, maxPages=${config.maxPages}, concurrency=${config.concurrency}`);\n logger.info(` viewport=${config.viewport.width}x${config.viewport.height}`);\n logger.info(` output=${config.outputDir}, format=${config.format}`);\n\n await ensureOutputDir(config.outputDir);\n await checkBrowserInstallation();\n\n // ----------------------------------------------------------------\n // 2. Launch browser\n // ----------------------------------------------------------------\n browser = await chromium.launch(getSecureLaunchOptions());\n logger.debug('Browser launched');\n\n // ----------------------------------------------------------------\n // 3. Create secure browser context\n // ----------------------------------------------------------------\n const contextOptions = getSecureContextOptions({\n viewport: config.viewport,\n recordHar: config.noHar\n ? undefined\n : {\n path: path.join(config.outputDir, 'crawl.har'),\n content: 'embed',\n mode: 'minimal',\n },\n });\n\n context = await browser.newContext(contextOptions);\n\n // Inject init script for DOM stability detection.\n await context.addInitScript(INIT_SCRIPT);\n\n // Register popup handler on individual pages instead of context.\n // context.on('page') fires for ALL pages including ones we create with\n // context.newPage(), which would close them before we can use them.\n const frontier = new Frontier({ maxDepth: config.depth });\n\n logger.debug('Browser context created with secure defaults');\n\n // ----------------------------------------------------------------\n // 4. Initialize frontier\n // ----------------------------------------------------------------\n frontier.enqueue({ url: config.targetUrl, depth: 0 });\n logger.debug(`Frontier initialized with entry URL: ${config.targetUrl}`);\n\n // ----------------------------------------------------------------\n // 5. BFS Crawl Loop\n // ----------------------------------------------------------------\n let visitedCount = 0;\n const crawlStartTime = Date.now();\n\n while (!frontier.isEmpty && visitedCount < config.maxPages && !interrupted) {\n // Check global time limit.\n if (config.maxTime > 0) {\n const elapsed = (Date.now() - crawlStartTime) / 1000;\n if (elapsed >= config.maxTime) {\n logger.warn(`Max crawl time reached (${config.maxTime}s) — stopping`);\n completionStatus = 'max_time_reached';\n break;\n }\n }\n\n const entry = frontier.dequeue();\n if (!entry) break;\n\n // SSRF check\n if (!config.allowPrivate) {\n const blockResult = await isBlockedUrl(entry.url);\n if (blockResult.blocked) {\n logger.warn(`SSRF blocked: ${entry.url} — ${blockResult.reason}`);\n errors.push({\n timestamp: new Date().toISOString(),\n url: entry.url,\n code: 'ERR_SECURITY_BLOCK',\n message: blockResult.reason || 'Blocked by SSRF protection',\n securityReason: 'private_ip',\n });\n continue;\n }\n }\n\n // Create new page in context.\n const page = await context.newPage();\n const pageStartTime = Date.now();\n\n // Handle popups opened from this page (window.open, target=\"_blank\").\n page.on('popup', async (popup) => {\n try {\n const popupUrl = popup.url();\n if (popupUrl && popupUrl !== 'about:blank') {\n logger.debug(`Popup detected: ${popupUrl}`);\n if (\n shouldCrawl(popupUrl, config.targetUrl, {\n include: config.include,\n exclude: config.exclude,\n followExternal: config.followExternal,\n })\n ) {\n frontier.enqueue({ url: popupUrl, depth: entry.depth + 1 });\n }\n }\n await popup.close();\n } catch {\n // Best-effort — popup may already be closed.\n }\n });\n const jsErrors: string[] = [];\n\n // Capture JS errors on the page (blocker #14).\n page.on('pageerror', (err) => {\n jsErrors.push(err.message);\n logger.debug(`JS error on ${entry.url}: ${err.message}`);\n });\n\n // Auto-dismiss dialogs.\n page.on('dialog', async (dialog) => {\n logger.debug(`Dialog on ${entry.url}: ${dialog.type()} \"${dialog.message()}\"`);\n await dialog.dismiss();\n });\n\n // Start network logger BEFORE navigation so it captures all requests.\n const networkLogger = createNetworkLogger(page, entry.url, {\n includeCookies: config.includeCookies,\n });\n networkLogger.start();\n\n // Visit the page.\n const visitResult = await visitPage(page, entry, config);\n visitedCount++;\n\n // Build the PageVisitResult.\n const pageVisitResult: PageVisitResult = {\n url: entry.url,\n finalUrl: visitResult.finalUrl,\n status: visitResult.status,\n httpStatus: visitResult.httpStatus,\n depth: entry.depth,\n referrer: entry.referrer,\n discoveryMethod: entry.referrer ? 'link' : 'entry',\n visitedAt: new Date(pageStartTime).toISOString(),\n durationMs: 0,\n collectorErrors: [],\n navigationEdges: [],\n };\n\n // If navigation succeeded, scan the page.\n if (visitResult.status === 'ok') {\n try {\n const scanResult = await scanPage(page, config.targetUrl);\n pageVisitResult.pageScan = scanResult;\n pageVisitResult.httpStatus = scanResult.statusCode;\n\n // Enqueue discovered links.\n for (const link of scanResult.links) {\n if (\n shouldCrawl(link.href, config.targetUrl, {\n include: config.include,\n exclude: config.exclude,\n followExternal: config.followExternal,\n })\n ) {\n const enqueued = frontier.enqueue({\n url: link.href,\n depth: entry.depth + 1,\n referrer: entry.url,\n });\n if (enqueued) {\n pageVisitResult.navigationEdges.push({\n from: entry.url,\n to: link.href,\n trigger: 'link',\n triggerText: link.text || undefined,\n });\n }\n }\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Scanner error on ${entry.url}: ${message}`);\n pageVisitResult.collectorErrors.push({\n collector: 'page-scanner',\n message,\n });\n }\n\n // Screenshot capture (if enabled).\n if (!config.noScreenshots) {\n try {\n const screenshotsDir = path.join(config.outputDir, 'screenshots');\n const ssResult = await captureScreenshots(page, config.targetUrl, screenshotsDir, config.viewport);\n pageVisitResult.screenshot = ssResult;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Screenshot error on ${entry.url}: ${message}`);\n pageVisitResult.collectorErrors.push({ collector: 'screenshot-capturer', message });\n }\n }\n\n // Form probing.\n try {\n const formResult = await probeForms(page, entry.url);\n pageVisitResult.formProbe = formResult;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Form probe error on ${entry.url}: ${message}`);\n pageVisitResult.collectorErrors.push({ collector: 'form-prober', message });\n }\n } else {\n // Log navigation failures.\n const errorMessage = visitResult.error || `Navigation failed: ${visitResult.status}`;\n errors.push({\n timestamp: new Date().toISOString(),\n url: entry.url,\n code: 'ERR_NAVIGATION',\n message: errorMessage,\n status: visitResult.status === 'timeout' ? 'timeout' : 'network_error',\n httpStatus: visitResult.httpStatus,\n });\n }\n\n // Stop network logging (always, regardless of navigation outcome).\n try {\n const networkResult = networkLogger.stop();\n pageVisitResult.networkLog = networkResult;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Network log error on ${entry.url}: ${message}`);\n pageVisitResult.collectorErrors.push({ collector: 'network-logger', message });\n }\n\n pageVisitResult.durationMs = Date.now() - pageStartTime;\n pages.push(pageVisitResult);\n\n // Close the page.\n try {\n await page.close();\n } catch {\n // Best-effort — page may already be closed.\n }\n\n // Delay between pages.\n if (config.delay > 0 && !interrupted) {\n await sleep(config.delay);\n }\n\n // Log progress.\n const statusIcon = visitResult.status === 'ok' ? 'OK' : visitResult.status.toUpperCase();\n logger.info(\n `[${visitedCount}/${frontier.totalSeen}] ${statusIcon} ${entry.url}${\n entry.depth > 0 ? ` (depth ${entry.depth})` : ''\n }`,\n );\n if (visitResult.status !== 'ok' && visitResult.error) {\n logger.warn(` Error: ${visitResult.error}`);\n }\n }\n\n // Determine completion status if not already set.\n if (!interrupted && completionStatus === 'complete') {\n if (visitedCount >= config.maxPages && !frontier.isEmpty) {\n completionStatus = 'max_pages_reached';\n }\n }\n\n // ----------------------------------------------------------------\n // 6. Teardown\n // ----------------------------------------------------------------\n logger.debug('Closing browser context...');\n if (context) {\n await context.close().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Context close error (HAR may not be written): ${message}`);\n });\n }\n\n logger.debug('Closing browser...');\n if (browser) {\n await browser.close().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Browser close error: ${message}`);\n });\n }\n\n // Nullify references so finally-block doesn't double-close.\n context = undefined;\n browser = undefined;\n\n // ----------------------------------------------------------------\n // 7. Generate output\n // ----------------------------------------------------------------\n const finishedAt = new Date();\n const durationMs = finishedAt.getTime() - startedAt.getTime();\n\n // Collect unvisited URLs from the frontier.\n const unvisitedUrls: string[] = [];\n while (!frontier.isEmpty) {\n const remaining = frontier.dequeue();\n if (remaining) unvisitedUrls.push(remaining.url);\n }\n\n const crawlResult: CrawlResult = {\n config: {\n targetUrl: config.targetUrl,\n depth: config.depth,\n maxPages: config.maxPages,\n concurrency: config.concurrency,\n viewport: config.viewport,\n followExternal: config.followExternal,\n deepClick: config.deepClick,\n },\n startedAt: startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n durationMs,\n pages,\n unvisitedUrls,\n errors,\n completionStatus,\n };\n\n // Write output files.\n let outputPaths: { sitemapJson?: string } = {};\n try {\n outputPaths = await writeOutputFiles(config.outputDir, crawlResult, pages);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`Failed to write output files: ${message}`);\n }\n\n // Build assembled artifacts.\n const artifacts = buildAssembledArtifacts(crawlResult, pages);\n\n // Write additional output files (forms, api-map, flow graph, OpenAPI, report).\n try {\n // Forms JSON\n if (artifacts.forms.length > 0) {\n await fs.writeFile(\n path.join(config.outputDir, 'forms.json'),\n JSON.stringify(artifacts.forms, null, 2),\n 'utf-8',\n );\n logger.info(` Forms: ${artifacts.forms.length} found`);\n }\n\n // API endpoint map\n if (artifacts.apiEndpoints.length > 0) {\n await fs.writeFile(\n path.join(config.outputDir, 'api-map.json'),\n JSON.stringify(artifacts.apiEndpoints, null, 2),\n 'utf-8',\n );\n logger.info(` API endpoints: ${artifacts.apiEndpoints.length} groups`);\n }\n\n // Flow graph (Mermaid definition)\n const allEdges = pages.flatMap(p => p.navigationEdges);\n if (allEdges.length > 0) {\n const flowGraph = buildFlowGraph(allEdges, config.targetUrl);\n const mermaidDef = generateMermaidDefinition(flowGraph);\n await fs.writeFile(\n path.join(config.outputDir, 'flow-graph.mmd'),\n mermaidDef,\n 'utf-8',\n );\n logger.info(` Flow graph: ${flowGraph.nodes.length} nodes, ${flowGraph.edges.length} edges`);\n }\n\n // OpenAPI spec\n if (artifacts.apiEndpoints.length > 0) {\n try {\n const spec = generateOpenApiSpec(artifacts.apiEndpoints, {\n title: `API - ${config.targetUrl}`,\n targetUrl: config.targetUrl,\n });\n await writeOpenApiSpec(spec, path.join(config.outputDir, 'openapi.json'));\n logger.info(' OpenAPI spec written');\n } catch (err: unknown) {\n logger.warn(`OpenAPI generation failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n // HTML report\n try {\n const reportInput = {\n targetUrl: config.targetUrl,\n crawlDate: new Date(crawlStartTime).toISOString(),\n duration: (Date.now() - crawlStartTime) / 1000,\n pagesVisited: visitedCount,\n errors: errors.length,\n pages: pages\n .filter(p => p.status === 'ok')\n .map(p => ({\n url: p.url,\n title: p.pageScan?.title || '',\n status: p.httpStatus || 0,\n depth: p.depth,\n contentHash: p.pageScan?.contentHash,\n })),\n forms: artifacts.forms.map(f => ({\n url: f.action || '',\n action: f.action,\n method: f.method,\n fields: f.fields.map(field => ({\n name: field.name,\n type: field.type,\n required: field.required,\n })),\n })),\n apiEndpoints: artifacts.apiEndpoints.map(g => ({\n pattern: g.pattern,\n method: g.method,\n examples: g.observedUrls.slice(0, 5).map(url => ({\n url,\n status: g.exampleResponse.statusCode,\n })),\n })),\n // Mermaid-to-SVG rendering is a later feature; omit flow diagram from HTML for now.\n flowDiagramSvg: undefined,\n };\n const html = generateReportHtml(reportInput);\n await fs.writeFile(path.join(config.outputDir, 'report.html'), html, 'utf-8');\n logger.success(`HTML report: ${path.join(config.outputDir, 'report.html')}`);\n } catch (err: unknown) {\n logger.warn(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`);\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logger.warn(`Additional output generation error: ${message}`);\n }\n\n // Log summary.\n logger.success(\n `Crawl complete: ${pages.length} pages visited, ${frontier.totalSeen} URLs discovered, ${errors.length} errors`,\n );\n logger.info(` Duration: ${(durationMs / 1000).toFixed(1)}s`);\n logger.info(` Status: ${completionStatus}`);\n logger.info(` Output: ${config.outputDir}`);\n\n // ----------------------------------------------------------------\n // 8. Return DigResult\n // ----------------------------------------------------------------\n return {\n artifacts,\n outputPaths: {\n outputDir: config.outputDir,\n sitemapJson: outputPaths.sitemapJson,\n harFile: config.noHar ? undefined : path.join(config.outputDir, 'crawl.har'),\n },\n errors,\n completionStatus,\n };\n } catch (err: unknown) {\n // Fatal error — still try to write partial results.\n const message = err instanceof Error ? err.message : String(err);\n logger.error(`Fatal crawl error: ${message}`);\n\n errors.push({\n timestamp: new Date().toISOString(),\n url: config.targetUrl,\n code: 'ERR_FATAL',\n message,\n });\n\n const finishedAt = new Date();\n const durationMs = finishedAt.getTime() - startedAt.getTime();\n\n // Attempt to write partial output.\n try {\n await ensureOutputDir(config.outputDir);\n await writeOutputFiles(\n config.outputDir,\n {\n config: {\n targetUrl: config.targetUrl,\n depth: config.depth,\n maxPages: config.maxPages,\n concurrency: config.concurrency,\n viewport: config.viewport,\n followExternal: config.followExternal,\n deepClick: config.deepClick,\n },\n startedAt: startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n durationMs,\n pages,\n unvisitedUrls: [],\n errors,\n completionStatus: 'error',\n },\n pages,\n );\n } catch {\n // Best-effort — output may not be writable.\n }\n\n const artifacts = buildAssembledArtifacts(\n {\n config: {\n targetUrl: config.targetUrl,\n depth: config.depth,\n maxPages: config.maxPages,\n concurrency: config.concurrency,\n viewport: config.viewport,\n followExternal: config.followExternal,\n deepClick: config.deepClick,\n },\n startedAt: startedAt.toISOString(),\n finishedAt: finishedAt.toISOString(),\n durationMs,\n pages,\n unvisitedUrls: [],\n errors,\n completionStatus: 'error',\n },\n pages,\n );\n\n return {\n artifacts,\n outputPaths: { outputDir: config.outputDir },\n errors,\n completionStatus: 'error',\n };\n } finally {\n // Remove SIGINT handler.\n process.removeListener('SIGINT', sigintHandler);\n\n // Ensure browser resources are freed.\n if (context) {\n await context.close().catch(() => {});\n }\n if (browser) {\n await browser.close().catch(() => {});\n }\n }\n}\n","/**\n * Network Guard — SSRF Protection\n *\n * Blocks requests to private IP ranges and dangerous protocols.\n * Default-on; can be bypassed with --allow-private.\n *\n * Blocked ranges:\n * - 10.0.0.0/8 (RFC1918)\n * - 172.16.0.0/12 (RFC1918)\n * - 192.168.0.0/16 (RFC1918)\n * - 169.254.0.0/16 (link-local, AWS/GCP metadata)\n * - 127.0.0.0/8 (loopback)\n * - 0.0.0.0/8 (unspecified)\n * - fd00::/8, fc00::/7 (IPv6 private)\n * - fe80::/10 (IPv6 link-local)\n * - ::1 (IPv6 loopback)\n * - metadata.google.internal\n * - kubernetes.default.svc\n *\n * Blocked protocols: file:, javascript:, data:, ftp:, chrome:, blob:, etc.\n * Allowed protocols: http:, https:\n */\n\nimport { lookup } from 'node:dns/promises';\n\nexport interface NetworkGuardOptions {\n allowPrivate?: boolean;\n}\n\n// -------------------------------------------------------------------------\n// Metadata hostnames\n// -------------------------------------------------------------------------\nconst METADATA_HOSTNAMES = new Set([\n 'metadata.google.internal',\n '169.254.169.254',\n 'kubernetes.default.svc',\n]);\n\n// -------------------------------------------------------------------------\n// Allowed protocols\n// -------------------------------------------------------------------------\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);\n\n// -------------------------------------------------------------------------\n// IPv4 parsing helpers\n// -------------------------------------------------------------------------\n\n/**\n * Parse an IPv4 octet that may be decimal, octal (0-prefixed), or hex (0x-prefixed).\n * Returns NaN if invalid.\n */\nfunction parseOctet(s: string): number {\n if (s === '') return NaN;\n if (/^0x/i.test(s)) {\n return parseInt(s, 16);\n }\n if (s.length > 1 && s.startsWith('0') && /^\\d+$/.test(s)) {\n // Octal\n return parseInt(s, 8);\n }\n const n = Number(s);\n return Number.isInteger(n) ? n : NaN;\n}\n\n/**\n * Parse an IPv4 address string (supporting decimal, octal, hex notations\n * and single-integer forms) into a 32-bit number. Returns null on failure.\n */\nfunction parseIpv4ToNumber(ip: string): number | null {\n // Single integer form (e.g., 2130706433 for 127.0.0.1)\n if (/^(0x[\\da-fA-F]+|\\d+)$/.test(ip)) {\n const n = ip.startsWith('0x') || ip.startsWith('0X')\n ? parseInt(ip, 16)\n : Number(ip);\n if (Number.isFinite(n) && n >= 0 && n <= 0xFFFFFFFF) {\n return n;\n }\n return null;\n }\n\n const parts = ip.split('.');\n if (parts.length !== 4) return null;\n\n let result = 0;\n for (let i = 0; i < 4; i++) {\n const octet = parseOctet(parts[i]);\n if (isNaN(octet) || octet < 0 || octet > 255) return null;\n result = (result << 8) | octet;\n }\n // Ensure unsigned\n return result >>> 0;\n}\n\n/**\n * Check if a 32-bit IPv4 number falls in a private/reserved range.\n */\nfunction isPrivateIpv4Number(num: number): boolean {\n // 0.0.0.0/8\n if ((num >>> 24) === 0) return true;\n // 10.0.0.0/8\n if ((num >>> 24) === 10) return true;\n // 127.0.0.0/8\n if ((num >>> 24) === 127) return true;\n // 172.16.0.0/12\n if ((num >>> 24) === 172) {\n const second = (num >>> 16) & 0xFF;\n if (second >= 16 && second <= 31) return true;\n }\n // 192.168.0.0/16\n if ((num >>> 16) === ((192 << 8) | 168)) return true;\n // 169.254.0.0/16\n if ((num >>> 16) === ((169 << 8) | 254)) return true;\n\n return false;\n}\n\n/**\n * Check if an IP address is in a private/reserved range.\n * Handles IPv4, IPv6, IPv4-mapped IPv6 (::ffff:x.x.x.x).\n * Returns true for private, false for public.\n * Invalid IPs return false (for isPrivateIp/isPrivateIP direct callers),\n * but isBlockedUrl treats DNS failures as blocked (fail-closed).\n */\nexport function isPrivateIp(ip: string): boolean {\n if (!ip || typeof ip !== 'string') return false;\n\n const trimmed = ip.trim();\n\n // --- IPv6 handling ---\n if (trimmed.includes(':')) {\n return isPrivateIpv6(trimmed);\n }\n\n // --- IPv4 handling ---\n const num = parseIpv4ToNumber(trimmed);\n if (num === null) return false;\n return isPrivateIpv4Number(num);\n}\n\n/**\n * Check if an IPv6 address is private/reserved.\n */\nfunction isPrivateIpv6(ip: string): boolean {\n // Remove zone ID (e.g., %eth0 or %25eth0)\n let cleaned = ip.replace(/%.*$/, '');\n\n // IPv4-mapped IPv6: ::ffff:x.x.x.x or 0:0:0:0:0:ffff:x.x.x.x\n const v4MappedMatch = cleaned.match(\n /^(?:::ffff:|0{1,4}:0{1,4}:0{1,4}:0{1,4}:0{1,4}:ffff:)(\\d+\\.\\d+\\.\\d+\\.\\d+)$/i\n );\n if (v4MappedMatch) {\n return isPrivateIp(v4MappedMatch[1]);\n }\n\n // Expand :: to full form for comparison\n const expanded = expandIpv6(cleaned);\n if (!expanded) return false;\n\n // ::1 (loopback)\n if (expanded === '0000:0000:0000:0000:0000:0000:0000:0001') return true;\n\n // :: (unspecified)\n if (expanded === '0000:0000:0000:0000:0000:0000:0000:0000') return true;\n\n const firstGroup = parseInt(expanded.substring(0, 4), 16);\n\n // fc00::/7 (includes fd00::/8)\n if ((firstGroup & 0xFE00) === 0xFC00) return true;\n\n // fe80::/10 (link-local)\n if ((firstGroup & 0xFFC0) === 0xFE80) return true;\n\n return false;\n}\n\n/**\n * Expand an IPv6 address to its full 8-group form.\n */\nfunction expandIpv6(ip: string): string | null {\n // Remove brackets if present\n let addr = ip.replace(/^\\[|\\]$/g, '');\n\n // Handle IPv4-mapped suffix\n const lastColon = addr.lastIndexOf(':');\n const possibleV4 = addr.substring(lastColon + 1);\n if (possibleV4.includes('.')) {\n const num = parseIpv4ToNumber(possibleV4);\n if (num === null) return null;\n const hi = (num >>> 16) & 0xFFFF;\n const lo = num & 0xFFFF;\n addr = addr.substring(0, lastColon + 1) +\n hi.toString(16).padStart(4, '0') + ':' +\n lo.toString(16).padStart(4, '0');\n }\n\n const parts = addr.split('::');\n if (parts.length > 2) return null;\n\n if (parts.length === 2) {\n const left = parts[0] ? parts[0].split(':') : [];\n const right = parts[1] ? parts[1].split(':') : [];\n const missing = 8 - left.length - right.length;\n if (missing < 0) return null;\n const mid = Array(missing).fill('0000');\n const all = [...left, ...mid, ...right];\n return all.map(g => g.padStart(4, '0')).join(':');\n }\n\n const groups = addr.split(':');\n if (groups.length !== 8) return null;\n return groups.map(g => g.padStart(4, '0')).join(':');\n}\n\n// Export alias for tests that import isPrivateIP (capital IP)\nexport { isPrivateIp as isPrivateIP };\n\n/**\n * Check if a hostname is a known metadata endpoint.\n */\nexport function isMetadataHostname(hostname: string): boolean {\n if (!hostname) return false;\n const lower = hostname.toLowerCase().trim();\n return METADATA_HOSTNAMES.has(lower);\n}\n\n/**\n * Check if a URL uses an allowed protocol.\n */\nexport function isAllowedProtocol(url: string): boolean {\n try {\n const parsed = new URL(url);\n return ALLOWED_PROTOCOLS.has(parsed.protocol);\n } catch {\n // If we can't parse, check for known dangerous prefixes\n const lower = url.toLowerCase().trim();\n if (lower.startsWith('http:') || lower.startsWith('https:')) return true;\n return false;\n }\n}\n\n/**\n * Extract the hostname from a URL, handling percent-encoding.\n */\nfunction extractHostname(urlStr: string): string {\n try {\n const parsed = new URL(urlStr);\n // URL constructor decodes percent-encoded hostnames\n return parsed.hostname;\n } catch {\n return '';\n }\n}\n\n/**\n * Check if a URL should be blocked.\n * Resolves DNS to detect private IPs behind public hostnames (DNS rebinding).\n * Fail-closed: blocks if DNS fails or URL is unparseable.\n */\nexport async function isBlockedUrl(\n url: string,\n options?: NetworkGuardOptions\n): Promise<{ blocked: boolean; reason?: string }> {\n const allowPrivate = options?.allowPrivate ?? false;\n\n // Check protocol first (even allowPrivate doesn't bypass protocol checks)\n if (!isAllowedProtocol(url)) {\n return { blocked: true, reason: 'Blocked protocol — only http: and https: are allowed' };\n }\n\n let hostname: string;\n try {\n hostname = extractHostname(url);\n } catch {\n return { blocked: true, reason: 'Failed to parse URL (fail-closed)' };\n }\n\n if (!hostname) {\n return { blocked: true, reason: 'Empty hostname (fail-closed)' };\n }\n\n // Decode percent-encoded hostname\n let decodedHostname: string;\n try {\n decodedHostname = decodeURIComponent(hostname);\n } catch {\n // Double-encoded or invalid — fail closed\n return { blocked: true, reason: 'Invalid hostname encoding (fail-closed)' };\n }\n\n // Check metadata hostnames\n if (isMetadataHostname(decodedHostname)) {\n if (allowPrivate) return { blocked: false };\n return { blocked: true, reason: 'Blocked metadata hostname' };\n }\n\n // Remove brackets from IPv6\n const cleanedHost = decodedHostname.replace(/^\\[|\\]$/g, '');\n\n // Check if hostname is directly a private IP (including encoded forms)\n if (isPrivateIp(cleanedHost)) {\n if (allowPrivate) return { blocked: false };\n return { blocked: true, reason: `Blocked private IP: ${cleanedHost}` };\n }\n\n // Check if it's a decimal/hex/octal-encoded IP\n const ipNum = parseIpv4ToNumber(cleanedHost);\n if (ipNum !== null) {\n if (isPrivateIpv4Number(ipNum)) {\n if (allowPrivate) return { blocked: false };\n return { blocked: true, reason: `Blocked private IP (encoded): ${cleanedHost}` };\n }\n // It's a valid public IP in non-standard form, allow it\n return { blocked: false };\n }\n\n // DNS resolution\n try {\n const result = await lookup(decodedHostname, { all: true });\n const addresses = Array.isArray(result) ? result : [result];\n\n for (const entry of addresses) {\n const addr = typeof entry === 'string' ? entry : entry.address;\n if (isPrivateIp(addr)) {\n if (allowPrivate) return { blocked: false };\n return { blocked: true, reason: `DNS resolved to private IP: ${addr}` };\n }\n }\n\n return { blocked: false };\n } catch {\n // DNS failure — fail closed\n return { blocked: true, reason: 'DNS resolution failed (fail-closed)' };\n }\n}\n\n/**\n * Create a Playwright route handler that blocks requests to private IPs.\n */\nexport function createNetworkGuard(options?: NetworkGuardOptions) {\n return async (route: any) => {\n const url = route.request().url();\n const result = await isBlockedUrl(url, options);\n\n if (result.blocked) {\n await route.abort('blockedbyclient');\n } else {\n await route.continue();\n }\n };\n}\n","/**\n * Browser Hardening\n *\n * Provides secure default options for Playwright browser contexts and launches.\n * Ensures CSP bypass for inspection, blocks downloads, blocks service workers,\n * disables WebRTC leaks, auto-dismisses dialogs, closes popups, and limits\n * resource types.\n */\n\nimport type { BrowserContextOptions, LaunchOptions, Page } from 'playwright';\n\nexport interface SecureContextOptions {\n viewport?: { width: number; height: number };\n storageState?: BrowserContextOptions['storageState'];\n recordHar?: { path: string; content?: 'embed' | 'attach'; mode?: 'full' | 'minimal' };\n userAgent?: string;\n acceptDownloads?: boolean;\n}\n\nexport const TOOL_VERSION = '0.1.0';\n\n/**\n * Maximum number of HTTP redirects to follow before aborting.\n */\nexport const MAX_REDIRECTS = 10;\n\n/**\n * Resource types that are blocked to reduce attack surface and noise.\n * Documents, stylesheets, images, scripts, xhr, and fetch are allowed\n * because they are needed for crawling and screenshot capture.\n */\nexport const BLOCKED_RESOURCE_TYPES: string[] = [\n 'media',\n 'font',\n 'websocket',\n];\n\n/**\n * Secure browser launch options.\n * - Preserves Chromium sandbox\n * - Disables WebRTC to prevent IP leaks\n * - Disables signal handling so the tool controls shutdown\n */\nexport const LAUNCH_OPTIONS: LaunchOptions = {\n handleSIGINT: false,\n handleSIGTERM: false,\n handleSIGHUP: false,\n args: [\n '--disable-webrtc',\n '--enforce-webrtc-ip-permission-check',\n '--force-webrtc-ip-handling-policy=disable_non_proxied_udp',\n ],\n};\n\n/**\n * Build BrowserContextOptions with secure defaults.\n *\n * - bypassCSP: true -- allows the tool to inspect pages behind CSP\n * - acceptDownloads: false -- prevents file downloads\n * - javaScriptEnabled: true -- required for SPA inspection\n * - ignoreHTTPSErrors: false -- enforces certificate validation\n * - serviceWorkers: 'block' -- prevents SW interference with network capture\n * - permissions: [] -- deny all permission prompts\n * - viewport: 1280x720 default\n * - userAgent: identifies this tool\n */\nexport function getSecureContextOptions(opts?: SecureContextOptions): BrowserContextOptions {\n return {\n bypassCSP: true,\n acceptDownloads: false,\n javaScriptEnabled: true,\n ignoreHTTPSErrors: false,\n serviceWorkers: 'block',\n permissions: [],\n viewport: opts?.viewport ?? { width: 1280, height: 720 },\n ...(opts?.userAgent && { userAgent: opts.userAgent }),\n ...(opts?.storageState && { storageState: opts.storageState }),\n ...(opts?.recordHar && { recordHar: opts.recordHar }),\n };\n}\n\n/**\n * Build secure launch options for Playwright browser.\n * Alias for LAUNCH_OPTIONS for backward compatibility.\n */\nexport function getSecureLaunchOptions(): LaunchOptions {\n return { ...LAUNCH_OPTIONS };\n}\n\n/**\n * Register safety event handlers on a Playwright Page.\n * - Auto-dismisses all dialog types (alert, confirm, prompt, beforeunload)\n * - Auto-closes popup windows\n */\nexport function registerPageSafetyHandlers(page: Page): void {\n // Auto-dismiss all dialogs\n page.on('dialog', async (dialog) => {\n await dialog.dismiss();\n });\n\n // Auto-close popup windows\n page.on('popup', async (popup) => {\n await popup.close();\n });\n}\n","/**\n * Diff Engine\n *\n * Compares two bundle directories to detect regressions.\n * Produces a DiffResult with structured change information for:\n * - Routes (sitemap diff)\n * - Forms (field-level diff)\n * - API endpoints (status code, content type, body size diff)\n * - Screenshots (hash-based change detection)\n *\n * No external diff dependencies -- uses JSON comparison and SHA-256 hashes.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type {\n BundleManifest,\n DiffResult,\n DiffSummary,\n SitemapDiff,\n SitemapRouteChange,\n FormDiff,\n FormChange,\n FieldChange,\n ApiDiff,\n ApiEndpointChange,\n ScreenshotDiff,\n ScreenshotChange,\n RouteInfo,\n FormInfo,\n FormField,\n ApiEndpointGroup,\n} from '../types/artifacts.js';\nimport { DiffError } from '../types/errors.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Load and parse a JSON file, returning null if it does not exist. */\nasync function loadJson<T>(filePath: string): Promise<T | null> {\n try {\n const content = await readFile(filePath, 'utf-8');\n return JSON.parse(content) as T;\n } catch {\n return null;\n }\n}\n\n/** Load manifest.json from a bundle directory. */\nasync function loadManifest(bundleDir: string): Promise<BundleManifest> {\n const manifestPath = join(bundleDir, 'manifest.json');\n const manifest = await loadJson<BundleManifest>(manifestPath);\n if (!manifest) {\n throw new DiffError(\n 'manifest_invalid',\n `Could not load manifest.json from bundle: ${bundleDir}`,\n );\n }\n if (manifest.version !== 1) {\n throw new DiffError(\n 'manifest_invalid',\n `Unsupported manifest version ${manifest.version} in: ${bundleDir}`,\n );\n }\n return manifest;\n}\n\n/** Create an identity key for a form (action:method or id). */\nfunction formKey(form: FormInfo): string {\n if (form.id) return `id:${form.id}`;\n return `${form.action}:${form.method}`;\n}\n\n/** Create an identity key for an API endpoint (method + pattern). */\nfunction apiKey(endpoint: ApiEndpointGroup): string {\n return `${endpoint.method} ${endpoint.pattern}`;\n}\n\n// ---------------------------------------------------------------------------\n// Route diffing\n// ---------------------------------------------------------------------------\n\nfunction diffRoutes(\n oldRoutes: RouteInfo[],\n newRoutes: RouteInfo[],\n): SitemapDiff {\n const oldByUrl = new Map(oldRoutes.map((r) => [r.url, r]));\n const newByUrl = new Map(newRoutes.map((r) => [r.url, r]));\n\n const added: RouteInfo[] = [];\n const removed: RouteInfo[] = [];\n const changed: SitemapRouteChange[] = [];\n let unchangedCount = 0;\n\n // Find added and changed routes\n for (const [url, newRoute] of newByUrl) {\n const oldRoute = oldByUrl.get(url);\n if (!oldRoute) {\n added.push(newRoute);\n continue;\n }\n\n // Compare for changes\n const changes: SitemapRouteChange['changes'] = {};\n let hasChanges = false;\n\n if (oldRoute.title !== newRoute.title) {\n changes.title = { old: oldRoute.title, new: newRoute.title };\n hasChanges = true;\n }\n if (oldRoute.statusCode !== newRoute.statusCode) {\n changes.statusCode = { old: oldRoute.statusCode, new: newRoute.statusCode };\n hasChanges = true;\n }\n if (oldRoute.contentHash !== newRoute.contentHash) {\n changes.contentHash = { old: oldRoute.contentHash, new: newRoute.contentHash };\n hasChanges = true;\n }\n\n if (hasChanges) {\n changed.push({ url, changes });\n } else {\n unchangedCount++;\n }\n }\n\n // Find removed routes\n for (const [url, oldRoute] of oldByUrl) {\n if (!newByUrl.has(url)) {\n removed.push(oldRoute);\n }\n }\n\n return { added, removed, changed, unchangedCount };\n}\n\n// ---------------------------------------------------------------------------\n// Form diffing\n// ---------------------------------------------------------------------------\n\nfunction diffFields(oldFields: FormField[], newFields: FormField[]): {\n fieldsAdded: FormField[];\n fieldsRemoved: FormField[];\n fieldsChanged: FieldChange[];\n} {\n const oldByName = new Map(oldFields.map((f) => [f.name, f]));\n const newByName = new Map(newFields.map((f) => [f.name, f]));\n\n const fieldsAdded: FormField[] = [];\n const fieldsRemoved: FormField[] = [];\n const fieldsChanged: FieldChange[] = [];\n\n for (const [name, newField] of newByName) {\n const oldField = oldByName.get(name);\n if (!oldField) {\n fieldsAdded.push(newField);\n continue;\n }\n\n const changes: FieldChange['changes'] = {};\n let hasChanges = false;\n\n if (oldField.type !== newField.type) {\n changes.type = { old: oldField.type, new: newField.type };\n hasChanges = true;\n }\n if (oldField.required !== newField.required) {\n changes.required = { old: oldField.required, new: newField.required };\n hasChanges = true;\n }\n if (oldField.pattern !== newField.pattern) {\n changes.validationPattern = {\n old: oldField.pattern ?? '',\n new: newField.pattern ?? '',\n };\n hasChanges = true;\n }\n if (oldField.placeholder !== newField.placeholder) {\n changes.placeholder = {\n old: oldField.placeholder ?? '',\n new: newField.placeholder ?? '',\n };\n hasChanges = true;\n }\n\n // Options diff (for select, radio, checkbox)\n if (oldField.options || newField.options) {\n const oldValues = new Set((oldField.options ?? []).map((o) => o.value));\n const newValues = new Set((newField.options ?? []).map((o) => o.value));\n const optionsAdded = [...newValues].filter((v) => !oldValues.has(v));\n const optionsRemoved = [...oldValues].filter((v) => !newValues.has(v));\n if (optionsAdded.length > 0 || optionsRemoved.length > 0) {\n changes.options = { added: optionsAdded, removed: optionsRemoved };\n hasChanges = true;\n }\n }\n\n if (hasChanges) {\n fieldsChanged.push({ name, changes });\n }\n }\n\n for (const [name, oldField] of oldByName) {\n if (!newByName.has(name)) {\n fieldsRemoved.push(oldField);\n }\n }\n\n return { fieldsAdded, fieldsRemoved, fieldsChanged };\n}\n\nfunction diffForms(oldForms: FormInfo[], newForms: FormInfo[]): FormDiff {\n const oldByKey = new Map(oldForms.map((f) => [formKey(f), f]));\n const newByKey = new Map(newForms.map((f) => [formKey(f), f]));\n\n const added: FormInfo[] = [];\n const removed: FormInfo[] = [];\n const changed: FormChange[] = [];\n let unchangedCount = 0;\n\n for (const [key, newForm] of newByKey) {\n const oldForm = oldByKey.get(key);\n if (!oldForm) {\n added.push(newForm);\n continue;\n }\n\n // Compare fields\n const fieldDiff = diffFields(oldForm.fields, newForm.fields);\n const changeEntry: FormChange['changes'] = {};\n let hasChanges = false;\n\n if (fieldDiff.fieldsAdded.length > 0) {\n changeEntry.fieldsAdded = fieldDiff.fieldsAdded;\n hasChanges = true;\n }\n if (fieldDiff.fieldsRemoved.length > 0) {\n changeEntry.fieldsRemoved = fieldDiff.fieldsRemoved;\n hasChanges = true;\n }\n if (fieldDiff.fieldsChanged.length > 0) {\n changeEntry.fieldsChanged = fieldDiff.fieldsChanged;\n hasChanges = true;\n }\n if (oldForm.action !== newForm.action) {\n changeEntry.actionChanged = { old: oldForm.action, new: newForm.action };\n hasChanges = true;\n }\n if (oldForm.method !== newForm.method) {\n changeEntry.methodChanged = { old: oldForm.method, new: newForm.method };\n hasChanges = true;\n }\n\n if (hasChanges) {\n changed.push({\n formId: key,\n pageUrl: newForm.action, // best available context\n changes: changeEntry,\n });\n } else {\n unchangedCount++;\n }\n }\n\n for (const [key, oldForm] of oldByKey) {\n if (!newByKey.has(key)) {\n removed.push(oldForm);\n }\n }\n\n return { added, removed, changed, unchangedCount };\n}\n\n// ---------------------------------------------------------------------------\n// API endpoint diffing\n// ---------------------------------------------------------------------------\n\nfunction diffApi(\n oldEndpoints: ApiEndpointGroup[],\n newEndpoints: ApiEndpointGroup[],\n): ApiDiff {\n const oldByKey = new Map(oldEndpoints.map((e) => [apiKey(e), e]));\n const newByKey = new Map(newEndpoints.map((e) => [apiKey(e), e]));\n\n const added: ApiEndpointGroup[] = [];\n const removed: ApiEndpointGroup[] = [];\n const changed: ApiEndpointChange[] = [];\n let unchangedCount = 0;\n\n for (const [key, newEp] of newByKey) {\n const oldEp = oldByKey.get(key);\n if (!oldEp) {\n added.push(newEp);\n continue;\n }\n\n const changes: ApiEndpointChange['changes'] = {};\n let hasChanges = false;\n\n // Status codes diff\n const oldStatuses = new Set(oldEp.observedStatusCodes);\n const newStatuses = new Set(newEp.observedStatusCodes);\n const statusCodesAdded = [...newStatuses].filter((s) => !oldStatuses.has(s));\n const statusCodesRemoved = [...oldStatuses].filter((s) => !newStatuses.has(s));\n if (statusCodesAdded.length > 0) {\n changes.statusCodesAdded = statusCodesAdded;\n hasChanges = true;\n }\n if (statusCodesRemoved.length > 0) {\n changes.statusCodesRemoved = statusCodesRemoved;\n hasChanges = true;\n }\n\n // Content type diff\n const oldContentTypes = oldEp.observedContentTypes;\n const newContentTypes = newEp.observedContentTypes;\n if (\n oldContentTypes.length > 0 &&\n newContentTypes.length > 0 &&\n oldContentTypes[0] !== newContentTypes[0]\n ) {\n changes.contentTypeChanged = {\n old: oldContentTypes[0],\n new: newContentTypes[0],\n };\n hasChanges = true;\n }\n\n // Body size comparison\n const oldSize = oldEp.exampleResponse.bodySize;\n const newSize = newEp.exampleResponse.bodySize;\n if (oldSize > 0 && newSize > 0) {\n const percentChange = Math.abs(((newSize - oldSize) / oldSize) * 100);\n if (percentChange > 10) {\n changes.bodySizeChange = {\n old: oldSize,\n new: newSize,\n percentChange: Math.round(percentChange * 100) / 100,\n };\n hasChanges = true;\n }\n }\n\n if (hasChanges) {\n changed.push({ endpointId: key, changes });\n } else {\n unchangedCount++;\n }\n }\n\n for (const [key, oldEp] of oldByKey) {\n if (!newByKey.has(key)) {\n removed.push(oldEp);\n }\n }\n\n return { added, removed, changed, unchangedCount };\n}\n\n// ---------------------------------------------------------------------------\n// Screenshot diffing (hash-based, no pixel comparison)\n// ---------------------------------------------------------------------------\n\ninterface ScreenshotEntry {\n url: string;\n sha256: string;\n path: string;\n}\n\nfunction diffScreenshots(\n oldScreenshots: ScreenshotEntry[],\n newScreenshots: ScreenshotEntry[],\n oldDir: string,\n newDir: string,\n): ScreenshotDiff {\n const oldByUrl = new Map(oldScreenshots.map((s) => [s.url, s]));\n const newByUrl = new Map(newScreenshots.map((s) => [s.url, s]));\n\n const added: ScreenshotDiff['added'] = [];\n const removed: ScreenshotDiff['removed'] = [];\n const changed: ScreenshotChange[] = [];\n let unchangedCount = 0;\n\n for (const [url, newSs] of newByUrl) {\n const oldSs = oldByUrl.get(url);\n if (!oldSs) {\n added.push({ url, screenshotPath: join(newDir, newSs.path) });\n continue;\n }\n\n if (oldSs.sha256 !== newSs.sha256) {\n // Hash-based: report 100% diff if hashes differ (no pixel-level analysis)\n changed.push({\n url,\n diffPercentage: 100,\n diffPixelCount: 0, // unknown without pixel analysis\n totalPixels: 0, // unknown without pixel analysis\n diffImagePath: '', // no diff image generated in hash-only mode\n oldScreenshotPath: join(oldDir, oldSs.path),\n newScreenshotPath: join(newDir, newSs.path),\n });\n } else {\n unchangedCount++;\n }\n }\n\n for (const [url] of oldByUrl) {\n if (!newByUrl.has(url)) {\n removed.push({ url });\n }\n }\n\n return { added, removed, changed, unchangedCount };\n}\n\n// ---------------------------------------------------------------------------\n// Screenshot manifest loading\n// ---------------------------------------------------------------------------\n\n/**\n * Extract screenshot entries from bundle manifest and screenshot-manifest.json.\n * Falls back to manifest file entries if screenshot-manifest.json is not available.\n */\nasync function loadScreenshotEntries(\n bundleDir: string,\n manifest: BundleManifest,\n): Promise<ScreenshotEntry[]> {\n // Try loading screenshot-manifest.json for URL mapping\n const screenshotManifest = await loadJson<\n Array<{ url: string; fullPageHash: string; fullPagePath: string }>\n >(join(bundleDir, 'data', 'screenshot-manifest.json'));\n\n if (screenshotManifest && Array.isArray(screenshotManifest)) {\n return screenshotManifest.map((s) => ({\n url: s.url,\n sha256: s.fullPageHash,\n path: s.fullPagePath,\n }));\n }\n\n // Fallback: use manifest file entries for screenshots (URL unknown, use path)\n return manifest.files\n .filter((f) => f.type === 'screenshot')\n .map((f) => ({\n url: f.path, // best available identifier\n sha256: f.sha256,\n path: f.path,\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Main diff function\n// ---------------------------------------------------------------------------\n\n/**\n * Compare two bundle directories and produce a DiffResult.\n *\n * @param oldDir - Path to the older bundle directory\n * @param newDir - Path to the newer bundle directory\n * @returns Structured diff result\n */\nexport async function diffBundles(\n oldDir: string,\n newDir: string,\n): Promise<DiffResult> {\n // Load manifests\n const [oldManifest, newManifest] = await Promise.all([\n loadManifest(oldDir),\n loadManifest(newDir),\n ]);\n\n // Load data files for comparison\n const [oldSitemap, newSitemap] = await Promise.all([\n loadJson<RouteInfo[]>(join(oldDir, 'data', 'sitemap.json')),\n loadJson<RouteInfo[]>(join(newDir, 'data', 'sitemap.json')),\n ]);\n\n const [oldForms, newForms] = await Promise.all([\n loadJson<FormInfo[]>(join(oldDir, 'data', 'forms.json')),\n loadJson<FormInfo[]>(join(newDir, 'data', 'forms.json')),\n ]);\n\n const [oldApiMap, newApiMap] = await Promise.all([\n loadJson<ApiEndpointGroup[]>(join(oldDir, 'data', 'api-map.json')),\n loadJson<ApiEndpointGroup[]>(join(newDir, 'data', 'api-map.json')),\n ]);\n\n // Load screenshot entries\n const [oldScreenshotEntries, newScreenshotEntries] = await Promise.all([\n loadScreenshotEntries(oldDir, oldManifest),\n loadScreenshotEntries(newDir, newManifest),\n ]);\n\n // Perform diffs\n const sitemap = diffRoutes(oldSitemap ?? [], newSitemap ?? []);\n const forms = diffForms(oldForms ?? [], newForms ?? []);\n const api = diffApi(oldApiMap ?? [], newApiMap ?? []);\n const screenshots = diffScreenshots(\n oldScreenshotEntries,\n newScreenshotEntries,\n oldDir,\n newDir,\n );\n\n // Build summary\n const summary: DiffSummary = {\n routes: {\n added: sitemap.added.length,\n removed: sitemap.removed.length,\n changed: sitemap.changed.length,\n unchanged: sitemap.unchangedCount,\n },\n forms: {\n added: forms.added.length,\n removed: forms.removed.length,\n changed: forms.changed.length,\n unchanged: forms.unchangedCount,\n },\n api: {\n added: api.added.length,\n removed: api.removed.length,\n changed: api.changed.length,\n unchanged: api.unchangedCount,\n },\n screenshots: {\n added: screenshots.added.length,\n removed: screenshots.removed.length,\n changed: screenshots.changed.length,\n unchanged: screenshots.unchangedCount,\n },\n };\n\n const hasChanges =\n summary.routes.added + summary.routes.removed + summary.routes.changed +\n summary.forms.added + summary.forms.removed + summary.forms.changed +\n summary.api.added + summary.api.removed + summary.api.changed +\n summary.screenshots.added + summary.screenshots.removed + summary.screenshots.changed > 0;\n\n return {\n meta: {\n oldBundle: {\n path: oldDir,\n createdAt: oldManifest.createdAt,\n targetUrl: oldManifest.config.targetUrl,\n toolVersion: oldManifest.toolVersion,\n },\n newBundle: {\n path: newDir,\n createdAt: newManifest.createdAt,\n targetUrl: newManifest.config.targetUrl,\n toolVersion: newManifest.toolVersion,\n },\n comparedAt: new Date().toISOString(),\n },\n hasChanges,\n sitemap,\n forms,\n api,\n screenshots,\n summary,\n };\n}\n","/**\n * Diff Report Generator\n *\n * Generates a self-contained HTML report showing all changes between\n * two crawl bundles. Color-coded: green for added, red for removed,\n * yellow for modified.\n *\n * Uses the same CSP and escaping as the main report.\n */\n\nimport { escapeHtml, escapeAttribute } from '../report/html/escape.js';\nimport type {\n DiffResult,\n DiffSummary,\n SitemapRouteChange,\n FormChange,\n ApiEndpointChange,\n ScreenshotChange,\n RouteInfo,\n FormInfo,\n ApiEndpointGroup,\n} from '../types/artifacts.js';\n\n// ---------------------------------------------------------------------------\n// CSP header (matches main report)\n// ---------------------------------------------------------------------------\n\nconst CSP_META = `<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:;\">`;\n\n// ---------------------------------------------------------------------------\n// CSS styles\n// ---------------------------------------------------------------------------\n\nconst STYLES = `\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1a1a2e; background: #f8f9fa; padding: 2rem; max-width: 1200px; margin: 0 auto; }\n h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }\n h2 { font-size: 1.4rem; margin: 2rem 0 1rem; border-bottom: 2px solid #e0e0e0; padding-bottom: 0.5rem; }\n h3 { font-size: 1.1rem; margin: 1rem 0 0.5rem; }\n .meta { color: #666; font-size: 0.9rem; margin-bottom: 2rem; }\n .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }\n .summary-card { background: #fff; border-radius: 8px; padding: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n .summary-card h3 { margin: 0 0 0.5rem; font-size: 1rem; }\n .summary-card .count { font-size: 2rem; font-weight: bold; }\n .no-changes { background: #d4edda; color: #155724; padding: 1rem; border-radius: 8px; text-align: center; font-size: 1.2rem; }\n .badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }\n .badge-added { background: #d4edda; color: #155724; }\n .badge-removed { background: #f8d7da; color: #721c24; }\n .badge-changed { background: #fff3cd; color: #856404; }\n .badge-unchanged { background: #e2e3e5; color: #383d41; }\n table { width: 100%; border-collapse: collapse; margin: 1rem 0; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e0e0e0; }\n th { background: #f1f3f5; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; color: #495057; }\n tr:last-child td { border-bottom: none; }\n .change-detail { font-size: 0.85rem; color: #495057; }\n .old-value { color: #721c24; text-decoration: line-through; }\n .new-value { color: #155724; font-weight: 500; }\n .section { margin-bottom: 2rem; }\n .empty-section { color: #6c757d; font-style: italic; padding: 1rem; }\n ul.change-list { list-style: none; padding: 0; }\n ul.change-list li { padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }\n ul.change-list li:last-child { border-bottom: none; }\n`;\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction badge(type: 'added' | 'removed' | 'changed' | 'unchanged'): string {\n return `<span class=\"badge badge-${type}\">${type}</span>`;\n}\n\nfunction summaryCard(\n title: string,\n counts: { added: number; removed: number; changed: number; unchanged: number },\n): string {\n const total = counts.added + counts.removed + counts.changed;\n return `\n <div class=\"summary-card\">\n <h3>${escapeHtml(title)}</h3>\n <div class=\"count\">${total}</div>\n <div class=\"change-detail\">\n ${counts.added > 0 ? `${badge('added')} ${counts.added}` : ''}\n ${counts.removed > 0 ? `${badge('removed')} ${counts.removed}` : ''}\n ${counts.changed > 0 ? `${badge('changed')} ${counts.changed}` : ''}\n ${counts.unchanged > 0 ? `${badge('unchanged')} ${counts.unchanged}` : ''}\n </div>\n </div>`;\n}\n\n// ---------------------------------------------------------------------------\n// Section renderers\n// ---------------------------------------------------------------------------\n\nfunction renderRoutesSection(diff: DiffResult): string {\n const { sitemap } = diff;\n const hasContent =\n sitemap.added.length + sitemap.removed.length + sitemap.changed.length > 0;\n\n if (!hasContent) {\n return `\n <div class=\"section\">\n <h2>Routes</h2>\n <p class=\"empty-section\">No route changes detected.</p>\n </div>`;\n }\n\n let html = `<div class=\"section\"><h2>Routes</h2>`;\n\n if (sitemap.added.length > 0) {\n html += `<h3>${badge('added')} Added Routes (${sitemap.added.length})</h3>`;\n html += `<table><thead><tr><th>URL</th><th>Title</th><th>Status</th></tr></thead><tbody>`;\n for (const route of sitemap.added) {\n html += `<tr><td>${escapeHtml(route.url)}</td><td>${escapeHtml(route.title)}</td><td>${route.statusCode}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n if (sitemap.removed.length > 0) {\n html += `<h3>${badge('removed')} Removed Routes (${sitemap.removed.length})</h3>`;\n html += `<table><thead><tr><th>URL</th><th>Title</th><th>Status</th></tr></thead><tbody>`;\n for (const route of sitemap.removed) {\n html += `<tr><td>${escapeHtml(route.url)}</td><td>${escapeHtml(route.title)}</td><td>${route.statusCode}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n if (sitemap.changed.length > 0) {\n html += `<h3>${badge('changed')} Modified Routes (${sitemap.changed.length})</h3>`;\n html += `<table><thead><tr><th>URL</th><th>Changes</th></tr></thead><tbody>`;\n for (const change of sitemap.changed) {\n const changeDescs: string[] = [];\n if (change.changes.title) {\n changeDescs.push(\n `Title: <span class=\"old-value\">${escapeHtml(change.changes.title.old)}</span> → <span class=\"new-value\">${escapeHtml(change.changes.title.new)}</span>`,\n );\n }\n if (change.changes.statusCode) {\n changeDescs.push(\n `Status: <span class=\"old-value\">${change.changes.statusCode.old}</span> → <span class=\"new-value\">${change.changes.statusCode.new}</span>`,\n );\n }\n if (change.changes.contentHash) {\n changeDescs.push(`Content changed (hash differs)`);\n }\n html += `<tr><td>${escapeHtml(change.url)}</td><td class=\"change-detail\">${changeDescs.join('<br>')}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n html += `</div>`;\n return html;\n}\n\nfunction renderFormsSection(diff: DiffResult): string {\n const { forms } = diff;\n const hasContent =\n forms.added.length + forms.removed.length + forms.changed.length > 0;\n\n if (!hasContent) {\n return `\n <div class=\"section\">\n <h2>Forms</h2>\n <p class=\"empty-section\">No form changes detected.</p>\n </div>`;\n }\n\n let html = `<div class=\"section\"><h2>Forms</h2>`;\n\n if (forms.added.length > 0) {\n html += `<h3>${badge('added')} Added Forms (${forms.added.length})</h3>`;\n html += `<table><thead><tr><th>Action</th><th>Method</th><th>Fields</th></tr></thead><tbody>`;\n for (const form of forms.added) {\n const fieldNames = form.fields.map((f) => escapeHtml(f.name)).join(', ');\n html += `<tr><td>${escapeHtml(form.action)}</td><td>${escapeHtml(form.method)}</td><td>${fieldNames}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n if (forms.removed.length > 0) {\n html += `<h3>${badge('removed')} Removed Forms (${forms.removed.length})</h3>`;\n html += `<table><thead><tr><th>Action</th><th>Method</th><th>Fields</th></tr></thead><tbody>`;\n for (const form of forms.removed) {\n const fieldNames = form.fields.map((f) => escapeHtml(f.name)).join(', ');\n html += `<tr><td>${escapeHtml(form.action)}</td><td>${escapeHtml(form.method)}</td><td>${fieldNames}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n if (forms.changed.length > 0) {\n html += `<h3>${badge('changed')} Modified Forms (${forms.changed.length})</h3>`;\n html += `<ul class=\"change-list\">`;\n for (const change of forms.changed) {\n html += `<li><strong>${escapeHtml(change.formId)}</strong>`;\n const descs: string[] = [];\n if (change.changes.fieldsAdded?.length) {\n descs.push(`Fields added: ${change.changes.fieldsAdded.map((f) => escapeHtml(f.name)).join(', ')}`);\n }\n if (change.changes.fieldsRemoved?.length) {\n descs.push(`Fields removed: ${change.changes.fieldsRemoved.map((f) => escapeHtml(f.name)).join(', ')}`);\n }\n if (change.changes.actionChanged) {\n descs.push(`Action: <span class=\"old-value\">${escapeHtml(change.changes.actionChanged.old)}</span> → <span class=\"new-value\">${escapeHtml(change.changes.actionChanged.new)}</span>`);\n }\n if (change.changes.methodChanged) {\n descs.push(`Method: <span class=\"old-value\">${escapeHtml(change.changes.methodChanged.old)}</span> → <span class=\"new-value\">${escapeHtml(change.changes.methodChanged.new)}</span>`);\n }\n html += `<div class=\"change-detail\">${descs.join('<br>')}</div></li>`;\n }\n html += `</ul>`;\n }\n\n html += `</div>`;\n return html;\n}\n\nfunction renderApiSection(diff: DiffResult): string {\n const { api } = diff;\n const hasContent =\n api.added.length + api.removed.length + api.changed.length > 0;\n\n if (!hasContent) {\n return `\n <div class=\"section\">\n <h2>API Endpoints</h2>\n <p class=\"empty-section\">No API endpoint changes detected.</p>\n </div>`;\n }\n\n let html = `<div class=\"section\"><h2>API Endpoints</h2>`;\n\n if (api.added.length > 0) {\n html += `<h3>${badge('added')} Added Endpoints (${api.added.length})</h3>`;\n html += `<table><thead><tr><th>Method</th><th>Pattern</th><th>Status Codes</th></tr></thead><tbody>`;\n for (const ep of api.added) {\n html += `<tr><td>${escapeHtml(ep.method)}</td><td>${escapeHtml(ep.pattern)}</td><td>${ep.observedStatusCodes.join(', ')}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n if (api.removed.length > 0) {\n html += `<h3>${badge('removed')} Removed Endpoints (${api.removed.length})</h3>`;\n html += `<table><thead><tr><th>Method</th><th>Pattern</th><th>Status Codes</th></tr></thead><tbody>`;\n for (const ep of api.removed) {\n html += `<tr><td>${escapeHtml(ep.method)}</td><td>${escapeHtml(ep.pattern)}</td><td>${ep.observedStatusCodes.join(', ')}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n if (api.changed.length > 0) {\n html += `<h3>${badge('changed')} Modified Endpoints (${api.changed.length})</h3>`;\n html += `<ul class=\"change-list\">`;\n for (const change of api.changed) {\n html += `<li><strong>${escapeHtml(change.endpointId)}</strong>`;\n const descs: string[] = [];\n if (change.changes.statusCodesAdded?.length) {\n descs.push(`New status codes: ${change.changes.statusCodesAdded.join(', ')}`);\n }\n if (change.changes.statusCodesRemoved?.length) {\n descs.push(`Removed status codes: ${change.changes.statusCodesRemoved.join(', ')}`);\n }\n if (change.changes.contentTypeChanged) {\n descs.push(`Content-Type: <span class=\"old-value\">${escapeHtml(change.changes.contentTypeChanged.old)}</span> → <span class=\"new-value\">${escapeHtml(change.changes.contentTypeChanged.new)}</span>`);\n }\n if (change.changes.bodySizeChange) {\n descs.push(`Body size: ${change.changes.bodySizeChange.old}B → ${change.changes.bodySizeChange.new}B (${change.changes.bodySizeChange.percentChange}%)`);\n }\n html += `<div class=\"change-detail\">${descs.join('<br>')}</div></li>`;\n }\n html += `</ul>`;\n }\n\n html += `</div>`;\n return html;\n}\n\nfunction renderScreenshotsSection(diff: DiffResult): string {\n const { screenshots } = diff;\n const hasContent =\n screenshots.added.length + screenshots.removed.length + screenshots.changed.length > 0;\n\n if (!hasContent) {\n return `\n <div class=\"section\">\n <h2>Screenshots</h2>\n <p class=\"empty-section\">No screenshot changes detected.</p>\n </div>`;\n }\n\n let html = `<div class=\"section\"><h2>Screenshots</h2>`;\n\n if (screenshots.added.length > 0) {\n html += `<h3>${badge('added')} New Screenshots (${screenshots.added.length})</h3>`;\n html += `<table><thead><tr><th>URL</th></tr></thead><tbody>`;\n for (const ss of screenshots.added) {\n html += `<tr><td>${escapeHtml(ss.url)}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n if (screenshots.removed.length > 0) {\n html += `<h3>${badge('removed')} Removed Screenshots (${screenshots.removed.length})</h3>`;\n html += `<table><thead><tr><th>URL</th></tr></thead><tbody>`;\n for (const ss of screenshots.removed) {\n html += `<tr><td>${escapeHtml(ss.url)}</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n if (screenshots.changed.length > 0) {\n html += `<h3>${badge('changed')} Changed Screenshots (${screenshots.changed.length})</h3>`;\n html += `<table><thead><tr><th>URL</th><th>Diff</th></tr></thead><tbody>`;\n for (const ss of screenshots.changed) {\n html += `<tr><td>${escapeHtml(ss.url)}</td><td>${ss.diffPercentage}% changed</td></tr>`;\n }\n html += `</tbody></table>`;\n }\n\n html += `</div>`;\n return html;\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a self-contained HTML diff report.\n *\n * @param diff - The DiffResult to render\n * @returns Complete HTML document as a string\n */\nexport function generateDiffReportHtml(diff: DiffResult): string {\n const { meta, summary, hasChanges } = diff;\n\n const summarySection = `\n <div class=\"summary\">\n ${summaryCard('Routes', summary.routes)}\n ${summaryCard('Forms', summary.forms)}\n ${summaryCard('API Endpoints', summary.api)}\n ${summaryCard('Screenshots', summary.screenshots)}\n </div>`;\n\n const noChangesNotice = !hasChanges\n ? `<div class=\"no-changes\">No changes detected between bundles.</div>`\n : '';\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n ${CSP_META}\n <title>Diff Report — playwright-archaeologist</title>\n <style>${STYLES}</style>\n</head>\n<body>\n <h1>Regression Diff Report</h1>\n <div class=\"meta\">\n <p>Old bundle: ${escapeHtml(meta.oldBundle.targetUrl || meta.oldBundle.path)} (${escapeHtml(meta.oldBundle.createdAt)})</p>\n <p>New bundle: ${escapeHtml(meta.newBundle.targetUrl || meta.newBundle.path)} (${escapeHtml(meta.newBundle.createdAt)})</p>\n <p>Compared at: ${escapeHtml(meta.comparedAt)}</p>\n </div>\n\n ${summarySection}\n ${noChangesNotice}\n\n ${renderRoutesSection(diff)}\n ${renderFormsSection(diff)}\n ${renderApiSection(diff)}\n ${renderScreenshotsSection(diff)}\n</body>\n</html>`;\n}\n","/**\n * Bundle Creator\n *\n * Scans a crawl output directory and creates a structured bundle directory\n * with a manifest.json at the root. The bundle layout:\n *\n * manifest.json -- BundleManifest describing all files\n * screenshots/ -- PNG screenshot files\n * data/ -- JSON artifacts (sitemap.json, forms.json, api-map.json, etc.)\n * report.html -- HTML report (if present)\n *\n * No external dependencies -- uses Node.js built-in crypto and fs.\n */\n\nimport { createHash } from 'node:crypto';\nimport {\n readdir,\n readFile,\n stat,\n mkdir,\n copyFile,\n writeFile,\n} from 'node:fs/promises';\nimport { join, basename, extname, relative } from 'node:path';\nimport type { BundleManifest, BundleFileEntry } from '../types/artifacts.js';\nimport { BundleError } from '../types/errors.js';\n\n/** File extensions mapped to bundle file types. */\nfunction classifyFile(filePath: string): BundleFileEntry['type'] | null {\n const ext = extname(filePath).toLowerCase();\n switch (ext) {\n case '.json':\n return 'json';\n case '.png':\n case '.jpg':\n case '.jpeg':\n case '.webp':\n return 'screenshot';\n case '.har':\n return 'api-snapshot';\n case '.html':\n return 'json'; // HTML report stored alongside data\n default:\n return null;\n }\n}\n\n/** Compute SHA-256 hex digest of a file. */\nexport async function computeSha256(filePath: string): Promise<string> {\n const content = await readFile(filePath);\n return createHash('sha256').update(content).digest('hex');\n}\n\n/**\n * Recursively collect all files in a directory.\n * Returns absolute paths.\n */\nasync function collectFiles(dir: string): Promise<string[]> {\n const results: string[] = [];\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return results;\n }\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n const nested = await collectFiles(fullPath);\n results.push(...nested);\n } else if (entry.isFile()) {\n results.push(fullPath);\n }\n }\n return results;\n}\n\n/**\n * Determine the target subdirectory within the bundle for a given file.\n */\nfunction targetSubdir(filePath: string): string {\n const ext = extname(filePath).toLowerCase();\n const name = basename(filePath).toLowerCase();\n\n if (name === 'report.html') return '';\n if (['.png', '.jpg', '.jpeg', '.webp'].includes(ext)) return 'screenshots';\n if (['.json', '.har', '.html'].includes(ext)) return 'data';\n return 'data';\n}\n\n/**\n * Load crawl stats from assembled JSON artifacts if available.\n */\nasync function loadStats(\n outputDir: string,\n): Promise<BundleManifest['stats']> {\n const defaults: BundleManifest['stats'] = {\n pagesVisited: 0,\n formsFound: 0,\n apiEndpoints: 0,\n screenshotCount: 0,\n duration: 0,\n };\n\n try {\n const sitemapPath = join(outputDir, 'sitemap.json');\n const sitemapContent = await readFile(sitemapPath, 'utf-8');\n const sitemap = JSON.parse(sitemapContent) as unknown[];\n if (Array.isArray(sitemap)) {\n defaults.pagesVisited = sitemap.length;\n }\n } catch {\n // sitemap may not exist\n }\n\n try {\n const formsPath = join(outputDir, 'forms.json');\n const formsContent = await readFile(formsPath, 'utf-8');\n const forms = JSON.parse(formsContent) as unknown[];\n if (Array.isArray(forms)) {\n defaults.formsFound = forms.length;\n }\n } catch {\n // forms may not exist\n }\n\n try {\n const apiPath = join(outputDir, 'api-map.json');\n const apiContent = await readFile(apiPath, 'utf-8');\n const api = JSON.parse(apiContent) as unknown[];\n if (Array.isArray(api)) {\n defaults.apiEndpoints = api.length;\n }\n } catch {\n // api-map may not exist\n }\n\n return defaults;\n}\n\n/**\n * Load crawl config from meta.json or sitemap.json if available.\n */\nasync function loadConfig(\n outputDir: string,\n): Promise<BundleManifest['config']> {\n const defaults: BundleManifest['config'] = {\n targetUrl: '',\n depth: 0,\n viewport: { width: 1280, height: 720 },\n concurrency: 1,\n maxPages: 100,\n followExternal: false,\n deepClick: false,\n };\n\n try {\n const metaPath = join(outputDir, 'meta.json');\n const content = await readFile(metaPath, 'utf-8');\n const meta = JSON.parse(content) as Record<string, unknown>;\n if (typeof meta.targetUrl === 'string') defaults.targetUrl = meta.targetUrl;\n if (typeof meta.depth === 'number') defaults.depth = meta.depth;\n if (typeof meta.duration === 'number') {\n // stats duration from meta\n }\n if (\n meta.viewport &&\n typeof meta.viewport === 'object' &&\n 'width' in meta.viewport &&\n 'height' in meta.viewport\n ) {\n defaults.viewport = meta.viewport as { width: number; height: number };\n }\n } catch {\n // meta may not exist\n }\n\n return defaults;\n}\n\n/**\n * Create a bundle directory from crawl output.\n *\n * @param outputDir - The crawl output directory containing artifacts\n * @param bundlePath - The destination bundle directory to create\n * @returns The BundleManifest describing the bundle contents\n */\nexport async function createBundle(\n outputDir: string,\n bundlePath: string,\n): Promise<BundleManifest> {\n // Validate outputDir exists\n try {\n const outputStat = await stat(outputDir);\n if (!outputStat.isDirectory()) {\n throw new BundleError(\n 'create_failed',\n `Output path is not a directory: ${outputDir}`,\n bundlePath,\n );\n }\n } catch (err) {\n if (err instanceof BundleError) throw err;\n throw new BundleError(\n 'create_failed',\n `Output directory not found: ${outputDir}`,\n bundlePath,\n { cause: err },\n );\n }\n\n // Create bundle directory structure\n await mkdir(join(bundlePath, 'screenshots'), { recursive: true });\n await mkdir(join(bundlePath, 'data'), { recursive: true });\n\n // Collect all files from output directory\n const allFiles = await collectFiles(outputDir);\n const fileEntries: BundleFileEntry[] = [];\n let screenshotCount = 0;\n\n for (const filePath of allFiles) {\n const fileType = classifyFile(filePath);\n if (fileType === null) continue; // skip unknown file types\n\n const fileName = basename(filePath);\n const subdir = targetSubdir(filePath);\n const bundleRelPath = subdir ? join(subdir, fileName) : fileName;\n const destPath = join(bundlePath, bundleRelPath);\n\n // Copy file to bundle\n await copyFile(filePath, destPath);\n\n // Compute hash and size\n const sha256 = await computeSha256(filePath);\n const fileStat = await stat(filePath);\n\n if (fileType === 'screenshot') {\n screenshotCount++;\n }\n\n fileEntries.push({\n path: bundleRelPath.replace(/\\\\/g, '/'), // normalize to forward slashes\n sha256,\n size: fileStat.size,\n type: fileType,\n });\n }\n\n // Load stats and config from output artifacts\n const [stats, config] = await Promise.all([\n loadStats(outputDir),\n loadConfig(outputDir),\n ]);\n stats.screenshotCount = screenshotCount;\n\n // Build manifest\n const manifest: BundleManifest = {\n version: 1,\n tool: 'playwright-archaeologist',\n toolVersion: '0.1.0', // TODO: read from package.json at build time\n createdAt: new Date().toISOString(),\n config,\n stats,\n files: fileEntries,\n };\n\n // Write manifest to bundle\n const manifestPath = join(bundlePath, 'manifest.json');\n await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');\n\n return manifest;\n}\n"],"mappings":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","names":["resolve","options","path","path","path","mkdir","path","resolve","readFile","join","readFile","join","CSP_META","renderFormsSection","renderApiSection","renderScreenshotsSection","createHash","readFile","stat","mkdir","writeFile","join","basename","extname","extname","computeSha256","readFile","createHash","join","basename","stat","mkdir","writeFile"]}
|