// src/index.ts
import { readFileSync } from "fs";
import { join } from "path";

// src/remote.entity.ts
import {
  useIdField,
  useTextField,
  useTextareaField as useTextareaField2,
  useNumberField,
  useSelectField as useSelectField2,
  useJsonField,
  useDatetimeField,
  useNameField,
  useDescriptionField
} from "@gzl10/nexus-sdk/fields";

// src/actions/test-connection.ts
var testConnectionAction = {
  key: "test-connection",
  label: { en: "Test Connection", es: "Probar conexion" },
  icon: "mdi:lan-connect",
  scope: "row",
  method: "POST",
  select: ["id", "name", "hostname"],
  output: {},
  handler: async (ctx, input) => {
    const { _record } = input;
    const service = ctx.services.get("remote");
    return service.testConnection(_record.id);
  },
  casl: { action: "execute" }
};

// src/actions/execute.ts
import { useTextareaField } from "@gzl10/nexus-sdk/fields";
var executeAction = {
  key: "execute",
  label: { en: "Execute Command", es: "Ejecutar comando" },
  icon: "mdi:console",
  scope: "row",
  method: "POST",
  select: ["id", "name", "hostname"],
  output: {},
  input: {
    command: useTextareaField({
      label: { en: "Command", es: "Comando" },
      required: true,
      placeholder: "ls -la /var/log"
    })
  },
  handler: async (ctx, input, req) => {
    const { _record, command } = input;
    const service = ctx.services.get("remote");
    const result = await service.execute(_record.id, command, { triggeredBy: "user" });
    ctx.events.notify("audit.log", {
      source: "plugin:remote",
      action: "command.executed",
      actorId: input._authUserId,
      resourceType: "host",
      resourceId: _record.id,
      ip: req?.ip,
      userAgent: req?.headers?.["user-agent"],
      metadata: { command: command.substring(0, 200) }
    });
    return result;
  },
  confirm: {
    type: "verify",
    message: { en: "This will execute a command on the remote host.", es: "Esto ejecutar\xE1 un comando en el host remoto." },
    verifyText: "EXECUTE",
    severity: "warning"
  },
  casl: { action: "execute" }
};

// src/actions/run-script.ts
import { useSelectField } from "@gzl10/nexus-sdk/fields";
var runScriptAction = {
  key: "run-script",
  label: { en: "Run Script", es: "Ejecutar script" },
  icon: "mdi:script-text-play",
  scope: "row",
  method: "POST",
  select: ["id", "name", "hostname"],
  output: {},
  input: {
    script_id: useSelectField({
      label: { en: "Script", es: "Script" },
      required: true,
      table: "scripts",
      column: "id",
      endpoint: "/remote/scripts",
      valueField: "id",
      labelField: "name",
      size: 26
    })
  },
  handler: async (ctx, input, req) => {
    const { _record, script_id } = input;
    const service = ctx.services.get("remote");
    const result = await service.runScript(_record.id, script_id, { triggeredBy: "user" });
    ctx.events.notify("audit.log", {
      source: "plugin:remote",
      action: "script.executed",
      actorId: input._authUserId,
      resourceType: "host",
      resourceId: _record.id,
      ip: req?.ip,
      userAgent: req?.headers?.["user-agent"],
      metadata: { scriptId: script_id }
    });
    return result;
  },
  confirm: {
    type: "simple",
    message: { en: "This will run the selected script on the remote host.", es: "Esto ejecutar\xE1 el script seleccionado en el host remoto." },
    severity: "warning"
  },
  casl: { action: "execute" }
};

