Ctrl K
ComunidadGuías…

src/domain/safety.js

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

Archivo original ↗Preguntar a KiBot
export function validateSiwcRegistrationId(value) {
  if (typeof value !== "string" || !/^[A-Za-z0-9_-]{8,64}$/u.test(value)) {
    throw new TypeError("The selected SIWC registration is invalid.");
  }
  return value;
}

export function validateSiwcRuntimeId(value) {
  if (typeof value !== "string" || !/^[A-Za-z0-9_-]{1,64}$/u.test(value)) {
    throw new TypeError("The selected SIWC runtime is invalid.");
  }
  return value;
}

export function validateSiwcAuthBinding(value) {
  if (
    !value || typeof value !== "object" || Array.isArray(value) ||
    Object.keys(value).length !== 5
  ) throw new TypeError("The selected SIWC account binding is invalid.");
  validateSiwcRegistrationId(value.registrationId);
  if (typeof value.clientId !== "string" || !/^[!-~]{1,256}$/u.test(value.clientId)) {
    throw new TypeError("The selected SIWC client is invalid.");
  }
  if (typeof value.generation !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value.generation)) {
    throw new TypeError("The selected SIWC generation is invalid.");
  }
  if (typeof value.ownerHostId !== "string" || !/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value.ownerHostId)) {
    throw new TypeError("The selected SIWC host is invalid.");
  }
  validateSiwcRuntimeId(value.ownerRuntimeId);
  return Object.freeze({ ...value });
}

const CATALOG_RECOVERIES = new Set([
  "retry-later", "reauthorize", "enable-plan", "manage-usage",
  "fix-request", "fix-configuration", "review-again", "resolve-handoff",
]);
const safeCatalogField = value => typeof value === "string" &&
  /^[A-Za-z0-9_.:-]{1,128}$/u.test(value) &&
  !/^sk-[A-Za-z0-9_-]{8,}$/u.test(value) &&
  !/^eyJ[A-Za-z0-9_-]{16,}\./u.test(value) ? value : undefined;

export function safeSiwcCatalogFailure(error) {
  return {
    error: "The selected account model catalog could not be verified.",
    status: Number.isInteger(error?.status) && error.status >= 400 &&
      error.status <= 599 ? error.status : 503,
    ...(safeCatalogField(error?.code) ? { code: error.code } : {}),
    ...(safeCatalogField(error?.param) ? { param: error.param } : {}),
    ...(safeCatalogField(error?.requestId) ? { requestId: error.requestId } : {}),
    recovery: CATALOG_RECOVERIES.has(error?.recovery) ? error.recovery : "retry-later",
  };
}

export function safeSiwcRuntimeFailure() {
  return {
    error: "The owned runtime could not be verified after transfer.",
    status: 503,
    recovery: "resolve-handoff",
  };
}


