Automate Pomerium tunnel creation for testing, CI/CD, and automation
connect-pomerium is a lightweight TypeScript/JavaScript library that simplifies the creation and management of Pomerium tunnels. It's designed specifically for automation scenarios where you need programmatic access to Pomerium-protected services without manual browser interaction.
- Node.js 20.0.0 or higher
- Pomerium CLI v0.29.0 or later (Download)
Note: This library requires pomerium-cli v0.29.0+ which uses JSON logging. If you're using an older CLI version, please use
connect-pomerium@1.xinstead.
- π Zero runtime dependencies - Lean and fast
- π Flexible authentication - Manual browser auth or automated (Playwright, Puppeteer, etc.)
- π Auto-reconnect support - Configurable reconnection for long-running processes
- π― TypeScript-first - Full type safety and IntelliSense support
- π₯οΈ Cross-platform - Works on Windows, macOS (Intel & ARM), and Linux
- π¦ Bundled binaries - Pomerium CLI included, no external dependencies
- π¨ Lifecycle hooks - Monitor connection state with callbacks
- β‘ Simple API - Get started with just 3 lines of code
npm install connect-pomeriumimport { PomeriumTunnel } from 'connect-pomerium';
const tunnel = new PomeriumTunnel({
targetHost: 'tcp+https://db.corp.pomerium.io:5432',
listenPort: 5432,
});
await tunnel.start(); // Browser opens for auth
console.log('Connected! Access your service at localhost:5432');
// Your automation code here...
await tunnel.stop();import { chromium } from 'playwright';
import { PomeriumTunnel } from 'connect-pomerium';
const tunnel = new PomeriumTunnel({
targetHost: 'tcp+https://api.corp.pomerium.io:443',
listenPort: 8443,
onAuthRequired: async (authUrl) => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(authUrl);
await page.fill('#username', process.env.USERNAME!);
await page.fill('#password', process.env.PASSWORD!);
await page.click('button[type="submit"]');
await browser.close();
},
});
await tunnel.start(); // No browser popup, fully automatedinterface PomeriumTunnelConfig {
// Required
targetHost: string; // Pomerium target (e.g., 'tcp+https://host:port')
listenPort: number; // Local port to listen on
// Optional
cliPath?: string; // Custom path to Pomerium CLI binary
onAuthRequired?: (url: string) => Promise<void>; // Auth handler
autoReconnect?: boolean; // Enable auto-reconnect (default: false)
maxReconnectAttempts?: number; // Max reconnection attempts (default: 0 = infinite)
reconnectDelay?: number; // Delay between retries in ms (default: 5000)
connectionTimeout?: number; // Connection timeout in ms (default: 60000)
// Lifecycle hooks
onConnected?: () => void;
onDisconnected?: () => void;
onReconnecting?: (attempt: number) => void;
onReconnectFailed?: () => void;
onError?: (error: Error) => void;
// NEW in v2.0.0
onLog?: (log: PomeriumLogEntry) => void; // Access raw CLI logs
logLevel?: 'debug' | 'info' | 'warn' | 'error'; // CLI log level (default: 'info')
debug?: boolean; // Enable stderr logging (default: false)
}Creates a new tunnel instance with the specified configuration.
Starts the tunnel and waits for connection establishment. If onAuthRequired is provided, the browser will be suppressed and your callback will handle authentication. Otherwise, Pomerium will open the browser for manual login.
Throws:
ConnectionTimeoutError- If connection times outProcessStartError- If the Pomerium process fails to startProcessExitError- If the process exits unexpectedly
Stops the tunnel and cleans up resources.
Returns the current tunnel state.
interface TunnelState {
connected: boolean;
reconnecting: boolean;
reconnectAttempts: number;
}Returns true if the tunnel is currently connected.
The examples/ directory contains complete working examples:
- 01-basic-manual-auth.ts - Simple usage with manual browser authentication
- 02-okta-playwright.ts - Automated Okta login using Playwright
- 03-google-puppeteer.ts - Automated Google SSO using Puppeteer
- 04-ci-cd-auto-reconnect.ts - CI/CD usage with auto-reconnect
To run an example:
# Install dependencies for the example
npm install playwright # or puppeteer
# Run the example
npx tsx examples/02-okta-playwright.tsRun integration tests against Pomerium-protected services:
const tunnel = new PomeriumTunnel({
targetHost: 'tcp+https://staging-api.corp.pomerium.io:443',
listenPort: 8443,
autoReconnect: true,
maxReconnectAttempts: 5,
onAuthRequired: async (url) => {
// Use service account credentials
await automateLogin(url);
},
});
await tunnel.start();
await runIntegrationTests();
await tunnel.stop();Access remote databases or services during development:
const tunnel = new PomeriumTunnel({
targetHost: 'tcp+https://prod-db.corp.pomerium.io:5432',
listenPort: 5432,
});
await tunnel.start(); // Manual auth in browser
// Now connect to localhost:5432 with your database clientBuild automation tools that interact with protected services:
const tunnel = new PomeriumTunnel({
targetHost: 'tcp+https://admin-api.corp.pomerium.io:443',
listenPort: 8443,
onAuthRequired: automateAuth,
});
await tunnel.start();
await performAdminTasks();
await tunnel.stop();If you don't provide onAuthRequired, Pomerium will open your default browser for authentication. This is the simplest approach for local development.
For CI/CD and automation, provide an onAuthRequired callback. The library supports any browser automation tool:
npm install playwrightonAuthRequired: async (authUrl) => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(authUrl);
// Your auth logic...
await browser.close();
}npm install puppeteeronAuthRequired: async (authUrl) => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(authUrl);
// Your auth logic...
await browser.close();
}The callback just needs to handle the authentication flow. You can use any tool that can interact with web pages.
By default, auto-reconnect is disabled (autoReconnect: false). This is intentional for automation scenarios where you want explicit control over failures.
To enable auto-reconnect:
const tunnel = new PomeriumTunnel({
targetHost: 'tcp+https://api.corp.pomerium.io:443',
listenPort: 8443,
autoReconnect: true,
maxReconnectAttempts: 3, // 0 = infinite
reconnectDelay: 5000, // 5 seconds between attempts
onReconnecting: (attempt) => {
console.log(`Reconnecting (${attempt}/3)...`);
},
onReconnectFailed: () => {
console.error('Failed to reconnect');
process.exit(1);
},
});Control the verbosity of pomerium-cli logs:
const tunnel = new PomeriumTunnel({
targetHost: 'tcp+https://example.com:443',
listenPort: 8443,
logLevel: 'debug', // Options: 'debug', 'info', 'warn', 'error'
});Get access to structured log entries from pomerium-cli:
const tunnel = new PomeriumTunnel({
targetHost: 'tcp+https://example.com:443',
listenPort: 8443,
onLog: (log) => {
// Log is a structured object with: level, message, component, etc.
console.log(`[${log.level.toUpperCase()}] ${log.message}`);
// Access additional fields
if (log.component) console.log(` Component: ${log.component}`);
if (log.error) console.error(` Error: ${log.error}`);
if (log['auth-url']) console.log(` Auth URL: ${log['auth-url']}`);
},
});You can also parse pomerium-cli logs yourself:
import { parsePomeriumLog, extractAuthUrl, type PomeriumLogEntry } from 'connect-pomerium';
// Parse JSON log line
const logEntry = parsePomeriumLog('{"level":"info","message":"connected"}');
if (logEntry) {
console.log(logEntry.level, logEntry.message);
}
// Extract auth URL from plain text
const url = extractAuthUrl('Your browser has been opened to visit:\n\nhttps://auth.example.com\n');
console.log(url); // "https://auth.example.com"If you see log parsing errors or connection issues:
-
Check your pomerium-cli version:
pomerium-cli --version
-
If < v0.29.0:
- Update to v0.29.0+ from https://github.com/pomerium/cli/releases
- OR use
connect-pomerium@1.x:npm install connect-pomerium@1.0.1
-
If already v0.29.0+ and still having issues, please open an issue
If you see BinaryNotFoundError, the Pomerium CLI binary might not be accessible. You can provide a custom path:
const tunnel = new PomeriumTunnel({
targetHost: '...',
listenPort: 8443,
cliPath: '/custom/path/to/pomerium-cli',
});If connections timeout frequently, increase the timeout:
const tunnel = new PomeriumTunnel({
targetHost: '...',
listenPort: 8443,
connectionTimeout: 120000, // 2 minutes
});If automated authentication fails:
- Check your credentials are correct
- Verify the auth flow by running with manual auth first
- Add error handling in your
onAuthRequiredcallback - Check Pomerium CLI logs (they're output to console by default)
On macOS, if the binary is quarantined by Gatekeeper, the library automatically removes the quarantine attribute. If you still have issues, manually run:
xattr -d com.apple.quarantine bin/pomerium-cli-darwin-*| Platform | Architecture | Binary Included |
|---|---|---|
| Windows | x64 | β |
| macOS | x64 (Intel) | β |
| macOS | arm64 (M1+) | β |
| Linux | x64 | β |
This library is written in TypeScript and provides full type definitions. All configuration options, methods, and errors are fully typed.
import type { PomeriumTunnelConfig, TunnelState } from 'connect-pomerium';The library exports custom error classes for different failure scenarios:
import {
PomeriumError, // Base error class
ConnectionTimeoutError, // Connection establishment timeout
BinaryNotFoundError, // Pomerium CLI binary not found
ProcessExitError, // Process exited unexpectedly
ProcessStartError, // Failed to start process
AuthenticationError, // Authentication callback failed
} from 'connect-pomerium';
try {
await tunnel.start();
} catch (error) {
if (error instanceof ConnectionTimeoutError) {
console.error('Connection timed out');
} else if (error instanceof AuthenticationError) {
console.error('Auth failed');
}
}We keep the library dependency-free so you can choose your own browser automation tool (or none at all). This keeps the package size small (~10MB with binaries) instead of bloated (~200MB with Playwright).
For CI/CD: Usually no. You want explicit failures so you can debug issues. For long-running processes: Yes. Enable it with reasonable retry limits. For local development: Depends on your workflow.
Yes! Create multiple PomeriumTunnel instances with different ports:
const dbTunnel = new PomeriumTunnel({ targetHost: '...', listenPort: 5432 });
const apiTunnel = new PomeriumTunnel({ targetHost: '...', listenPort: 8443 });
await Promise.all([dbTunnel.start(), apiTunnel.start()]);No. This library is specifically for automation scenarios (testing, CI/CD, scripts). For production Pomerium deployments, use the official Pomerium installation methods.
npm run buildnpm testnpm run lint
npm run formatnpm run check:exportsThis package uses automated publishing via GitHub Actions.
- Make your changes and commit them
- Update the version:
npm version patch # for bug fixes npm version minor # for new features npm version major # for breaking changes
- Push the changes and tags:
git push && git push --tags - Package automatically publishes to npm π
MIT Β© oharu121
Contributions are welcome! Please feel free to submit a Pull Request.
If you encounter any issues, please report them at GitHub Issues.
- Issues: GitHub Issues
- Pomerium Docs: pomerium.com/docs