@aikidosec/safe-chain 0.0.4-connect-timeout-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +257 -0
  3. package/bin/aikido-bun.js +14 -0
  4. package/bin/aikido-bunx.js +14 -0
  5. package/bin/aikido-npm.js +14 -0
  6. package/bin/aikido-npx.js +14 -0
  7. package/bin/aikido-pip.js +20 -0
  8. package/bin/aikido-pip3.js +21 -0
  9. package/bin/aikido-pnpm.js +14 -0
  10. package/bin/aikido-pnpx.js +14 -0
  11. package/bin/aikido-python.js +30 -0
  12. package/bin/aikido-python3.js +30 -0
  13. package/bin/aikido-uv.js +16 -0
  14. package/bin/aikido-yarn.js +14 -0
  15. package/bin/safe-chain.js +190 -0
  16. package/docs/banner.svg +151 -0
  17. package/docs/npm-to-binary-migration.md +89 -0
  18. package/docs/safe-package-manager-demo.gif +0 -0
  19. package/docs/safe-package-manager-demo.png +0 -0
  20. package/docs/shell-integration.md +149 -0
  21. package/package.json +68 -0
  22. package/src/api/aikido.js +54 -0
  23. package/src/api/npmApi.js +71 -0
  24. package/src/config/cliArguments.js +138 -0
  25. package/src/config/configFile.js +192 -0
  26. package/src/config/environmentVariables.js +7 -0
  27. package/src/config/settings.js +100 -0
  28. package/src/environment/environment.js +14 -0
  29. package/src/environment/userInteraction.js +122 -0
  30. package/src/main.js +104 -0
  31. package/src/packagemanager/_shared/matchesCommand.js +18 -0
  32. package/src/packagemanager/bun/createBunPackageManager.js +53 -0
  33. package/src/packagemanager/currentPackageManager.js +72 -0
  34. package/src/packagemanager/npm/createPackageManager.js +72 -0
  35. package/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +74 -0
  36. package/src/packagemanager/npm/dependencyScanner/nullScanner.js +9 -0
  37. package/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +144 -0
  38. package/src/packagemanager/npm/runNpmCommand.js +25 -0
  39. package/src/packagemanager/npm/utils/abbrevs-generated.js +359 -0
  40. package/src/packagemanager/npm/utils/cmd-list.js +174 -0
  41. package/src/packagemanager/npm/utils/npmCommands.js +34 -0
  42. package/src/packagemanager/npx/createPackageManager.js +15 -0
  43. package/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +43 -0
  44. package/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +130 -0
  45. package/src/packagemanager/npx/runNpxCommand.js +25 -0
  46. package/src/packagemanager/pip/createPackageManager.js +21 -0
  47. package/src/packagemanager/pip/pipSettings.js +30 -0
  48. package/src/packagemanager/pip/runPipCommand.js +175 -0
  49. package/src/packagemanager/pnpm/createPackageManager.js +57 -0
  50. package/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +35 -0
  51. package/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +109 -0
  52. package/src/packagemanager/pnpm/runPnpmCommand.js +36 -0
  53. package/src/packagemanager/uv/createUvPackageManager.js +18 -0
  54. package/src/packagemanager/uv/runUvCommand.js +71 -0
  55. package/src/packagemanager/yarn/createPackageManager.js +41 -0
  56. package/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +35 -0
  57. package/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +128 -0
  58. package/src/packagemanager/yarn/runYarnCommand.js +41 -0
  59. package/src/registryProxy/certBundle.js +95 -0
  60. package/src/registryProxy/certUtils.js +128 -0
  61. package/src/registryProxy/http-utils.js +17 -0
  62. package/src/registryProxy/interceptors/createInterceptorForEcoSystem.js +25 -0
  63. package/src/registryProxy/interceptors/interceptorBuilder.js +140 -0
  64. package/src/registryProxy/interceptors/npm/modifyNpmInfo.js +177 -0
  65. package/src/registryProxy/interceptors/npm/npmInterceptor.js +47 -0
  66. package/src/registryProxy/interceptors/npm/parseNpmPackageUrl.js +43 -0
  67. package/src/registryProxy/interceptors/pipInterceptor.js +115 -0
  68. package/src/registryProxy/mitmRequestHandler.js +231 -0
  69. package/src/registryProxy/plainHttpProxy.js +95 -0
  70. package/src/registryProxy/registryProxy.js +184 -0
  71. package/src/registryProxy/tunnelRequestHandler.js +180 -0
  72. package/src/scanning/audit/index.js +129 -0
  73. package/src/scanning/index.js +82 -0
  74. package/src/scanning/malwareDatabase.js +131 -0
  75. package/src/shell-integration/helpers.js +213 -0
  76. package/src/shell-integration/path-wrappers/templates/unix-wrapper.template.sh +22 -0
  77. package/src/shell-integration/path-wrappers/templates/windows-wrapper.template.cmd +24 -0
  78. package/src/shell-integration/setup-ci.js +170 -0
  79. package/src/shell-integration/setup.js +127 -0
  80. package/src/shell-integration/shellDetection.js +37 -0
  81. package/src/shell-integration/startup-scripts/include-python/init-fish.fish +94 -0
  82. package/src/shell-integration/startup-scripts/include-python/init-posix.sh +81 -0
  83. package/src/shell-integration/startup-scripts/include-python/init-pwsh.ps1 +115 -0
  84. package/src/shell-integration/startup-scripts/init-fish.fish +71 -0
  85. package/src/shell-integration/startup-scripts/init-posix.sh +58 -0
  86. package/src/shell-integration/startup-scripts/init-pwsh.ps1 +92 -0
  87. package/src/shell-integration/supported-shells/bash.js +134 -0
  88. package/src/shell-integration/supported-shells/fish.js +77 -0
  89. package/src/shell-integration/supported-shells/powershell.js +73 -0
  90. package/src/shell-integration/supported-shells/windowsPowershell.js +73 -0
  91. package/src/shell-integration/supported-shells/zsh.js +74 -0
  92. package/src/shell-integration/teardown.js +64 -0
  93. package/src/utils/safeSpawn.js +137 -0
  94. package/tsconfig.json +21 -0
