{"version":3,"file":"tls-ca.d.ts","sourceRoot":"","sources":["../../src/utils/tls-ca.ts"],"names":[],"mappings":"AAgHA;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CA+B5C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,EAAE,CAY7C","sourcesContent":["/**\n * App-level TLS CA trust for hoocode's own outbound traffic (provider calls,\n * GitHub API, tool downloads). This lets hoocode work behind corporate\n * TLS-intercepting proxies WITH certificate validation kept ON, replacing the\n * insecure `NODE_TLS_REJECT_UNAUTHORIZED=0` workaround.\n *\n * Invariants:\n * - Verification is NEVER disabled (we never set `rejectUnauthorized: false`).\n * - Trust is ADDITIVE to Node's bundled roots — a custom CA extends the trust\n *   set, it does not replace it.\n * - Fail closed: a missing/invalid CA source warns once and is skipped; we never\n *   fall back to trusting everything, and there is no trust-on-first-use.\n *\n * This does NOT cover the `webfetch`/`websearch` tools — those shell out to a\n * separate `webtools` binary with its own TLS stack.\n */\nimport chalk from \"chalk\";\nimport { readFileSync, statSync } from \"fs\";\nimport { globalAgent } from \"https\";\nimport { getCACertificates, rootCertificates } from \"tls\";\n\n// Warnings are deduplicated by message so a given problem is reported at most\n// once for the life of the process (\"warn once\").\nconst warnedMessages = new Set<string>();\nfunction warnOnce(message: string): void {\n\tif (warnedMessages.has(message)) return;\n\twarnedMessages.add(message);\n\tconsole.warn(chalk.yellow(`[tls] ${message}`));\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\n/** Scan `process.argv` for `--flag value` or `--flag=value`, returning the value. */\nfunction readArgValue(flag: string): string | undefined {\n\tconst argv = process.argv;\n\tfor (let i = 0; i < argv.length; i++) {\n\t\tconst current = argv[i];\n\t\tif (current === flag) {\n\t\t\tconst next = argv[i + 1];\n\t\t\tif (next !== undefined && !next.startsWith(\"-\")) {\n\t\t\t\tconst trimmed = next.trim();\n\t\t\t\treturn trimmed.length > 0 ? trimmed : undefined;\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\t\tif (current.startsWith(`${flag}=`)) {\n\t\t\tconst trimmed = current.slice(flag.length + 1).trim();\n\t\t\treturn trimmed.length > 0 ? trimmed : undefined;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Scan `process.argv` for a boolean `--flag`. */\nfunction hasArgFlag(flag: string): boolean {\n\treturn process.argv.includes(flag);\n}\n\n/**\n * Resolve the path to an explicit PEM CA bundle from the first configured\n * source, in precedence order: `--ca-cert <path>` > `HOOCODE_CA_CERT` >\n * `NODE_EXTRA_CA_CERTS`.\n */\nfunction resolveExplicitCAPath(): string | undefined {\n\tconst fromFlag = readArgValue(\"--ca-cert\");\n\tif (fromFlag) return fromFlag;\n\n\tconst fromHoocodeEnv = process.env.HOOCODE_CA_CERT?.trim();\n\tif (fromHoocodeEnv) return fromHoocodeEnv;\n\n\tconst fromNodeEnv = process.env.NODE_EXTRA_CA_CERTS?.trim();\n\tif (fromNodeEnv) return fromNodeEnv;\n\n\treturn undefined;\n}\n\n/** Read a PEM bundle from a readable regular file, warning and skipping on failure. */\nfunction readCABundle(path: string): string | undefined {\n\ttry {\n\t\tif (!statSync(path).isFile()) {\n\t\t\twarnOnce(`CA certificate path is not a regular file, skipping: ${path}`);\n\t\t\treturn undefined;\n\t\t}\n\t\treturn readFileSync(path, \"utf8\");\n\t} catch (error) {\n\t\twarnOnce(`Could not read CA certificate file, skipping: ${path} (${errorMessage(error)})`);\n\t\treturn undefined;\n\t}\n}\n\n/** True when the OS trust store has been explicitly opted into. */\nfunction isSystemStoreOptedIn(): boolean {\n\tif (hasArgFlag(\"--use-system-ca\")) return true;\n\tconst value = process.env.HOOCODE_USE_SYSTEM_CA?.trim().toLowerCase();\n\treturn value === \"1\" || value === \"true\" || value === \"yes\";\n}\n\n/** De-duplicate certificate strings while preserving insertion order. */\nfunction dedupe(certs: string[]): string[] {\n\tconst seen = new Set<string>();\n\tconst result: string[] = [];\n\tfor (const cert of certs) {\n\t\tconst key = cert.trim();\n\t\tif (key.length === 0 || seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tresult.push(cert);\n\t}\n\treturn result;\n}\n\n/**\n * Build the additive set of trusted CA certificates:\n *   (a) Node's bundled root certificates (always),\n *   (b) an explicit PEM bundle from the first configured source (always, when set),\n *   (c) the OS trust store, ONLY when explicitly opted in.\n *\n * Never throws: every source that fails is warned-once and skipped, and the\n * bundled defaults are always retained.\n */\nexport function resolveTrustedCAs(): string[] {\n\tconst certs: string[] = [];\n\n\t// (a) Bundled defaults — always present so trust stays additive.\n\tfor (const cert of rootCertificates) {\n\t\tcerts.push(cert);\n\t}\n\n\t// (b) Explicit PEM bundle (first configured source wins).\n\tconst explicitPath = resolveExplicitCAPath();\n\tif (explicitPath) {\n\t\tconst bundle = readCABundle(explicitPath);\n\t\tif (bundle) certs.push(bundle);\n\t}\n\n\t// (c) OS trust store — opt-in only, and only if the runtime supports it.\n\tif (isSystemStoreOptedIn()) {\n\t\tif (typeof getCACertificates === \"function\") {\n\t\t\ttry {\n\t\t\t\tfor (const cert of getCACertificates(\"system\")) {\n\t\t\t\t\tcerts.push(cert);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\twarnOnce(`Could not read the system CA store, skipping: ${errorMessage(error)}`);\n\t\t\t}\n\t\t} else {\n\t\t\twarnOnce(\"System CA store requested, but this Node runtime does not support tls.getCACertificates().\");\n\t\t}\n\t}\n\n\treturn dedupe(certs);\n}\n\n/**\n * Install the resolved CA set on the global HTTPS agent and return it so the\n * caller can thread the same trust set into other dispatchers (e.g. undici).\n * Warns once if `NODE_TLS_REJECT_UNAUTHORIZED=0` is set, since that disables\n * verification globally and defeats the purpose of trusting a specific CA.\n */\nexport function configureGlobalTLS(): string[] {\n\tconst ca = resolveTrustedCAs();\n\tglobalAgent.options.ca = ca;\n\n\tif (process.env.NODE_TLS_REJECT_UNAUTHORIZED === \"0\") {\n\t\twarnOnce(\n\t\t\t\"NODE_TLS_REJECT_UNAUTHORIZED=0 disables all TLS certificate verification and is insecure. \" +\n\t\t\t\t\"Prefer --ca-cert <path> (or --use-system-ca) to trust your proxy's CA with verification kept on.\",\n\t\t);\n\t}\n\n\treturn ca;\n}\n"]}