Ctrl K
ComunidadGuías…

src/services/local-n8n-stack-credentials.js

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

Archivo original ↗Preguntar a KiBot
import { createHash, randomUUID } from "node:crypto";
import * as defaultFileSystem from "node:fs/promises";
import { homedir, platform as hostPlatform } from "node:os";
import { join } from "node:path";

import { validateLocalN8nStackMarker, getLocalN8nStackLabels } from "../domain/local-n8n-stack.js";
import { lockDownLocalPath, runLocalProcess, validateLocalDockerHost } from "../infrastructure/local-process.js";
import { resolveLocalN8nStackInstallRoot, writePrivateFile } from "./local-n8n-stack-installer.js";

const FEATURES = Object.freeze({
  chatgpt: { name: "Relmio ChatGPT plan", url: "http://n8n-openai-oauth:10531/v1" },
  "local-model": { name: "Relmio local model", url: "http://n8n-local-model:11434/v1" },
  supergrok: { name: "Relmio SuperGrok", url: "http://n8n-supergrok:14502/v1" },
  claude: { name: "Relmio Claude", url: "http://relmio-claude:14502/v1" },
});
const ID = /^[a-f0-9]{64}$/u;
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

export function localStackCredentialId(installId, feature) {
  if (!/^[a-f0-9]{32}$/u.test(installId) || !Object.hasOwn(FEATURES, feature)) throw new TypeError("Invalid credential identity.");
  let value = BigInt(`0x${createHash("sha256").update(`${installId}:${feature}`).digest("hex")}`);
  let encoded = "";
  for (let index = 0; index < 16; index++) {
    encoded = ALPHABET[Number(value % 62n)] + encoded;
    value /= 62n;
  }
  return encoded;
}

export async function importLocalStackN8nCredential({
  marker, credential, runProcess = runLocalProcess, dockerHost,
  fileSystem = defaultFileSystem, homeDirectory = homedir(),
  env = process.env, platform = hostPlatform(), cwd = process.cwd(),
  lockDownPath = lockDownLocalPath,
} = {}) {
  const feature = credential?.feature;
  const settings = Object.hasOwn(FEATURES, feature ?? "") ? FEATURES[feature] : null;
  const response = { state: "failed", name: settings?.name ?? "Relmio credential" };
  try {
    if (!settings || typeof credential.apiKey !== "string" || !credential.apiKey ||
        credential.apiKey.length > 512 || /[\0\r\n]/u.test(credential.apiKey)) return response;
    const safe = validateLocalN8nStackMarker(marker);
    if (validateLocalDockerHost(dockerHost, { platform }) !== safe.dockerHost) return response;
    const root = await resolveLocalN8nStackInstallRoot({ fileSystem, homeDirectory, env, platform });
    const markerPath = join(root, ".managed-by-relmio.json");
    const markerStat = await fileSystem.lstat(markerPath);
    if (!markerStat.isFile() || markerStat.isSymbolicLink() || markerStat.size > 4096 ||
        (platform !== "win32" && (markerStat.mode & 0o077) !== 0)) return response;
    await lockDownPath(markerPath, { platform, kind: "file", verifyOnly: true, verifyEffectiveOwnerOnly: true });
    const onDisk = validateLocalN8nStackMarker(JSON.parse(await fileSystem.readFile(markerPath, "utf8")));
    if (onDisk.installId !== safe.installId || onDisk.projectName !== safe.projectName ||
        onDisk.dockerHost !== safe.dockerHost) return response;
    const containerName = `${safe.projectName}-n8n-1`;
    const inspected = await runProcess({ file: "docker", args: ["container", "inspect", "--format", "{{json .}}", containerName],
      cwd: root, dockerHost, timeoutMs: 60_000, maxOutputBytes: 32_768 });
    if (inspected.code !== 0 || Buffer.byteLength(inspected.stdout ?? "") > 32_768) return response;
    const container = JSON.parse(inspected.stdout);
    const labels = container?.Config?.Labels;
    if (!ID.test(container?.Id) || container.Name !== `/${containerName}` ||
        container.State?.Running !== true || labels?.["com.docker.compose.project"] !== safe.projectName ||
        labels?.["com.docker.compose.service"] !== "n8n" ||
        Object.entries(getLocalN8nStackLabels(safe)).some(([key, value]) => labels?.[key] !== value)) return response;
    const runtime = join(root, ".runtime");
    const directory = await fileSystem.lstat(runtime);
    if (!directory.isDirectory() || directory.isSymbolicLink() ||
        (platform !== "win32" && (directory.mode & 0o077) !== 0)) return response;
    await lockDownPath(runtime, { platform, verifyOnly: true });
    const recordPath = join(runtime, "credentials.json");
    let previous = {};
    try {
      const stat = await fileSystem.lstat(recordPath);
      if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096 ||
          (platform !== "win32" && (stat.mode & 0o077) !== 0)) return response;
      await lockDownPath(recordPath, { platform, kind: "file", verifyOnly: true, verifyEffectiveOwnerOnly: true });
      previous = JSON.parse(await fileSystem.readFile(recordPath, "utf8"));
      if (!previous || typeof previous !== "object" || Array.isArray(previous) ||
          Object.entries(previous).some(([name, entry]) => !Object.hasOwn(FEATURES, name) ||
            entry?.id !== localStackCredentialId(safe.installId, name) ||
            entry.name !== FEATURES[name].name || !Number.isSafeInteger(entry.importedAt))) return response;
    } catch (error) {
      if (error?.code !== "ENOENT") return response;
    }
    const id = localStackCredentialId(safe.installId, feature);
    const input = JSON.stringify([{ id, name: settings.name, type: "openAiApi",
      data: { apiKey: credential.apiKey, url: settings.url } }]);
    const imported = await runProcess({ file: "docker",
      args: ["exec", "-i", container.Id, "n8n", "import:credentials", "--input=/dev/stdin"],
      input, cwd: root, dockerHost, timeoutMs: 60_000, maxOutputBytes: 4096 });
    if (imported.code !== 0) return response;
    const temporary = join(runtime, `credentials.json-${randomUUID()}`);
    await writePrivateFile(fileSystem, temporary,
      `${JSON.stringify({ ...previous, [feature]: { id, name: settings.name, importedAt: Date.now() } })}\n`,
      0o600, { platform, lockDownPath });
    try { await fileSystem.rename(temporary, recordPath); }
    catch { await fileSystem.unlink(temporary); return response; }
    await lockDownPath(recordPath, { platform, kind: "file", verifyOnly: true, verifyEffectiveOwnerOnly: true });
    return { state: previous[feature] ? "updated" : "created", name: settings.name };
  } catch {
    return response;
  }
}