// src/remote.entity.ts
var SENSITIVE_FIELDS = ["private_key", "passphrase", "password"];
function encryptSensitiveFields(ctx, data) {
  for (const field of SENSITIVE_FIELDS) {
    const value = data[field];
    if (typeof value === "string" && value.length > 0) {
      data[field] = ctx.core.crypto.encrypt(value);
    }
  }
  return data;
}
var remoteSettingsEntity = {
  type: "single",
  key: "remote_settings",
  label: { en: "Settings", es: "Configuracion" },
  fields: {
    default_user: useTextField({
      label: { en: "Default User", es: "Usuario por defecto" },
      defaultValue: "root",
      size: 100
    }),
    default_port: useNumberField({
      label: { en: "Default Port", es: "Puerto por defecto" },
      defaultValue: 22,
      validation: { min: 1, max: 65535 }
    }),
    default_timeout: useNumberField({
      label: { en: "Default Timeout (ms)", es: "Timeout por defecto (ms)" },
      defaultValue: 3e4,
      validation: { min: 1e3, max: 3e5 }
    }),
    private_key: useTextareaField2({
      label: { en: "Server Private Key", es: "Clave privada del servidor" },
      hidden: true,
      meta: { exportable: false, searchable: false }
    }),
    public_key: useTextareaField2({
      label: { en: "Server Public Key", es: "Clave publica del servidor" },
      disabled: true
    })
  },
  defaults: {
    default_user: "root",
    default_port: 22,
    default_timeout: 3e4,
    private_key: null,
    public_key: null
  },
  hooks: (ctx) => ({
    beforeCreate: async (data) => {
      const d = data;
      if (typeof d["private_key"] === "string" && d["private_key"].length > 0) {
        d["private_key"] = ctx.core.crypto.encrypt(d["private_key"]);
      }
      return d;
    },
    beforeUpdate: async (_id, data) => {
      const d = data;
      if (typeof d["private_key"] === "string" && d["private_key"].length > 0) {
        d["private_key"] = ctx.core.crypto.encrypt(d["private_key"]);
      }
      return d;
    }
  }),
  casl: {
    subject: "RemoteSettings",
    permissions: {
      ADMIN: { actions: ["read", "update"] }
    }
  }
};
var remoteCredentialsEntity = {
  type: "collection",
  table: "credentials",
  label: { en: "Credentials", es: "Credenciales" },
  labelPlural: { en: "Credentials", es: "Credenciales" },
  labelField: "name",
  timestamps: true,
  audit: true,
  fields: {
    id: useIdField(),
    name: useNameField({
      unique: true,
      size: 100
    }),
    type: useSelectField2({
      label: { en: "Type", es: "Tipo" },
      required: true,
      options: [
        { value: "key_pair", label: { en: "Key Pair", es: "Par de claves" } },
        { value: "password", label: { en: "Password", es: "Contrasena" } }
      ]
    }),
    private_key: useTextareaField2({
      label: { en: "Private Key", es: "Clave privada" },
      hidden: true,
      meta: { exportable: false, searchable: false, showInDisplay: false }
    }),
    public_key: useTextareaField2({
      label: { en: "Public Key", es: "Clave publica" },
      meta: { showInDisplay: false }
    }),
    passphrase: useTextareaField2({
      label: { en: "Passphrase", es: "Frase de paso" },
      hidden: true,
      meta: { exportable: false, searchable: false, showInDisplay: false }
    }),
    password: useTextareaField2({
      label: { en: "Password", es: "Contrasena" },
      hidden: true,
      meta: { exportable: false, searchable: false, showInDisplay: false }
    }),
    notes: useTextareaField2({
      label: { en: "Notes", es: "Notas" },
      meta: { showInDisplay: false }
    })
  },
  hooks: (ctx) => ({
    beforeCreate: async (data) => encryptSensitiveFields(ctx, data),
    beforeUpdate: async (_id, data) => encryptSensitiveFields(ctx, data)
  }),
  casl: {
    subject: "RemoteCredential",
    permissions: {
      ADMIN: { actions: ["manage"] }
    }
  }
};
var remoteHostsEntity = {
  type: "collection",
  realtime: "sync",
  table: "hosts",
  label: { en: "Hosts", es: "Hosts" },
  labelPlural: { en: "Hosts", es: "Hosts" },
  labelField: "name",
  timestamps: true,
  audit: true,
  fields: {
    id: useIdField(),
    name: useNameField({
      unique: true,
      size: 100
    }),
    hostname: useTextField({
      label: { en: "Hostname / IP", es: "Hostname / IP" },
      required: true,
      size: 255
    }),
    port: useNumberField({
      label: { en: "Port", es: "Puerto" },
      defaultValue: 22,
      validation: { min: 1, max: 65535 }
    }),
    username: useTextField({
      label: { en: "Username", es: "Usuario" },
      required: true,
      defaultValue: "root",
      size: 100
    }),
    auth_method: useSelectField2({
      label: { en: "Auth Method", es: "Metodo de autenticacion" },
      required: true,
      defaultValue: "default_key",
      options: [
        { value: "default_key", label: { en: "Server Default Key", es: "Clave por defecto del servidor" } },
        { value: "credential", label: { en: "Credential", es: "Credencial" } },
        { value: "agent", label: { en: "SSH Agent", es: "Agente SSH" } }
      ]
    }),
    credential_id: useSelectField2({
      label: { en: "Credential", es: "Credencial" },
      nullable: true,
      table: "credentials",
      column: "id",
      onDelete: "SET NULL",
      endpoint: "/remote/credentials",
      valueField: "id",
      labelField: "name",
      size: 26
    }),
    tags: useJsonField({
      label: { en: "Tags", es: "Etiquetas" }
    }),
    status: useSelectField2({
      label: { en: "Status", es: "Estado" },
      defaultValue: "unknown",
      disabled: true,
      options: [
        { value: "unknown", label: { en: "Unknown", es: "Desconocido" } },
        { value: "online", label: { en: "Online", es: "En linea" } },
        { value: "offline", label: { en: "Offline", es: "Desconectado" } },
        { value: "error", label: { en: "Error", es: "Error" } }
      ]
    }),
    last_seen: useDatetimeField({
      label: { en: "Last Seen", es: "Ultima conexion" },
      meta: { showInForm: false }
    }),
    fingerprint: useTextField({
      label: { en: "Fingerprint", es: "Huella digital" },
      hidden: true,
      size: 500,
      meta: { showInForm: false }
    }),
    notes: useTextareaField2({
      label: { en: "Notes", es: "Notas" },
      meta: { showInDisplay: false }
    })
  },
  actions: [
    testConnectionAction,
    executeAction,
    runScriptAction
  ],
  casl: {
    subject: "RemoteHost",
    permissions: {
      ADMIN: { actions: ["manage"] },
      SERVICE: { actions: ["read", "execute"] },
      SUPPORT: { actions: ["read"] }
    }
  }
};
var remoteScriptsEntity = {
  type: "collection",
  table: "scripts",
  label: { en: "Scripts", es: "Scripts" },
  labelPlural: { en: "Scripts", es: "Scripts" },
  labelField: "name",
  timestamps: true,
  audit: true,
  fields: {
    id: useIdField(),
    name: useNameField({
      unique: true,
      size: 100
    }),
    description: useDescriptionField(),
    content: useTextareaField2({
      label: { en: "Script Content", es: "Contenido del script" },
      required: true
    }),
    interpreter: useSelectField2({
      label: { en: "Interpreter", es: "Interprete" },
      required: true,
      defaultValue: "bash",
      options: [
        { value: "bash", label: { en: "Bash", es: "Bash" } },
        { value: "sh", label: { en: "sh", es: "sh" } },
        { value: "python", label: { en: "Python", es: "Python" } },
        { value: "node", label: { en: "Node.js", es: "Node.js" } },
        { value: "custom", label: { en: "Custom", es: "Personalizado" } }
      ]
    }),
    tags: useJsonField({
      label: { en: "Tags", es: "Etiquetas" }
    }),
    timeout: useNumberField({
      label: { en: "Timeout (ms)", es: "Timeout (ms)" },
      defaultValue: 3e4,
      validation: { min: 1e3, max: 3e5 }
    })
  },
  casl: {
    subject: "RemoteScript",
    permissions: {
      ADMIN: { actions: ["manage"] },
      SERVICE: { actions: ["read", "execute"] },
      SUPPORT: { actions: ["read"] }
    }
  }
};
var remoteExecutionsEntity = {
  type: "collection",
  realtime: "sync",
  immutable: true,
  table: "executions",
  label: { en: "Executions", es: "Ejecuciones" },
  labelPlural: { en: "Executions", es: "Ejecuciones" },
  labelField: "host_name",
  timestamps: true,
  retention: { days: 30 },
  fields: {
    id: useIdField(),
    host_id: useSelectField2({
      label: { en: "Host", es: "Host" },
      nullable: true,
      table: "hosts",
      column: "id",
      onDelete: "SET NULL",
      endpoint: "/remote/hosts",
      valueField: "id",
      labelField: "name",
      size: 26
    }),
    host_name: useTextField({
      label: { en: "Host Name", es: "Nombre del host" },
      size: 100
    }),
    script_id: useTextField({
      label: { en: "Script", es: "Script" },
      nullable: true,
      size: 26
    }),
    command: useTextareaField2({
      label: { en: "Command", es: "Comando" }
    }),
    exit_code: useNumberField({
      label: { en: "Exit Code", es: "Codigo de salida" },
      nullable: true
    }),
    stdout: useTextareaField2({
      label: { en: "Standard Output", es: "Salida estandar" },
      meta: { showInDisplay: false }
    }),
    stderr: useTextareaField2({
      label: { en: "Standard Error", es: "Error estandar" },
      meta: { showInDisplay: false }
    }),
    duration_ms: useNumberField({
      label: { en: "Duration (ms)", es: "Duracion (ms)" }
    }),
    status: useSelectField2({
      label: { en: "Status", es: "Estado" },
      options: [
        { value: "success", label: { en: "Success", es: "Exitoso" } },
        { value: "error", label: { en: "Error", es: "Error" } },
        { value: "timeout", label: { en: "Timeout", es: "Timeout" } },
        { value: "cancelled", label: { en: "Cancelled", es: "Cancelado" } }
      ]
    }),
    triggered_by: useSelectField2({
      label: { en: "Triggered By", es: "Disparado por" },
      options: [
        { value: "user", label: { en: "User", es: "Usuario" } },
        { value: "schedule", label: { en: "Schedule", es: "Programacion" } }
      ]
    }),
    triggered_by_id: useTextField({
      label: { en: "Triggered By ID", es: "ID del disparador" },
      size: 50,
      meta: { showInDisplay: false }
    })
  },
  casl: {
    subject: "RemoteExecution",
    permissions: {
      ADMIN: { actions: ["read"] },
      SERVICE: { actions: ["read"] },
      SUPPORT: { actions: ["read"] }
    }
  }
};