export const INSTALL_ROOT = "/docker/n8n-openai-oauth";
export const PROJECT_NAME = "n8n-openai-oauth";
export const SERVICE_NAME = "openai-oauth";
export const MANAGED_MARKER_PATH = `${INSTALL_ROOT}/.managed-by-n8n-openai-oauth`;
export const SHARED_ROOT_MARKER_PATH = `${INSTALL_ROOT}/.managed-by-relmio-root`;
export const SHARED_ROOT_MARKER_CONTENT = "relmio-managed-root:v1\n";
export const SIDECAR_MARKER_CONTENT = "Managed by n8n-openai-oauth-setup.\n";
export const SIWC_MIGRATION_PENDING_PATH = `${INSTALL_ROOT}/.siwc-migration-pending`;
export const SIWC_MIGRATED_PATH = `${INSTALL_ROOT}/.siwc-migrated`;
export const SIWC_MIGRATION_MARKER_CONTENT = "relmio-siwc-migration:v1\n";
export const SIWC_STAGING_PATH = `${INSTALL_ROOT}/.siwc-staging.json`;
export const SIDECAR_BUILD_IGNORE_PATH = `${INSTALL_ROOT}/Dockerfile.dockerignore`;
export const SIDECAR_BUILD_IGNORE_CONTENT = "*\n!Dockerfile\n!package.json\n!package-lock.json\n!services/\n!services/siwc-session.mjs\n!services/siwc-handoff.mjs\n!services/local-integration-lifecycle-lock.js\n!services/codex-images.mjs\n!services/model-discovery.mjs\n!gateway/\n!gateway/openai-oauth-sidecar.mjs\n!infrastructure/\n!infrastructure/local-process.js\n!infrastructure/process-identity.js\n";
export const SIDECAR_BUILD_IGNORE_GUARD = `[ ! -L ${SIDECAR_BUILD_IGNORE_PATH} ] && { [ ! -e ${SIDECAR_BUILD_IGNORE_PATH} ] || { [ -f ${SIDECAR_BUILD_IGNORE_PATH} ] && [ "$(stat -c %u:%h ${SIDECAR_BUILD_IGNORE_PATH})" = 0:1 ] && [ $((0$(stat -c %a ${SIDECAR_BUILD_IGNORE_PATH}) & 022)) -eq 0 ] && [ "$(stat -c %s ${SIDECAR_BUILD_IGNORE_PATH})" = ${Buffer.byteLength(SIDECAR_BUILD_IGNORE_CONTENT)} ] && [ "$(cat ${SIDECAR_BUILD_IGNORE_PATH})" = '${SIDECAR_BUILD_IGNORE_CONTENT.trim()}' ]; }; }`;
export const SIDECAR_FRESH_CONTEXT_GUARD = `for p in ${MANAGED_MARKER_PATH} ${SIDECAR_BUILD_IGNORE_PATH} ${INSTALL_ROOT}/Dockerfile ${INSTALL_ROOT}/docker-compose.yml ${INSTALL_ROOT}/package.json ${INSTALL_ROOT}/package-lock.json ${INSTALL_ROOT}/services ${INSTALL_ROOT}/gateway ${INSTALL_ROOT}/infrastructure; do [ ! -e "$p" ] && [ ! -L "$p" ] || exit 42; done; [ ! -L ${INSTALL_ROOT}/siwc ] || exit 43; if [ -d ${INSTALL_ROOT}/siwc ]; then [ -z "$(find ${INSTALL_ROOT}/siwc -mindepth 1 -maxdepth 1 -print -quit)" ] || exit 42; fi; if [ -d ${INSTALL_ROOT}/auth ]; then [ -z "$(find ${INSTALL_ROOT}/auth -mindepth 1 -maxdepth 1 -print -quit)" ] || exit 42; fi`;
export const SIDECAR_MANAGED_CONTEXT_GUARD = `for p in /docker ${INSTALL_ROOT} ${INSTALL_ROOT}/services ${INSTALL_ROOT}/gateway ${INSTALL_ROOT}/infrastructure ${INSTALL_ROOT}/legacy; do if [ -e "$p" ] || [ -L "$p" ]; then [ ! -L "$p" ] && [ -d "$p" ] && [ "$(stat -c %u "$p")" = 0 ] && [ $((0$(stat -c %a "$p") & 022)) -eq 0 ] || exit 43; fi; done; for p in ${SHARED_ROOT_MARKER_PATH} ${MANAGED_MARKER_PATH} ${SIWC_STAGING_PATH} ${SIWC_MIGRATION_PENDING_PATH} ${SIWC_MIGRATED_PATH} ${SIDECAR_BUILD_IGNORE_PATH} ${INSTALL_ROOT}/Dockerfile ${INSTALL_ROOT}/docker-compose.yml ${INSTALL_ROOT}/package.json ${INSTALL_ROOT}/package-lock.json ${INSTALL_ROOT}/services/siwc-session.mjs ${INSTALL_ROOT}/services/siwc-handoff.mjs ${INSTALL_ROOT}/services/local-integration-lifecycle-lock.js ${INSTALL_ROOT}/services/codex-images.mjs ${INSTALL_ROOT}/services/model-discovery.mjs ${INSTALL_ROOT}/gateway/openai-oauth-sidecar.mjs ${INSTALL_ROOT}/gateway/codex-chat.js ${INSTALL_ROOT}/gateway/codex-app-server.mjs ${INSTALL_ROOT}/infrastructure/local-process.js ${INSTALL_ROOT}/infrastructure/process-identity.js ${INSTALL_ROOT}/openai-oauth-sidecar.mjs; do if [ -e "$p" ] || [ -L "$p" ]; then [ ! -L "$p" ] && [ -f "$p" ] && [ "$(stat -c %u:%h "$p")" = 0:1 ] && [ $((0$(stat -c %a "$p") & 022)) -eq 0 ] || exit 43; fi; done; if [ -e ${INSTALL_ROOT}/siwc ] || [ -L ${INSTALL_ROOT}/siwc ]; then [ ! -L ${INSTALL_ROOT}/siwc ] && [ -d ${INSTALL_ROOT}/siwc ] && [ "$(stat -c %u:%g:%a ${INSTALL_ROOT}/siwc)" = 1000:1000:700 ] || exit 43; fi`;

