Some checks failed
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / windows node 24 / wine blocking (push) Has been skipped
CI / wine apt cache (push) Successful in 58s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 2m46s
Deploy documentation / deploy (push) Has been skipped
E2E (real DeepSeek API) / e2e (push) Failing after 1m18s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m18s
Landlock Run / Matrix (push) Successful in 13s
Release (vendor) / Pack npm tarballs (push) Failing after 3m43s
Release (dsh) / Pack npm tarballs (push) Failing after 1m53s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m51s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
176 lines
6.1 KiB
TypeScript
176 lines
6.1 KiB
TypeScript
/**
|
|
* Overlay HTTP proxy variables onto the source `dsh` Node process only.
|
|
* Bootstrap proxy names cannot come from `.env` files; this overlay is the
|
|
* checkout-local way to give OpenRouter (and other HTTPS fetches) an HTTP
|
|
* proxy without changing the calling shell.
|
|
* @module
|
|
*/
|
|
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
/** Repo-root file the source launcher reads when present; gitignored. */
|
|
export const SOURCE_DSH_HTTP_PROXY_FILE = '.dsh-http-proxy.env'
|
|
|
|
/** Process variables Node, undici, and pi-ai consult for HTTP(S) proxies. */
|
|
export const SOURCE_DSH_PROXY_VAR_NAMES = [
|
|
'HTTPS_PROXY',
|
|
'HTTP_PROXY',
|
|
'ALL_PROXY',
|
|
'https_proxy',
|
|
'http_proxy',
|
|
'all_proxy',
|
|
] as const
|
|
|
|
const PROXY_VAR_NAME_SET = new Set<string>(SOURCE_DSH_PROXY_VAR_NAMES)
|
|
|
|
const LOCAL_NO_PROXY = 'localhost,127.0.0.1,::1'
|
|
|
|
/**
|
|
* Write one HTTP(S) proxy URL onto every name Node and pi-ai read, including
|
|
* leftover inherited SOCKS values the overlay file did not mention.
|
|
*/
|
|
function installHttpProxy(env: NodeJS.ProcessEnv, url: string): void {
|
|
for (const name of SOURCE_DSH_PROXY_VAR_NAMES) {
|
|
env[name] = url
|
|
}
|
|
env.NODE_USE_ENV_PROXY = '1'
|
|
if (!env.NO_PROXY && !env.no_proxy) {
|
|
env.NO_PROXY = LOCAL_NO_PROXY
|
|
env.no_proxy = LOCAL_NO_PROXY
|
|
}
|
|
}
|
|
|
|
/**
|
|
* First non-SOCKS proxy URL among the names Node consults.
|
|
* @param env - environment after overlay assignments.
|
|
*/
|
|
function firstHttpProxyUrl(env: NodeJS.ProcessEnv): string | undefined {
|
|
for (const name of SOURCE_DSH_PROXY_VAR_NAMES) {
|
|
const value = env[name]?.trim()
|
|
if (value !== undefined && value !== '' && !isSocksProxyUrl(value)) return value
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Set on the child after this wrapper respawns so `fetch` sees the HTTP overlay
|
|
* at process start. Node `--env-file` does not replace an inherited SOCKS
|
|
* `HTTPS_PROXY`, and `NODE_USE_ENV_PROXY` assigned after start is ignored.
|
|
*/
|
|
export const SOURCE_DSH_PROXY_REEXEC_MARKER = 'DSH_SOURCE_HTTP_PROXY_APPLIED'
|
|
|
|
/** @returns whether `value` is a SOCKS URL Node cannot use for fetch/pnpm. */
|
|
export function isSocksProxyUrl(value: string): boolean {
|
|
return /^socks5h?:\/\//i.test(value.trim())
|
|
}
|
|
|
|
/**
|
|
* Parse a dotenv-like overlay file into assignments.
|
|
* @param text - file contents.
|
|
* @returns name → value; empty lines and `#` comments are skipped.
|
|
*/
|
|
export function parseSourceDshHttpProxyFile(text: string): Record<string, string> {
|
|
const out: Record<string, string> = {}
|
|
for (const raw of text.split(/\r?\n/)) {
|
|
const line = raw.trim()
|
|
if (line === '' || line.startsWith('#')) continue
|
|
const body = line.startsWith('export ') ? line.slice('export '.length).trim() : line
|
|
const eq = body.indexOf('=')
|
|
if (eq <= 0) continue
|
|
const name = body.slice(0, eq).trim()
|
|
let value = body.slice(eq + 1).trim()
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"'))
|
|
|| (value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1)
|
|
}
|
|
if (name !== '') out[name] = value
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Inputs for {@link applySourceDshHttpProxy}. Tests pass a fake env and file text.
|
|
*/
|
|
export interface ApplySourceDshHttpProxyInput {
|
|
/** Mutable environment of this Node process (normally `process.env`). */
|
|
env: NodeJS.ProcessEnv
|
|
/** Contents of `.dsh-http-proxy.env`; `null` when the file is absent. */
|
|
fileText: string | null
|
|
/** Optional explicit HTTP overlay URL (`DSH_HTTP_PROXY`), which outranks the file. */
|
|
overlayUrl?: string | undefined
|
|
}
|
|
|
|
/**
|
|
* Apply an HTTP proxy overlay onto `env` for this process only.
|
|
* @param input - env, optional file, optional `DSH_HTTP_PROXY`.
|
|
* @throws when the winning overlay is a SOCKS URL.
|
|
*/
|
|
export function applySourceDshHttpProxy(input: ApplySourceDshHttpProxyInput): void {
|
|
const overlayUrl = input.overlayUrl?.trim()
|
|
if (overlayUrl) {
|
|
if (isSocksProxyUrl(overlayUrl)) {
|
|
throw new Error(
|
|
`DSH_HTTP_PROXY=${overlayUrl}. Node cannot fetch through SOCKS. Use an HTTP proxy (for example http://127.0.0.1:3067).`,
|
|
)
|
|
}
|
|
installHttpProxy(input.env, overlayUrl)
|
|
return
|
|
}
|
|
if (input.fileText === null) return
|
|
const assignments = parseSourceDshHttpProxyFile(input.fileText)
|
|
for (const [name, value] of Object.entries(assignments)) {
|
|
if (PROXY_VAR_NAME_SET.has(name) && isSocksProxyUrl(value)) {
|
|
throw new Error(
|
|
`${name}=${value}. Node cannot fetch through SOCKS. Use an HTTP proxy (for example http://127.0.0.1:3067).`,
|
|
)
|
|
}
|
|
input.env[name] = value
|
|
}
|
|
const httpUrl = firstHttpProxyUrl(input.env)
|
|
if (httpUrl !== undefined) installHttpProxy(input.env, httpUrl)
|
|
}
|
|
|
|
/**
|
|
* HTTP overlay URL now on `env`, if any.
|
|
* @param env - environment after {@link applySourceDshHttpProxy}.
|
|
*/
|
|
export function sourceDshAppliedHttpProxyUrl(env: NodeJS.ProcessEnv): string | undefined {
|
|
return firstHttpProxyUrl(env)
|
|
}
|
|
|
|
/** Absolute path of the gitignored overlay file beside this repository root. */
|
|
export function sourceDshHttpProxyFilePath(): string {
|
|
return fileURLToPath(new URL(`../${SOURCE_DSH_HTTP_PROXY_FILE}`, import.meta.url))
|
|
}
|
|
|
|
/**
|
|
* Read the overlay file when it exists.
|
|
* @param path - absolute path; defaults to the repo-root gitignored file.
|
|
* @returns file text, or `null` when absent.
|
|
*/
|
|
export function readSourceDshHttpProxyFile(path = sourceDshHttpProxyFilePath()): string | null {
|
|
if (!existsSync(path)) return null
|
|
return readFileSync(path, 'utf8')
|
|
}
|
|
|
|
/**
|
|
* Whether an overlay source is present (file or `DSH_HTTP_PROXY`).
|
|
* @param overlayUrl - `DSH_HTTP_PROXY` value.
|
|
* @param fileText - overlay file contents, or `null` when absent.
|
|
*/
|
|
export function sourceDshHttpProxyOverlayPresent(overlayUrl: string | undefined, fileText: string | null): boolean {
|
|
return Boolean(overlayUrl?.trim()) || fileText !== null
|
|
}
|
|
|
|
/**
|
|
* Whether this process must respawn so `fetch` binds the overlay at start.
|
|
* @param env - the environment after {@link applySourceDshHttpProxy}.
|
|
* @param overlayPresent - {@link sourceDshHttpProxyOverlayPresent}.
|
|
*/
|
|
export function shouldReexecSourceDshForFetchProxy(env: NodeJS.ProcessEnv, overlayPresent: boolean): boolean {
|
|
return overlayPresent && env[SOURCE_DSH_PROXY_REEXEC_MARKER] !== '1'
|
|
}
|