// src/actions/generate-keypair.ts
var generateKeypairAction = {
  key: "generate-keypair",
  label: { en: "Generate Key Pair", es: "Generar par de claves" },
  icon: "mdi:key-plus",
  scope: "module",
  method: "POST",
  output: {},
  handler: async (ctx) => {
    const service = ctx.services.get("remote");
    const { publicKey, privateKey } = await service.generateKeypair();
    const settingsService = ctx.services.get("remote_settings");
    await settingsService.update("remote_settings", {
      private_key: privateKey,
      public_key: publicKey
    });
    ctx.core.logger.info("SSH key pair generated and stored");
    return {
      publicKey,
      message: "Ed25519 key pair generated. Public key shown below. Private key stored encrypted."
    };
  },
  confirm: {
    type: "simple",
    message: { en: "This will replace the existing SSH key pair. All hosts using the current key will need to be reconfigured.", es: "Esto reemplazar\xE1 el par de claves SSH existente. Todos los hosts que usen la clave actual necesitar\xE1n ser reconfigurados." },
    severity: "warning"
  },
  casl: { action: "manage" }
};

// src/remote.service.ts
import { createHash } from "crypto";
import { Client } from "ssh2";
var MAX_TIMEOUT = 3e5;
var MAX_OUTPUT_SIZE = 1048576;
var DEFAULT_TIMEOUT = 3e4;
var TABLES = {
  hosts: "rmt_hosts",
  credentials: "rmt_credentials",
  scripts: "rmt_scripts",
  executions: "rmt_executions"
};
var RemoteService = class {
  constructor(ctx) {
    this.ctx = ctx;
    this.db = ctx.db.knex;
    this.logger = ctx.core.logger.child({ service: "remote" });
    this.generateId = ctx.core.generateId;
    this.crypto = ctx.core.crypto;
    this.errors = ctx.core.errors;
    this.safeJsonParse = ctx.core.safeJsonParse;
  }
  db;
  logger;
  generateId;
  crypto;
  errors;
  safeJsonParse;
  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------
  /**
   * Executes a command on a remote host via SSH.
   */
  async execute(hostId, command, options) {
    const host = await this.getHost(hostId);
    const config = await this.getConnectionConfig(host);
    const timeout = Math.min(options?.timeout ?? config.readyTimeout, MAX_TIMEOUT);
    const startTime = Date.now();
    let client = null;
    try {
      client = await this.connect(config, host.fingerprint ?? void 0);
      const result = await this.execCommand(client, command, timeout);
      await this.logExecution({
        hostId: host.id,
        hostName: host.name,
        command,
        exitCode: result.exitCode,
        stdout: result.stdout,
        stderr: result.stderr,
        durationMs: result.durationMs,
        status: result.status,
        triggeredBy: options?.triggeredBy ?? "user",
        triggeredById: options?.triggeredById ?? null
      });
      return result;
    } catch (error) {
      const durationMs = Date.now() - startTime;
      const errorMessage = error instanceof Error ? error.message : String(error);
      const result = {
        exitCode: null,
        stdout: "",
        stderr: errorMessage,
        durationMs,
        status: "error"
      };
      await this.logExecution({
        hostId: host.id,
        hostName: host.name,
        command,
        exitCode: null,
        stdout: "",
        stderr: errorMessage,
        durationMs,
        status: "error",
        triggeredBy: options?.triggeredBy ?? "user",
        triggeredById: options?.triggeredById ?? null
      });
      return result;
    } finally {
      client?.end();
    }
  }
  /**
   * Runs a saved script on a remote host.
   */
  async runScript(hostId, scriptId, options) {
    const script = await this.db(TABLES.scripts).where("id", scriptId).first();
    if (!script) {
      throw new this.errors.NotFoundError(`Script ${scriptId} not found`);
    }
    const interpreterMap = {
      bash: "/bin/bash",
      sh: "/bin/sh",
      python: "/usr/bin/env python3",
      node: "/usr/bin/env node"
    };
    let command;
    const interpreter = script.interpreter;
    const content = script.content;
    if (interpreter === "custom") {
      command = content;
    } else {
      const bin = interpreterMap[interpreter] ?? "/bin/bash";
      const encoded = Buffer.from(content, "utf-8").toString("base64");
      command = `printf '%s' '${encoded}' | base64 -d | ${bin}`;
    }
    const timeout = options?.timeout ?? script.timeout ?? DEFAULT_TIMEOUT;
    return this.execute(hostId, command, {
      ...options,
      timeout
    });
  }
  /**
   * Tests SSH connectivity to a host.
   */
  async testConnection(hostId) {
    const host = await this.getHost(hostId);
    const config = await this.getConnectionConfig(host);
    const startTime = Date.now();
    let client = null;
    let fingerprint;
    try {
      client = await this.connect(config, host.fingerprint ?? void 0, (fp) => {
        fingerprint = fp;
      });
      if (fingerprint && !/^SHA256:[A-Za-z0-9+/]+=*$/.test(fingerprint)) {
        this.logger.warn("Invalid fingerprint format, discarding");
        fingerprint = void 0;
      }
      const durationMs = Date.now() - startTime;
      await this.db(TABLES.hosts).where("id", hostId).update({
        status: "online",
        last_seen: this.ctx.db.formatTimestamp(this.db),
        ...fingerprint ? { fingerprint } : {}
      });
      this.logger.info({ hostId, host: host.name, durationMs }, "Connection test successful");
      return { success: true, fingerprint, durationMs };
    } catch (error) {
      const durationMs = Date.now() - startTime;
      const errorMessage = error instanceof Error ? error.message : String(error);
      await this.db(TABLES.hosts).where("id", hostId).update({ status: "error" });
      this.logger.warn({ hostId, host: host.name, error: errorMessage }, "Connection test failed");
      return { success: false, error: errorMessage, durationMs };
    } finally {
      client?.end();
    }
  }
  /**
   * Generates an Ed25519 SSH key pair.
   */
  async generateKeypair() {
    const { execSync } = await import("child_process");
    const { mkdtempSync, readFileSync: readFileSync2, rmSync } = await import("fs");
    const { join: join2 } = await import("path");
    const { tmpdir } = await import("os");
    try {
      execSync("which ssh-keygen", { stdio: "pipe" });
    } catch {
      throw new this.errors.ValidationError(
        "ssh-keygen not found on the server. Install openssh-client to generate key pairs."
      );
    }
    const dir = mkdtempSync(join2(tmpdir(), "nexus-ssh-"));
    const keyPath = join2(dir, "key");
    try {
      execSync(`ssh-keygen -t ed25519 -f "${keyPath}" -N "" -C "nexus-remote"`, { stdio: "pipe" });
      const privateKey = readFileSync2(keyPath, "utf-8");
      const publicKey = readFileSync2(`${keyPath}.pub`, "utf-8").trim();
      return { publicKey, privateKey };
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  }
  // ---------------------------------------------------------------------------
  // Private
  // ---------------------------------------------------------------------------
  async getHost(hostId) {
    const host = await this.db(TABLES.hosts).where("id", hostId).first();
    if (!host) {
      throw new this.errors.NotFoundError(`Host ${hostId} not found`);
    }
    return host;
  }
  /**
   * Resolves the full SSH connection config for a host.
   */
  async getConnectionConfig(host) {
    const config = {
      host: host.hostname,
      port: host.port ?? 22,
      username: host.username,
      readyTimeout: DEFAULT_TIMEOUT
    };
    switch (host.auth_method) {
      case "credential": {
        if (!host.credential_id) {
          throw new this.errors.ValidationError("Host uses credential auth but no credential_id is set");
        }
        const cred = await this.db(TABLES.credentials).where("id", host.credential_id).first();
        if (!cred) {
          throw new this.errors.NotFoundError(`Credential ${host.credential_id} not found`);
        }
        if (cred.type === "key_pair") {
          if (!cred.private_key) {
            throw new this.errors.ValidationError(
              `Credential "${cred.name}" is type key_pair but has no private key configured`
            );
          }
          config.privateKey = this.decryptField(cred.private_key) ?? void 0;
          config.passphrase = this.decryptField(cred.passphrase) ?? void 0;
        } else if (cred.type === "password") {
          if (!cred.password) {
            throw new this.errors.ValidationError(
              `Credential "${cred.name}" is type password but has no password configured`
            );
          }
          config.password = this.decryptField(cred.password) ?? void 0;
        } else {
          throw new this.errors.ValidationError(`Unknown credential type: ${cred.type}`);
        }
        break;
      }
      case "default_key": {
        const settings = await this.getSettings();
        if (!settings.private_key) {
          throw new this.errors.ValidationError(
            "Host uses default_key auth but no server key pair is configured. Generate one first."
          );
        }
        config.privateKey = this.decryptField(settings.private_key) ?? void 0;
        config.username = host.username || settings.default_user;
        break;
      }
      case "agent": {
        config.agent = process.env["SSH_AUTH_SOCK"] ?? void 0;
        if (!config.agent) {
          throw new this.errors.ValidationError("SSH_AUTH_SOCK not available for agent auth");
        }
        break;
      }
      default:
        throw new this.errors.ValidationError(`Unknown auth_method: ${host.auth_method}`);
    }
    return config;
  }
  /**
   * Creates an SSH connection with TOFU host key verification.
   * If knownFingerprint is provided, rejects connections with mismatched keys.
   */
  connect(config, knownFingerprint, onFingerprint) {
    return new Promise((resolve, reject) => {
      const client = new Client();
      const timer = setTimeout(() => {
        client.end();
        reject(new Error(`SSH connection timeout after ${config.readyTimeout}ms`));
      }, config.readyTimeout);
      client.on("ready", () => {
        clearTimeout(timer);
        resolve(client);
      }).on("error", (err) => {
        clearTimeout(timer);
        reject(err);
      }).connect({
        host: config.host,
        port: config.port,
        username: config.username,
        privateKey: config.privateKey,
        passphrase: config.passphrase,
        password: config.password,
        agent: config.agent,
        readyTimeout: config.readyTimeout,
        hostVerifier: (key) => {
          const fp = `SHA256:${createHash("sha256").update(key).digest("base64")}`;
          onFingerprint?.(fp);
          if (knownFingerprint && fp !== knownFingerprint) {
            this.logger.error("SSH host key mismatch \u2014 possible MITM attack. Expected fingerprint does not match.");
            return false;
          }
          return true;
        }
      });
    });
  }
  /**
   * Executes a command on a connected SSH client.
   */
  execCommand(client, command, timeout) {
    return new Promise((resolve, reject) => {
      const startTime = Date.now();
      let stdoutBuf = "";
      let stderrBuf = "";
      let resolved = false;
      const timer = setTimeout(() => {
        resolved = true;
        client.end();
        resolve({
          exitCode: null,
          stdout: stdoutBuf,
          stderr: stderrBuf + "\n[Execution timed out]",
          durationMs: Date.now() - startTime,
          status: "timeout"
        });
      }, timeout);
      client.exec(command, (err, stream) => {
        if (err) {
          clearTimeout(timer);
          reject(err);
          return;
        }
        stream.on("close", (code) => {
          if (resolved) return;
          clearTimeout(timer);
          const durationMs = Date.now() - startTime;
          const status = code === 0 ? "success" : "error";
          resolve({ exitCode: code, stdout: stdoutBuf, stderr: stderrBuf, durationMs, status });
        });
        stream.on("data", (data) => {
          if (!resolved && stdoutBuf.length < MAX_OUTPUT_SIZE) {
            stdoutBuf += data.toString();
          }
        });
        stream.stderr.on("data", (data) => {
          if (!resolved && stderrBuf.length < MAX_OUTPUT_SIZE) {
            stderrBuf += data.toString();
          }
        });
      });
    });
  }
  /**
   * Logs an execution to the remote_executions event table.
   */
  async logExecution(data) {
    try {
      await this.db(TABLES.executions).insert({
        id: this.generateId(),
        host_id: data.hostId,
        host_name: data.hostName,
        script_id: data.scriptId ?? null,
        command: data.command.substring(0, 1e4),
        // Limit stored command size
        exit_code: data.exitCode,
        stdout: data.stdout.substring(0, MAX_OUTPUT_SIZE),
        stderr: data.stderr.substring(0, MAX_OUTPUT_SIZE),
        duration_ms: data.durationMs,
        status: data.status,
        triggered_by: data.triggeredBy,
        triggered_by_id: data.triggeredById ?? null,
        created_at: this.ctx.db.formatTimestamp(this.db)
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.error({ error: message }, "Failed to log execution");
    }
  }
  /**
   * Gets remote settings via SingleService.
   */
  async getSettings() {
    const settingsService = this.ctx.services.get("remote_settings");
    const settings = await settingsService.findById("remote_settings");
    if (!settings) {
      return {
        default_user: "root",
        default_port: 22,
        default_timeout: DEFAULT_TIMEOUT,
        private_key: null,
        public_key: null
      };
    }
    return settings;
  }
  /**
   * Decrypts a field value. Throws on failure — never falls back to plaintext.
   */
  decryptField(value) {
    if (!value) return null;
    try {
      return this.crypto.decrypt(value);
    } catch {
      throw new this.errors.ValidationError(
        "Failed to decrypt credential field. The value may be corrupted or was stored before encryption was enabled."
      );
    }
  }
};

// src/remote-module.ts
var remoteModule = {
  name: "remote",
  label: { en: "Remote", es: "Remoto" },
  icon: "mdi:server-network",
  description: {
    en: "SSH host management and remote execution",
    es: "Gestion de hosts SSH y ejecucion remota"
  },
  category: "integrations",
  definitions: [
    remoteSettingsEntity,
    remoteCredentialsEntity,
    remoteHostsEntity,
    remoteScriptsEntity,
    remoteExecutionsEntity
  ],
  actions: [generateKeypairAction],
  routePrefix: "/remote",
  init: async (ctx) => {
    const service = new RemoteService(ctx);
    ctx.services.register("remote", service);
    const schedulesService = ctx.services.getOptional("schedules");
    if (schedulesService) {
      schedulesService.registerFunction("remote:execute", async (args) => {
        const hostId = args?.["hostId"];
        const command = args?.["command"];
        if (!hostId || !command) {
          throw new Error("remote:execute requires hostId and command arguments");
        }
        return service.execute(hostId, command, { triggeredBy: "schedule" });
      });
      schedulesService.registerFunction("remote:run-script", async (args) => {
        const hostId = args?.["hostId"];
        const scriptId = args?.["scriptId"];
        if (!hostId || !scriptId) {
          throw new Error("remote:run-script requires hostId and scriptId arguments");
        }
        return service.runScript(hostId, scriptId, { triggeredBy: "schedule" });
      });
      ctx.core.logger.debug("Remote functions registered in schedules");
    }
    ctx.core.logger.debug("Remote module initialized");
  }
};

// src/index.ts
var pkg = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf-8"));
var remotePlugin = {
  name: pkg.name,
  code: "rmt",
  label: { en: "Remote", es: "Remoto" },
  icon: "mdi:server-network",
  category: "integrations",
  version: pkg.version,
  description: {
    en: "SSH host management and remote execution",
    es: "Gesti\xF3n de hosts SSH y ejecuci\xF3n remota"
  },
  optionalDependencies: ["@gzl10/nexus-plugin-schedules"],
  modules: [remoteModule]
};
var index_default = remotePlugin;
export {
  RemoteService,
  index_default as default,
  remoteCredentialsEntity,
  remoteExecutionsEntity,
  remoteHostsEntity,
  remotePlugin,
  remoteSettingsEntity
};
//# sourceMappingURL=index.js.map