Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
84 changes: 84 additions & 0 deletions src/utils/proxy.ts
Original file line number Diff line number Diff line change
@@ -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);
}
137 changes: 137 additions & 0 deletions tests/unit/proxy-config.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});