From 2812692088f20f37e8af017638e0818c55d2364a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Jan 2026 06:43:18 +0000 Subject: [PATCH 1/2] feat: Add HTTP proxy support via undici Node.js native fetch doesn't respect HTTP_PROXY/HTTPS_PROXY environment variables. This adds automatic proxy detection and configuration using undici's ProxyAgent. When HTTP_PROXY, HTTPS_PROXY, http_proxy, or https_proxy environment variables are set, requests are automatically routed through the proxy. The proxyTls option is set to allow connections through proxies that intercept HTTPS traffic (common in corporate environments), while maintaining full TLS verification for the upstream Linear API connection. --- package.json | 3 ++- src/main.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 33b8887..58c3fb8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "homepage": "https://github.com/czottmann/linearis#readme", "dependencies": { "@linear/sdk": "^58.1.0", - "commander": "^14.0.0" + "commander": "^14.0.0", + "undici": "^7.0.0" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/src/main.ts b/src/main.ts index 538dbdc..d498a61 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,23 @@ * - Complete API coverage with optimized queries */ +// Proxy support: Node.js native fetch doesn't respect HTTP_PROXY env vars. +// This configures undici's global dispatcher to route through the proxy. +import { ProxyAgent, setGlobalDispatcher } from "undici"; +const proxyUrl = + process.env.HTTPS_PROXY || + process.env.HTTP_PROXY || + process.env.https_proxy || + process.env.http_proxy; +if (proxyUrl) { + setGlobalDispatcher( + new ProxyAgent({ + uri: proxyUrl, + proxyTls: { rejectUnauthorized: false }, + }) + ); +} + import { program } from "commander"; import pkg from "../package.json" with { type: "json" }; import { setupCommentsCommands } from "./commands/comments.js"; From 671b9e32f621821abe4ae1028d1723543fdbb454 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Jan 2026 06:52:28 +0000 Subject: [PATCH 2/2] refactor: Address PR review feedback 1. Security: Make insecure proxy TLS opt-in via LINEARIS_PROXY_INSECURE - Secure by default (TLS verification enabled) - Users must explicitly set LINEARIS_PROXY_INSECURE=1 to disable - Warning printed when insecure mode is active 2. NO_PROXY support: Respect NO_PROXY/no_proxy environment variables - Allows excluding hosts from proxy routing 3. Tests: Add comprehensive unit tests for proxy detection - Test proxy URL detection and priority order - Test NO_PROXY detection - Test insecure mode opt-in 4. Documentation: Add Proxy Configuration section to README - Document all supported environment variables - Explain priority order - Provide usage examples - Document security implications of insecure mode 5. Refactor: Extract proxy logic to src/utils/proxy.ts - Separates concerns for better testability - Exports detectProxyConfig() and configureProxy() functions --- README.md | 42 ++++++++++ src/main.ts | 19 +---- src/utils/proxy.ts | 84 ++++++++++++++++++++ tests/unit/proxy-config.test.ts | 137 ++++++++++++++++++++++++++++++++ 4 files changed, 266 insertions(+), 16 deletions(-) create mode 100644 src/utils/proxy.ts create mode 100644 tests/unit/proxy-config.test.ts diff --git a/README.md b/README.md index 3930830..6897a34 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,48 @@ linearis issues list 1. Go to _Settings_ → _Security & Access_ → _Personal API keys_ 1. Create a new API key +## Proxy Configuration + +Linearis automatically detects and uses HTTP proxy settings from standard environment variables. This is useful in corporate networks, Docker containers, or CI/CD environments that require proxy configuration. + +### Supported Environment Variables + +| Variable | Description | +|----------|-------------| +| `HTTPS_PROXY` | Proxy URL for HTTPS requests (highest priority) | +| `HTTP_PROXY` | Proxy URL for HTTP requests | +| `https_proxy` | Lowercase alternative for HTTPS_PROXY | +| `http_proxy` | Lowercase alternative for HTTP_PROXY | +| `NO_PROXY` | Comma-separated list of hosts to bypass proxy | +| `no_proxy` | Lowercase alternative for NO_PROXY | + +Priority order: `HTTPS_PROXY` > `HTTP_PROXY` > `https_proxy` > `http_proxy` + +### Usage Example + +```bash +# Set proxy and use linearis normally +export HTTPS_PROXY="http://proxy.example.com:8080" +linearis issues list + +# With authentication +export HTTPS_PROXY="http://user:password@proxy.example.com:8080" +linearis issues list + +# Exclude certain hosts from proxy +export NO_PROXY="localhost,127.0.0.1,.internal.company.com" +``` + +### Insecure Proxy Mode + +If your proxy uses a self-signed certificate or intercepts HTTPS traffic, you may need to disable TLS verification for the proxy connection: + +```bash +export LINEARIS_PROXY_INSECURE=1 +``` + +**Warning:** This disables TLS certificate verification for the connection between Linearis and the proxy. Only use this in trusted environments where you understand the security implications. + ## Example rule for your LLM agent ```markdown diff --git a/src/main.ts b/src/main.ts index d498a61..60d8e3c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,22 +14,9 @@ * - Complete API coverage with optimized queries */ -// Proxy support: Node.js native fetch doesn't respect HTTP_PROXY env vars. -// This configures undici's global dispatcher to route through the proxy. -import { ProxyAgent, setGlobalDispatcher } from "undici"; -const proxyUrl = - process.env.HTTPS_PROXY || - process.env.HTTP_PROXY || - process.env.https_proxy || - process.env.http_proxy; -if (proxyUrl) { - setGlobalDispatcher( - new ProxyAgent({ - uri: proxyUrl, - proxyTls: { rejectUnauthorized: false }, - }) - ); -} +// Initialize proxy support before any HTTP requests +import { initializeProxy } from "./utils/proxy.js"; +initializeProxy(); import { program } from "commander"; import pkg from "../package.json" with { type: "json" }; diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts new file mode 100644 index 0000000..05c38c8 --- /dev/null +++ b/src/utils/proxy.ts @@ -0,0 +1,84 @@ +/** + * Proxy configuration utilities for Linearis CLI + * + * Node.js native fetch doesn't respect HTTP_PROXY/HTTPS_PROXY environment + * variables. This module provides utilities to detect and configure proxy + * settings using undici's ProxyAgent. + */ + +import { ProxyAgent, setGlobalDispatcher } from "undici"; + +export interface ProxyConfig { + proxyUrl: string | undefined; + noProxy: string; + insecure: boolean; +} + +/** + * Detect proxy configuration from environment variables. + * + * Priority order: HTTPS_PROXY > HTTP_PROXY > https_proxy > http_proxy + */ +export function detectProxyConfig(): ProxyConfig { + const proxyUrl = + process.env.HTTPS_PROXY || + process.env.HTTP_PROXY || + process.env.https_proxy || + process.env.http_proxy; + + const noProxy = process.env.NO_PROXY || process.env.no_proxy || ""; + + const insecure = + process.env.LINEARIS_PROXY_INSECURE === "1" || + process.env.LINEARIS_PROXY_INSECURE === "true"; + + return { proxyUrl, noProxy, insecure }; +} + +/** + * Configure the global fetch dispatcher to use a proxy. + * + * @param config - Proxy configuration from detectProxyConfig() + * @param warn - Function to output warnings (defaults to console.error) + */ +export function configureProxy( + config: ProxyConfig, + warn: (msg: string) => void = console.error +): void { + if (!config.proxyUrl) { + return; + } + + interface ProxyAgentOptions { + uri: string; + noProxy?: string; + proxyTls?: { rejectUnauthorized: boolean }; + } + + const proxyOptions: ProxyAgentOptions = { + uri: config.proxyUrl, + }; + + if (config.noProxy) { + proxyOptions.noProxy = config.noProxy; + } + + if (config.insecure) { + proxyOptions.proxyTls = { rejectUnauthorized: false }; + warn( + "[linearis] Warning: LINEARIS_PROXY_INSECURE is enabled. " + + "TLS certificate verification for proxy connections is disabled." + ); + } + + setGlobalDispatcher(new ProxyAgent(proxyOptions)); +} + +/** + * Initialize proxy support from environment variables. + * This is the main entry point called at application startup. + */ +export function initializeProxy(): void { + const config = detectProxyConfig(); + configureProxy(config); +} diff --git a/tests/unit/proxy-config.test.ts b/tests/unit/proxy-config.test.ts new file mode 100644 index 0000000..dd4c77d --- /dev/null +++ b/tests/unit/proxy-config.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; +import { detectProxyConfig } from "../../src/utils/proxy.js"; + +/** + * Unit tests for proxy configuration detection + * + * Tests the detectProxyConfig function which reads proxy settings + * from environment variables. + */ + +describe("detectProxyConfig", () => { + const originalEnv = process.env; + + beforeEach(() => { + // Reset environment before each test + process.env = { ...originalEnv }; + delete process.env.HTTPS_PROXY; + delete process.env.HTTP_PROXY; + delete process.env.https_proxy; + delete process.env.http_proxy; + delete process.env.NO_PROXY; + delete process.env.no_proxy; + delete process.env.LINEARIS_PROXY_INSECURE; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe("proxy URL detection", () => { + it("should return undefined when no proxy is configured", () => { + const config = detectProxyConfig(); + expect(config.proxyUrl).toBeUndefined(); + }); + + it("should detect HTTPS_PROXY", () => { + process.env.HTTPS_PROXY = "http://proxy.example.com:8080"; + const config = detectProxyConfig(); + expect(config.proxyUrl).toBe("http://proxy.example.com:8080"); + }); + + it("should detect HTTP_PROXY", () => { + process.env.HTTP_PROXY = "http://proxy.example.com:8080"; + const config = detectProxyConfig(); + expect(config.proxyUrl).toBe("http://proxy.example.com:8080"); + }); + + it("should detect lowercase https_proxy", () => { + process.env.https_proxy = "http://proxy.example.com:8080"; + const config = detectProxyConfig(); + expect(config.proxyUrl).toBe("http://proxy.example.com:8080"); + }); + + it("should detect lowercase http_proxy", () => { + process.env.http_proxy = "http://proxy.example.com:8080"; + const config = detectProxyConfig(); + expect(config.proxyUrl).toBe("http://proxy.example.com:8080"); + }); + + it("should prioritize HTTPS_PROXY over HTTP_PROXY", () => { + process.env.HTTPS_PROXY = "http://https-proxy.example.com:8080"; + process.env.HTTP_PROXY = "http://http-proxy.example.com:8080"; + const config = detectProxyConfig(); + expect(config.proxyUrl).toBe("http://https-proxy.example.com:8080"); + }); + + it("should prioritize uppercase over lowercase", () => { + process.env.HTTPS_PROXY = "http://uppercase.example.com:8080"; + process.env.https_proxy = "http://lowercase.example.com:8080"; + const config = detectProxyConfig(); + expect(config.proxyUrl).toBe("http://uppercase.example.com:8080"); + }); + + it("should handle proxy URL with authentication", () => { + process.env.HTTPS_PROXY = "http://user:pass@proxy.example.com:8080"; + const config = detectProxyConfig(); + expect(config.proxyUrl).toBe("http://user:pass@proxy.example.com:8080"); + }); + }); + + describe("NO_PROXY detection", () => { + it("should return empty string when NO_PROXY is not set", () => { + const config = detectProxyConfig(); + expect(config.noProxy).toBe(""); + }); + + it("should detect NO_PROXY", () => { + process.env.NO_PROXY = "localhost,127.0.0.1,.local"; + const config = detectProxyConfig(); + expect(config.noProxy).toBe("localhost,127.0.0.1,.local"); + }); + + it("should detect lowercase no_proxy", () => { + process.env.no_proxy = "localhost,127.0.0.1"; + const config = detectProxyConfig(); + expect(config.noProxy).toBe("localhost,127.0.0.1"); + }); + + it("should prioritize uppercase NO_PROXY", () => { + process.env.NO_PROXY = "uppercase.local"; + process.env.no_proxy = "lowercase.local"; + const config = detectProxyConfig(); + expect(config.noProxy).toBe("uppercase.local"); + }); + }); + + describe("insecure mode detection", () => { + it("should default to secure mode", () => { + const config = detectProxyConfig(); + expect(config.insecure).toBe(false); + }); + + it("should detect LINEARIS_PROXY_INSECURE=1", () => { + process.env.LINEARIS_PROXY_INSECURE = "1"; + const config = detectProxyConfig(); + expect(config.insecure).toBe(true); + }); + + it("should detect LINEARIS_PROXY_INSECURE=true", () => { + process.env.LINEARIS_PROXY_INSECURE = "true"; + const config = detectProxyConfig(); + expect(config.insecure).toBe(true); + }); + + it("should not enable insecure mode for other values", () => { + process.env.LINEARIS_PROXY_INSECURE = "yes"; + const config = detectProxyConfig(); + expect(config.insecure).toBe(false); + }); + + it("should not enable insecure mode for empty string", () => { + process.env.LINEARIS_PROXY_INSECURE = ""; + const config = detectProxyConfig(); + expect(config.insecure).toBe(false); + }); + }); +});