Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .simple-release.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { execSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";

import { NpmProject } from "@simple-release/npm";
Expand All @@ -21,6 +22,7 @@ class ArchgateProject extends NpmProject {

if (changed) {
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
execSync("bun install", { stdio: "inherit" });
}
}

Expand Down
10 changes: 3 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
clearCredentials,
} from "../helpers/auth";
import { logError, logInfo } from "../helpers/log";
import { isTlsError, tlsHintMessage } from "../helpers/tls";

export function registerLoginCommand(program: Command) {
const login = program
Expand All @@ -32,6 +33,10 @@ export function registerLoginCommand(program: Command) {

await runDeviceFlow();
} catch (err) {
if (isTlsError(err)) {
logError(tlsHintMessage());
process.exit(1);
}
logError(err instanceof Error ? err.message : String(err));
process.exit(1);
}
Expand Down Expand Up @@ -67,6 +72,10 @@ export function registerLoginCommand(program: Command) {
await clearCredentials();
await runDeviceFlow();
} catch (err) {
if (isTlsError(err)) {
logError(tlsHintMessage());
process.exit(1);
}
logError(err instanceof Error ? err.message : String(err));
process.exit(1);
}
Expand Down
40 changes: 40 additions & 0 deletions src/helpers/tls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* tls.ts — Detect TLS/SSL interception errors common in corporate environments
* and provide actionable guidance to the user.
*/

const TLS_ERROR_PATTERNS = [
"self signed certificate",
"unable to get local issuer certificate",
"certificate has expired",
"unable to verify the first certificate",
"CERT_HAS_EXPIRED",
"DEPTH_ZERO_SELF_SIGNED_CERT",
"SELF_SIGNED_CERT_IN_CHAIN",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
];

/**
* Returns true when the error looks like a TLS certificate verification
* failure — typically caused by a corporate proxy performing SSL inspection.
*/
export function isTlsError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
return TLS_ERROR_PATTERNS.some((pattern) => message.includes(pattern));
}

/**
* Human-readable hint explaining the TLS failure and how to fix it.
*/
export function tlsHintMessage(): string {
return [
"TLS certificate verification failed.",
"This typically happens behind a corporate proxy that performs SSL inspection.",
"",
"To fix this, set the NODE_EXTRA_CA_CERTS environment variable to your corporate CA certificate:",
"",
' export NODE_EXTRA_CA_CERTS="/path/to/corporate-ca.pem"',
"",
"Then retry the command.",
].join("\n");
}
50 changes: 50 additions & 0 deletions tests/helpers/tls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test";

import { isTlsError, tlsHintMessage } from "../../src/helpers/tls";

describe("isTlsError", () => {
test("detects 'self signed certificate in certificate chain'", () => {
const err = new Error("self signed certificate in certificate chain");
expect(isTlsError(err)).toBe(true);
});

test("detects 'self signed certificate' (without chain suffix)", () => {
expect(isTlsError(new Error("self signed certificate"))).toBe(true);
});

test("detects SELF_SIGNED_CERT_IN_CHAIN code", () => {
expect(isTlsError(new Error("SELF_SIGNED_CERT_IN_CHAIN"))).toBe(true);
});

test("detects 'unable to verify the first certificate'", () => {
expect(
isTlsError(new Error("unable to verify the first certificate"))
).toBe(true);
});

test("detects UNABLE_TO_GET_ISSUER_CERT_LOCALLY code", () => {
expect(isTlsError(new Error("UNABLE_TO_GET_ISSUER_CERT_LOCALLY"))).toBe(
true
);
});

test("returns false for unrelated errors", () => {
expect(isTlsError(new Error("fetch failed"))).toBe(false);
expect(isTlsError(new Error("ECONNREFUSED"))).toBe(false);
});

test("handles non-Error values", () => {
expect(isTlsError("self signed certificate")).toBe(true);
expect(isTlsError(42)).toBe(false);
});
});

describe("tlsHintMessage", () => {
test("mentions NODE_EXTRA_CA_CERTS", () => {
expect(tlsHintMessage()).toContain("NODE_EXTRA_CA_CERTS");
});

test("mentions corporate proxy", () => {
expect(tlsHintMessage()).toContain("corporate proxy");
});
});
Loading