@@ -0,0 +1,71 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
4
+ import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
5
+
6
+ /**
7
+ * Sets CA bundle environment variables used by Python libraries and uv.
8
+ *
9
+ * @param {NodeJS.ProcessEnv} env - Env object
10
+ * @param {string} combinedCaPath - Path to the combined CA bundle
11
+ */
12
+ function setUvCaBundleEnvironmentVariables(env, combinedCaPath) {
13
+ // SSL_CERT_FILE: Used by Python SSL libraries and underlying HTTP clients
14
+ if (env.SSL_CERT_FILE) {
15
+ ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
16
+ }
17
+ env.SSL_CERT_FILE = combinedCaPath;
18
+
19
+ // REQUESTS_CA_BUNDLE: Used by the requests library (which uv may use internally)
20
+ if (env.REQUESTS_CA_BUNDLE) {
21
+ ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
22
+ }
23
+ env.REQUESTS_CA_BUNDLE = combinedCaPath;
24
+
25
+ // PIP_CERT: Some underlying pip operations may respect this
26
+ if (env.PIP_CERT) {
27
+ ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
28
+ }
29
+ env.PIP_CERT = combinedCaPath;
30
+ }
31
+
32
+ /**
33
+ * Runs a uv command with safe-chain's certificate bundle and proxy configuration.
34
+ *
35
+ * uv respects standard environment variables for proxy and TLS configuration:
36
+ * - HTTP_PROXY / HTTPS_PROXY: Proxy settings
37
+ * - SSL_CERT_FILE / REQUESTS_CA_BUNDLE: CA bundle for TLS verification
38
+ *
39
+ * Unlike pip (which requires a temporary config file for cert configuration), uv directly
40
+ * honors environment variables, so no config/ini file is needed.
41
+ *
42
+ * @param {string} command - The uv command to execute (typically 'uv')
43
+ * @param {string[]} args - Command line arguments to pass to uv
44
+ * @returns {Promise<{status: number}>} Exit status of the uv command
45
+ */
46
+ export async function runUv(command, args) {
47
+ try {
48
+ const env = mergeSafeChainProxyEnvironmentVariables(process.env);
49
+
50
+ const combinedCaPath = getCombinedCaBundlePath();
51
+ setUvCaBundleEnvironmentVariables(env, combinedCaPath);
52
+
53
+ // Note: uv uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
54
+ // These are already set by mergeSafeChainProxyEnvironmentVariables
55
+
56
+ const result = await safeSpawn(command, args, {
57
+ stdio: "inherit",
58
+ env,
59
+ });
60
+
61
+ return { status: result.status };
62
+ } catch (/** @type any */ error) {
63
+ if (error.status) {
64
+ return { status: error.status };
65
+ } else {
66
+ ui.writeError(`Error executing command: ${error.message}`);
67
+ ui.writeError(`Is '${command}' installed and available on your system?`);
68
+ return { status: 1 };
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,41 @@
1
+ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
2
+ import { runYarnCommand } from "./runYarnCommand.js";
3
+
4
+ const scanner = commandArgumentScanner();
5
+
6
+ /**
7
+ * @returns {import("../currentPackageManager.js").PackageManager}
8
+ */
9
+ export function createYarnPackageManager() {
10
+ return {
11
+ runCommand: runYarnCommand,
12
+ isSupportedCommand: (args) =>
13
+ matchesCommand(args, "add") ||
14
+ matchesCommand(args, "global", "add") ||
15
+ matchesCommand(args, "install") ||
16
+ matchesCommand(args, "up") ||
17
+ matchesCommand(args, "upgrade") ||
18
+ matchesCommand(args, "global", "upgrade") ||
19
+ matchesCommand(args, "dlx"),
20
+ getDependencyUpdatesForCommand: (args) => scanner.scan(args),
21
+ };
22
+ }
23
+
24
+ /**
25
+ * @param {string[]} args
26
+ * @param {...string} commandArgs
27
+ * @returns {boolean}
28
+ */
29
+ function matchesCommand(args, ...commandArgs) {
30
+ if (args.length < commandArgs.length) {
31
+ return false;
32
+ }
33
+
34
+ for (var i = 0; i < commandArgs.length; i++) {
35
+ if (args[i].toLowerCase() !== commandArgs[i].toLowerCase()) {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ return true;
41
+ }
@@ -0,0 +1,35 @@
1
+ import { resolvePackageVersion } from "../../../api/npmApi.js";
2
+ import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
3
+
4
+ /**
5
+ * @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
6
+ */
7
+ export function commandArgumentScanner() {
8
+ return {
9
+ scan: (args) => scanDependencies(args),
10
+ shouldScan: () => true, // There's no dry run for yarn, so we always scan
11
+ };
12
+ }
13
+
14
+ /**
15
+ * @param {string[]} args
16
+ * @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
17
+ */
18
+ async function scanDependencies(args) {
19
+ const changes = [];
20
+ const packageUpdates = parsePackagesFromArguments(args);
21
+
22
+ for (const packageUpdate of packageUpdates) {
23
+ var exactVersion = await resolvePackageVersion(
24
+ packageUpdate.name,
25
+ packageUpdate.version
26
+ );
27
+ if (exactVersion) {
28
+ packageUpdate.version = exactVersion;
29
+ }
30
+
31
+ changes.push({ ...packageUpdate, type: "add" });
32
+ }
33
+
34
+ return changes;
35
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @param {string[]} args
3
+ * @returns {{name: string, version: string}[]}
4
+ */
5
+ export function parsePackagesFromArguments(args) {
6
+ const changes = [];
7
+ let defaultTag = "latest";
8
+
9
+ for (let i = 1; i < args.length; i++) {
10
+ const arg = args[i];
11
+ const option = getOption(arg);
12
+
13
+ if (option) {
14
+ // If the option has a parameter, skip the next argument as well
15
+ i += option.numberOfParameters;
16
+
17
+ continue;
18
+ }
19
+
20
+ const packageDetails = parsePackagename(arg, defaultTag);
21
+ if (packageDetails) {
22
+ changes.push(packageDetails);
23
+ }
24
+ }
25
+
26
+ return changes;
27
+ }
28
+
29
+ /**
30
+ * @param {string} arg
31
+ *
32
+ * @returns {{name: string, numberOfParameters: number} | undefined}
33
+ */
34
+ function getOption(arg) {
35
+ if (isOptionWithParameter(arg)) {
36
+ return {
37
+ name: arg,
38
+ numberOfParameters: 1,
39
+ };
40
+ }
41
+
42
+ // Arguments starting with "-" or "--" are considered options
43
+ // except for "--package=" which contains the package name
44
+ if (arg.startsWith("-")) {
45
+ return {
46
+ name: arg,
47
+ numberOfParameters: 0,
48
+ };
49
+ }
50
+
51
+ return undefined;
52
+ }
53
+
54
+ /**
55
+ * @param {string} arg
56
+ *
57
+ * @returns {boolean}
58
+ */
59
+ function isOptionWithParameter(arg) {
60
+ const optionsWithParameters = [
61
+ "--use-yarnrc",
62
+ "--link-folder",
63
+ "--global-folder",
64
+ "--modules-folder",
65
+ "--preferred-cache-folder",
66
+ "--cache-folder",
67
+ "--mutex",
68
+ "--cwd",
69
+ "--proxy",
70
+ "--https-proxy",
71
+ "--registry",
72
+ "--network-concurrency",
73
+ "--network-timeout",
74
+ "--scripts-prepend-node-path",
75
+ "--otp",
76
+ ];
77
+
78
+ return optionsWithParameters.includes(arg);
79
+ }
80
+
81
+ /**
82
+ * @param {string} arg
83
+ * @param {string} defaultTag
84
+ *
85
+ * @returns {{name: string, version: string}}
86
+ */
87
+ function parsePackagename(arg, defaultTag) {
88
+ // format can be --package=name@version
89
+ // in that case, we need to remove the --package= part
90
+ if (arg.startsWith("--package=")) {
91
+ arg = arg.slice(10);
92
+ }
93
+
94
+ arg = removeAlias(arg);
95
+
96
+ // Split at the last "@" to separate the package name and version
97
+ const lastAtIndex = arg.lastIndexOf("@");
98
+
99
+ let name, version;
100
+ // The index of the last "@" should be greater than 0
101
+ // If the index is 0, it means the package name starts with "@" (eg: "@vercel/otel")
102
+ if (lastAtIndex > 0) {
103
+ name = arg.slice(0, lastAtIndex);
104
+ version = arg.slice(lastAtIndex + 1);
105
+ } else {
106
+ name = arg;
107
+ version = defaultTag; // No tag specified (eg: "http-server"), use the default tag
108
+ }
109
+
110
+ return {
111
+ name,
112
+ version,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * @param {string} arg
118
+ * @returns {string}
119
+ */
120
+ function removeAlias(arg) {
121
+ // removes the alias.
122
+ // Eg.: server@npm:http-server@latest becomes http-server@latest
123
+ const aliasIndex = arg.indexOf("@npm:");
124
+ if (aliasIndex !== -1) {
125
+ return arg.slice(aliasIndex + 5);
126
+ }
127
+ return arg;
128
+ }
@@ -0,0 +1,41 @@
1
+ import { ui } from "../../environment/userInteraction.js";
2
+ import { safeSpawn } from "../../utils/safeSpawn.js";
3
+ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
4
+
5
+ /**
6
+ * @param {string[]} args
7
+ *
8
+ * @returns {Promise<{status: number}>}
9
+ */
10
+ export async function runYarnCommand(args) {
11
+ try {
12
+ const env = mergeSafeChainProxyEnvironmentVariables(process.env);
13
+ await fixYarnProxyEnvironmentVariables(env);
14
+
15
+ const result = await safeSpawn("yarn", args, {
16
+ stdio: "inherit",
17
+ env,
18
+ });
19
+ return { status: result.status };
20
+ } catch (/** @type any */ error) {
21
+ if (error.status) {
22
+ return { status: error.status };
23
+ } else {
24
+ ui.writeError("Error executing command:", error.message);
25
+ return { status: 1 };
26
+ }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * @param {Record<string, string>} env
32
+ *
33
+ * @returns {Promise<void>}
34
+ */
35
+ async function fixYarnProxyEnvironmentVariables(env) {
36
+ // Yarn ignores standard proxy environment variable HTTPS_PROXY
37
+ // It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though.
38
+ // Don't use YARN_HTTPS_CA_FILE_PATH or YARN_CA_FILE_PATH though, it causes yarn to ignore all system CAs
39
+
40
+ env.YARN_HTTPS_PROXY = env.HTTPS_PROXY;
41
+ }
@@ -0,0 +1,95 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ // @ts-ignore - certifi has no type definitions
5
+ import certifi from "certifi";
6
+ import tls from "node:tls";
7
+ import { X509Certificate } from "node:crypto";
8
+ import { getCaCertPath } from "./certUtils.js";
9
+
10
+ /**
11
+ * Check if a PEM string contains only parsable cert blocks.
12
+ * @param {string} pem - PEM-encoded certificate string
13
+ * @returns {boolean}
14
+ */
15
+ function isParsable(pem) {
16
+ if (!pem || typeof pem !== "string") return false;
17
+ const begin = "-----BEGIN CERTIFICATE-----";
18
+ const end = "-----END CERTIFICATE-----";
19
+ const blocks = [];
20
+
21
+ let idx = 0;
22
+ while (idx < pem.length) {
23
+ const start = pem.indexOf(begin, idx);
24
+ if (start === -1) break;
25
+ const stop = pem.indexOf(end, start + begin.length);
26
+ if (stop === -1) break;
27
+ const blockEnd = stop + end.length;
28
+ blocks.push(pem.slice(start, blockEnd));
29
+ idx = blockEnd;
30
+ }
31
+
32
+ if (blocks.length === 0) return false;
33
+ try {
34
+ for (const b of blocks) {
35
+ // throw if invalid
36
+ new X509Certificate(b);
37
+ }
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /** @type {string | null} */
45
+ let cachedPath = null;
46
+
47
+ /**
48
+ * Build a combined CA bundle for Python and Node HTTPS flows.
49
+ * - Includes Safe Chain CA (for MITM of known registries)
50
+ * - Includes Mozilla roots via npm `certifi` (public HTTPS)
51
+ * - Includes Node's built-in root certificates as a portable fallback
52
+ * @returns {string} Path to the combined CA bundle PEM file
53
+ */
54
+ export function getCombinedCaBundlePath() {
55
+ if (cachedPath && fs.existsSync(cachedPath)) return cachedPath;
56
+
57
+ // Concatenate PEM files
58
+ const parts = [];
59
+
60
+ // 1) Safe Chain CA (for MITM'd registries)
61
+ const safeChainPath = getCaCertPath();
62
+ try {
63
+ const safeChainPem = fs.readFileSync(safeChainPath, "utf8");
64
+ if (isParsable(safeChainPem)) parts.push(safeChainPem.trim());
65
+ } catch {
66
+ // Ignore if Safe Chain CA is not available
67
+ }
68
+
69
+ // 2) certifi (Mozilla CA bundle for all public HTTPS)
70
+ try {
71
+ const certifiPem = fs.readFileSync(certifi, "utf8");
72
+ if (isParsable(certifiPem)) parts.push(certifiPem.trim());
73
+ } catch {
74
+ // Ignore if certifi bundle is not available
75
+ }
76
+
77
+ // 3) Node's built-in root certificates
78
+ try {
79
+ const nodeRoots = tls.rootCertificates;
80
+ if (Array.isArray(nodeRoots) && nodeRoots.length) {
81
+ for (const rootPem of nodeRoots) {
82
+ if (typeof rootPem !== "string") continue;
83
+ if (isParsable(rootPem)) parts.push(rootPem.trim());
84
+ }
85
+ }
86
+ } catch {
87
+ // Ignore if unavailable
88
+ }
89
+
90
+ const combined = parts.filter(Boolean).join("\n");
91
+ const target = path.join(os.tmpdir(), "safe-chain-ca-bundle.pem");
92
+ fs.writeFileSync(target, combined, { encoding: "utf8" });
93
+ cachedPath = target;
94
+ return cachedPath;
95
+ }
@@ -0,0 +1,128 @@
1
+ import forge from "node-forge";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import os from "os";
5
+
6
+ const certFolder = path.join(os.homedir(), ".safe-chain", "certs");
7
+ const ca = loadCa();
8
+
9
+ const certCache = new Map();
10
+
11
+ export function getCaCertPath() {
12
+ return path.join(certFolder, "ca-cert.pem");
13
+ }
14
+
15
+ /**
16
+ * @param {string} hostname
17
+ * @returns {{privateKey: string, certificate: string}}
18
+ */
19
+ export function generateCertForHost(hostname) {
20
+ let existingCert = certCache.get(hostname);
21
+ if (existingCert) {
22
+ return existingCert;
23
+ }
24
+
25
+ const keys = forge.pki.rsa.generateKeyPair(2048);
26
+ const cert = forge.pki.createCertificate();
27
+ cert.publicKey = keys.publicKey;
28
+ cert.serialNumber = "01";
29
+ cert.validity.notBefore = new Date();
30
+ cert.validity.notAfter = new Date();
31
+ cert.validity.notAfter.setHours(cert.validity.notBefore.getHours() + 1);
32
+
33
+ const attrs = [{ name: "commonName", value: hostname }];
34
+ cert.setSubject(attrs);
35
+ cert.setIssuer(ca.certificate.subject.attributes);
36
+ cert.setExtensions([
37
+ {
38
+ name: "subjectAltName",
39
+ altNames: [
40
+ {
41
+ type: 2, // DNS
42
+ value: hostname,
43
+ },
44
+ ],
45
+ },
46
+ {
47
+ name: "keyUsage",
48
+ digitalSignature: true,
49
+ keyEncipherment: true,
50
+ },
51
+ {
52
+ /*
53
+ extKeyUsage serverAuth is required for TLS server authentication.
54
+ This is especially important for Python venv environments, which use their own
55
+ certificate validation logic and will reject certificates lacking the serverAuth EKU.
56
+ Adding serverAuth does not impact other usages
57
+ */
58
+ name: "extKeyUsage",
59
+ serverAuth: true,
60
+ },
61
+ ]);
62
+ cert.sign(ca.privateKey, forge.md.sha256.create());
63
+
64
+ const result = {
65
+ privateKey: forge.pki.privateKeyToPem(keys.privateKey),
66
+ certificate: forge.pki.certificateToPem(cert),
67
+ };
68
+
69
+ certCache.set(hostname, result);
70
+
71
+ return result;
72
+ }
73
+
74
+ function loadCa() {
75
+ const keyPath = path.join(certFolder, "ca-key.pem");
76
+ const certPath = path.join(certFolder, "ca-cert.pem");
77
+
78
+ if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
79
+ const privateKeyPem = fs.readFileSync(keyPath, "utf8");
80
+ const certPem = fs.readFileSync(certPath, "utf8");
81
+ const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
82
+ const certificate = forge.pki.certificateFromPem(certPem);
83
+
84
+ // Don't return a cert that is valid for less than 1 hour
85
+ const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000);
86
+ if (certificate.validity.notAfter > oneHourFromNow) {
87
+ return { privateKey, certificate };
88
+ }
89
+ }
90
+
91
+ const { privateKey, certificate } = generateCa();
92
+ fs.mkdirSync(certFolder, { recursive: true });
93
+ fs.writeFileSync(keyPath, forge.pki.privateKeyToPem(privateKey));
94
+ fs.writeFileSync(certPath, forge.pki.certificateToPem(certificate));
95
+ return { privateKey, certificate };
96
+ }
97
+
98
+ function generateCa() {
99
+ const keys = forge.pki.rsa.generateKeyPair(2048);
100
+ const cert = forge.pki.createCertificate();
101
+ cert.publicKey = keys.publicKey;
102
+ cert.serialNumber = "01";
103
+ cert.validity.notBefore = new Date();
104
+ cert.validity.notAfter = new Date();
105
+ cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + 1);
106
+
107
+ const attrs = [{ name: "commonName", value: "safe-chain proxy" }];
108
+ cert.setSubject(attrs);
109
+ cert.setIssuer(attrs);
110
+ cert.setExtensions([
111
+ {
112
+ name: "basicConstraints",
113
+ cA: true,
114
+ },
115
+ {
116
+ name: "keyUsage",
117
+ keyCertSign: true,
118
+ digitalSignature: true,
119
+ keyEncipherment: true,
120
+ },
121
+ ]);
122
+ cert.sign(keys.privateKey, forge.md.sha256.create());
123
+
124
+ return {
125
+ privateKey: keys.privateKey,
126
+ certificate: cert,
127
+ };
128
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @param {NodeJS.Dict<string | string[]> | undefined} headers
3
+ * @param {string} headerName
4
+ */
5
+ export function getHeaderValueAsString(headers, headerName) {
6
+ if (!headers) {
7
+ return undefined;
8
+ }
9
+
10
+ let header = headers[headerName];
11
+
12
+ if (Array.isArray(header)) {
13
+ return header.join(", ");
14
+ }
15
+
16
+ return header;
17
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ ECOSYSTEM_JS,
3
+ ECOSYSTEM_PY,
4
+ getEcoSystem,
5
+ } from "../../config/settings.js";
6
+ import { npmInterceptorForUrl } from "./npm/npmInterceptor.js";
7
+ import { pipInterceptorForUrl } from "./pipInterceptor.js";
8
+
9
+ /**
10
+ * @param {string} url
11
+ * @returns {import("./interceptorBuilder.js").Interceptor | undefined}
12
+ */
13
+ export function createInterceptorForUrl(url) {
14
+ const ecosystem = getEcoSystem();
15
+
16
+ if (ecosystem === ECOSYSTEM_JS) {
17
+ return npmInterceptorForUrl(url);
18
+ }
19
+
20
+ if (ecosystem === ECOSYSTEM_PY) {
21
+ return pipInterceptorForUrl(url);
22
+ }
23
+
24
+ return undefined;
25
+ }