import { readFileSync } from 'node:fs';
import https from 'node:https';
import { createSecureContext } from 'node:tls';
import * as dotenv from 'dotenv';
import { Agent } from 'undici';
import type { SecureContextOptions } from 'node:tls';
export interface CliTlsOptions {
cert?: string | undefined;
key?: string | undefined;
ca?: string | undefined;
passphrase?: string | undefined;
}
type EnvRecord = Record<string, string | undefined>;
function extractCertificates(pem: string): string | undefined {
const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
return blocks?.join('\n');
}
function extractPrivateKey(pem: string): string | undefined {
return pem.match(/-----BEGIN (?:RSA |EC |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |ENCRYPTED )?PRIVATE KEY-----/)?.[0];
}
function readPemFile(filePath: string, envVar: string): string {
let content: string;
try {
content = readFileSync(filePath, 'utf8');
} catch (err) {
throw new Error(`Unable to read ${envVar} at '${filePath}'`, { cause: err });
}
const trimmed = content.trim();
if (!trimmed.includes('-----BEGIN')) {
throw new Error(`${envVar} at '${filePath}' does not contain a PEM block`);
}
return trimmed;
}
export function loadCliTlsOptions(env: EnvRecord = process.env): CliTlsOptions | undefined {
const certPath = env['NANGO_CLI_TLS_CERT']?.trim();
const keyPath = env['NANGO_CLI_TLS_KEY']?.trim();
const caPath = env['NANGO_CLI_TLS_CA']?.trim();
let cert: string | undefined;
let keyFromCertFile: string | undefined;
let key: string | undefined;
let ca: string | undefined;
if (certPath) {
const content = readPemFile(certPath, 'NANGO_CLI_TLS_CERT');
cert = extractCertificates(content);
keyFromCertFile = extractPrivateKey(content);
if (!cert) {
throw new Error(`NANGO_CLI_TLS_CERT at '${certPath}' does not contain a certificate PEM block`);
}
}
if (keyPath) {
const content = readPemFile(keyPath, 'NANGO_CLI_TLS_KEY');
key = extractPrivateKey(content);
if (!key) {
throw new Error(`NANGO_CLI_TLS_KEY at '${keyPath}' does not contain a private key PEM block`);
}
} else {
key = keyFromCertFile;
}
if (caPath) {
const content = readPemFile(caPath, 'NANGO_CLI_TLS_CA');
ca = extractCertificates(content);
if (!ca) {
throw new Error(`NANGO_CLI_TLS_CA at '${caPath}' does not contain a certificate PEM block`);
}
}
if (Boolean(cert) !== Boolean(key)) {
throw new Error('NANGO_CLI_TLS_CERT and NANGO_CLI_TLS_KEY must be set together (or provide both in the cert file).');
}
if (!cert && !ca) {
return undefined;
}
const passphrase = env['NANGO_CLI_TLS_KEY_PASSPHRASE'];
return {
...(cert ? { cert } : {}),
...(key ? { key } : {}),
...(ca ? { ca } : {}),
...(passphrase ? { passphrase } : {})
};
}
* Both agent types accept a wrong passphrase or a mismatched cert/key pair without complaint and
* only fail on the first request, so force the parse up front.
*/
export function assertUsable(options: CliTlsOptions): void {
try {
createSecureContext(options as SecureContextOptions);
} catch (err) {
throw new Error('NANGO_CLI_TLS_* assets were rejected. Check the cert/key pair and NANGO_CLI_TLS_KEY_PASSPHRASE.', { cause: err });
}
}
let loaded: CliTlsOptions | undefined;
let loadError: Error | undefined;
let initialized = false;
let dispatcher: Agent | undefined;
let httpsAgent: https.Agent | undefined;
function getOptions(): CliTlsOptions | undefined {
if (!initialized) {
dotenv.config();
try {
loaded = loadCliTlsOptions();
if (loaded) {
assertUsable(loaded);
}
} catch (err) {
loadError = err instanceof Error ? err : new Error(String(err));
}
initialized = true;
}
if (loadError) {
throw loadError;
}
return loaded;
}
function getCliDispatcher(): Agent | undefined {
const options = getOptions();
if (!options) {
return undefined;
}
dispatcher ??= new Agent({ connect: options as SecureContextOptions });
return dispatcher;
}
export function withCliTls(init?: RequestInit): RequestInit {
const agent = getCliDispatcher();
if (!agent) {
return init ?? {};
}
return { ...init, dispatcher: agent } as unknown as RequestInit;
}
export function cliFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
return fetch(input, withCliTls(init));
}
export function getCliHttpsAgent(): https.Agent | undefined {
const options = getOptions();
if (!options) {
return undefined;
}
if (!httpsAgent) {
httpsAgent = new https.Agent({ keepAlive: true, ...options });
Object.defineProperty(httpsAgent, 'toJSON', { value: () => '[cliTlsAgent]' });
}
return httpsAgent;
}
export function getCliTlsProps(): { httpsAgent: https.Agent } | Record<string, never> {
const agent = getCliHttpsAgent();
return agent ? { httpsAgent: agent } : {};
}