#!/usr/bin/env node /** * tls-client-node postinstall repair (#7802). * * tls-client-node's own postinstall.js fetches a platform-specific native * binary (.so/.dylib/.dll) from the bogdanfinn/tls-client GitHub Releases * API. That script is blocked by `npm ci --ignore-scripts` (the Dockerfile * builder stage runs with scripts disabled for supply-chain hygiene) and, * even when it does run, silently no-ops on a rate-limited/failed GitHub API * call instead of raising — so `node_modules/tls-client-node/bin/` can end * up empty with no visible signal until the first live request throws * TlsClientUnavailableError (chatgpt-web/claude-web/perplexity-web/grok-web/ * notion-web/lmarena all share this transport). * * This module: * 1. Accepts only bogdanfinn/tls-client v1.15.1 assets whose SHA-256 matches * the digest published by GitHub for the tagged release. * 2. Copies the verified root asset into the standalone * `dist/node_modules/tls-client-node/bin/` bundle (same pattern as * fixWreqJsBinary), so the published npm package works even though its * own `files` allowlist never ships the binary. * 3. When that verified asset is absent, downloads the exact tagged release * asset directly and retries with exponential backoff. * * Normal npm postinstall remains best-effort and warns on failure. Docker and * release callers use --strict, which fails closed instead of shipping an * absent or unverified binary. */ import { createHash, randomUUID } from "node:crypto"; import { closeSync, constants as fsConstants, existsSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from "node:fs"; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { pathToFileURL } from "node:url"; const DEFAULT_RETRY_DELAYS_MS = [1_000, 3_000, 8_000]; const DEFAULT_DOWNLOAD_TIMEOUT_MS = 30_000; const MAX_NATIVE_ASSET_BYTES = 64 * 1024 * 1024; const NATIVE_ASSET_READ_CHUNK_BYTES = 64 * 1024; const TLS_CLIENT_RELEASE_DOWNLOAD_BASE = "https://github.com/bogdanfinn/tls-client/releases/download"; const NATIVE_MANIFEST = JSON.parse( readFileSync( new URL("../../open-sse/config/tlsClientNativeManifest.json", import.meta.url), "utf8" ) ); export const TLS_CLIENT_NATIVE_VERSION = NATIVE_MANIFEST.version; export const TLS_CLIENT_NATIVE_ASSETS = NATIVE_MANIFEST.assets; /** @typedef {{ file: string; sha256: string }} NativeAsset */ /** @param {NativeAsset} asset */ function validateNativeAsset(asset) { if ( !asset?.file || asset.file === "." || asset.file === ".." || basename(asset.file) !== asset.file || asset.file.includes("/") || asset.file.includes("\\") || asset.file.includes("\0") ) { throw new Error(`Invalid tls-client native asset path: ${JSON.stringify(asset?.file)}`); } if (!/^[a-f0-9]{64}$/.test(asset.sha256)) { throw new Error(`Invalid SHA-256 in tls-client native manifest for ${asset.file}`); } } /** * Resolve the exact native asset supported by tls-client-node@0.2.0. * * @param {NodeJS.Platform} [platform] * @param {string} [arch] * @returns {NativeAsset} */ export function resolveTlsClientNativeAsset(platform = process.platform, arch = process.arch) { const asset = TLS_CLIENT_NATIVE_ASSETS[`${platform}-${arch}`]; if (!asset) { throw new Error(`Unsupported platform for tls-client-node native asset: ${platform}/${arch}`); } validateNativeAsset(asset); return asset; } function sameFileIdentity(left, right) { return String(left.dev) === String(right.dev) && String(left.ino) === String(right.ino); } /** @param {string} filePath */ function lstatIfPresent(filePath) { try { return lstatSync(filePath); } catch (err) { if (err?.code === "ENOENT") return undefined; throw err; } } function assertNativeAssetSize(size, label) { if (!Number.isSafeInteger(size) || size < 0 || size > MAX_NATIVE_ASSET_BYTES) { throw new Error(`${label} exceeds the 64 MiB limit`); } } function pathEscapesRoot(rootPath, candidatePath) { const relativePath = relative(rootPath, candidatePath); return isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${sep}`); } function readSafeDirectoryIdentity(directoryPath) { const initialStats = lstatIfPresent(directoryPath); if (!initialStats) return undefined; if (initialStats.isSymbolicLink() || !initialStats.isDirectory()) { throw new Error( `Unsafe tls-client native destination ancestor (symlink/non-directory): ${directoryPath}` ); } const canonicalPath = realpathSync(directoryPath); const finalStats = lstatSync(directoryPath); if ( finalStats.isSymbolicLink() || !finalStats.isDirectory() || !sameFileIdentity(initialStats, finalStats) ) { throw new Error( `Unsafe tls-client native destination ancestor changed during verification: ${directoryPath}` ); } return canonicalPath; } /** * Validate every existing directory component without following symlinks. The * trusted root itself must already be a real directory; descendants may be * absent because the caller creates them only after this check succeeds. */ function assertSafeDestinationAncestors(trustedRoot, destinationPath) { const resolvedRoot = resolve(trustedRoot); const resolvedDestination = resolve(destinationPath); const destinationRelativePath = relative(resolvedRoot, resolvedDestination); if ( destinationRelativePath === "" || isAbsolute(destinationRelativePath) || destinationRelativePath === ".." || destinationRelativePath.startsWith(`..${sep}`) ) { throw new Error( `Unsafe tls-client native destination outside trusted root: ${resolvedDestination}` ); } const canonicalRoot = readSafeDirectoryIdentity(resolvedRoot); if (!canonicalRoot) { throw new Error(`Trusted tls-client native destination root not found: ${resolvedRoot}`); } let currentPath = resolvedRoot; const relativeParent = relative(resolvedRoot, dirname(resolvedDestination)); for (const component of relativeParent.split(sep).filter(Boolean)) { currentPath = join(currentPath, component); const canonicalPath = readSafeDirectoryIdentity(currentPath); if (canonicalPath && pathEscapesRoot(canonicalRoot, canonicalPath)) { throw new Error( `Unsafe tls-client native destination ancestor outside trusted root: ${currentPath}` ); } } return resolvedDestination; } function collectAllowedNativeAssets(nativeAssets, targetAsset) { const assetsByFile = new Map(); for (const candidate of Object.values(nativeAssets ?? {})) { validateNativeAsset(candidate); const previous = assetsByFile.get(candidate.file); if (previous && previous.sha256 !== candidate.sha256) { throw new Error(`Ambiguous SHA-256 for tls-client native asset: ${candidate.file}`); } assetsByFile.set(candidate.file, candidate); } if (targetAsset) { validateNativeAsset(targetAsset); // A deterministic test target intentionally overrides the production asset // with the same filename. Production calls do not provide this seam. assetsByFile.set(targetAsset.file, targetAsset); } return assetsByFile; } function assertNativeAssetDirectoryInventory( trustedRoot, binDir, allowedAssets, verifyDigests = false ) { const auditSentinel = join(binDir, ".tls-client-native-audit"); assertSafeDestinationAncestors(trustedRoot, auditSentinel); if (!lstatIfPresent(binDir)) return; for (const entryName of readdirSync(binDir)) { const expectedAsset = allowedAssets.get(entryName); if (!expectedAsset) { throw new Error( `Unlisted tls-client native sibling is not in the manifest: ${join(binDir, entryName)}` ); } const entryPath = join(binDir, entryName); const entryStats = lstatIfPresent(entryPath); if (!entryStats || entryStats.isSymbolicLink() || !entryStats.isFile()) { throw new Error(`Unsafe tls-client native sibling (symlink/non-regular file): ${entryPath}`); } if (verifyDigests && !isVerifiedBinary(entryPath, expectedAsset)) { throw new Error( `Manifested tls-client native sibling has an unverified SHA-256: ${entryPath}` ); } } assertSafeDestinationAncestors(trustedRoot, auditSentinel); } function readFileDescriptorBounded(fd, filePath) { assertNativeAssetSize(fstatSync(fd).size, `Local tls-client native asset: ${filePath}`); const chunks = []; let totalBytes = 0; while (true) { const remainingWithSentinel = MAX_NATIVE_ASSET_BYTES - totalBytes + 1; const chunk = Buffer.allocUnsafe( Math.min(NATIVE_ASSET_READ_CHUNK_BYTES, remainingWithSentinel) ); const bytesRead = readSync(fd, chunk, 0, chunk.byteLength, null); if (bytesRead === 0) break; totalBytes += bytesRead; assertNativeAssetSize(totalBytes, `Local tls-client native asset: ${filePath}`); chunks.push(chunk.subarray(0, bytesRead)); } const finalStats = fstatSync(fd); assertNativeAssetSize(finalStats.size, `Local tls-client native asset: ${filePath}`); const bytes = Buffer.concat(chunks, totalBytes); assertNativeAssetSize(bytes.length, `Local tls-client native asset: ${filePath}`); return bytes; } /** * @param {string} filePath * @param {NativeAsset} asset * @param {(filePath: string) => void} [afterInitialStat] */ function readVerifiedBinary(filePath, asset, afterInitialStat) { const pathStats = lstatIfPresent(filePath); if (!pathStats) return undefined; if (pathStats.isSymbolicLink() || !pathStats.isFile()) { throw new Error(`Unsafe tls-client native path (symlink/non-regular file): ${filePath}`); } const fd = openSync(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0)); try { const openedStats = fstatSync(fd); if (!openedStats.isFile()) { throw new Error(`Unsafe tls-client native path (not a regular file): ${filePath}`); } assertNativeAssetSize(openedStats.size, `Local tls-client native asset: ${filePath}`); afterInitialStat?.(filePath); const bytes = readFileDescriptorBounded(fd, filePath); const currentStats = lstatSync(filePath); if ( currentStats.isSymbolicLink() || !currentStats.isFile() || !sameFileIdentity(openedStats, currentStats) ) { throw new Error(`Unsafe tls-client native path changed during verification: ${filePath}`); } if (createHash("sha256").update(bytes).digest("hex") !== asset.sha256) return undefined; if (process.platform !== "win32") fchmodSync(fd, 0o555); return bytes; } finally { closeSync(fd); } } /** @param {string} filePath @param {NativeAsset} asset */ function isVerifiedBinary(filePath, asset) { try { return Boolean(readVerifiedBinary(filePath, asset)); } catch { return false; } } /** * @param {string} destinationPath * @param {Uint8Array} bytes * @param {NativeAsset} asset * @param {string} trustedRoot */ function writeVerifiedBinary(destinationPath, bytes, asset, trustedRoot) { assertNativeAssetSize(bytes.byteLength, `tls-client native asset: ${asset.file}`); if (createHash("sha256").update(bytes).digest("hex") !== asset.sha256) { throw new Error(`SHA-256 mismatch for tls-client native asset: ${asset.file}`); } const resolvedDestination = assertSafeDestinationAncestors(trustedRoot, destinationPath); const destinationStats = lstatIfPresent(resolvedDestination); if (destinationStats?.isSymbolicLink() || (destinationStats && !destinationStats.isFile())) { throw new Error(`Unsafe tls-client native destination path: ${resolvedDestination}`); } mkdirSync(dirname(resolvedDestination), { recursive: true }); assertSafeDestinationAncestors(trustedRoot, resolvedDestination); const temporaryPath = join( dirname(resolvedDestination), `.${asset.file}.${process.pid}.${randomUUID()}.tmp` ); try { assertSafeDestinationAncestors(trustedRoot, resolvedDestination); writeFileSync(temporaryPath, bytes, { flag: "wx", mode: 0o555 }); if (!isVerifiedBinary(temporaryPath, asset)) { throw new Error(`SHA-256 mismatch after writing ${asset.file}`); } assertSafeDestinationAncestors(trustedRoot, resolvedDestination); renameSync(temporaryPath, resolvedDestination); assertSafeDestinationAncestors(trustedRoot, resolvedDestination); if (!isVerifiedBinary(resolvedDestination, asset)) { throw new Error(`SHA-256 mismatch after installing ${asset.file}`); } assertSafeDestinationAncestors(trustedRoot, resolvedDestination); } finally { try { assertSafeDestinationAncestors(trustedRoot, temporaryPath); removeIfPresent(temporaryPath); } catch { // Never follow a destination ancestor that changed while the write was in progress. } } } /** * @param {string} sourcePath * @param {string} destinationPath * @param {NativeAsset} asset * @param {string} trustedRoot */ function copyVerifiedBinary(sourcePath, destinationPath, asset, trustedRoot) { assertSafeDestinationAncestors(trustedRoot, sourcePath); const bytes = readVerifiedBinary(sourcePath, asset); if (!bytes) throw new Error(`Source native binary is absent or unverified: ${sourcePath}`); assertSafeDestinationAncestors(trustedRoot, sourcePath); writeVerifiedBinary(destinationPath, bytes, asset, trustedRoot); } function removeIfPresent(filePath) { if (lstatIfPresent(filePath)) unlinkSync(filePath); } async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function pinnedReleaseAssetUrl(version, asset) { if (!/^\d+\.\d+\.\d+$/.test(version)) { throw new Error(`Invalid pinned tls-client native version: ${version}`); } return `${TLS_CLIENT_RELEASE_DOWNLOAD_BASE}/v${version}/${encodeURIComponent(asset.file)}`; } async function readBoundedResponseBytes(response, asset) { const contentLength = response.headers?.get?.("content-length"); if (contentLength !== null && contentLength !== undefined) { if (!/^\d+$/.test(contentLength)) { throw new Error(`Invalid Content-Length for tls-client native asset: ${asset.file}`); } assertNativeAssetSize( Number(contentLength), `Downloaded tls-client native asset: ${asset.file}` ); } const reader = response.body?.getReader?.(); if (!reader) { throw new Error(`Downloaded tls-client native asset has no readable body: ${asset.file}`); } const chunks = []; let totalBytes = 0; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = Buffer.from(value); totalBytes += chunk.byteLength; try { assertNativeAssetSize(totalBytes, `Downloaded tls-client native asset: ${asset.file}`); } catch (err) { // Cancellation is advisory after the size violation; preserve the actionable size error. await reader.cancel().catch(() => {}); throw err; } chunks.push(chunk); } const bytes = Buffer.concat(chunks, totalBytes); assertNativeAssetSize(bytes.length, `Downloaded tls-client native asset: ${asset.file}`); return bytes; } async function downloadPinnedAssetWithRetry( rootTlsClientDir, trustedRoot, asset, version, retryDelaysMs, downloadTimeoutMs, fetchImpl, log ) { const binaryPath = join(rootTlsClientDir, "bin", asset.file); assertSafeDestinationAncestors(trustedRoot, binaryPath); const initialStats = lstatIfPresent(binaryPath); if (initialStats?.isSymbolicLink() || (initialStats && !initialStats.isFile())) { throw new Error(`Unsafe tls-client native path (symlink/non-regular file): ${binaryPath}`); } if (initialStats && !isVerifiedBinary(binaryPath, asset)) { assertSafeDestinationAncestors(trustedRoot, binaryPath); removeIfPresent(binaryPath); assertSafeDestinationAncestors(trustedRoot, binaryPath); log(` ⚠️ Removed tls-client-node binary with an invalid SHA-256: ${asset.file}`); assertSafeDestinationAncestors(trustedRoot, binaryPath); } if (typeof fetchImpl !== "function") { throw new Error("Fetch is unavailable; cannot download the pinned tls-client native asset"); } if (!Number.isFinite(downloadTimeoutMs) || downloadTimeoutMs <= 0) { throw new Error(`Invalid tls-client native download timeout: ${downloadTimeoutMs}`); } const downloadUrl = pinnedReleaseAssetUrl(version, asset); for (let attempt = 0; attempt <= retryDelaysMs.length; attempt++) { if (attempt > 0) { log( ` ⏳ tls-client-node ${asset.file} still missing — retrying pinned download ` + `(attempt ${attempt + 1}/${retryDelaysMs.length + 1})...` ); await sleep(retryDelaysMs[attempt - 1]); } assertSafeDestinationAncestors(trustedRoot, binaryPath); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), downloadTimeoutMs); timeout.unref?.(); try { const response = await fetchImpl(downloadUrl, { signal: controller.signal }); if (!response?.ok) { throw new Error(`HTTP ${response?.status ?? "unknown"}`); } const bytes = await readBoundedResponseBytes(response, asset); writeVerifiedBinary(binaryPath, bytes, asset, trustedRoot); } catch (err) { assertSafeDestinationAncestors(trustedRoot, binaryPath); log( ` ⚠️ Pinned tls-client native download attempt failed for ${asset.file}: ` + `${err.message.split("\n")[0]}` ); assertSafeDestinationAncestors(trustedRoot, binaryPath); } finally { clearTimeout(timeout); } assertSafeDestinationAncestors(trustedRoot, binaryPath); if (isVerifiedBinary(binaryPath, asset)) return true; const currentStats = lstatIfPresent(binaryPath); if (currentStats?.isSymbolicLink() || (currentStats && !currentStats.isFile())) { throw new Error(`Unsafe tls-client native path after download: ${binaryPath}`); } if (currentStats) { assertSafeDestinationAncestors(trustedRoot, binaryPath); removeIfPresent(binaryPath); assertSafeDestinationAncestors(trustedRoot, binaryPath); } } return false; } function normalizeTargetPlatform(platform) { if (typeof platform !== "string" || !/^[a-z0-9]+$/.test(platform)) { throw new Error(`Invalid tls-client target platform: ${JSON.stringify(platform)}`); } return platform; } function normalizeTargetArches(arches) { const values = (Array.isArray(arches) ? arches : [arches]) .flatMap((arch) => (typeof arch === "string" ? arch.split(",") : [])) .map((arch) => arch.trim()) .filter(Boolean); if (values.length === 0 || values.some((arch) => !/^[a-z0-9_-]+$/.test(arch))) { throw new Error(`Invalid tls-client target arches: ${JSON.stringify(arches)}`); } return [...new Set(values)]; } function resolveTargetNativeAsset(platform, arch, nativeAssets) { const expectedAsset = nativeAssets?.[`${platform}-${arch}`]; if (!expectedAsset) { throw new Error(`Unsupported platform for tls-client-node native asset: ${platform}/${arch}`); } validateNativeAsset(expectedAsset); return expectedAsset; } async function fixTlsClientNodeTarget({ rootDir, rootTlsClientDir, distTlsClientDir, expectedAsset, targetPlatform, targetArch, version, log, retryDelaysMs, downloadTimeoutMs, fetchImpl, strict, standaloneDir, requireStandalone, afterSourceStat, }) { const rootBinDir = join(rootTlsClientDir, "bin"); const rootBinaryPath = join(rootBinDir, expectedAsset.file); try { assertSafeDestinationAncestors(rootDir, rootBinaryPath); } catch (err) { if (strict) throw err; console.warn(` ⚠️ ${err.message}`); return; } const rootBinaryStats = lstatIfPresent(rootBinaryPath); if (rootBinaryStats?.isSymbolicLink() || (rootBinaryStats && !rootBinaryStats.isFile())) { const message = `Unsafe tls-client native source path: ${rootBinaryPath}`; if (strict) throw new Error(message); console.warn(` ⚠️ ${message}`); return; } let rootBinaryVerified; try { rootBinaryVerified = Boolean( readVerifiedBinary(rootBinaryPath, expectedAsset, afterSourceStat) ); } catch (err) { if (strict) throw err; console.warn(` ⚠️ ${err.message}`); return; } if (!rootBinaryVerified) { log( `\n 🔧 tls-client-node native binary missing or unverified — fetching pinned ` + `v${version} for ${targetPlatform}/${targetArch} and checking SHA-256...\n` ); let recovered = false; try { recovered = await downloadPinnedAssetWithRetry( rootTlsClientDir, rootDir, expectedAsset, version, retryDelaysMs, downloadTimeoutMs, fetchImpl, log ); } catch (err) { if (strict) throw err; console.warn(` ⚠️ Could not recover tls-client-node binary: ${err.message}`); return; } if (!recovered) { const message = `Could not fetch tls-client-node v${version} verified native binary ` + `(${expectedAsset.file}, ${targetPlatform}/${targetArch}) after retries.`; if (strict) throw new Error(message); console.warn(`\n ⚠️ ${message} GitHub may be rate-limited or unreachable.`); console.warn( " chatgpt-web/claude-web/perplexity-web/grok-web/notion-web/lmarena will " + "raise a clear TlsClientUnavailableError on first use until this is resolved." ); console.warn( ` Verified repair: node ${join(rootDir, "scripts", "build", "fixTlsClientNodeBinary.mjs")} --strict\n` ); return; } try { assertSafeDestinationAncestors(rootDir, rootBinaryPath); } catch (err) { if (strict) throw err; console.warn(` ⚠️ ${err.message}`); return; } log(" ✅ tls-client-node native binary fetched successfully!\n"); try { assertSafeDestinationAncestors(rootDir, rootBinaryPath); } catch (err) { if (strict) throw err; console.warn(` ⚠️ ${err.message}`); return; } } if (!isVerifiedBinary(rootBinaryPath, expectedAsset)) { const message = `tls-client-node v${version} root native binary failed post-recovery verification ` + `(${expectedAsset.file}); refusing to copy or seed standalone artifacts.`; if (strict) throw new Error(message); console.warn(` ⚠️ ${message}`); return; } if (existsSync(distTlsClientDir)) { const distBinaryPath = join(distTlsClientDir, "bin", expectedAsset.file); try { assertSafeDestinationAncestors(rootDir, distBinaryPath); if (!isVerifiedBinary(distBinaryPath, expectedAsset)) { copyVerifiedBinary(rootBinaryPath, distBinaryPath, expectedAsset, rootDir); log( ` ✅ Verified tls-client-node v${version} native binary copied to standalone ` + "dist/node_modules.\n" ); } } catch (err) { if (strict) throw err; console.warn(` ⚠️ Could not copy tls-client-node binary into dist/: ${err.message}`); } } if (requireStandalone && !standaloneDir) { throw new Error("Final standalone artifact path is required for strict verification"); } if (standaloneDir) { const resolvedStandaloneDir = resolve(rootDir, standaloneDir); if (!existsSync(resolvedStandaloneDir)) { const message = `Final standalone artifact not found: ${resolvedStandaloneDir}`; if (requireStandalone || strict) throw new Error(message); console.warn(` ⚠️ ${message}`); return; } const runtimeBinaryPath = join( resolvedStandaloneDir, "runtime-assets", "tls-client", "bin", expectedAsset.file ); try { assertSafeDestinationAncestors(rootDir, runtimeBinaryPath); if (!isVerifiedBinary(runtimeBinaryPath, expectedAsset)) { copyVerifiedBinary(rootBinaryPath, runtimeBinaryPath, expectedAsset, rootDir); } if (!isVerifiedBinary(runtimeBinaryPath, expectedAsset)) { throw new Error(`Final standalone runtime seed is unverified: ${runtimeBinaryPath}`); } log(` ✅ Verified tls-client native runtime seed: ${runtimeBinaryPath}\n`); } catch (err) { if (strict || requireStandalone) throw err; console.warn(` ⚠️ Could not seed standalone TLS runtime binary: ${err.message}`); } } } /** * @param {object} opts * @param {string} opts.rootDir - repo root * @param {(msg: string) => void} [opts.log] * @param {number[]} [opts.retryDelaysMs] - override for tests (avoid real sleeps) * @param {number} [opts.downloadTimeoutMs] - timeout for each pinned fetch attempt * @param {NativeAsset} [opts.asset] - legacy single-target injection for deterministic tests * @param {Record} [opts.nativeAssets] - manifest injection for tests * @param {typeof fetch} [opts.fetchImpl] - pinned download boundary, injectable for tests * @param {string} [opts.platform] - target platform (defaults to the current host) * @param {string|string[]} [opts.arches] - one or more target arches (defaults to host arch) * @param {(filePath: string) => void} [opts.afterSourceStat] - deterministic race hook for tests * @param {boolean} [opts.strict] - fail instead of warning (Docker/release builds) * @param {string} [opts.standaloneDir] - final standalone root to seed and verify * @param {boolean} [opts.requireStandalone] - fail if standaloneDir does not exist */ export async function fixTlsClientNodeBinary({ rootDir, log = (m) => console.log(m), retryDelaysMs = DEFAULT_RETRY_DELAYS_MS, downloadTimeoutMs = DEFAULT_DOWNLOAD_TIMEOUT_MS, asset, nativeAssets = TLS_CLIENT_NATIVE_ASSETS, fetchImpl = globalThis.fetch, platform = process.platform, arches = [process.arch], afterSourceStat, strict = false, standaloneDir, requireStandalone = false, } = {}) { const version = TLS_CLIENT_NATIVE_VERSION; const rootTlsClientDir = join(rootDir, "node_modules", "tls-client-node"); const distTlsClientDir = join(rootDir, "dist", "node_modules", "tls-client-node"); const resolvedStandaloneDir = standaloneDir ? resolve(rootDir, standaloneDir) : undefined; if (!existsSync(rootTlsClientDir)) { if (strict) throw new Error("tls-client-node is not installed; cannot verify native binary"); return; } if (requireStandalone && !standaloneDir) { throw new Error("Final standalone artifact path is required for strict verification"); } if (resolvedStandaloneDir) { if (!existsSync(resolvedStandaloneDir)) { const message = `Final standalone artifact not found: ${resolvedStandaloneDir}`; if (requireStandalone || strict) throw new Error(message); console.warn(` ⚠️ ${message}`); return; } } let allowedNativeAssets; let strictBinDirs; if (strict) { allowedNativeAssets = collectAllowedNativeAssets(nativeAssets, asset); const resolvedRootDir = resolve(rootDir); const configuredNextDistDir = resolve( resolvedRootDir, process.env.NEXT_DIST_DIR || ".build/next" ); if (pathEscapesRoot(resolvedRootDir, configuredNextDistDir)) { throw new Error( `Unsafe NEXT_DIST_DIR outside tls-client trusted root: ${configuredNextDistDir}` ); } const relativeNextDistDir = relative(resolvedRootDir, configuredNextDistDir); strictBinDirs = [ join(rootTlsClientDir, "bin"), join(distTlsClientDir, "bin"), join(configuredNextDistDir, "node_modules", "tls-client-node", "bin"), ...(resolvedStandaloneDir ? [ join(resolvedStandaloneDir, "node_modules", "tls-client-node", "bin"), join( resolvedStandaloneDir, "projects", "OmniRoute", "node_modules", "tls-client-node", "bin" ), join( resolvedStandaloneDir, basename(resolvedRootDir), "node_modules", "tls-client-node", "bin" ), join( resolvedStandaloneDir, relativeNextDistDir, "node_modules", "tls-client-node", "bin" ), join(resolvedStandaloneDir, "runtime-assets", "tls-client", "bin"), ] : []), ]; for (const binDir of new Set(strictBinDirs)) { assertNativeAssetDirectoryInventory(rootDir, binDir, allowedNativeAssets); } } let targetPlatform; let targetArches; try { targetPlatform = normalizeTargetPlatform(platform); targetArches = normalizeTargetArches(arches); if (asset && targetArches.length !== 1) { throw new Error("A synthetic tls-client asset can only be used with one target arch"); } } catch (err) { if (strict) throw err; console.warn(` ⚠️ ${err.message}`); return; } for (const targetArch of targetArches) { let expectedAsset; try { expectedAsset = asset ?? resolveTargetNativeAsset(targetPlatform, targetArch, nativeAssets); validateNativeAsset(expectedAsset); } catch (err) { if (strict) throw err; console.warn(` ⚠️ ${err.message}`); continue; } await fixTlsClientNodeTarget({ rootDir, rootTlsClientDir, distTlsClientDir, expectedAsset, targetPlatform, targetArch, version, log, retryDelaysMs, downloadTimeoutMs, fetchImpl, strict, standaloneDir, requireStandalone, afterSourceStat, }); } if (strict) { for (const binDir of new Set(strictBinDirs)) { assertNativeAssetDirectoryInventory(rootDir, binDir, allowedNativeAssets, true); } } } function readCliOptionValues(argv, optionNames) { const values = []; for (let index = 0; index < argv.length; index++) { const argument = argv[index]; const matchingName = optionNames.find( (optionName) => argument === optionName || argument.startsWith(`${optionName}=`) ); if (!matchingName) continue; if (argument === matchingName) { const value = argv[index + 1]; if (!value || value.startsWith("--")) { throw new Error(`${matchingName} requires a value`); } values.push(value); index += 1; } else { const value = argument.slice(matchingName.length + 1); if (!value) throw new Error(`${matchingName} requires a value`); values.push(value); } } return values; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { try { const cliArgs = process.argv.slice(2); const standaloneValues = readCliOptionValues(cliArgs, ["--standalone-dir"]); const platformValues = readCliOptionValues(cliArgs, ["--platform"]); const archValues = readCliOptionValues(cliArgs, ["--arch", "--arches"]); if (standaloneValues.length > 1) throw new Error("--standalone-dir may only be passed once"); if (platformValues.length > 1) throw new Error("--platform may only be passed once"); const standaloneDir = standaloneValues[0]; await fixTlsClientNodeBinary({ rootDir: process.cwd(), strict: cliArgs.includes("--strict"), platform: platformValues[0], arches: archValues.length > 0 ? archValues : undefined, standaloneDir, requireStandalone: standaloneValues.length > 0, }); } catch (err) { console.error(` ❌ ${err.message}`); process.exitCode = 1; } }