const COMPOSE_PREFIX =
  "docker compose --project-name n8n-openai-oauth --file /docker/n8n-openai-oauth/docker-compose.yml";

export const PRECHECK_COMMAND = `if [ -e ${INSTALL_ROOT} ]; then [ ! -L ${INSTALL_ROOT} ] && [ -d ${INSTALL_ROOT} ] || exit 43; for p in ${SHARED_ROOT_MARKER_PATH} ${MANAGED_MARKER_PATH} ${SIDECAR_BUILD_IGNORE_PATH} ${SIWC_MIGRATION_PENDING_PATH} ${SIWC_MIGRATED_PATH} ${INSTALL_ROOT}/auth ${INSTALL_ROOT}/siwc ${INSTALL_ROOT}/Dockerfile ${INSTALL_ROOT}/package.json ${INSTALL_ROOT}/package-lock.json ${INSTALL_ROOT}/services ${INSTALL_ROOT}/services/siwc-session.mjs ${INSTALL_ROOT}/services/siwc-handoff.mjs ${INSTALL_ROOT}/services/local-integration-lifecycle-lock.js ${INSTALL_ROOT}/services/codex-images.mjs ${INSTALL_ROOT}/services/model-discovery.mjs ${INSTALL_ROOT}/gateway ${INSTALL_ROOT}/gateway/openai-oauth-sidecar.mjs ${INSTALL_ROOT}/infrastructure ${INSTALL_ROOT}/infrastructure/local-process.js ${INSTALL_ROOT}/infrastructure/process-identity.js ${INSTALL_ROOT}/docker-compose.yml; do [ ! -L "$p" ] || exit 43; done; if [ -e ${INSTALL_ROOT}/auth/auth.json ]; then [ -f ${SIWC_MIGRATED_PATH} ] && [ "$(cat ${SIWC_MIGRATED_PATH})" = '${SIWC_MIGRATION_MARKER_CONTENT.trim()}' ] || exit 42; fi; if [ -e ${SHARED_ROOT_MARKER_PATH} ]; then [ -f ${SHARED_ROOT_MARKER_PATH} ] && [ "$(cat ${SHARED_ROOT_MARKER_PATH})" = "${SHARED_ROOT_MARKER_CONTENT.trim()}" ] || exit 42; if [ -e ${MANAGED_MARKER_PATH} ]; then [ -f ${MANAGED_MARKER_PATH} ] && [ "$(cat ${MANAGED_MARKER_PATH})" = "${SIDECAR_MARKER_CONTENT.trim()}" ] || exit 42; printf '%s\\n' managed; else [ ! -e ${INSTALL_ROOT}/siwc/host.json ] || exit 42; printf '%s\\n' new; fi; elif [ -f ${MANAGED_MARKER_PATH} ] && [ "$(cat ${MANAGED_MARKER_PATH})" = "${SIDECAR_MARKER_CONTENT.trim()}" ]; then printf '%s\\n' managed; else exit 42; fi; else printf '%s\\n' new; fi`;

