Ctrl K
ComunidadGuías…

electron/main/utils/envUtil.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

import crypto from 'crypto';
import fs from 'fs';
import os from 'os';
import path from 'path';

export const ENV_START = '# === MCP INTEGRATION ENV START ===';
export const ENV_END = '# === MCP INTEGRATION ENV END ===';

export function restrictEnvFilePermissions(envPath: string) {
  try {
    fs.chmodSync(envPath, 0o600);
  } catch {
    // best-effort on platforms without POSIX perms
  }
}

export function writeEnvFile(envPath: string, content: string) {
  // Preserve existing symlink bindings, but reject dangling links instead of
  // silently replacing them with a new file.
  const targetPath = fs
    .lstatSync(envPath, { throwIfNoEntry: false })
    ?.isSymbolicLink()
    ? fs.realpathSync(envPath)
    : envPath;
  const temporaryPath = path.join(
    path.dirname(targetPath),
    `.env-${crypto.randomUUID()}.tmp`
  );
  let descriptor: number | null = null;
  let created = false;
  try {
    descriptor = fs.openSync(temporaryPath, 'wx', 0o600);
    created = true;
    if (process.platform !== 'win32') {
      // Unlike best-effort loading, writes must fail before storing secrets
      // if the filesystem cannot enforce owner-only access.
      fs.fchmodSync(descriptor, 0o600);
      if ((fs.fstatSync(descriptor).mode & 0o777) !== 0o600) {
        throw new Error('Cannot enforce owner-only env file permissions');
      }
    }
    fs.writeFileSync(descriptor, content, 'utf-8');
    fs.fsyncSync(descriptor);
    fs.closeSync(descriptor);
    descriptor = null;
    // Replace the old inode so existing readers of a formerly public file
    // cannot read newly saved credentials through an already-open handle.
    fs.renameSync(temporaryPath, targetPath);
  } finally {
    try {
      if (descriptor !== null) fs.closeSync(descriptor);
    } finally {
      if (created) fs.rmSync(temporaryPath, { force: true });
    }
  }
}

export function getEnvPath(email: string) {
  const tempEmail = email
    .split('@')[0]
    .replace(/[\\/*?:"<>|\s]/g, '_')
    .replace('.', '_');
  const eigentDir = path.join(os.homedir(), '.eigent');

  // Ensure .eigent directory exists
  if (!fs.existsSync(eigentDir)) {
    fs.mkdirSync(eigentDir, { recursive: true });
  }

  const envPath = path.join(eigentDir, '.env.' + tempEmail);
  const defaultEnv = path.join(process.resourcesPath, 'backend', '.env');
  if (!fs.existsSync(envPath) && fs.existsSync(defaultEnv)) {
    writeEnvFile(envPath, fs.readFileSync(defaultEnv, 'utf-8'));
  }
  if (fs.existsSync(envPath)) {
    restrictEnvFilePermissions(envPath);
  }

  return envPath;
}

export function parseEnvBlock(content: string) {
  const lines = content.split(/\r?\n/);
  let start = lines.findIndex((l) => l.trim() === ENV_START);
  let end = lines.findIndex((l) => l.trim() === ENV_END);
  if (start === -1) start = lines.length;
  if (end === -1) end = lines.length;
  return { lines, start, end };
}

export function updateEnvBlock(lines: string[], kv: Record<string, string>) {
  //  Extract block
  let start = lines.findIndex((l) => l.trim() === ENV_START);
  let end = lines.findIndex((l) => l.trim() === ENV_END);
  if (start === -1 || end === -1 || end < start) {
    //  No block, append
    lines.push(ENV_START);
    Object.entries(kv).forEach(([k, v]) => {
      lines.push(`${k}=${v}`);
    });
    lines.push(ENV_END);
    return lines;
  }
  //  Parse block content
  const block = lines.slice(start + 1, end);
  const map: Record<string, string> = {};
  block.forEach((line) => {
    const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
    if (m) map[m[1]] = m[2];
  });
  //  Overwrite/add
  Object.entries(kv).forEach(([k, v]) => {
    map[k] = v;
  });
  //  Reassemble block
  const newBlock = Object.entries(map).map(([k, v]) => `${k}=${v}`);
  return [...lines.slice(0, start + 1), ...newBlock, ...lines.slice(end)];
}

export function removeEnvKey(lines: string[], key: string) {
  let start = lines.findIndex((l) => l.trim() === ENV_START);
  let end = lines.findIndex((l) => l.trim() === ENV_END);
  if (start === -1 || end === -1 || end < start) return lines;
  const block = lines.slice(start + 1, end);
  const newBlock = block.filter((line) => !line.startsWith(key + '='));
  return [...lines.slice(0, start + 1), ...newBlock, ...lines.slice(end)];
}

/**
 * Read the value of a key from the global ~/.eigent/.env file.
 */
export function readGlobalEnvKey(key: string): string | null {
  try {
    const globalEnvPath = path.join(os.homedir(), '.eigent', '.env');
    if (!fs.existsSync(globalEnvPath)) return null;
    const content = fs.readFileSync(globalEnvPath, 'utf-8');
    const prefix = key + '=';
    for (const line of content.split(/\r?\n/)) {
      if (line.startsWith(prefix)) {
        let value = line.slice(prefix.length).trim();
        // Strip surrounding quotes (single or double)
        if (
          (value.startsWith('"') && value.endsWith('"')) ||
          (value.startsWith("'") && value.endsWith("'"))
        ) {
          value = value.slice(1, -1);
        }
        return value;
      }
    }
  } catch {
    // ignore read errors
  }
  return null;
}

/**
 * Mask credentials in a proxy URL for safe logging.
 * e.g. "http://user:pass@host:port" → "http://***:***@host:port"
 */
export function maskProxyUrl(url: string): string {
  try {
    const parsed = new URL(url);
    if (parsed.username || parsed.password) {
      parsed.username = '***';
      parsed.password = '***';
      return parsed.toString();
    }
  } catch {
    // not a valid URL, return as-is
  }
  return url;
}

export function getEmailFolderPath(email: string) {
  const tempEmail = email
    .split('@')[0]
    .replace(/[\\/*?:"<>|\s]/g, '_')
    .replace('.', '_');
  const MCP_CONFIG_DIR = path.join(os.homedir(), '.eigent');
  const MCP_REMOTE_CONFIG_DIR = path.join(MCP_CONFIG_DIR, tempEmail);
  if (!fs.existsSync(MCP_REMOTE_CONFIG_DIR)) {
    fs.mkdirSync(MCP_REMOTE_CONFIG_DIR, { recursive: true });
  }
  const mcpRemoteDir = path.join(MCP_REMOTE_CONFIG_DIR, 'mcp-remote-0.1.22');
  let hasToken = false;
  try {
    const tokenFile = fs
      .readdirSync(mcpRemoteDir)
      .find((file) => file.includes('token'));
    if (tokenFile) {
      console.log('tokenFile', tokenFile);
      hasToken = true;
    } else {
      hasToken = false;
    }
  } catch (_error) {
    hasToken = false;
  }

  return { MCP_REMOTE_CONFIG_DIR, MCP_CONFIG_DIR, tempEmail, hasToken };
}