271 lines
9.2 KiB
JavaScript
271 lines
9.2 KiB
JavaScript
const EDGE_GATEWAY_ERROR_MESSAGES = {
|
|
EDGE_GATEWAY_OFFLINE: {
|
|
title: "Gateway offline",
|
|
description: "Gatewayen sender ikke heartbeat. Kontrollér service og netværk, og prøv igen.",
|
|
},
|
|
EDGE_GATEWAY_STALE_HEARTBEAT: {
|
|
title: "Forældet heartbeat",
|
|
description: "Gatewayen svarer langsomt eller ustabilt. Kontrollér forbindelsen, og prøv igen.",
|
|
},
|
|
EDGE_GATEWAY_INVALID_TOKEN: {
|
|
title: "Ugyldige legitimationsoplysninger",
|
|
description: "Gateway-token blev afvist. Rotér credentials, og genstart agenten.",
|
|
},
|
|
EDGE_GATEWAY_OPERATION_TIMEOUT: {
|
|
title: "Operation timed out",
|
|
description: "Gatewayoperationen tog for lang tid. Gennemgå logs og forsøg igen.",
|
|
},
|
|
EDGE_GATEWAY_UNSUPPORTED_VERSION: {
|
|
title: "Version ikke understøttet",
|
|
description: "Målversionen eller artefaktet understøttes ikke af den installerede gateway.",
|
|
},
|
|
EDGE_GATEWAY_CONFLICT: {
|
|
title: "Operation i konflikt",
|
|
description: "En anden gatewayoperation kører allerede. Vent til den er afsluttet eller fejlet.",
|
|
},
|
|
EDGE_GATEWAY_VALIDATION_FAILED: {
|
|
title: "Ugyldig anmodning",
|
|
description: "Kontrollér input og prøv igen.",
|
|
},
|
|
EDGE_GATEWAY_CANCELLED: {
|
|
title: "Operation cancelled",
|
|
description: "The gateway operation was cancelled before it completed.",
|
|
},
|
|
};
|
|
|
|
const MESSAGE_CODE_PATTERNS = [
|
|
[/offline/i, "EDGE_GATEWAY_OFFLINE"],
|
|
[/heartbeat/i, "EDGE_GATEWAY_STALE_HEARTBEAT"],
|
|
[/token|credential/i, "EDGE_GATEWAY_INVALID_TOKEN"],
|
|
[/timeout/i, "EDGE_GATEWAY_OPERATION_TIMEOUT"],
|
|
[/version/i, "EDGE_GATEWAY_UNSUPPORTED_VERSION"],
|
|
[/conflict|already active|already in progress/i, "EDGE_GATEWAY_CONFLICT"],
|
|
[/cancel/i, "EDGE_GATEWAY_CANCELLED"],
|
|
[/validation|required|unsupported/i, "EDGE_GATEWAY_VALIDATION_FAILED"],
|
|
];
|
|
|
|
const WEBSOCKET_CLOSE_MESSAGES = {
|
|
agent_offline: "Gateway agent is not connected to the broker. Restart the edge agent or check the broker URL.",
|
|
agent_disconnected: "Gateway agent disconnected from the broker.",
|
|
agent_exit: "Gateway shell exited.",
|
|
shell_open_timeout: "Gateway agent did not confirm that the shell opened before the broker timeout.",
|
|
shell_spawn_failed: "Gateway failed to start the shell process.",
|
|
shell_session_expired: "The terminal session expired before the shell opened.",
|
|
upgrade_rejected: "The broker rejected the terminal websocket upgrade.",
|
|
socket_error: "Terminal websocket connection failed.",
|
|
};
|
|
|
|
const SENSITIVE_DIAGNOSTIC_KEYS = new Set([
|
|
"accesstoken",
|
|
"agenttoken",
|
|
"apikey",
|
|
"authorization",
|
|
"auth",
|
|
"clientsecret",
|
|
"jwt",
|
|
"key",
|
|
"password",
|
|
"secret",
|
|
"signature",
|
|
"token",
|
|
]);
|
|
|
|
const normalizeDiagnosticKey = (key) =>
|
|
String(key || "")
|
|
.replace(/[^a-z0-9]/gi, "")
|
|
.toLowerCase();
|
|
|
|
const isSensitiveDiagnosticKey = (key) => SENSITIVE_DIAGNOSTIC_KEYS.has(normalizeDiagnosticKey(key));
|
|
|
|
const redactSensitiveAssignments = (value) =>
|
|
String(value)
|
|
.replace(/\b(authorization|auth)\s*[:=]\s*(?:Bearer|Basic)\s+[^\s,;&]+/gi, (_match, key) => `${key}=***`)
|
|
.replace(
|
|
/([?&;,\s]?(access[_-]?token|agent[_-]?token|api[_-]?key|authorization|auth|client[_-]?secret|jwt|key|password|secret|signature|token)\s*[:=]\s*)("[^"\s,;&]*"|'[^'\s,;&]*'|[^\s,;&]+)/gi,
|
|
(_match, prefix) => `${prefix}***`
|
|
);
|
|
|
|
export function redactEdgeGatewayUrl(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const url = new URL(String(value), typeof window !== "undefined" ? window.location.origin : "http://localhost");
|
|
if (url.username) {
|
|
url.username = "***";
|
|
}
|
|
if (url.password) {
|
|
url.password = "***";
|
|
}
|
|
Array.from(url.searchParams.keys()).forEach((key) => {
|
|
if (isSensitiveDiagnosticKey(key)) {
|
|
url.searchParams.set(key, "***");
|
|
}
|
|
});
|
|
return url.toString();
|
|
} catch (_error) {
|
|
return redactSensitiveAssignments(String(value));
|
|
}
|
|
}
|
|
|
|
export function redactEdgeGatewayDiagnostic(value) {
|
|
if (value === null || value === undefined) {
|
|
return value;
|
|
}
|
|
|
|
return redactSensitiveAssignments(
|
|
String(value).replace(/\b(?:wss?|https?|mqtts?):\/\/[^\s<>"')]+/gi, (match) => redactEdgeGatewayUrl(match))
|
|
);
|
|
}
|
|
|
|
const formatDiagnosticLines = (diagnostics, prefix = "Diagnostic") => {
|
|
if (!diagnostics || typeof diagnostics !== "object" || Array.isArray(diagnostics)) {
|
|
return [];
|
|
}
|
|
|
|
const lines = [];
|
|
const push = (label, value) => {
|
|
if (value === null || value === undefined || value === "") {
|
|
return;
|
|
}
|
|
lines.push(`${label}: ${redactEdgeGatewayDiagnostic(value)}`);
|
|
};
|
|
|
|
push(`${prefix} code`, diagnostics.reason_code || diagnostics.error_code);
|
|
push("Broker URL", diagnostics.broker_url);
|
|
push("WebSocket URL", diagnostics.ws_url);
|
|
push("Derived URL warning", diagnostics.derived_warning);
|
|
if (diagnostics.broker_presence && typeof diagnostics.broker_presence === "object") {
|
|
push("Broker connected", diagnostics.broker_presence.connected === true ? "yes" : "no");
|
|
push("Broker connection", diagnostics.broker_presence.connection_id);
|
|
push("Broker last seen", diagnostics.broker_presence.last_seen_at);
|
|
push(
|
|
"Broker age",
|
|
Number.isFinite(Number(diagnostics.broker_presence.age_seconds))
|
|
? `${Number(diagnostics.broker_presence.age_seconds)}s`
|
|
: null
|
|
);
|
|
push("Broker last error", diagnostics.broker_presence.last_error);
|
|
push("Broker disconnect reason", diagnostics.broker_presence.disconnect_reason);
|
|
}
|
|
|
|
return lines;
|
|
};
|
|
|
|
export function inferEdgeGatewayErrorCode(error) {
|
|
const payload = error?.response?.data?.data || error?.response?.data || {};
|
|
const explicitCode = payload?.error_code || payload?.code || null;
|
|
if (explicitCode) {
|
|
return explicitCode;
|
|
}
|
|
|
|
const message =
|
|
payload?.message || error?.message || error?.response?.data?.message || error?.response?.statusText || "";
|
|
const matched = MESSAGE_CODE_PATTERNS.find(([pattern]) => pattern.test(String(message)));
|
|
return matched ? matched[1] : "EDGE_GATEWAY_VALIDATION_FAILED";
|
|
}
|
|
|
|
export function normalizeEdgeGatewayError(error) {
|
|
const code = inferEdgeGatewayErrorCode(error);
|
|
const entry = EDGE_GATEWAY_ERROR_MESSAGES[code] || EDGE_GATEWAY_ERROR_MESSAGES.EDGE_GATEWAY_VALIDATION_FAILED;
|
|
const payload = error?.response?.data?.data || error?.response?.data || {};
|
|
const message = payload?.message || error?.message || entry.description;
|
|
const method = error?.config?.method ? String(error.config.method).toUpperCase() : null;
|
|
const url = error?.config?.url || null;
|
|
const status = Number.isInteger(error?.response?.status) ? error.response.status : null;
|
|
const requestId =
|
|
payload?.correlation_id ||
|
|
payload?.request_id ||
|
|
error?.response?.headers?.["x-request-id"] ||
|
|
error?.response?.headers?.["x-correlation-id"] ||
|
|
null;
|
|
const details = [];
|
|
|
|
if (method && url) {
|
|
details.push(`Request: ${method} ${url}`);
|
|
}
|
|
if (status) {
|
|
details.push(`Status: HTTP ${status}`);
|
|
}
|
|
if (payload?.error_code) {
|
|
details.push(`Backend code: ${payload.error_code}`);
|
|
}
|
|
if (requestId) {
|
|
details.push(`Request ID: ${requestId}`);
|
|
}
|
|
details.push(...formatDiagnosticLines(payload?.diagnostics, "Backend diagnostic"));
|
|
|
|
return {
|
|
code,
|
|
title: entry.title,
|
|
description: entry.description,
|
|
message,
|
|
details,
|
|
technical: {
|
|
method,
|
|
url,
|
|
status,
|
|
requestId,
|
|
backendCode: payload?.error_code || null,
|
|
diagnostics: payload?.diagnostics || null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function normalizeGatewayWebSocketClose(event = {}, context = {}) {
|
|
const payload = event && typeof event === "object" && !Array.isArray(event) ? event : { reason: event };
|
|
const reason = String(payload.reason || payload.reason_code || context.reason || "socket_closed").trim();
|
|
const codeValue = payload.code ?? context.code;
|
|
const code = Number.isFinite(Number(codeValue)) ? Number(codeValue) : null;
|
|
const wasClean = typeof payload.wasClean === "boolean" ? payload.wasClean : null;
|
|
const readyState = Number.isFinite(Number(payload.readyState ?? context.readyState))
|
|
? Number(payload.readyState ?? context.readyState)
|
|
: null;
|
|
const url = redactEdgeGatewayUrl(payload.url || payload.ws_url || context.url || context.ws_url);
|
|
const details = [];
|
|
|
|
if (code !== null) {
|
|
details.push(`WebSocket code: ${code}`);
|
|
}
|
|
if (reason) {
|
|
details.push(`Close reason: ${redactEdgeGatewayDiagnostic(reason)}`);
|
|
}
|
|
if (wasClean !== null) {
|
|
details.push(`Clean close: ${wasClean ? "yes" : "no"}`);
|
|
}
|
|
if (readyState !== null) {
|
|
details.push(`Ready state: ${readyState}`);
|
|
}
|
|
if (url) {
|
|
details.push(`WebSocket URL: ${url}`);
|
|
}
|
|
details.push(...formatDiagnosticLines(payload.details || payload.diagnostics || context.diagnostics));
|
|
|
|
const explicitMessage = String(payload.message || "").trim();
|
|
const mappedMessage = WEBSOCKET_CLOSE_MESSAGES[reason] || "";
|
|
const handshakeMessage =
|
|
code === 1005 || code === 1006
|
|
? "Broker did not complete the terminal WebSocket handshake. Verify EDGE_PUBLIC_BROKER_URL, proxy routing, and broker logs."
|
|
: "";
|
|
|
|
return {
|
|
reason,
|
|
code,
|
|
wasClean,
|
|
readyState,
|
|
url,
|
|
message: explicitMessage || mappedMessage || handshakeMessage || "",
|
|
details,
|
|
technical: {
|
|
reason,
|
|
code,
|
|
wasClean,
|
|
readyState,
|
|
url,
|
|
},
|
|
};
|
|
}
|
|
|
|
export { EDGE_GATEWAY_ERROR_MESSAGES };
|