// Relmio's shared folder is for root only. Both commands change the server, so a caller runs
// them only after it has checked the owner's final confirmation. Status reads, sign-in polls
// and operation locks never run them. The session folder inside keeps its own owner; Docker
// mounts it for the container.
const SHARED_ROOT_PARENT_GUARD = `[ -d /docker ] && [ ! -L /docker ] && [ "$(stat -c %u /docker)" = 0 ] && [ $((0$(stat -c %a /docker) & 022)) -eq 0 ] && [ ! -L ${INSTALL_ROOT} ]`;
// The one place the folder's mode is set: only on a real folder that is already root's own.
const SHARED_ROOT_SET_MODE = `[ -d ${INSTALL_ROOT} ] && [ "$(stat -c %u ${INSTALL_ROOT})" = 0 ] && [ $((0$(stat -c %a ${INSTALL_ROOT}) & 022)) -eq 0 ] && chmod 0700 ${INSTALL_ROOT}`;
// A confirmed install or replacement: creates the folder with mode 0700, or sets an existing one.
export const SHARED_ROOT_COMMAND = `${SHARED_ROOT_PARENT_GUARD} && if [ ! -e ${INSTALL_ROOT} ]; then mkdir -m 0700 ${INSTALL_ROOT} && chown -h 0:0 ${INSTALL_ROOT}; fi && ${SHARED_ROOT_SET_MODE}`;
// A confirmed change to something already installed: sets an existing folder and never creates one.
export const SHARED_ROOT_TIGHTEN_COMMAND = `${SHARED_ROOT_PARENT_GUARD} && ${SHARED_ROOT_SET_MODE}`;

const DEPLOYMENT_COMMANDS = Object.freeze([
  SHARED_ROOT_COMMAND,
  `install -d -m 0700 -o 1000 -g 1000 ${INSTALL_ROOT}/siwc`,
  `install -d -m 0755 ${INSTALL_ROOT}/services ${INSTALL_ROOT}/gateway ${INSTALL_ROOT}/infrastructure`,
  `${COMPOSE_PREFIX} config --quiet`,
  `${COMPOSE_PREFIX} build ${SERVICE_NAME}`,
  `${COMPOSE_PREFIX} up -d --wait --wait-timeout 60 --no-build --no-deps ${SERVICE_NAME}`,
]);

