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/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..60d8e3c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,6 +14,10 @@ * - Complete API coverage with optimized queries */ +// 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" }; import { setupCommentsCommands } from "./commands/comments.js"; 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); + }); + }); +});