src/services/local-dashboard.js
Rama main · réplica de lectura del repositorio original.
import {
getLocalDockerStatus,
getManagedLocalEndpointStatus,
} from "./local-installer.js";
import { getLocalN8nAssistantStatus } from "./local-n8n-assistant-installer.js";
import { getLocalN8nSidecarStatus } from "./local-n8n-sidecar-installer.js";
import { getLocalN8nSuperGrokStatus } from "./local-n8n-supergrok-installer.js";
import { getLocalN8nModelStatus } from "./local-n8n-model-installer.js";
import { getLocalN8nClaudeStatus } from "./local-n8n-claude-installer.js";
import { createSafeLocalN8nClaudeSnapshot } from "../domain/local-n8n-claude.js";
const SERVICE_DEFINITIONS = Object.freeze([
Object.freeze({
target: "codex-chatgpt",
label: "Codex (ChatGPT plan)",
kind: "endpoint",
}),
Object.freeze({ target: "codex-chat", label: "Codex Chat adapter", kind: "endpoint" }),
Object.freeze({ target: "xai-grok-build", label: "SuperGrok", kind: "endpoint" }),
Object.freeze({ target: "local-n8n-stack", label: "Local n8n stack", kind: "n8n-stack" }),
Object.freeze({
target: "n8n-openai-oauth",
label: "ChatGPT plan sidecar",
kind: "n8n-oauth-bridge",
}),
Object.freeze({
target: "local-n8n-assistant",
label: "AI Assistant tools",
kind: "n8n-assistant",
}),
Object.freeze({ target: "n8n-supergrok-oauth", label: "SuperGrok for n8n", kind: "n8n-supergrok" }),
Object.freeze({ target: "n8n-claude-oauth", label: "Claude for n8n", kind: "n8n-claude" }),
Object.freeze({ target: "n8n-local-model", label: "Local model for n8n", kind: "n8n-local-model" }),
]);
const STATES = new Set(["absent", "healthy", "stopped", "staged", "partial", "legacy", "unavailable"]);
const ASSISTANT_MODES = new Set(["disabled", "sandbox", "sandbox-with-searxng"]);
const PROVIDER_DEFINITIONS = Object.freeze([
Object.freeze({
target: "codex-chatgpt",
label: "ChatGPT",
authentication: "provider-oauth",
}),
Object.freeze({
target: "codex-chat",
label: "ChatGPT",
authentication: "provider-oauth",
}),
Object.freeze({
target: "xai-grok-build",
label: "SuperGrok",
authentication: "provider-oauth",
}),
Object.freeze({ target: "n8n-supergrok-oauth", label: "SuperGrok (n8n)", authentication: "provider-oauth" }),
Object.freeze({ target: "n8n-claude-oauth", label: "Claude (n8n)", authentication: "provider-oauth" }),
]);
function unavailableService(definition) {
return {
...definition,
managed: false,
state: "unavailable",
snapshot: null,
actions: [],
};
}
function absentService(definition) {
return {
...definition,
managed: false,
state: "absent",
snapshot: null,
actions: ["setup"],
};
}
function readExplicitLoopbackPort(value) {
const match = /^(?:http|ws):\/\/127\.0\.0\.1:(\d{1,5})(?:\/|$)/u.exec(value);
const port = Number(match?.[1]);
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new TypeError();
}
return port;
}
function validateLoopbackEndpoint(value, { target }) {
if (typeof value !== "string" || value.length > 128) throw new TypeError();
const parsed = new URL(value);
if (parsed.hostname !== "127.0.0.1" || parsed.username || parsed.password) {
throw new TypeError();
}
const expectedProtocol = target === "codex-chatgpt" ? "ws:" : "http:";
const expectedPath = "/";
const port = readExplicitLoopbackPort(value);
if (
parsed.protocol !== expectedProtocol ||
parsed.pathname !== expectedPath ||
parsed.search ||
parsed.hash ||
!Number.isInteger(port) ||
port < 1 ||
port > 65_535
) {
throw new TypeError();
}
return value;
}
function validatePublicNgrokUrl(value) {
if (typeof value !== "string" || value.length > 256) throw new TypeError();
const parsed = new URL(value);
if (
parsed.protocol !== "https:" ||
parsed.port ||
parsed.pathname !== "/" ||
parsed.search ||
parsed.hash ||
parsed.username ||
parsed.password ||
!/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/u.test(parsed.hostname)
) {
throw new TypeError();
}
return value;
}
function copyBooleanRecord(value, keys) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError();
return Object.fromEntries(keys.map((key) => {
if (typeof value[key] !== "boolean") throw new TypeError();
return [key, value[key]];
}));
}
const SIWC_SESSIONS = new Set(["signed-out", "connected", "reauthorize"]);
const SIWC_OWNERSHIP = new Set(["owned", "handoff-pending", "transferred"]);
export function copySiwcAccountView(account) {
if (!account || typeof account !== "object" || Array.isArray(account) ||
typeof account.registrationId !== "string" ||
!/^[A-Za-z0-9_-]{8,128}$/u.test(account.registrationId) ||
typeof account.label !== "string" || account.label.length > 160 ||
/[\u0000-\u001f\u007f]/u.test(account.label) ||
(account.email !== undefined && (typeof account.email !== "string" ||
account.email.length > 254 || /[\u0000-\u001f\u007f]/u.test(account.email))) ||
account.identity !== "verified" || !SIWC_SESSIONS.has(account.session) ||
!["granted", "not-granted"].includes(account.planPermission) ||
typeof account.planEnabled !== "boolean" ||
!SIWC_OWNERSHIP.has(account.ownership) ||
typeof account.generation !== "string" || !/^[A-Za-z0-9_-]{8,128}$/u.test(account.generation) ||
typeof account.needsPlanWelcome !== "boolean" ||
typeof account.ownerHostId !== "string" ||
!/^urn:uuid:[a-f0-9-]{36}$/iu.test(account.ownerHostId) ||
typeof account.ownerRuntimeId !== "string" ||
!/^[A-Za-z0-9_-]{1,128}$/u.test(account.ownerRuntimeId)) throw new TypeError();
return {
registrationId: account.registrationId, label: account.label,
...(account.email !== undefined ? { email: account.email } : {}),
identity: "verified", session: account.session, planPermission: account.planPermission,
planEnabled: account.planEnabled, ownership: account.ownership,
generation: account.generation, ownerHostId: account.ownerHostId,
ownerRuntimeId: account.ownerRuntimeId, needsPlanWelcome: account.needsPlanWelcome,
};
}
export function copySiwcStaging(value) {
if (!value || !/^[A-Za-z0-9_-]{8,128}$/u.test(value.installId ?? "") ||
!/^[A-Za-z0-9_-]{8,128}$/u.test(value.registrationId ?? "") ||
!/^[a-z][a-z0-9-]{0,63}$/u.test(value.stage ?? "")) throw new TypeError("The staged installation is invalid.");
return { installId: value.installId, registrationId: value.registrationId, stage: value.stage };
}
function copyInstalledSiwc(snapshot) {
if (typeof snapshot.migrationRequired !== "boolean" ||
typeof snapshot.auth?.configured !== "boolean") throw new TypeError();
if (snapshot.migrationState !== undefined &&
(snapshot.migrationRequired !== true ||
!["incomplete", "prepared", "stopped"].includes(snapshot.migrationState) ||
snapshot.legacyResourcesPreserved !== true)) throw new TypeError();
const migration = snapshot.migrationState
? { migrationState: snapshot.migrationState, legacyResourcesPreserved: true } : {};
if (snapshot.migrationRequired) {
if (snapshot.registrationId !== undefined || snapshot.auth.account !== undefined ||
snapshot.auth.configured) throw new TypeError();
return { migrationRequired: true, ...migration, auth: { configured: false } };
}
if (!/^[A-Za-z0-9_-]{8,128}$/u.test(snapshot.registrationId ?? "")) throw new TypeError();
const account = snapshot.auth.account === undefined ? null : copySiwcAccountView(snapshot.auth.account);
if (account && account.registrationId !== snapshot.registrationId) throw new TypeError();
const active = account?.ownership === "owned" && account.session === "connected" &&
account.planPermission === "granted" && account.planEnabled === true;
if (snapshot.auth.configured !== active) throw new TypeError();
return {
registrationId: snapshot.registrationId, migrationRequired: false, ...migration,
auth: { configured: active, ...(account ? { account } : {}) },
};
}
function copyEndpointSnapshot(target, snapshot) {
if (!snapshot || snapshot.target !== target ||
snapshot.auth?.disclosure !== "rotate-only" ||
snapshot.canRotateCredential !== (snapshot.migrationRequired !== true)) throw new TypeError();
const chatgpt = target === "codex-chatgpt" || target === "codex-chat";
const siwc = chatgpt ? copyInstalledSiwc(snapshot) : null;
if (!chatgpt && snapshot.auth.configured !== true) throw new TypeError();
return {
target,
endpoint: validateLoopbackEndpoint(snapshot.endpoint, { target }),
auth: { configured: chatgpt ? siwc.auth.configured : true, disclosure: "rotate-only",
...(siwc?.auth.account ? { account: siwc.auth.account } : {}) },
...(siwc ? { registrationId: siwc.registrationId, migrationRequired: siwc.migrationRequired,
...(siwc.migrationState ? { migrationState: siwc.migrationState, legacyResourcesPreserved: true } : {}) } : {}),
canRotateCredential: snapshot.canRotateCredential,
};
}
function copyStackSnapshot(snapshot) {
if (
snapshot?.target !== "local-n8n-stack" ||
!["none", "ngrok"].includes(snapshot.publicAccess) ||
!ASSISTANT_MODES.has(snapshot.assistantMode) ||
typeof snapshot.canResume !== "boolean" ||
snapshot.canRemove !== true ||
!/^relmio-local-n8n-[a-f0-9]{32}-n8n-1$/u.test(snapshot.n8nContainerName) ||
snapshot.networkName !== `${snapshot.n8nContainerName.slice(0, -6)}_edge` ||
!/^http:\/\/localhost:(?:[1-9][0-9]{0,4})$/u.test(snapshot.localUrl) ||
snapshot.endpoints?.n8nLocal !== snapshot.localUrl ||
snapshot.endpoints?.ngrokPublic !== snapshot.ngrokPublicUrl ||
snapshot.components?.ngrok !== (snapshot.publicAccess === "ngrok") ||
(snapshot.publicAccess === "none" && snapshot.ngrokPublicUrl !== null)
) throw new TypeError();
const publicUrl = snapshot.publicAccess === "ngrok"
? validatePublicNgrokUrl(snapshot.ngrokPublicUrl) : null;
return {
target: "local-n8n-stack",
publicAccess: snapshot.publicAccess,
localUrl: snapshot.localUrl,
ngrokPublicUrl: publicUrl,
n8nContainerName: snapshot.n8nContainerName,
networkName: snapshot.networkName,
assistantMode: snapshot.assistantMode,
endpoints: {
n8nLocal: snapshot.localUrl,
ngrokPublic: publicUrl,
},
components: copyBooleanRecord(snapshot.components, [
"n8n", "ngrok", "codeSandbox", "searxng",
]),
canResume: snapshot.canResume,
canRemove: true,
};
}
function copySidecarSnapshot(snapshot) {
if (snapshot?.target !== "n8n-openai-oauth" ||
snapshot.endpoint !== "http://n8n-openai-oauth:10531/v1" ||
snapshot.auth?.disclosure !== "server-managed" ||
snapshot.canRefreshCredential !== false ||
snapshot.canRemove !== true) throw new TypeError();
const siwc = copyInstalledSiwc(snapshot);
return {
target: "n8n-openai-oauth",
endpoint: snapshot.endpoint,
auth: { configured: siwc.auth.configured, disclosure: "server-managed",
...(siwc.auth.account ? { account: siwc.auth.account } : {}) },
registrationId: siwc.registrationId,
migrationRequired: siwc.migrationRequired,
...(siwc.migrationState ? { migrationState: siwc.migrationState, legacyResourcesPreserved: true } : {}),
canRefreshCredential: false,
canRemove: true,
};
}
function copySuperGrokSnapshot(snapshot) {
if (
snapshot?.target !== "n8n-supergrok-oauth" ||
snapshot.endpoint !== "http://n8n-supergrok:14502/v1" ||
snapshot.auth?.configured !== true ||
snapshot.auth?.disclosure !== "one-time" ||
snapshot.canRemove !== true
) throw new TypeError();
return {
target: "n8n-supergrok-oauth",
endpoint: "http://n8n-supergrok:14502/v1",
auth: { configured: true, disclosure: "one-time" },
canRemove: true,
};
}
const MODEL_STATES = new Set(["missing", "downloading", "ready", "failed", "partial"]);
const MODEL_IDS = new Set(["qwen3:0.6b", "qwen3:1.7b", "qwen3.5:2b", "qwen3.5:4b", "qwen3.5:9b"]);
function copyModelSnapshot(snapshot) {
if (
snapshot?.target !== "n8n-local-model" ||
snapshot.endpoint !== "http://n8n-local-model:11434/v1" ||
!MODEL_STATES.has(snapshot.model?.state) ||
!MODEL_IDS.has(snapshot.model?.id) ||
typeof snapshot.canRetry !== "boolean" ||
snapshot.canRetry !== ["missing", "failed"].includes(snapshot.model.state) ||
snapshot.canRemove !== (snapshot.model.state !== "downloading") ||
(snapshot.model.digest !== null && !/^[a-f0-9]{64}$/u.test(snapshot.model.digest)) ||
(snapshot.model.state === "ready" && snapshot.model.digest === null)
) throw new TypeError();
return {
target: "n8n-local-model",
endpoint: snapshot.endpoint,
model: {
state: snapshot.model.state,
id: snapshot.model.id,
digest: snapshot.model.digest,
},
canRetry: snapshot.canRetry,
canRemove: snapshot.canRemove,
};
}
function copyAssistantSnapshot(snapshot) {
if (
snapshot?.target !== "local-n8n-assistant" ||
snapshot.auth?.sandboxConfigured !== true ||
snapshot.auth?.disclosure !== "one-time" ||
snapshot.canRemove !== true
) {
throw new TypeError();
}
return {
target: "local-n8n-assistant",
components: copyBooleanRecord(snapshot.components, ["codeSandbox", "searxng"]),
auth: { sandboxConfigured: true, disclosure: "one-time" },
canRemove: true,
};
}
function copySnapshot(definition, snapshot) {
if (definition.kind === "endpoint") return copyEndpointSnapshot(definition.target, snapshot);
if (definition.kind === "n8n-stack") return copyStackSnapshot(snapshot);
if (definition.kind === "n8n-oauth-bridge") return copySidecarSnapshot(snapshot);
if (definition.kind === "n8n-supergrok") return copySuperGrokSnapshot(snapshot);
if (definition.kind === "n8n-claude") return createSafeLocalN8nClaudeSnapshot(snapshot);
if (definition.kind === "n8n-local-model") return copyModelSnapshot(snapshot);
return copyAssistantSnapshot(snapshot);
}
function actionsFor(definition, state, snapshot) {
const actions = [];
if (state === "legacy") return snapshot?.migrationRequired === true ? ["setup"] : [];
if (state === "partial" && snapshot?.migrationState) return [];
if (state === "stopped" && snapshot?.registrationId &&
(["codex-chatgpt", "codex-chat"].includes(definition.target) ||
definition.kind === "n8n-oauth-bridge")) actions.push("inspect-stopped-chatgpt");
if (state === "stopped" && snapshot?.canResume === true) {
actions.push("resume");
}
if (
state === "healthy" &&
definition.kind === "endpoint" &&
snapshot?.canRotateCredential === true
) {
if (["codex-chatgpt", "codex-chat"].includes(definition.target)) {
actions.push("setup");
if (snapshot.auth.account?.ownership === "owned") {
actions.push("sign-out-chatgpt");
if (snapshot.auth.account.planEnabled) actions.push("disable-chatgpt-plan");
}
} else if (definition.target === "xai-grok-build") {
actions.push("sign-in-grok-build", "sign-out-grok-build");
}
actions.push("rotate-local-capability");
}
if (definition.kind === "n8n-oauth-bridge" && state === "healthy") {
actions.push("setup");
if (snapshot.auth.account?.ownership === "owned") {
actions.push("sign-out-chatgpt");
if (snapshot.auth.account.planEnabled) actions.push("disable-chatgpt-plan");
}
}
if (definition.kind === "n8n-local-model" && snapshot?.canRetry === true) {
actions.push("retry-model");
}
if (definition.kind === "n8n-claude") actions.push("manage-claude");
if (definition.kind === "n8n-supergrok") {
if (state === "healthy") actions.push("sign-in-grok-build", "sign-out-grok-build");
if (snapshot?.canRemove === true) actions.push("remove-owned-supergrok");
} else if (snapshot?.canRemove === true) actions.push("remove");
return actions;
}
function providerRuntimeEntry(definition) {
return {
...definition,
readiness: "runtime-owned",
};
}
function getProviderReadiness() {
return PROVIDER_DEFINITIONS.map(providerRuntimeEntry);
}
function sanitizeService(definition, result) {
if (!result || !STATES.has(result.state)) throw new TypeError();
if (result.state === "absent") {
return result.managed === false && result.snapshot == null
? absentService(definition)
: unavailableService(definition);
}
if (result.state === "unavailable" || result.managed !== true) {
return unavailableService(definition);
}
if (result.state === "staged") {
if (!["codex-chatgpt", "codex-chat", "n8n-openai-oauth"].includes(definition.target)) throw new TypeError();
return { ...definition, managed: true, state: "staged", snapshot: null,
staging: copySiwcStaging(result.staging), actions: ["setup"] };
}
if (result.state === "partial" && result.snapshot == null) {
return {
...definition,
managed: true,
state: "partial",
snapshot: null,
actions: [],
};
}
const snapshot = copySnapshot(definition, result.snapshot);
return {
...definition,
managed: true,
state: result.state,
snapshot,
actions: actionsFor(definition, result.state, snapshot),
};
}
function sanitizeDocker(result) {
if (result?.dockerAvailable !== true) {
return { available: false, version: null, composeVersion: null };
}
const version = /^[A-Za-z0-9.+-]{1,64}$/u.test(result.dockerVersion ?? "")
? result.dockerVersion
: null;
const composeVersion = /^[A-Za-z0-9.+-]{1,64}$/u.test(result.composeVersion ?? "")
? result.composeVersion
: null;
if (!version || !composeVersion) {
return { available: false, version: null, composeVersion: null };
}
return { available: true, version, composeVersion };
}
function dashboardModelStatus(result) {
if (result?.status === "absent") return { managed: false, state: "absent", snapshot: null };
if (result?.status === "unavailable") return { managed: false, state: "unavailable", snapshot: null };
if (result?.status === "partial") {
if (result.managed !== true || !MODEL_IDS.has(result.modelId) ||
result.endpoint !== "http://n8n-local-model:11434/v1") throw new TypeError();
return {
managed: true, state: "partial",
snapshot: {
target: "n8n-local-model", endpoint: result.endpoint,
model: { state: "partial", id: result.modelId, digest: null },
canRetry: false, canRemove: true,
},
};
}
const modelStates = {
"runtime-ready": "missing",
downloading: "downloading",
"model-ready": "ready",
"model-error": "failed",
};
const modelState = modelStates[result?.status];
if (
result?.managed !== true || !modelState ||
!MODEL_IDS.has(result.modelId) ||
result.endpoint !== "http://n8n-local-model:11434/v1"
) throw new TypeError();
return {
managed: true,
state: "healthy",
snapshot: {
target: "n8n-local-model",
endpoint: result.endpoint,
model: { state: modelState, id: result.modelId, digest: result.modelDigest ?? null },
canRetry: ["missing", "failed"].includes(modelState),
canRemove: modelState !== "downloading",
},
};
}
async function inspectService(definition, inspectors) {
try {
let result;
if (definition.kind === "endpoint") {
result = await inspectors.inspectLocalEndpoint({ target: definition.target });
} else if (definition.kind === "n8n-stack") {
result = await inspectors.inspectLocalN8nStack();
} else if (definition.kind === "n8n-oauth-bridge") {
result = await inspectors.inspectLocalN8nSidecar();
} else if (definition.kind === "n8n-supergrok") {
result = await inspectors.inspectLocalN8nSuperGrok();
} else if (definition.kind === "n8n-claude") {
result = await inspectors.inspectLocalN8nClaude();
} else if (definition.kind === "n8n-local-model") {
result = dashboardModelStatus(await inspectors.inspectLocalN8nModel());
} else {
result = await inspectors.inspectLocalN8nAssistant();
}
return sanitizeService(definition, result);
} catch {
return unavailableService(definition);
}
}
const unavailableInspector = async () => ({ managed: false, state: "unavailable" });
export async function getLocalDashboardStatus({
now = () => new Date(),
getDockerStatus = getLocalDockerStatus,
inspectLocalEndpoint = getManagedLocalEndpointStatus,
inspectLocalN8nStack = unavailableInspector,
inspectLocalN8nSidecar = getLocalN8nSidecarStatus,
inspectLocalN8nAssistant = getLocalN8nAssistantStatus,
inspectLocalN8nSuperGrok = getLocalN8nSuperGrokStatus,
inspectLocalN8nClaude = getLocalN8nClaudeStatus,
inspectLocalN8nModel = getLocalN8nModelStatus,
} = {}) {
let generatedAt;
try {
generatedAt = now().toISOString();
} catch {
generatedAt = new Date(0).toISOString();
}
let docker;
try {
docker = sanitizeDocker(await getDockerStatus());
} catch {
docker = { available: false, version: null, composeVersion: null };
}
const inspectors = {
inspectLocalEndpoint,
inspectLocalN8nStack,
inspectLocalN8nSidecar,
inspectLocalN8nAssistant,
inspectLocalN8nSuperGrok,
inspectLocalN8nClaude,
inspectLocalN8nModel,
};
const [services, providers] = await Promise.all([
Promise.all(
SERVICE_DEFINITIONS.map((definition) => inspectService(definition, inspectors)),
),
getProviderReadiness(),
]);
return {
schemaVersion: 1,
generatedAt,
docker,
auth: { secretsRevealable: false },
services,
providers,
};
}