const VERIFICATION_COMMANDS = Object.freeze({
  runningService: `${COMPOSE_PREFIX} ps --status running --services`,
  publicationState: `${COMPOSE_PREFIX} ps --format json ${SERVICE_NAME}`,
  managementConfig: `${COMPOSE_PREFIX} config --format json`,
  ownerContainer: `docker inspect --format '{{json .}}' n8n-openai-oauth-openai-oauth-1`,
  ownerPresence: `docker container ls --all --no-trunc --filter 'name=^/n8n-openai-oauth-openai-oauth-1$' --format '{{.ID}}'`,
  ownerImage: `docker image inspect --format '{{json .}}' n8n-openai-oauth:local`,
  sharedRoot: `${SIDECAR_MANAGED_CONTEXT_GUARD}; if [ -e ${SHARED_ROOT_MARKER_PATH} ]; then [ "$(cat ${SHARED_ROOT_MARKER_PATH})" = '${SHARED_ROOT_MARKER_CONTENT.trim()}' ] && printf managed; else printf absent; fi`,
  staging: `${SIDECAR_MANAGED_CONTEXT_GUARD}; if [ -e ${SIWC_STAGING_PATH} ]; then [ "$(stat -c %s ${SIWC_STAGING_PATH})" -le 16384 ] && cat ${SIWC_STAGING_PATH}; else printf null; fi`,
  stagedFiles: `${SIDECAR_MANAGED_CONTEXT_GUARD}; for p in ${MANAGED_MARKER_PATH} ${SIDECAR_BUILD_IGNORE_PATH} ${INSTALL_ROOT}/Dockerfile ${INSTALL_ROOT}/docker-compose.yml ${INSTALL_ROOT}/package.json ${INSTALL_ROOT}/package-lock.json ${INSTALL_ROOT}/services/siwc-session.mjs ${INSTALL_ROOT}/services/siwc-handoff.mjs ${INSTALL_ROOT}/services/local-integration-lifecycle-lock.js ${INSTALL_ROOT}/services/codex-images.mjs ${INSTALL_ROOT}/services/model-discovery.mjs ${INSTALL_ROOT}/gateway/openai-oauth-sidecar.mjs ${INSTALL_ROOT}/gateway/codex-chat.js ${INSTALL_ROOT}/gateway/codex-app-server.mjs ${INSTALL_ROOT}/infrastructure/local-process.js ${INSTALL_ROOT}/infrastructure/process-identity.js ${INSTALL_ROOT}/openai-oauth-sidecar.mjs; do if [ -e "$p" ]; then sha256sum "$p" || exit 43; fi; done`,
  legacyCredential: `[ ! -L ${INSTALL_ROOT}/auth/auth.json ] && [ -f ${INSTALL_ROOT}/auth/auth.json ] && [ "$(stat -c %u:%a ${INSTALL_ROOT}/auth/auth.json)" = 1000:600 ] && [ "$(stat -c %s ${INSTALL_ROOT}/auth/auth.json)" -le 131072 ] && stat -c '%d:%i:%u:%a:%s:%Y' ${INSTALL_ROOT}/auth/auth.json`,
  archiveLegacy: `${SIDECAR_MANAGED_CONTEXT_GUARD}; [ ! -L ${INSTALL_ROOT}/legacy ] && if [ ! -e ${INSTALL_ROOT}/legacy ]; then install -d -m 0700 ${INSTALL_ROOT}/legacy; fi && for p in ${INSTALL_ROOT}/Dockerfile ${INSTALL_ROOT}/docker-compose.yml ${MANAGED_MARKER_PATH}; do [ ! -L "$p" ] && [ -f "$p" ] || exit 43; done && cp -p -n ${INSTALL_ROOT}/Dockerfile ${INSTALL_ROOT}/docker-compose.yml ${MANAGED_MARKER_PATH} ${INSTALL_ROOT}/legacy/ && for p in ${INSTALL_ROOT}/openai-oauth-sidecar.mjs ${SIDECAR_BUILD_IGNORE_PATH}; do if [ -e "$p" ]; then [ ! -L "$p" ] && [ -f "$p" ] && cp -p -n "$p" ${INSTALL_ROOT}/legacy/ || exit 43; fi; done`,
  legacyArchiveFiles: `${SIDECAR_MANAGED_CONTEXT_GUARD}; if [ ! -e ${INSTALL_ROOT}/legacy ]; then printf absent; else for p in ${INSTALL_ROOT}/legacy/Dockerfile ${INSTALL_ROOT}/legacy/docker-compose.yml ${INSTALL_ROOT}/legacy/.managed-by-n8n-openai-oauth ${INSTALL_ROOT}/legacy/openai-oauth-sidecar.mjs ${INSTALL_ROOT}/legacy/Dockerfile.dockerignore; do if [ -e "$p" ] || [ -L "$p" ]; then [ ! -L "$p" ] && [ -f "$p" ] && [ "$(stat -c %u:%h "$p")" = 0:1 ] && [ $((0$(stat -c %a "$p") & 022)) -eq 0 ] && sha256sum "$p" || exit 43; fi; done; fi`,
  legacyStatus: `if [ -L ${INSTALL_ROOT} ] || [ -L ${MANAGED_MARKER_PATH} ] || [ -L ${INSTALL_ROOT}/auth ] || [ -L ${INSTALL_ROOT}/auth/auth.json ] || [ -L ${SIWC_MIGRATION_PENDING_PATH} ] || [ -L ${SIWC_MIGRATED_PATH} ]; then exit 43; elif [ -f ${SIWC_MIGRATED_PATH} ] && [ "$(cat ${SIWC_MIGRATED_PATH})" = '${SIWC_MIGRATION_MARKER_CONTENT.trim()}' ]; then printf managed; elif [ -f ${SIWC_MIGRATION_PENDING_PATH} ] && [ "$(cat ${SIWC_MIGRATION_PENDING_PATH})" = '${SIWC_MIGRATION_MARKER_CONTENT.trim()}' ]; then printf partial; elif [ -f ${MANAGED_MARKER_PATH} ] && [ "$(cat ${MANAGED_MARKER_PATH})" = "${SIDECAR_MARKER_CONTENT.trim()}" ] && [ -f ${INSTALL_ROOT}/auth/auth.json ]; then printf legacy; elif [ -f ${MANAGED_MARKER_PATH} ] && [ "$(cat ${MANAGED_MARKER_PATH})" = "${SIDECAR_MARKER_CONTENT.trim()}" ]; then printf managed; else printf absent; fi`,
  host: `${COMPOSE_PREFIX} run --rm --no-deps -T --entrypoint node ${SERVICE_NAME} /app/services/siwc-handoff.mjs host`,
  accept: `${COMPOSE_PREFIX} run --rm --no-deps -T --entrypoint node ${SERVICE_NAME} /app/services/siwc-handoff.mjs accept`,
  receipt: `${COMPOSE_PREFIX} run --rm --no-deps -T --entrypoint node ${SERVICE_NAME} /app/services/siwc-handoff.mjs receipt`,
  oneOffContainers: "docker ps -a --filter label=com.docker.compose.project=n8n-openai-oauth --filter label=com.docker.compose.oneoff=True -q",
  account: `${COMPOSE_PREFIX} run --rm --no-deps -T --entrypoint node ${SERVICE_NAME} /app/services/siwc-handoff.mjs account`,
  accountLive: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/siwc-handoff.mjs account`,
  imagesStatus: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/codex-images.mjs status`,
  imagesLoginStart: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/codex-images.mjs login-start`,
  imagesLoginPoll: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/codex-images.mjs login-poll`,
  imagesLoginCancel: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/codex-images.mjs login-cancel`,
  imagesSignOut: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/codex-images.mjs sign-out`,
  modelsDiscoveryStatus: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/model-discovery.mjs status`,
  modelChecksOn: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/model-discovery.mjs checks-on`,
  modelChecksOff: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/model-discovery.mjs checks-off`,
  usageStatus: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node /app/services/model-discovery.mjs usage`,
  signOut: `${COMPOSE_PREFIX} run --rm --no-deps -T --entrypoint node ${SERVICE_NAME} /app/services/siwc-handoff.mjs sign-out`,
  disablePlan: `${COMPOSE_PREFIX} run --rm --no-deps -T --entrypoint node ${SERVICE_NAME} /app/services/siwc-handoff.mjs disable-plan`,
  enablePlan: `${COMPOSE_PREFIX} run --rm --no-deps -T --entrypoint node ${SERVICE_NAME} /app/services/siwc-handoff.mjs enable-plan`,
  stop: `${COMPOSE_PREFIX} stop -t 30 ${SERVICE_NAME}`,
  models: `${COMPOSE_PREFIX} exec -T ${SERVICE_NAME} node -e '(async()=>{let key="";for await(const chunk of process.stdin){key+=chunk;if(key.length>128)process.exit(1)}const response=await fetch("http://127.0.0.1:10531/v1/models",{headers:{Authorization:"Bearer "+key}});console.log(await response.text());process.exit(response.ok?0:1)})().catch(()=>{console.log("RELMIO_MODEL_CHECK_UNREACHABLE");process.exit(1)})'`,
});

const ALLOWED_SIDECAR_COMMANDS = new Set([
  PRECHECK_COMMAND,
  SIDECAR_BUILD_IGNORE_GUARD,
  SIDECAR_FRESH_CONTEXT_GUARD,
  SIDECAR_MANAGED_CONTEXT_GUARD,
  SHARED_ROOT_TIGHTEN_COMMAND,
  ...DEPLOYMENT_COMMANDS,
  ...Object.values(VERIFICATION_COMMANDS),
]);

export function createDeploymentCommands() {
  return [...DEPLOYMENT_COMMANDS];
}

export function createVerificationCommands() {
  return { ...VERIFICATION_COMMANDS };
}

export function assertSidecarOnlyCommands(commands) {
  if (!Array.isArray(commands) || commands.length === 0) {
    throw new TypeError("A sidecar command list is required.");
  }

  for (const command of commands) {
    if (
      typeof command !== "string" ||
      !ALLOWED_SIDECAR_COMMANDS.has(command)
    ) {
      throw new Error(
        "Command rejected: only the installer-managed sidecar may be changed; n8n must remain untouched.",
      );
    }
  }

  return commands;
}

export function parseManagedFileHashes(output) {
  if (typeof output !== "string" || Buffer.byteLength(output) > 8192) {
    throw new Error("The managed file identities are invalid.");
  }
  const files = {};
  for (const line of output.trim().split("\n").filter(Boolean)) {
    const match = /^([a-f0-9]{64})  (\/docker\/n8n-openai-oauth\/[A-Za-z0-9/._-]+)$/u.exec(line);
    if (!match || match[2].split("/").some(part => part === "." || part === "..") ||
        Object.hasOwn(files, match[2])) throw new Error("The managed file identities are invalid.");
    files[match[2]] = match[1];
  }
  return files;
}